From d31587691c50a0c4e1959dbaa4a61c96eb721866 Mon Sep 17 00:00:00 2001 From: Sudip Ghimire Date: Thu, 2 Mar 2023 02:15:23 +0545 Subject: [PATCH 1/4] feat(khalti): added khalti client and test for intent --- nepali_wallets/client/_khalti.py | 1 - poetry.lock | 239 +++++++++++++++++++++++--- pyproject.toml | 4 + requirements.txt => tests/__init__.py | 0 tests/clients/__init__.py | 0 tests/clients/test_khalti.py | 35 ++++ tests/conftest.py | 1 + tests/connectors.py | 14 ++ tests/fixtures.py | 22 +++ 9 files changed, 289 insertions(+), 27 deletions(-) rename requirements.txt => tests/__init__.py (100%) create mode 100644 tests/clients/__init__.py create mode 100644 tests/clients/test_khalti.py create mode 100644 tests/conftest.py create mode 100644 tests/connectors.py create mode 100644 tests/fixtures.py diff --git a/nepali_wallets/client/_khalti.py b/nepali_wallets/client/_khalti.py index ab54629..d17ef79 100644 --- a/nepali_wallets/client/_khalti.py +++ b/nepali_wallets/client/_khalti.py @@ -21,7 +21,6 @@ def _get_request_body(self) -> dict: "public_key": self.public_key, } - def create_intent( self, amount: int, diff --git a/poetry.lock b/poetry.lock index c5bdb61..459482c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,24 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "attrs" +version = "22.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] + [[package]] name = "certifi" version = "2022.9.24" @@ -5,6 +26,10 @@ description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, + {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, +] [[package]] name = "charset-normalizer" @@ -13,9 +38,40 @@ description = "The Real First Universal Charset Detector. Open, modern and activ category = "main" optional = false python-versions = ">=3.6.0" +files = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] + +[package.extras] +unicode-backport = ["unicodedata2"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, +] [package.extras] -unicode_backport = ["unicodedata2"] +test = ["pytest (>=6)"] [[package]] name = "idna" @@ -24,6 +80,111 @@ description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.0.0" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, + {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, +] + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "23.0" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, +] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "7.2.1" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, + {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "python-decouple" +version = "3.7" +description = "Strict separation of settings from code." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "python-decouple-3.7.tar.gz", hash = "sha256:e88a8d6bdf3b07d471a854099e455e20a6fa7a4d6ecf8631b250e3db654336e6"}, + {file = "python_decouple-3.7-py3-none-any.whl", hash = "sha256:1596dad2670cca5b1f87d087d9adb6a1958c590df346b85d4b19a9d6f0d52cef"}, +] [[package]] name = "requests" @@ -32,6 +193,10 @@ description = "Python HTTP for Humans." category = "main" optional = false python-versions = ">=3.7, <4" +files = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] [package.dependencies] certifi = ">=2017.4.17" @@ -41,7 +206,31 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.5.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] [[package]] name = "urllib3" @@ -50,35 +239,33 @@ description = "HTTP library with thread-safe connection pooling, file post, and category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +files = [ + {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, + {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, +] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + [metadata] -lock-version = "1.1" +lock-version = "2.0" python-versions = "^3.7" -content-hash = "2dc647ca443198bca358c1c10622f6fee0bd23baf333f470dade324cc9c39055" - -[metadata.files] -certifi = [ - { file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" }, - { file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14" }, -] -charset-normalizer = [ - { file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845" }, - { file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" }, -] -idna = [ - { file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" }, - { file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4" }, -] -requests = [ - { file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" }, - { file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983" }, -] -urllib3 = [ - { file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" }, - { file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e" }, -] +content-hash = "f49c50e1305b96fdeb9dcef8f7dfa58af5ab3653a9fcb42c0a097f08b631b376" diff --git a/pyproject.toml b/pyproject.toml index f6bbca3..f6b99d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,10 @@ python = "^3.7" requests = "^2.27.1" +[tool.poetry.group.dev.dependencies] +pytest = "^7.2.1" +python-decouple = "^3.7" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/tests/__init__.py similarity index 100% rename from requirements.txt rename to tests/__init__.py diff --git a/tests/clients/__init__.py b/tests/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/clients/test_khalti.py b/tests/clients/test_khalti.py new file mode 100644 index 0000000..0ce83fc --- /dev/null +++ b/tests/clients/test_khalti.py @@ -0,0 +1,35 @@ +from datetime import datetime + +from ..connectors import khalti_client + + +class TestKhalti: + def test_khali_intent__success(self): + current_timestamp = datetime.now().timestamp() + response = khalti_client.create_intent( + amount=1000, + order_id=f'test_order_{current_timestamp}', + order_name=f"Test Order {current_timestamp}", + customer_info={ + 'name': 'John Doe', + 'email': 'johndoe@example.com', + } + ) + assert response.status_code == 200 + assert 'pidx' in response + + def test_khalti_intent__duplicate_order(self): + current_timestamp = datetime.now().timestamp() + data = { + 'amount': 1000, + 'order_id': f'test_order_{current_timestamp}', + 'order_name': f"Test Order {current_timestamp}", + 'customer_info': { + 'name': 'John Doe', + 'email': 'johndoe@example.com', + } + } + khalti_client.create_intent(**data) + response = khalti_client.create_intent(**data) + + assert response != 200 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d4bb209 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +from .fixtures import * diff --git a/tests/connectors.py b/tests/connectors.py new file mode 100644 index 0000000..6f54810 --- /dev/null +++ b/tests/connectors.py @@ -0,0 +1,14 @@ +from nepali_wallets.client import KhaltiClient, EsewaClient +from decouple import config + +__all__ = [ + 'khalti_client' +] + +khalti_client = KhaltiClient( + public_key=config('KHALTI_PUBLIC_KEY'), + secret_key=config('KHALTI_SECRET_KEY'), + return_url=config('KHALTI_SUCCESS_URL'), + website_url=config('KHALTI_FAILURE_URL'), + sandbox=True +) diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 0000000..7368a8e --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,22 @@ +import pytest + +from .connectors import khalti_client +from datetime import datetime + +__all__ = [ + 'intent__khalti', +] + +@pytest.fixture +def intent__khalti(): + current_timestamp = datetime.now().timestamp() + intent_response = khalti_client.create_intent( + amount=1000, + order_id=f'order_{current_timestamp}', + order_name="Test Order", + customer_info={ + 'name': 'Customer 1', + 'email': 'abc@email.com', + } + ) + return intent_response From 7718f46af5fc7a80c76804d5cecd51f2b0fcc279 Mon Sep 17 00:00:00 2001 From: Sudip Ghimire Date: Thu, 2 Mar 2023 02:16:12 +0545 Subject: [PATCH 2/4] feat(khalti): added test environment example --- .env.example | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..283bcd1 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +KHALTI_PUBLIC_KEY="__________YOUR_KEY__________" +KHALTI_SECRET_KEY="__________YOUR_KEY__________" + +ESEWA_ID="__________YOUR_KEY__________" +ESEWA_SUCCESS_URL="__________YOUR_KEY__________" +ESEWA_FAILURE_URL="__________YOUR_KEY__________" + +NCHL_APP_ID="__________YOUR_KEY__________" +NCHL_APP_NAME="__________YOUR_KEY__________" +NCHL_APP_PASSWORD="__________YOUR_KEY__________" +NCHL_MERCHANT_ID="__________YOUR_KEY__________" +NCHL_CREDITOR_PASSWORD="__________YOUR_KEY__________" From f17cb3bdd11da1b463b451b87af0710f5ba0bb1b Mon Sep 17 00:00:00 2001 From: Sudip Ghimire Date: Thu, 2 Mar 2023 02:17:30 +0545 Subject: [PATCH 3/4] feat(khalti): added more test environments --- .env.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env.example b/.env.example index 283bcd1..8df319b 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,7 @@ KHALTI_PUBLIC_KEY="__________YOUR_KEY__________" KHALTI_SECRET_KEY="__________YOUR_KEY__________" +KHALTI_SUCCESS_URL="https://example.com/success" +KHALTI_FAILURE_URL="https://example.com/failure" ESEWA_ID="__________YOUR_KEY__________" ESEWA_SUCCESS_URL="__________YOUR_KEY__________" From a71887b9b2c0d1c56470b438deba05b2fef6f68c Mon Sep 17 00:00:00 2001 From: Sudip Ghimire Date: Thu, 2 Mar 2023 04:36:05 +0545 Subject: [PATCH 4/4] feat(khalti): added scalable intent for the first phase --- nepali_wallets/client/_khalti.py | 39 ++++++++++++++---------- nepali_wallets/client/base.py | 51 +++++++++++++++++++++++++++++++- tests/clients/test_khalti.py | 10 +++---- 3 files changed, 78 insertions(+), 22 deletions(-) diff --git a/nepali_wallets/client/_khalti.py b/nepali_wallets/client/_khalti.py index d17ef79..2d8393f 100644 --- a/nepali_wallets/client/_khalti.py +++ b/nepali_wallets/client/_khalti.py @@ -1,7 +1,12 @@ import json from typing import Union -from .base import BasePaymentClient +from .base import BasePaymentClient, BasePaymentIntent + + +class KhaltiIntent(BasePaymentIntent): + def _get_intent_id(self) -> str: + return self.data.get('pidx') class KhaltiClient(BasePaymentClient): @@ -30,20 +35,24 @@ def create_intent( amount_breakdown: Union[list, None] = None, product_details: Union[list, None] = None - ): - return self.session.post( - f'{self.base_url}/epayment/initiate/', - data=json.dumps({ - "return_url": self.return_url, - "website_url": self.website_url, - "amount": amount, # amount in paisa - 'purchase_order_id': order_id, - "purchase_order_name": order_name, - "customer_info": customer_info, - "amount_breakdown": amount_breakdown or [], - "product_details": product_details or [] - }), - headers=self._get_request_headers() + ) -> KhaltiIntent: + return KhaltiIntent( + self.session.post( + f'{self.base_url}/epayment/initiate/', + data=json.dumps( + { + "return_url": self.return_url, + "website_url": self.website_url, + "amount": amount, # amount in paisa + 'purchase_order_id': order_id, + "purchase_order_name": order_name, + "customer_info": customer_info, + "amount_breakdown": amount_breakdown or [], + "product_details": product_details or [] + } + ), + headers=self._get_request_headers() + ) ) def complete_payment(self, token: str, confirmation_code: str): diff --git a/nepali_wallets/client/base.py b/nepali_wallets/client/base.py index 9a42c1c..67f74f2 100644 --- a/nepali_wallets/client/base.py +++ b/nepali_wallets/client/base.py @@ -1,11 +1,60 @@ from abc import ABC, abstractmethod +from typing import Union + import requests +__all__ = [ + 'BasePaymentIntent', + 'BasePaymentClient', + 'PaymentClientError' +] + +from requests import JSONDecodeError + class PaymentClientError(BaseException): pass +class BasePaymentIntent(ABC): + """ + **BasePaymentIntent** is used by a ``BasePaymentClient`` instance to track the + response of the intent + `response` + :cvar response: a ``requests.Response`` instance used to track the original response from the server exists + :cvar data: ``dict`` used only when there is no ``requests.Response`` instance exist + """ + response: Union[requests.Response, None] + data: dict + + def __init__(self, response: Union[requests.Response, dict], **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + if isinstance(response, requests.Response): + self.response = response + try: + self.data = response.json() + except JSONDecodeError: + self.data = {} + else: + self.data = response + self.response = None + + @abstractmethod + def _get_intent_id(self) -> str: + pass + + @property + def id(self): + return self._get_intent_id() + + @property + def text(self): + if self.response: + return self.response.text + return None + + class BasePaymentClient(ABC): base_url: str merchant_id: str @@ -39,7 +88,7 @@ def _get_request_body(self) -> dict: pass @abstractmethod - def create_intent(self, *args, **kwargs): + def create_intent(self, *args, **kwargs) -> BasePaymentIntent: """ It creates the payment intent for specific payment gateway """ diff --git a/tests/clients/test_khalti.py b/tests/clients/test_khalti.py index 0ce83fc..6c9a230 100644 --- a/tests/clients/test_khalti.py +++ b/tests/clients/test_khalti.py @@ -6,7 +6,7 @@ class TestKhalti: def test_khali_intent__success(self): current_timestamp = datetime.now().timestamp() - response = khalti_client.create_intent( + intent = khalti_client.create_intent( amount=1000, order_id=f'test_order_{current_timestamp}', order_name=f"Test Order {current_timestamp}", @@ -15,8 +15,7 @@ def test_khali_intent__success(self): 'email': 'johndoe@example.com', } ) - assert response.status_code == 200 - assert 'pidx' in response + assert intent.id is not None def test_khalti_intent__duplicate_order(self): current_timestamp = datetime.now().timestamp() @@ -30,6 +29,5 @@ def test_khalti_intent__duplicate_order(self): } } khalti_client.create_intent(**data) - response = khalti_client.create_intent(**data) - - assert response != 200 + intent = khalti_client.create_intent(**data) + assert intent.id is None