diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c08c5b9 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +PAYCOM_BASE_URL=https://checkout.test.paycom.uz/api +PAYCOM_ID= +PAYCOM_KEY= diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..6deafc2 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml deleted file mode 100644 index 4046520..0000000 --- a/.github/workflows/pylint.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Pylint - -on: [push] - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.11"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements/requirements.txt - pip install pylint - - name: Analysing the code with pylint - run: | - pylint ./lib/* diff --git a/.github/workflows/telegram-event-notifier.yml b/.github/workflows/telegram-event-notifier.yml new file mode 100644 index 0000000..7bb69d5 --- /dev/null +++ b/.github/workflows/telegram-event-notifier.yml @@ -0,0 +1,28 @@ +name: Telegram Gate + +on: + issue_comment: + types: [created] + issues: + types: [opened, edited, pinned, closed, reopened, assigned, labeled] + pull_request: + types: + [opened, closed, edited, ready_for_review, review_requested, reopened] + pull_request_review_comment: + types: [created] + push: + +jobs: + build: + name: Telegram Gate + runs-on: ubuntu-latest + steps: + - name: Notifier + uses: ethyaan/tgate-action@v1.0.0 + if: always() + with: + token: ${{ secrets.TELEGRAM_BOT_TOKEN }} + to: ${{ secrets.TELEGRAM_CHAT_ID }} + thread_id: ${{ secrets.TELEGRAM_CHAT_TOPIC }} + disable_web_page_preview: true + disable_notification: false diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 0000000..66453f4 --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,36 @@ +name: Testing project + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + name: Testing project + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.11"] + + env: + PAYME_ID: ${{ secrets.PAYME_ID }} + PAYME_KEY: ${{ secrets.PAYME_KEY }} + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements/dev-requirements.txt + pip install -r requirements/requirements.txt + + - name: Run unit tests + run: | + python tests.py diff --git a/.gitignore b/.gitignore index 998cacb..1133b65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,23 @@ +# virtual env +env/ +venv/ +.env + +# dev test core/ +my_app/ db.sqlite3 +manage.py dist/ -venv/ + +# ide configs .vscode/ .idea/ + +# caches __pycache__/ paymentsuz.egg-info/ payme_pkg.egg-info/ -manage.py \ No newline at end of file + +.DS_Store +build/ \ No newline at end of file diff --git a/.pylintrc b/.pylintrc index 70226f1..26c9410 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,6 +1,5 @@ [MASTER] -disable=no-member, - unnecessary-pass, - useless-option-value, - too-few-public-methods, - missing-module-docstring +disable=W0613,E1101,W0718,W0223,W0621,W1203,W0622,C0116,E0213, C0114 + +ignore-long-lines = True +max-line-length = 120 diff --git a/LICENSE.txt b/LICENSE.txt index aeb6397..1eb4dd5 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,20 +1,24 @@ -Copyright (c) 2021 Giorgos Myrianthous +MIT License +----------- -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: +Copyright (c) 2023 Muhammadali Akbarov +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index a0685d4..b5ccc5c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,23 @@ -# Payme Uzbekistan Integration Uzcard and Humo +

Payme PKG

+ +

- - + + + + +

+ +

Welcome to payme-pkg, the open source payme sdk for python.

+ +

You can use for test and production mode. Join our community and ask everything you need. +

+ +

Visit to full documentation for Merchant and Subcribe Api

+

+

-Support Group - Telegram
-YouTube - Watch Video
-Implementation Sample - https://github.com/PayTechUz/payme-sample ## Installation @@ -14,40 +25,9 @@ Implementation Sample - https://github.com/PayTechUz/payme-sample pip install payme-pkg ``` -### Test-Credentials - -``` -Card Numer: 8600 4954 7331 6478 Expire Date: 03/99 SMS Code: 666666 -Card Numer: 8600 0691 9540 6311 Expire Date: 03/99 SMS Code: 666666 -``` - -## Documentation - -- [Merchant API](#merchant-api) -- [Generate Pay Link](#generate-pay-link) - -- Subscribe Cards - - - [Cards Create](#cards-create) - - [Cards Get Verify Code](#cards-get-verify-code) - - [Cards Verify](#cards-verify) - - [Cards Check](#cards-check) - - [Cards Remove](#cards-remove) - -- Subscribe Receipts - - [Receipts Create](#receipts-create) - - [Receipts Pay](#receipts-pay) - - [Receipts Send](#receipts-send) - - [Receipts Cancel](#receipts-cancel) - - [Receipts Check](#receipts-check) - - [Receipts Get](#receipts-get) - - [Receipts Get All ](#receipts-get-all) - -# Merchant API - ## Installation to Django -Add `'payme'` in to your settings. +Add `'payme'` in to your settings.py ```python INSTALLED_APPS = [ @@ -57,35 +37,50 @@ INSTALLED_APPS = [ ] ``` -Add `'payme'` credentials inside to settings. +Add `'payme'` credentials inside to settings.py +One time payment configuration settings.py ```python -PAYME: dict = { - 'PAYME_ID': 'payme-id', - 'PAYME_KEY': 'payme-key', - 'PAYME_URL': 'payme-checkout-url', - 'PAYME_CALL_BACK_URL': 'your-callback-url', # merchant api callback url - 'PAYME_MIN_AMOUNT': 'payme-min-amount', # integer field - 'PAYME_ACCOUNT': 'order-id', -} +PAYME_ID = "670cb597e51de1c6a3a5934b" +PAYME_KEY = "KCR4W1kzqPxxUeW&BHNM0AOtR3?&0@E%R?P@" +PAYME_ACCOUNT_FIELD = "id" +PAYME_AMOUNT_FIELD = "total_amount" +PAYME_ACCOUNT_MODEL = "orders.models.Orders" +PAYME_ONE_TIME_PAYMENT = True +``` -ORDER_MODEL = 'your_app.models.Your_Order_Model' +Multi payment configuration settings.py +```python +PAYME_ID = "670cb597e51de1c6a3a5934b" +PAYME_KEY = "KCR4W1kzqPxxUeW&BHNM0AOtR3?&0@E%R?P@" +PAYME_ACCOUNT_FIELD = "id" +PAYME_ACCOUNT_MODEL = "clients.models.Client" +PAYME_ONE_TIME_PAYMENT = False ``` Create a new View that about handling call backs ```python -from payme.views import MerchantAPIView +from payme.views import PaymeWebHookAPIView -class PaymeCallBackAPIView(MerchantAPIView): - def create_transaction(self, order_id, action, *args, **kwargs) -> None: - print(f"create_transaction for order_id: {order_id}, response: {action}") +class PaymeCallBackAPIView(PaymeWebHookAPIView): + def handle_created_payment(self, params, result, *args, **kwargs): + """ + Handle the successful payment. You can override this method + """ + print(f"Transaction created for this params: {params} and cr_result: {result}") - def perform_transaction(self, order_id, action, *args, **kwargs) -> None: - print(f"perform_transaction for order_id: {order_id}, response: {action}") + def handle_successfully_payment(self, params, result, *args, **kwargs): + """ + Handle the successful payment. You can override this method + """ + print(f"Transaction successfully performed for this params: {params} and performed_result: {result}") - def cancel_transaction(self, order_id, action, *args, **kwargs) -> None: - print(f"cancel_transaction for order_id: {order_id}, response: {action}") + def handle_cancelled_payment(self, params, result, *args, **kwargs): + """ + Handle the cancelled payment. You can override this method + """ + print(f"Transaction cancelled for this params: {params} and cancelled_result: {result}") ``` Add a `payme` path to core of urlpatterns: @@ -98,17 +93,15 @@ from your_app.views import PaymeCallBackAPIView urlpatterns = [ ... - path("payments/merchant/", PaymeCallBackAPIView.as_view()), + path("payments/updates/", PaymeCallBackAPIView.as_view()), ... ] ``` Run migrations - ```shell -python manage.py migrate +python3 manage.py makemigrations && python manage.py migrate ``` - 🎉 Congratulations you have been integrated merchant api methods with django, keep reading docs. After successfull migrations check your admin panel and see results what happened. ## Generate Pay Link @@ -118,776 +111,15 @@ Example to generate link: - Input ```python -from pprint import pprint +from payme import Payme -from payme.methods.generate_link import GeneratePayLink - -pay_link = GeneratePayLink( - order_id=999, - amount=9999 -).generate_link() - -pprint(pay_link) +payme = Payme(payme_id="your-payme-id") +pay_link = payme.initializer.generate_pay_link(id=123456, amount=5000, return_url="https://example.com") +print(pay_link) ``` - Output ``` -Link: https://checkout.paycom.uz/bT01ZTczMGU4ZTBiODUyYTQxN2FhNDljZWI7YWMub3JkZXItaWQ9OTk5O2E9OTk5OTtjPXlvdXItY2FsbGJhY2stdXJs -``` - -## Cards Create - -Example for cards create method for to generate token from card: - -- Request - -```python -from pprint import pprint - -from payme.cards.subscribe_cards import PaymeSubscribeCards - -client = PaymeSubscribeCards( - base_url="https://checkout.test.paycom.uz/api/", - paycom_id=$paycom_id -) - -resp = client.cards_create( - number="8600069195406311", - expire="0399", - save=True -) - -pprint(resp) -``` - -- Response - -```json -{ - "jsonrpc": "2.0", - "result": { - "card": { - "number": "860006******6311", - "expire": "03/99", - "token": "63119784d15d8d8d093b37b8_ADHrAykwnAIc2hm4hPPriktZ8nnuvR96S9Kzmjb3Fcix25IrJmMKrGxP9VUEP9rRDKRhtYjUw0vsXON7PYEyMCHtDKpMuM4krrIk8jdnyK7bXkSBSCyiGs2aahIrep6TSodIAxqutMJ4g3O8FZ8vC1DSMKzOaX0UF8fDKNexXV039Qnj4bNQc6NcpKGJn0wUX8d0RBqkmKid4WyUQnT987ZQDM2mT2IGNZtugvN4tDJTXBVTpqCWkXnZ0YWj64Ye0ztr91Mibtndo0Y1s5nCA6wufUZZugJ6c7rse19XNFSSieFM7AWi9VqybMe4eeWiZEBriAbFhrf8kQvrpBmwUEp05GjvFMgH0ku3vyUtSuJI36exHheXuJK66KstcX1i69EaF3", - "recurrent": true, - "verify": false, - "type": "22618" - } - } -} -``` - -## Cards Get Verify Code - -Example for cards get verify: - -- Request - -```python -from pprint import pprint - -from payme.cards.subscribe_cards import PaymeSubscribeCards - - -client = PaymeSubscribeCards( - base_url="https://checkout.test.paycom.uz/api/", - paycom_id=$paycom_id -) - -resp = client.card_get_verify_code( - token="630e5ffdd15d8d8d093b379b_2fsaoABWafecn20kofV4PFafFZjeGDWS9adM1PmboQaEZbbaxMcnaskctMbU9Iv8qgrOuKGz8SnjvZvYXDK64m1eS9gA5jZ7BBRaQybMXrDPtFPJ1fwek5B1KoIv5cMiCWYXj7ezpYEdJAKTIQw0Np9HsTXjqco4gQG3m8MOfeH9ovkdm66O6yj45oKXRmJyAK5i0SchXNNomACH3Oq80KyoRE1VoBRxvoKyMkOx0xcepXovxK9d3v26a8z7UtyokwY33N8MupviM3A5WHB5Xh35WZJJyFnxTSi1vvnYnG7uVd6Bb1GjV2yAHnimss8aEZGW5V7ZiPrhf8r6WJAeHciYDGK3msRKZJBQTfjgOdE9tGrEnMezVkxr1JXX0xSn5qqec2" -) - -pprint(resp) -``` - -- Response - -```json -{ - "jsonrpc": "2.0", - "result": { - "sent": true, - "phone": "99890*****66", - "wait": 60000 - } -} -``` - -## Cards Verify - -Example for cards verify method: - -- Request - -```python -from pprint import pprint - -from payme.cards.subscribe_cards import PaymeSubscribeCards - - -client = PaymeSubscribeCards( - base_url="https://checkout.test.paycom.uz/api/", - paycom_id=$paycom_id -) - -resp = client.cards_verify( - verify_code="666666", - token="630e691fd15d8d8d093b379c_70mKyzqS8d1wTWzovIGjt9dKmjpn1KI8Y9XakPrfpbUASTBaZYbC1DjDcjYRmuNJep9gZrTRtHyEGBQYmBaPufuozF51bv4qEPsQnodq1VcD7tYyREwUXjMXXZUeu7Ek0REQCekCvVHX6rtNBpb4vtViJoNVjp94XpTqu0Bn3yYYb0CHu951wFydzRsieGxjGNrvx1oKyBcq0CdOUwoffRIt2VPvx5R2aVmc6ahwyhn387FEEcpO1PnjIJkWKTBWdI35ZPQnb1u1oss5aPg06E279THXRkoTThixbeqiD2JkWSXweNVGGDhTS30V4j61G3NWEPO2H3k4uFmCjjIQSzx4TxKzUgHg1i2q953PRUGjT4JZBRHMDxaN5tWuctEMNmY06p" -) - -pprint(resp) -``` - -- Response - -```json -{ - "jsonrpc": "2.0", - "result": { - "card": { - "number": "860006******6311", - "expire": "03/99", - "token": "63118a5dd15d8d8d093b37b7_X2j34OIJPnROfsgzYZCZ0w7OcC50zzwiowTsotEVO1uUbxkzaDrvdOno6jicQTrcRmxvibxrye4vUS3AynTNPaPCTGpfk3RCKmT9NaOAyyTmctAjWsjwvqGR5XUzAP1Xcx12GkhuQi6VJ4BeaIXOokSRu06rRjaivmJQ8HTiJiR9b3OmZtrhkIRNcNXnnp9zYm1mFP4BuqGpS8BMnY0ASIE6ffxWykjgBcDTAfWBFt4mg7O9Dsvx0aj3IB8z3RIbZYtDZJnUVhCZrwW7ONVI9uEAdxNthorjO6PbV7TQ8XCjrztgGf6uCtOwwxasiIUVZN6tCVDk8A8NvVSUzUHXQHVkaPn5heJNa3K4WsffIckq7SwMbiw3UbawipeZKyD3iwk1Km", - "recurrent": true, - "verify": true, - "type": "22618" - } - } -} -``` - -## Cards Check - -Example for cards check: - -- Request - -```python -from pprint import pprint - -from payme.cards.subscribe_cards import PaymeSubscribeCards - - -client = PaymeSubscribeCards( - base_url="https://checkout.test.paycom.uz/api/", - paycom_id=$paycom_id -) - -resp = client.cards_check( - token="630e691fd15d8d8d093b379c_70mKyzqS8d1wTWzovIGjt9dKmjpn1KI8Y9XakPrfpbUASTBaZYbC1DjDcjYRmuNJep9gZrTRtHyEGBQYmBaPufuozF51bv4qEPsQnodq1VcD7tYyREwUXjMXXZUeu7Ek0REQCekCvVHX6rtNBpb4vtViJoNVjp94XpTqu0Bn3yYYb0CHu951wFydzRsieGxjGNrvx1oKyBcq0CdOUwoffRIt2VPvx5R2aVmc6ahwyhn387FEEcpO1PnjIJkWKTBWdI35ZPQnb1u1oss5aPg06E279THXRkoTThixbeqiD2JkWSXweNVGGDhTS30V4j61G3NWEPO2H3k4uFmCjjIQSzx4TxKzUgHg1i2q953PRUGjT4JZBRHMDxaN5tWuctEMNmY06p" -) - -pprint(resp) -``` - -- Response - -```json -{ - "jsonrpc": "2.0", - "result": { - "card": { - "number": "860006******6311", - "expire": "03/99", - "token": "63119b36d15d8d8d093b37c1_IJtHxZ46h5viyo8RIJCmQyE8qBw6PUWUdFKTMCVWrPoMMi4kJYsKyVdjQrIx6a12jDfEPVhhEqWm94FYvYh7IEjIs4xn0n3mM8Quw5dhd6ZT0dOK6u1spqWRMIDBpDMhHj2Ga8zZMAfeoiDAcrWScXS1AP2tkQHcJ40rBzHGHS6DoVeIheF70c0wO1kVQG0G5hDWguSGf2ZRFcBtpabv5BQkqSchxWKdCSVPIGiS6X7eF8YStdz1aGPzFyjDbaKT0vXNUMbQ7gaKh4PeQbruVVwFDfeIWqGeNmgCCPU4X0wCHFjTt8K61e9VOauNeU81ckoKHD8XGzCwGFJHrC4sHvNv4no3RifWhHCQF9GmFKf8cP2qh4pqTKwu3gOITaX5Ss71tC", - "recurrent": true, - "verify": true, - "type": "22618" - } - } -} -``` - -## Cards Remove - -Example for cards create method for to generate token from card: - -- Request - -```python -from pprint import pprint - -from payme.cards.subscribe_cards import PaymeSubscribeCards - - -client = PaymeSubscribeCards( - base_url="https://checkout.test.paycom.uz/api/", - paycom_id=$paycom_id -) - -resp = client.cards_remove( - token="630e691fd15d8d8d093b379c_70mKyzqS8d1wTWzovIGjt9dKmjpn1KI8Y9XakPrfpbUASTBaZYbC1DjDcjYRmuNJep9gZrTRtHyEGBQYmBaPufuozF51bv4qEPsQnodq1VcD7tYyREwUXjMXXZUeu7Ek0REQCekCvVHX6rtNBpb4vtViJoNVjp94XpTqu0Bn3yYYb0CHu951wFydzRsieGxjGNrvx1oKyBcq0CdOUwoffRIt2VPvx5R2aVmc6ahwyhn387FEEcpO1PnjIJkWKTBWdI35ZPQnb1u1oss5aPg06E279THXRkoTThixbeqiD2JkWSXweNVGGDhTS30V4j61G3NWEPO2H3k4uFmCjjIQSzx4TxKzUgHg1i2q953PRUGjT4JZBRHMDxaN5tWuctEMNmY06p" -) - -pprint(resp) -``` - -- Response - -```json -{ - "jsonrpc": "2.0", - "result": { - "success": true - } -} -``` - -## Receipts Create - -Example for receipts create method: - -- Request - -```python -from pprint import pprint - -from payme.receipts.subscribe_receipts import PaymeSubscribeReceipts - - -rclient = PaymeSubscribeReceipts( - base_url="https://checkout.test.paycom.uz/api/", - paycom_id=$paycom_id, - paycom_key=$paycom_key -) - -resp = rclient.receipts_create( - amount=10000, - order_id="1" -) - -pprint(resp) -``` - -- Response - -```json -{ - "jsonrpc": "2.0", - "result": { - "receipt": { - "_id": "63119becc4420cbf2712a24c", - "create_time": 1662098412270, - "pay_time": 0, - "cancel_time": 0, - "state": 0, - "type": 2, - "external": false, - "operation": -1, - "category": null, - "error": null, - "description": "", - "detail": null, - "amount": 400000, - "currency": 860, - "commission": 0, - "account": [ - { - "name": "transaction", - "title": "Номер чека", - "value": "2326", - "main": true - } - ], - "card": null, - "merchant": { - "_id": "5e730e8e0b852a417aa49ceb", - "name": "test", - "organization": "ЧП «test test»", - "address": "", - "business_id": "5e730e740b852a417aa49cea", - "epos": { - "merchantId": "106600000050000", - "terminalId": "20660000" - }, - "date": 1584598670296, - "logo": null, - "type": "Internet", - "terms": null - }, - "meta": { - "source": "subscribe", - "owner": "5e730e8e0b852a417aa49ceb" - }, - "processing_id": null - } - } -} -``` - -## Receipts Pay - -Example for receipts pay method: - -- Request - -```python -from pprint import pprint - -from payme.receipts.subscribe_receipts import PaymeSubscribeReceipts - - -rclient = PaymeSubscribeReceipts( - base_url="https://checkout.test.paycom.uz/api/", - paycom_id=$paycom_id, - paycom_key=$paycom_key -) - -resp = rclient.receipts_pay( - invoice_id="631186b6c4420cbf2712a243", - token="63118a5dd15d8d8d093b37b7_X2j34OIJPnROfsgzYZCZ0w7OcC50zzwiowTsotEVO1uUbxkzaDrvdOno6jicQTrcRmxvibxrye4vUS3AynTNPaPCTGpfk3RCKmT9NaOAyyTmctAjWsjwvqGR5XUzAP1Xcx12GkhuQi6VJ4BeaIXOokSRu06rRjaivmJQ8HTiJiR9b3OmZtrhkIRNcNXnnp9zYm1mFP4BuqGpS8BMnY0ASIE6ffxWykjgBcDTAfWBFt4mg7O9Dsvx0aj3IB8z3RIbZYtDZJnUVhCZrwW7ONVI9uEAdxNthorjO6PbV7TQ8XCjrztgGf6uCtOwwxasiIUVZN6tCVDk8A8NvVSUzUHXQHVkaPn5heJNa3K4WsffIckq7SwMbiw3UbawipeZKyD3iwk1Km", - phone="998901304527" -) - -pprint(resp) -``` - -- Response - -```json -{ - "jsonrpc": "2.0", - "id": 123, - "result": { - "receipt": { - "_id": "63119becc4420cbf2712a24c", - "create_time": 1662098438706, - "pay_time": 1662098438804, - "cancel_time": 0, - "state": 4, - "type": 2, - "external": false, - "operation": -1, - "category": null, - "error": null, - "description": "", - "detail": null, - "amount": 400000, - "currency": 860, - "commission": 0, - "account": [ - { - "name": "transaction", - "title": "Номер чека", - "value": "2326", - "main": true - } - ], - "card": { - "number": "860006******6311", - "expire": "9903" - }, - "merchant": { - "_id": "5e730e8e0b852a417aa49ceb", - "name": "test", - "organization": "ЧП «test test»", - "address": "", - "business_id": "5e730e740b852a417aa49cea", - "epos": { - "merchantId": "106600000050000", - "terminalId": "20660000" - }, - "date": 1584598670296, - "logo": null, - "type": "Internet", - "terms": null - }, - "meta": { - "source": "subscribe", - "owner": "5e730e8e0b852a417aa49ceb" - }, - "processing_id": 0 - } - } -} -``` - -## Receipts Send - -Example for receipts send method: - -- Request - -```python -from pprint import pprint - -from payme.receipts.subscribe_receipts import PaymeSubscribeReceipts - - -rclient = PaymeSubscribeReceipts( - base_url="https://checkout.test.paycom.uz/api/", - paycom_id=$paycom_id, - paycom_key=$paycom_key -) - -resp = rclient.receipts_send( - invoice_id="631186b6c4420cbf2712a243", - phone="998901304527" -) - -pprint(resp) -``` - -- Response - -```json -{ - "jsonrpc": "2.0", - "id": 123, - "result": { - "success": true - } -} -``` - -## Receipts Cancel - -Example for receipts cancel method: - -- Request - -```python -from pprint import pprint - -from payme.receipts.subscribe_receipts import PaymeSubscribeReceipts - - -rclient = PaymeSubscribeReceipts( - base_url="https://checkout.test.paycom.uz/api/", - paycom_id=$paycom_id, - paycom_key=$paycom_key -) - -resp = rclient.receipts_cancel( - invoice_id="63119303c4420cbf2712a245" -) - -pprint(resp) -``` - -- Response - -```json -{ - "jsonrpc": "2.0", - "id": 123, - "result": { - "receipt": { - "_id": "63119becc4420cbf2712a24c", - "create_time": 1662098438706, - "pay_time": 1662098438804, - "cancel_time": 0, - "state": 21, - "type": 2, - "external": false, - "operation": -1, - "category": null, - "error": null, - "description": "", - "detail": null, - "amount": 400000, - "currency": 860, - "commission": 0, - "account": [ - { - "name": "transaction", - "title": "Номер чека", - "value": "2326", - "main": true - } - ], - "card": { - "number": "860006******6311", - "expire": "9903" - }, - "merchant": { - "_id": "5e730e8e0b852a417aa49ceb", - "name": "test", - "organization": "ЧП «test test»", - "address": "", - "business_id": "5e730e740b852a417aa49cea", - "epos": { - "merchantId": "106600000050000", - "terminalId": "20660000" - }, - "date": 1584598670296, - "logo": null, - "type": "Internet", - "terms": null - }, - "meta": { - "source": "subscribe", - "owner": "5e730e8e0b852a417aa49ceb", - "source_cancel": "subscribe" - }, - "processing_id": null - } - } -} -``` - -## Receipts Check - -Example for receipts check method: - -- Request - -```python -from pprint import pprint - -from payme.receipts.subscribe_receipts import PaymeSubscribeReceipts - - -rclient = PaymeSubscribeReceipts( - base_url="https://checkout.test.paycom.uz/api/", - paycom_id=$paycom_id, - paycom_key=$paycom_key -) - -resp = rclient.receipts_check( - invoice_id="63119303c4420cbf2712a245" -) - -pprint(resp) -``` - -- Response - -```json -{ - "jsonrpc": "2.0", - "id": 123, - "result": { - "state": 0 - } -} -``` - -## Receipts Get - -Example for receipts get method: - -- Request - -```python -from pprint import pprint - -from payme.receipts.subscribe_receipts import PaymeSubscribeReceipts - - -rclient = PaymeSubscribeReceipts( - base_url="https://checkout.test.paycom.uz/api/", - paycom_id=$paycom_id, - paycom_key=$paycom_key -) - -resp = rclient.reciepts_get( - invoice_id="6311946bc4420cbf2712a247" -) - -pprint(resp) -``` - -- Response - -```json -{ - "jsonrpc": "2.0", - "id": 123, - "result": { - "receipt": { - "_id": "6311946bc4420cbf2712a247", - "create_time": 1662096491076, - "pay_time": 0, - "cancel_time": 0, - "state": 0, - "type": 2, - "external": false, - "operation": -1, - "category": null, - "error": null, - "description": "", - "detail": null, - "amount": 400000, - "currency": 860, - "commission": 0, - "account": [ - { - "name": "transaction", - "title": "Номер чека", - "value": "2325", - "main": true - } - ], - "card": null, - "merchant": { - "_id": "5e730e8e0b852a417aa49ceb", - "name": "test", - "organization": "ЧП «test test»", - "address": "", - "business_id": "5e730e740b852a417aa49cea", - "epos": { - "merchantId": "106600000050000", - "terminalId": "20660000" - }, - "date": 1584598670296, - "logo": null, - "type": "Internet", - "terms": null - }, - "meta": { - "source": "subscribe", - "owner": "5e730e8e0b852a417aa49ceb" - }, - "processing_id": null - } - } -} -``` - -## Receipts Get All - -Example for receipts get all method: - -- Request - -```python -from pprint import pprint - -from payme.receipts.subscribe_receipts import PaymeSubscribeReceipts - - -rclient = PaymeSubscribeReceipts( - base_url="https://checkout.test.paycom.uz/api/", - paycom_id=$paycom_id, - paycom_key=$paycom_key -) - -resp = rclient.reciepts_get_all( - count=2, - _from=1636398000000, - to=1636398000000, - offset=0 -) - -pprint(resp) -``` - -- Response - -```json -{ - "jsonrpc": "2.0", - "id": 123, - "result": [ - { - "_id": "6311946bc4420cbf2712a247", - "create_time": 1662096491076, - "pay_time": 0, - "cancel_time": 0, - "state": 0, - "type": 2, - "external": false, - "operation": -1, - "category": null, - "error": null, - "description": "", - "detail": null, - "amount": 400000, - "currency": 860, - "commission": 0, - "account": [ - { - "name": "transaction", - "title": { - "ru": "Номер чека", - "uz": "Chek raqami" - }, - "value": 2325, - "main": true - } - ], - "card": null, - "merchant": { - "_id": "5e730e8e0b852a417aa49ceb", - "name": "test", - "organization": "ЧП «test test»", - "address": "", - "business_id": "5e730e740b852a417aa49cea", - "epos": { - "merchantId": "106600000050000", - "terminalId": "20660000" - }, - "date": 1584598670296, - "logo": null, - "type": { - "ru": "Internet", - "uz": "Internet" - }, - "terms": null - }, - "meta": { - "source": "subscribe", - "owner": "5e730e8e0b852a417aa49ceb" - }, - "processing_id": null - }, - { - "_id": "63119303c4420cbf2712a245", - "create_time": 1662096131667, - "pay_time": 0, - "cancel_time": 1662096182979, - "state": 50, - "type": 2, - "external": false, - "operation": -1, - "category": null, - "error": null, - "description": "", - "detail": null, - "amount": 400000, - "currency": 860, - "commission": 0, - "account": [ - { - "name": "transaction", - "title": { - "ru": "Номер чека", - "uz": "Chek raqami" - }, - "value": 2324, - "main": true - } - ], - "card": null, - "merchant": { - "_id": "5e730e8e0b852a417aa49ceb", - "name": "test", - "organization": "ЧП «test test»", - "address": "", - "business_id": "5e730e740b852a417aa49cea", - "epos": { - "merchantId": "106600000050000", - "terminalId": "20660000" - }, - "date": 1584598670296, - "logo": null, - "type": { - "ru": "Internet", - "uz": "Internet" - }, - "terms": null - }, - "meta": { - "source": "subscribe", - "owner": "5e730e8e0b852a417aa49ceb", - "source_cancel": "subscribe" - }, - "processing_id": null - } - ] -} +https://checkout.paycom.uz/bT15b3VyLXBheW1lLWlkO2FjLmlkPTEyMzQ1NjthPTUwMDAwMDtjPWh0dHBzOi8vZXhhbXBsZS5jb20= ``` diff --git a/docs/collections/postman/paytechuz-payme_postman.json b/docs/collections/postman/paytechuz-payme_postman.json deleted file mode 100644 index 4154905..0000000 --- a/docs/collections/postman/paytechuz-payme_postman.json +++ /dev/null @@ -1,928 +0,0 @@ -{ - "info": { - "_postman_id": "7a2b3ba8-d989-415f-b26a-cdbce1d52d01", - "name": "paytechuz-payme", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "description": "" - }, - "item": [ - { - "name": "Merchant-API", - "item": [ - { - "name": "create-transaction", - "request": { - "method": "POST", - "url": { - "raw": "http://localhost:8000/payments/merchant/", - "path": [ - "payments", - "merchant" - ], - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8000" - }, - "header": [ - { - "key": "AUTHORIZATION", - "value": "Basic XXX" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"jsonrpc\": \"2.0\",\n \"method\": \"CreateTransaction\",\n \"params\": {\n \"account\": {\n \"order_id\": \"4\"\n },\n \"amount\": 100,\n \"id\": \"64266c93432361b4e0342bdd\",\n \"time\": 1680239763901\n }\n}\n" - } - } - }, - { - "name": "incorrect-order", - "request": { - "method": "POST", - "url": { - "raw": "http://localhost:8000/payments/merchant/", - "path": [ - "payments", - "merchant" - ], - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8000" - }, - "header": [ - { - "key": "AUTHORIZATION", - "value": "Basic XXX" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"jsonrpc\": \"2.0\",\n \"id\": 67828,\n \"method\": \"CheckPerformTransaction\",\n \"params\": {\n \"amount\": 999999999,\n \"account\": {\n \"order_id\": \"999999999\"\n }\n }\n}\n" - } - } - }, - { - "name": "check-transaction", - "request": { - "method": "POST", - "url": { - "raw": "http://localhost:8000/payments/merchant/", - "path": [ - "payments", - "merchant" - ], - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8000" - }, - "header": [ - { - "key": "AUTHORIZATION", - "value": "Basic XXX" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"jsonrpc\": \"2.0\",\n \"method\": \"CheckTransaction\",\n \"params\": {\n \"id\": \"6346454fc67a522e0887022b\"\n }\n}\n" - } - } - }, - { - "name": "incorrect-auth", - "request": { - "method": "POST", - "url": { - "raw": "http://localhost:8000/payments/merchant/?AUTHORIZATION=Basic XXX", - "query": [ - { - "key": "AUTHORIZATION", - "value": "Basic XXX" - } - ], - "variable": [], - "path": [ - "payments", - "merchant" - ], - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8000" - }, - "header": [ - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "auth": { - "type": "noauth" - } - } - }, - { - "name": "perform-transaction ", - "request": { - "method": "POST", - "url": { - "raw": "http://localhost:8000/payments/merchant/", - "path": [ - "payments", - "merchant" - ], - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8000" - }, - "header": [ - { - "key": "AUTHORIZATION", - "value": "Basic XXX" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"jsonrpc\": \"2.0\",\n \"method\": \"CancelTransaction\",\n \"params\": {\n \"id\": \"64266c93432361b4e0342bdd\",\n \"reason\": 5\n }\n}" - } - } - }, - { - "name": "check-perform-transaction", - "request": { - "method": "POST", - "url": { - "raw": "http://localhost:8000/payments/merchant/", - "path": [ - "payments", - "merchant" - ], - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8000" - }, - "header": [ - { - "key": "AUTHORIZATION", - "value": "Basic XXX" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"jsonrpc\": \"2.0\",\n \"method\": \"CheckPerformTransaction\",\n \"params\": {\n \"amount\": 100,\n \"account\": {\n \"order_id\": \"3\"\n }\n }\n}" - } - } - }, - { - "name": "cancel-transaction", - "request": { - "method": "POST", - "url": { - "raw": "http://localhost:8000/payments/merchant/", - "path": [ - "payments", - "merchant" - ], - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8000" - }, - "header": [ - { - "key": "AUTHORIZATION", - "value": "Basic XXX" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"jsonrpc\": \"2.0\",\n \"method\": \"CheckPerformTransaction\",\n \"params\": {\n \"amount\": 100,\n \"account\": {\n \"order_id\": \"3\"\n }\n }\n}" - } - } - }, - { - "name": "get-statement", - "request": { - "method": "POST", - "url": { - "raw": "http://localhost:8000/payments/merchant/", - "path": [ - "payments", - "merchant" - ], - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8000" - }, - "header": [ - { - "key": "AUTHORIZATION", - "value": "Basic XXX" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"method\" : \"GetStatement\",\n \"params\" : {\n \"from\" : 1666462755066,\n \"to\" : 1690672447727\n }\n}" - } - } - } - ] - }, - { - "name": "cards_create", - "request": { - "method": "POST", - "url": { - "raw": "https://checkout.test.paycom.uz/api/", - "path": [ - "api" - ], - "protocol": "https", - "host": [ - "checkout", - "test", - "paycom", - "uz" - ] - }, - "header": [ - { - "key": "X-Auth", - "value": "5e730e8e0b852a417aa49ceb" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"cards.create\",\n \"params\": {\"card\": {\"number\": \"8600495473316478\",\"expire\": \"0399\"},\n \"save\": true\n }\n}" - } - } - }, - { - "name": "cards_get_veriy_code", - "request": { - "method": "POST", - "url": { - "raw": "https://checkout.test.paycom.uz/api/", - "path": [ - "api" - ], - "protocol": "https", - "host": [ - "checkout", - "test", - "paycom", - "uz" - ] - }, - "header": [ - { - "key": "X-Auth", - "value": "$payme_id" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"id\": \"paytechyz\",\n \"method\": \"cards.get_verify_code\",\n \"params\": {\n \"token\": \"63468a065a046a41490e664e_GQdx3ewoSpAIUieZ55EVBT9o3XauKiITgDjIzMhdsTBho8ctGV8sKXE7M47uyNMSGt4b9hMvcKMssNvs74UV03J7TZ6QIHfR5z8regqQjaS58x0ABm91TNpH07rd7TGxsoHN4WEs1iIr2W9MUUHCAyZePsBmcuYfvbOBbfeV4xcXh9kTsPEwV768KjEjtCjokNeER1i0pbJrnCoGoRyAr37ZaYsu28IOBQzzzGCThoVxIn3uM6pQFeW1xuDKD5cMRk9tQNc2zJWaoHQ6eHTR7EadOkUEJXIYgyeYBXkiYBN1pW58Msb89Kt4tMDhJKanCwQbBo4eiUdxWtI8xRfksXdgGDjqvCNKrsciw5MSrDdcRqupo61WvXD0d4IianWyTEfS5p\"\n }\n}" - } - } - }, - { - "name": "cards_verify", - "request": { - "method": "POST", - "url": { - "raw": "https://checkout.test.paycom.uz/api/", - "path": [ - "api" - ], - "protocol": "https", - "host": [ - "checkout", - "test", - "paycom", - "uz" - ] - }, - "header": [ - { - "key": "X-Auth", - "value": "$payme_id" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"cards.verify\",\n \"params\": {\n \"token\": \"63317a19d15d8d8d093b3b73_sF6OYNgJ5zXd2BXNpEZgDZJQVyPnaVAQY7ziO6nDx11ZyX67gCTQxspvWN2ZMGaWfKG8YuKsgoRecgN3IrnYhQxubavr7Z8zreOAXF4GRJW8M2gNfF4RnCu2cYCYzQ2dIRRvp3uS5b2B48aVsyQGwo0v3tQqNAdOfYskgiwbChNObvo4TnzewRYaGZbpqfEiN78anY03EMShjU6mRh0YouIRENr4VCXd4eBAoAVWAas8ZikYHYh0aTBFMproAKx2Pf2ZYuiBsyO2f2cTYpuu1kRciHJZ2ZEN5TRupJKgg06pwxBK5pu5dsOXomQWNGCvOetsahOPmX0nU25E5cEfGHfRDMIUWOjbR2MbginPdP9BXS50Q2Q1DOQO0pHSmf9nRvXKTm\",\n \"code\": \"666666\"\n }\n}" - } - } - }, - { - "name": "cards_remove", - "request": { - "method": "POST", - "url": { - "raw": "https://checkout.test.paycom.uz/api/", - "path": [ - "api" - ], - "protocol": "https", - "host": [ - "checkout", - "test", - "paycom", - "uz" - ] - }, - "header": [ - { - "key": "X-Auth", - "value": "$payme_id" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"cards.remove\",\n \"params\": {\n \"token\": \"63119afed15d8d8d093b37bf_FZKC9EDMu7Ke7wPvEoYpMgyA1wFDWynDewB9AQSkYk6ic2isbQdYatJuVN1GknSmhEwftPxVZ1wZPw6TzJc4BYqWRmPyV3eyqJz4cK9D91aEkZ5TNBkMnhxxgXefDryHGYdpSqUuMV914QwpcA5e8cB5yqIaCp8P6W8FBeY6vKcNIfSSBaDniMZfsKV9vSyroqupKWfYcPHig2m5KyN5aBWU12f5yBtpOt6IRwbXoDre3BBVMfTKrXesQfghcrs4bUgOmcPoNDGXh8nxEkucFSNmc5EymAZZ56hSnXpYgtv4QhGxNiX3tSsk6raRMWstJNqMBb4vQABs54Zd8IohboSAGZW3M87CRiVmQEDKwvrC8y4aaNmfV33q03EppQnaJArZDx\"\n }\n}" - } - } - }, - { - "name": "cards_check", - "request": { - "method": "POST", - "url": { - "raw": "https://checkout.test.paycom.uz/api/", - "path": [ - "api" - ], - "protocol": "https", - "host": [ - "checkout", - "test", - "paycom", - "uz" - ] - }, - "header": [ - { - "key": "X-Auth", - "value": "$payme_id" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"id\": 123,\n \"method\": \"cards.check\",\n \"params\": {\n \"token\": \"6321688682dcb3b90def1ff5_JMry9v8Ko0SAT8D5HGHZfCxi5RFHIudX9adt6ZqWD3uQgy9VTYGovP9ceIoXOq4KjPe9pyCm33cfGAUJ2WZgrThXRqnNzYrzvKo34fyH3UrEde9kaeqtk79sz4ZN3RNXHn6PwknEqONVi9yhS0Uv16OgY7wsiIfCZufayiwfYAKevCngcprYsZrJNpnheUjS56hnFyaRMR4KaPnEXCJXZkdxuiICk2m8BipM5hYhmfopQImik4j5GOZucTmUj3Ez3vGzwrwM3Z4sEBpmBJpaiaIahbncCosCEG11RDqHWj9jWOJrR3ipc7i0xCHM2zRIq1ICG9nO78rZvQ9v1FRQAuef0fUODQUmcOYcGAuKsdfKFrBwo5rxEHKTVpzuZDGN2zMS5J\"\n }\n}" - } - } - }, - { - "name": "receipts_create", - "request": { - "method": "POST", - "url": { - "raw": "https://checkout.test.paycom.uz/api/", - "path": [ - "api" - ], - "protocol": "https", - "host": [ - "checkout", - "test", - "paycom", - "uz" - ] - }, - "header": [ - { - "key": "X-Auth", - "value": "$payme_id" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.create\",\n \"params\": {\n \"amount\": 400000,\n \"account\": {\n \"order_id\": 106\n }\n }\n}" - } - } - }, - { - "name": "receipts_pay", - "request": { - "method": "POST", - "url": { - "raw": "https://checkout.test.paycom.uz/api/", - "path": [ - "api" - ], - "protocol": "https", - "host": [ - "checkout", - "test", - "paycom", - "uz" - ] - }, - "header": [ - { - "key": "X-Auth", - "value": "$payme_id:$payme_key" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.pay\",\n \"params\": {\n \"id\": \"63119becc4420cbf2712a24c\",\n \"token\": \"64232286890565aa4e37c2b0_TnOzC2HkPKJu6ndDeHKdyR4cQU6aIvauqqVqKmmmMFMrNTH0sByAQuPpOUTaPswth0Bxj9VKYDUNsgBhRIgIQydvH8C1iyca0wJING9kNvFemDEWUUutbBW5e1Fjh8VxYNak7Qm6Yac1tty6qgDxHnTfFMzfw5Svrq3fM8RjCr5F7IDgySRi02CJiXyNwCZxnhZtgmmimhApsXDySxyFjW1hDERh2hvZYEZMWyohnX6yaadKpOFv0QiOSWYyrMXPa1HfMmpFFaiCzjX6eOBFJJZhuJJsGxqxxgQBeuRBUOtnSAnrmzxj8j1sOEAMnIISjpZo3wABui2h7KpwIEWg9cmsuRHzYiw3NZFPKmWu5Q7WAYXDcuCBx7X5XwtJFIdUo559iN\",\n \"payer\": {\n \"phone\": \"998901304527\"\n }\n }\n}" - } - } - }, - { - "name": "receipts_send", - "request": { - "method": "POST", - "url": { - "raw": "https://checkout.test.paycom.uz/api/", - "path": [ - "api" - ], - "protocol": "https", - "host": [ - "checkout", - "test", - "paycom", - "uz" - ] - }, - "header": [ - { - "key": "X-Auth", - "value": "$payme_id:$payme_key" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.send\",\n \"params\": {\n \"id\": \"63119becc4420cbf2712a24c\",\n \"phone\": \"998901304527\"\n }\n}" - } - } - }, - { - "name": "receipts_cancel", - "request": { - "method": "POST", - "url": { - "raw": "https://checkout.test.paycom.uz/api/", - "path": [ - "api" - ], - "protocol": "https", - "host": [ - "checkout", - "test", - "paycom", - "uz" - ] - }, - "header": [ - { - "key": "X-Auth", - "value": "$payme_id:$payme_key" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.cancel\",\n \"params\": {\n \"id\": \"63119becc4420cbf2712a24c\"\n }\n}" - } - } - }, - { - "name": "receipts_check", - "request": { - "method": "POST", - "url": { - "raw": "https://checkout.test.paycom.uz/api/", - "path": [ - "api" - ], - "protocol": "https", - "host": [ - "checkout", - "test", - "paycom", - "uz" - ] - }, - "header": [ - { - "key": "X-Auth", - "value": "$payme_id:$payme_key" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.check\",\n \"params\": {\n \"id\": \"635aa3295f103c97ef1c7606\"\n }\n}" - } - } - }, - { - "name": "reciepts_get", - "request": { - "method": "POST", - "url": { - "raw": "https://checkout.test.paycom.uz/api/", - "path": [ - "api" - ], - "protocol": "https", - "host": [ - "checkout", - "test", - "paycom", - "uz" - ] - }, - "header": [ - { - "key": "X-Auth", - "value": "$payme_id:$payme_key" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.get\",\n \"params\": {\n \"id\": \"6311946bc4420cbf2712a247\"\n }\n}" - } - } - }, - { - "name": "reciepts_get_all", - "request": { - "method": "POST", - "url": { - "raw": "https://checkout.test.paycom.uz/api/", - "path": [ - "api" - ], - "protocol": "https", - "host": [ - "checkout", - "test", - "paycom", - "uz" - ] - }, - "header": [ - { - "key": "X-Auth", - "value": "$payme_id:$payme_key" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.get_all\",\n \"params\": {\n \"count\": 2,\n \"from\": 1612640000,\n \"to\": 1612726400,\n \"offset\": 0\n }\n}" - } - } - }, - { - "name": "get_fiskal_data", - "request": { - "method": "POST", - "url": { - "raw": "https://checkout.test.paycom.uz/api/", - "path": [ - "api" - ], - "protocol": "https", - "host": [ - "checkout", - "test", - "paycom", - "uz" - ] - }, - "header": [ - { - "key": "X-Auth", - "value": "$payme_id:$payme_key" - }, - { - "key": "Accept", - "value": "*/*", - "disabled": true - }, - { - "key": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.set_fiscal_data\",\n \"params\": {\n \"id\": 1,\n \"fiscal_data\": {\n \"status_code\": 1,\n \"message\": 1,\n \"terminal_id\": 1,\n \"receipt_id\": 1,\n \"date\": 1,\n \"fiscal_sign\": 1,\n \"qr_code_url\": 1\n }\n }\n}" - } - } - } - ] -} \ No newline at end of file diff --git a/docs/collections/thunder/paytechuz-payme_thunder.json b/docs/collections/thunder/paytechuz-payme_thunder.json deleted file mode 100644 index c93cd1a..0000000 --- a/docs/collections/thunder/paytechuz-payme_thunder.json +++ /dev/null @@ -1,741 +0,0 @@ -{ - "client": "Thunder Client", - "collectionName": "paytechuz-payme", - "dateExported": "2023-08-04T06:43:15.236Z", - "version": "1.1", - "folders": [ - { - "_id": "41aa7417-7725-4863-9baa-b3fa3e069fc6", - "name": "Merchant-API", - "containerId": "", - "created": "2023-08-04T06:35:53.675Z", - "sortNum": 10000, - "settings": {} - } - ], - "requests": [ - { - "_id": "ada2fbab-a260-41f0-b689-505d3c531edd", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "41aa7417-7725-4863-9baa-b3fa3e069fc6", - "name": "create-transaction", - "url": "http://localhost:8000/payments/merchant/", - "method": "POST", - "sortNum": 10000, - "created": "2023-08-04T06:35:53.676Z", - "modified": "2023-08-04T06:40:26.964Z", - "headers": [ - { - "name": "AUTHORIZATION", - "value": "Basic XXX" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"jsonrpc\": \"2.0\",\n \"method\": \"CreateTransaction\",\n \"params\": {\n \"account\": {\n \"order_id\": \"4\"\n },\n \"amount\": 100,\n \"id\": \"64266c93432361b4e0342bdd\",\n \"time\": 1680239763901\n }\n}\n", - "form": [] - }, - "tests": [] - }, - { - "_id": "05282e20-e6cb-45a2-8810-aa896d0a64a8", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "41aa7417-7725-4863-9baa-b3fa3e069fc6", - "name": "incorrect-order", - "url": "http://localhost:8000/payments/merchant/", - "method": "POST", - "sortNum": 15000, - "created": "2023-08-04T06:35:53.677Z", - "modified": "2023-08-04T06:40:19.588Z", - "headers": [ - { - "name": "AUTHORIZATION", - "value": "Basic XXX" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"jsonrpc\": \"2.0\",\n \"id\": 67828,\n \"method\": \"CheckPerformTransaction\",\n \"params\": {\n \"amount\": 999999999,\n \"account\": {\n \"order_id\": \"999999999\"\n }\n }\n}\n", - "form": [] - }, - "tests": [] - }, - { - "_id": "032d6b97-ed12-410e-82ab-b580b343238a", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "41aa7417-7725-4863-9baa-b3fa3e069fc6", - "name": "check-transaction", - "url": "http://localhost:8000/payments/merchant/", - "method": "POST", - "sortNum": 20000, - "created": "2023-08-04T06:35:53.678Z", - "modified": "2023-08-04T06:40:45.730Z", - "headers": [ - { - "name": "AUTHORIZATION", - "value": "Basic XXX" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"jsonrpc\": \"2.0\",\n \"method\": \"CheckTransaction\",\n \"params\": {\n \"id\": \"6346454fc67a522e0887022b\"\n }\n}\n", - "form": [] - }, - "tests": [] - }, - { - "_id": "9e8fe1f2-b827-41a4-98e1-428227ae7379", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "41aa7417-7725-4863-9baa-b3fa3e069fc6", - "name": "incorrect-auth", - "url": "http://localhost:8000/payments/merchant/?AUTHORIZATION=Basic XXX", - "method": "POST", - "sortNum": 30000, - "created": "2023-08-04T06:35:53.679Z", - "modified": "2023-08-04T06:35:53.679Z", - "headers": [ - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [ - { - "name": "AUTHORIZATION", - "value": "Basic XXX", - "isPath": false - } - ], - "auth": { - "type": "none" - }, - "tests": [] - }, - { - "_id": "316f7d8f-d77e-45bd-9679-1e35de792616", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "41aa7417-7725-4863-9baa-b3fa3e069fc6", - "name": "perform-transaction ", - "url": "http://localhost:8000/payments/merchant/", - "method": "POST", - "sortNum": 40000, - "created": "2023-08-04T06:35:53.680Z", - "modified": "2023-08-04T06:41:04.798Z", - "headers": [ - { - "name": "AUTHORIZATION", - "value": "Basic XXX" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"jsonrpc\": \"2.0\",\n \"method\": \"CancelTransaction\",\n \"params\": {\n \"id\": \"64266c93432361b4e0342bdd\",\n \"reason\": 5\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "245e1fca-4f9f-4c1f-855f-f8dff59be4dd", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "", - "name": "cards_create", - "url": "https://checkout.test.paycom.uz/api/", - "method": "POST", - "sortNum": 50000, - "created": "2023-08-04T06:35:53.681Z", - "modified": "2023-08-04T06:35:53.681Z", - "headers": [ - { - "name": "X-Auth", - "value": "5e730e8e0b852a417aa49ceb" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"cards.create\",\n \"params\": {\"card\": {\"number\": \"8600495473316478\",\"expire\": \"0399\"},\n \"save\": true\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "7c2ec61a-233f-4575-abec-773671505179", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "", - "name": "cards_get_veriy_code", - "url": "https://checkout.test.paycom.uz/api/", - "method": "POST", - "sortNum": 60000, - "created": "2023-08-04T06:35:53.682Z", - "modified": "2023-08-04T06:37:20.662Z", - "headers": [ - { - "name": "X-Auth", - "value": "$payme_id" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"id\": \"paytechyz\",\n \"method\": \"cards.get_verify_code\",\n \"params\": {\n \"token\": \"63468a065a046a41490e664e_GQdx3ewoSpAIUieZ55EVBT9o3XauKiITgDjIzMhdsTBho8ctGV8sKXE7M47uyNMSGt4b9hMvcKMssNvs74UV03J7TZ6QIHfR5z8regqQjaS58x0ABm91TNpH07rd7TGxsoHN4WEs1iIr2W9MUUHCAyZePsBmcuYfvbOBbfeV4xcXh9kTsPEwV768KjEjtCjokNeER1i0pbJrnCoGoRyAr37ZaYsu28IOBQzzzGCThoVxIn3uM6pQFeW1xuDKD5cMRk9tQNc2zJWaoHQ6eHTR7EadOkUEJXIYgyeYBXkiYBN1pW58Msb89Kt4tMDhJKanCwQbBo4eiUdxWtI8xRfksXdgGDjqvCNKrsciw5MSrDdcRqupo61WvXD0d4IianWyTEfS5p\"\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "0d4a3e29-573a-4efb-b859-4e844ec2fdb3", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "", - "name": "cards_verify", - "url": "https://checkout.test.paycom.uz/api/", - "method": "POST", - "sortNum": 70000, - "created": "2023-08-04T06:35:53.683Z", - "modified": "2023-08-04T06:37:31.620Z", - "headers": [ - { - "name": "X-Auth", - "value": "$payme_id" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"cards.verify\",\n \"params\": {\n \"token\": \"63317a19d15d8d8d093b3b73_sF6OYNgJ5zXd2BXNpEZgDZJQVyPnaVAQY7ziO6nDx11ZyX67gCTQxspvWN2ZMGaWfKG8YuKsgoRecgN3IrnYhQxubavr7Z8zreOAXF4GRJW8M2gNfF4RnCu2cYCYzQ2dIRRvp3uS5b2B48aVsyQGwo0v3tQqNAdOfYskgiwbChNObvo4TnzewRYaGZbpqfEiN78anY03EMShjU6mRh0YouIRENr4VCXd4eBAoAVWAas8ZikYHYh0aTBFMproAKx2Pf2ZYuiBsyO2f2cTYpuu1kRciHJZ2ZEN5TRupJKgg06pwxBK5pu5dsOXomQWNGCvOetsahOPmX0nU25E5cEfGHfRDMIUWOjbR2MbginPdP9BXS50Q2Q1DOQO0pHSmf9nRvXKTm\",\n \"code\": \"666666\"\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "1422768f-ccf2-4585-91cb-c6d83bfb2a17", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "", - "name": "cards_remove", - "url": "https://checkout.test.paycom.uz/api/", - "method": "POST", - "sortNum": 80000, - "created": "2023-08-04T06:35:53.684Z", - "modified": "2023-08-04T06:37:40.613Z", - "headers": [ - { - "name": "X-Auth", - "value": "$payme_id" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"cards.remove\",\n \"params\": {\n \"token\": \"63119afed15d8d8d093b37bf_FZKC9EDMu7Ke7wPvEoYpMgyA1wFDWynDewB9AQSkYk6ic2isbQdYatJuVN1GknSmhEwftPxVZ1wZPw6TzJc4BYqWRmPyV3eyqJz4cK9D91aEkZ5TNBkMnhxxgXefDryHGYdpSqUuMV914QwpcA5e8cB5yqIaCp8P6W8FBeY6vKcNIfSSBaDniMZfsKV9vSyroqupKWfYcPHig2m5KyN5aBWU12f5yBtpOt6IRwbXoDre3BBVMfTKrXesQfghcrs4bUgOmcPoNDGXh8nxEkucFSNmc5EymAZZ56hSnXpYgtv4QhGxNiX3tSsk6raRMWstJNqMBb4vQABs54Zd8IohboSAGZW3M87CRiVmQEDKwvrC8y4aaNmfV33q03EppQnaJArZDx\"\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "963b9526-893e-4ff4-9543-6d841afc6a15", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "", - "name": "cards_check", - "url": "https://checkout.test.paycom.uz/api/", - "method": "POST", - "sortNum": 90000, - "created": "2023-08-04T06:35:53.685Z", - "modified": "2023-08-04T06:37:50.007Z", - "headers": [ - { - "name": "X-Auth", - "value": "$payme_id" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"id\": 123,\n \"method\": \"cards.check\",\n \"params\": {\n \"token\": \"6321688682dcb3b90def1ff5_JMry9v8Ko0SAT8D5HGHZfCxi5RFHIudX9adt6ZqWD3uQgy9VTYGovP9ceIoXOq4KjPe9pyCm33cfGAUJ2WZgrThXRqnNzYrzvKo34fyH3UrEde9kaeqtk79sz4ZN3RNXHn6PwknEqONVi9yhS0Uv16OgY7wsiIfCZufayiwfYAKevCngcprYsZrJNpnheUjS56hnFyaRMR4KaPnEXCJXZkdxuiICk2m8BipM5hYhmfopQImik4j5GOZucTmUj3Ez3vGzwrwM3Z4sEBpmBJpaiaIahbncCosCEG11RDqHWj9jWOJrR3ipc7i0xCHM2zRIq1ICG9nO78rZvQ9v1FRQAuef0fUODQUmcOYcGAuKsdfKFrBwo5rxEHKTVpzuZDGN2zMS5J\"\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "86e70250-4947-4770-9a90-b4ed583c7f50", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "", - "name": "receipts_create", - "url": "https://checkout.test.paycom.uz/api/", - "method": "POST", - "sortNum": 100000, - "created": "2023-08-04T06:35:53.686Z", - "modified": "2023-08-04T06:37:59.590Z", - "headers": [ - { - "name": "X-Auth", - "value": "$payme_id" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.create\",\n \"params\": {\n \"amount\": 400000,\n \"account\": {\n \"order_id\": 106\n }\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "a4eed948-ab23-4b9f-8693-4d59a35e0ac2", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "", - "name": "receipts_pay", - "url": "https://checkout.test.paycom.uz/api/", - "method": "POST", - "sortNum": 120000, - "created": "2023-08-04T06:35:53.688Z", - "modified": "2023-08-04T06:38:33.561Z", - "headers": [ - { - "name": "X-Auth", - "value": "$payme_id:$payme_key" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.pay\",\n \"params\": {\n \"id\": \"63119becc4420cbf2712a24c\",\n \"token\": \"64232286890565aa4e37c2b0_TnOzC2HkPKJu6ndDeHKdyR4cQU6aIvauqqVqKmmmMFMrNTH0sByAQuPpOUTaPswth0Bxj9VKYDUNsgBhRIgIQydvH8C1iyca0wJING9kNvFemDEWUUutbBW5e1Fjh8VxYNak7Qm6Yac1tty6qgDxHnTfFMzfw5Svrq3fM8RjCr5F7IDgySRi02CJiXyNwCZxnhZtgmmimhApsXDySxyFjW1hDERh2hvZYEZMWyohnX6yaadKpOFv0QiOSWYyrMXPa1HfMmpFFaiCzjX6eOBFJJZhuJJsGxqxxgQBeuRBUOtnSAnrmzxj8j1sOEAMnIISjpZo3wABui2h7KpwIEWg9cmsuRHzYiw3NZFPKmWu5Q7WAYXDcuCBx7X5XwtJFIdUo559iN\",\n \"payer\": {\n \"phone\": \"998901304527\"\n }\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "6762ba18-7042-4f51-b6de-4db048d21ef6", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "", - "name": "receipts_send", - "url": "https://checkout.test.paycom.uz/api/", - "method": "POST", - "sortNum": 130000, - "created": "2023-08-04T06:35:53.689Z", - "modified": "2023-08-04T06:38:45.337Z", - "headers": [ - { - "name": "X-Auth", - "value": "$payme_id:$payme_key" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.send\",\n \"params\": {\n \"id\": \"63119becc4420cbf2712a24c\",\n \"phone\": \"998901304527\"\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "eb2be749-6f9f-4e9e-a15c-9da61e0b56fc", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "", - "name": "receipts_cancel", - "url": "https://checkout.test.paycom.uz/api/", - "method": "POST", - "sortNum": 140000, - "created": "2023-08-04T06:35:53.690Z", - "modified": "2023-08-04T06:38:55.408Z", - "headers": [ - { - "name": "X-Auth", - "value": "$payme_id:$payme_key" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.cancel\",\n \"params\": {\n \"id\": \"63119becc4420cbf2712a24c\"\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "5b920c33-0fff-4c3c-ad64-5b45c3437bbf", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "", - "name": "receipts_check", - "url": "https://checkout.test.paycom.uz/api/", - "method": "POST", - "sortNum": 150000, - "created": "2023-08-04T06:35:53.691Z", - "modified": "2023-08-04T06:39:09.282Z", - "headers": [ - { - "name": "X-Auth", - "value": "$payme_id:$payme_key" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.check\",\n \"params\": {\n \"id\": \"635aa3295f103c97ef1c7606\"\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "37802143-30de-493f-a439-3b2e35c397c7", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "", - "name": "reciepts_get", - "url": "https://checkout.test.paycom.uz/api/", - "method": "POST", - "sortNum": 160000, - "created": "2023-08-04T06:35:53.692Z", - "modified": "2023-08-04T06:39:20.085Z", - "headers": [ - { - "name": "X-Auth", - "value": "$payme_id:$payme_key" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.get\",\n \"params\": {\n \"id\": \"6311946bc4420cbf2712a247\"\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "8fde8e80-d6e7-4020-961a-e4e0b6fbabd9", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "", - "name": "reciepts_get_all", - "url": "https://checkout.test.paycom.uz/api/", - "method": "POST", - "sortNum": 170000, - "created": "2023-08-04T06:35:53.693Z", - "modified": "2023-08-04T06:39:31.013Z", - "headers": [ - { - "name": "X-Auth", - "value": "$payme_id:$payme_key" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.get_all\",\n \"params\": {\n \"count\": 2,\n \"from\": 1612640000,\n \"to\": 1612726400,\n \"offset\": 0\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "228215da-5087-4c3d-b694-8e3f02516a38", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "", - "name": "get_fiskal_data", - "url": "https://checkout.test.paycom.uz/api/", - "method": "POST", - "sortNum": 190000, - "created": "2023-08-04T06:35:53.694Z", - "modified": "2023-08-04T06:39:38.314Z", - "headers": [ - { - "name": "X-Auth", - "value": "$payme_id:$payme_key" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"id\": \"paytechuz\",\n \"method\": \"receipts.set_fiscal_data\",\n \"params\": {\n \"id\": 1,\n \"fiscal_data\": {\n \"status_code\": 1,\n \"message\": 1,\n \"terminal_id\": 1,\n \"receipt_id\": 1,\n \"date\": 1,\n \"fiscal_sign\": 1,\n \"qr_code_url\": 1\n }\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "c8d1791b-75a5-4287-aab2-abe656eba1f1", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "41aa7417-7725-4863-9baa-b3fa3e069fc6", - "name": "check-perform-transaction", - "url": "http://localhost:8000/payments/merchant/", - "method": "POST", - "sortNum": 210000, - "created": "2023-08-04T06:35:53.695Z", - "modified": "2023-08-04T06:41:24.064Z", - "headers": [ - { - "name": "AUTHORIZATION", - "value": "Basic XXX" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"jsonrpc\": \"2.0\",\n \"method\": \"CheckPerformTransaction\",\n \"params\": {\n \"amount\": 100,\n \"account\": {\n \"order_id\": \"3\"\n }\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "909d337d-dad1-4673-bc83-48fc31c0dac5", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "41aa7417-7725-4863-9baa-b3fa3e069fc6", - "name": "cancel-transaction", - "url": "http://localhost:8000/payments/merchant/", - "method": "POST", - "sortNum": 220000, - "created": "2023-08-04T06:35:53.696Z", - "modified": "2023-08-04T06:41:36.355Z", - "headers": [ - { - "name": "AUTHORIZATION", - "value": "Basic XXX" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"jsonrpc\": \"2.0\",\n \"method\": \"CheckPerformTransaction\",\n \"params\": {\n \"amount\": 100,\n \"account\": {\n \"order_id\": \"3\"\n }\n }\n}", - "form": [] - }, - "tests": [] - }, - { - "_id": "eafb09b0-43b8-4e28-aa44-5cc39c631a8b", - "colId": "aa8793c8-dbc4-44d0-844a-fe6a53e53f61", - "containerId": "41aa7417-7725-4863-9baa-b3fa3e069fc6", - "name": "get-statement", - "url": "http://localhost:8000/payments/merchant/", - "method": "POST", - "sortNum": 230000, - "created": "2023-08-04T06:35:53.697Z", - "modified": "2023-08-04T06:41:52.831Z", - "headers": [ - { - "name": "AUTHORIZATION", - "value": "Basic XXX" - }, - { - "name": "Accept", - "value": "*/*", - "isDisabled": true - }, - { - "name": "User-Agent", - "value": "Thunder Client (https://www.thunderclient.com)", - "isDisabled": true - } - ], - "params": [], - "body": { - "type": "json", - "raw": "{\n \"method\" : \"GetStatement\",\n \"params\" : {\n \"from\" : 1666462755066,\n \"to\" : 1690672447727\n }\n}", - "form": [] - }, - "tests": [] - } - ], - "settings": { - "headers": [ - { - "name": "Authorization", - "value": "Basic UGF5Y29tOnlISTNSQTFSTiZINWYwU3ZjcnhAdnE5bXVOc21IVW80OWRUdg==" - } - ], - "tests": [] - } -} \ No newline at end of file diff --git a/lib/payme/admin.py b/lib/payme/admin.py deleted file mode 100644 index c5cf4b1..0000000 --- a/lib/payme/admin.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.contrib import admin - -from payme.models import CUSTOM_ORDER -from payme.models import Order as DefaultOrderModel - -from payme.models import MerchatTransactionsModel - -if not CUSTOM_ORDER: - admin.site.register(DefaultOrderModel) - -admin.site.register(MerchatTransactionsModel) diff --git a/lib/payme/cards/__init__.py b/lib/payme/cards/__init__.py deleted file mode 100644 index c11d497..0000000 --- a/lib/payme/cards/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .import subscribe_cards diff --git a/lib/payme/cards/subscribe_cards.py b/lib/payme/cards/subscribe_cards.py deleted file mode 100644 index e748ffa..0000000 --- a/lib/payme/cards/subscribe_cards.py +++ /dev/null @@ -1,166 +0,0 @@ -from payme.utils.to_json import to_json -from payme.decorators.decorators import payme_request - - -class PaymeSubscribeCards: - """ - The PaymeSubscribeCards class inclues - all paycom methods which are belongs to cards. - - Parameters - ---------- - base_url: str — The base url of the paycom api - paycom_id: str — The paycom_id uses to identify - timeout: int — How many seconds to wait for the server to send data - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-subscribe-api/ - """ - def __init__( - self, - base_url: str, - paycom_id: str, - timeout=5 - ) -> "PaymeSubscribeCards": - self.base_url: str = base_url - self.timeout: int = timeout - self.headers: dict = { - "X-Auth": paycom_id, - } - self.__methods: dict = { - "cards_check": "cards.check", - "cards_create": "cards.create", - "cards_remove": "cards.remove", - "cards_verify": "cards.verify", - "cards_get_verify_code": "cards.get_verify_code", - } - - @payme_request - def __request(self, data) -> dict: - """ - Use this private method to request. - On success,response will be OK with format JSON. - - Parameters - ---------- - data: dict — Includes request data. - - Returns dictionary Payme Response - --------------------------------- - """ - return data - - def cards_create(self, number: str, expire: str, save: bool = True) -> dict: - """ - Use this method to create a new card's token. - - Parameters - ---------- - number: str — The card number maximum length 18 char - expire: str — The card expiration string maximum length 5 char - save: bool \ - Type of token. Optional parameter - The option is enabled or disabled depending on the application's business logic - If the flag is true, the token can be used for further payments - if the flag is false the token can only be used once - The one-time token is deleted after payment - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-subscribe-api/cards.create - """ - data: dict = { - "method": self.__methods.get("cards_create"), - "params": { - "card": { - "number": number, - "expire": expire, - }, - "save": save, - } - } - return self.__request(to_json(**data)) - - def card_get_verify_code(self, token: str) -> dict: - """ - Use this method to get the verification code. - - Parameters - ---------- - token: str — The card's non-active token - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-subscribe-api/cards.get_verify_code - """ - data: dict = { - "method": self.__methods.get('cards_get_verify_code'), - "params": { - "token": token, - } - } - return self.__request(to_json(**data)) - - def cards_verify(self, verify_code: int, token: str) -> dict: - """ - Verification of the card using the code sent via SMS. - - Parameters - ---------- - verify_code: int — Code for verification - token: str — The card's non-active token - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-subscribe-api/cards.verify - """ - data: dict = { - "method": self.__methods.get("cards_verify"), - "params": { - "token": token, - "code": verify_code - } - } - return self.__request(to_json(**data)) - - def cards_check(self, token: str) -> dict: - """ - Checking the card token active or non-active. - - Parameters - ---------- - token: str — The card's non-active token - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-subscribe-api/cards.check - """ - data: dict = { - "method": self.__methods.get("cards_check"), - "params": { - "token": token, - } - } - - return self.__request(to_json(**data)) - - def cards_remove(self, token: str) -> dict: - """ - Delete card's token on success returns success. - - Parameters - ---------- - token: str — The card's non-active token - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-subscribe-api/cards.remove - """ - data: dict = { - "method": self.__methods.get("cards_remove"), - "params": { - "token": token, - } - } - return self.__request(to_json(**data)) diff --git a/lib/payme/decorators/decorators.py b/lib/payme/decorators/decorators.py deleted file mode 100644 index b035333..0000000 --- a/lib/payme/decorators/decorators.py +++ /dev/null @@ -1,34 +0,0 @@ -import functools - -from requests import request -from requests.exceptions import Timeout -from requests.exceptions import RequestException - -from payme.utils.logging import logger - -from ..errors.exceptions import PaymeTimeoutException - - -def payme_request(func): - """ - Payme request decorator. - """ - @functools.wraps(func) - def wrapper(self, data): - response = None - req_data = { - "method": "POST", - "url": self.base_url, - "data": data, - "headers": self.headers, - "timeout": self.timeout, - } - try: - response = request(**req_data) - response.raise_for_status() - except (Timeout, RequestException) as error: - logger.info("Payme request has been failed as error: %s", error) - raise PaymeTimeoutException() from error - return response.json() - - return wrapper diff --git a/lib/payme/errors/exceptions.py b/lib/payme/errors/exceptions.py deleted file mode 100644 index feb2e78..0000000 --- a/lib/payme/errors/exceptions.py +++ /dev/null @@ -1,89 +0,0 @@ -from rest_framework.exceptions import APIException - - -class BasePaymeException(APIException): - """ - BasePaymeException it's APIException. - """ - status_code = 200 - error_code = None - message = None - - # pylint: disable=super-init-not-called - def __init__(self, error_message: str = None): - detail: dict = { - "error": { - "code": self.error_code, - "message": self.message, - "data": error_message - } - } - self.detail = detail - - -class PermissionDenied(BasePaymeException): - """ - PermissionDenied APIException \ - That is raised when the client is not allowed to server. - """ - status_code = 200 - error_code = -32504 - message = "Permission denied" - - -class MethodNotFound(BasePaymeException): - """ - MethodNotFound APIException \ - That is raised when the method does not exist. - """ - status_code = 405 - error_code = -32601 - message = 'Method not found' - - -class TooManyRequests(BasePaymeException): - """ - TooManyRequests APIException \ - That is raised when the request exceeds the limit. - """ - status_code = 200 - error_code = -31099 - message = { - "uz": "Buyurtma tolovni amalga oshirish jarayonida", - "ru": "Транзакция в очереди", - "en": "Order payment status is queued" - } - - -class IncorrectAmount(BasePaymeException): - """ - IncorrectAmount APIException \ - That is raised when the amount is not incorrect. - """ - status_code = 200 - error_code = -31001 - message = { - 'ru': 'Неверная сумма', - 'uz': 'Incorrect amount', - 'en': 'Incorrect amount', - } - - -class PerformTransactionDoesNotExist(BasePaymeException): - """ - PerformTransactionDoesNotExist APIException \ - That is raised when a transaction does not exist or deleted. - """ - status_code = 200 - error_code = -31050 - message = { - "uz": "Buyurtma topilmadi", - "ru": "Заказ не существует", - "en": "Order does not exists" - } - - -class PaymeTimeoutException(Exception): - """ - Payme timeout exception that means that payme is working slowly. - """ diff --git a/lib/payme/methods/cancel_transaction.py b/lib/payme/methods/cancel_transaction.py deleted file mode 100644 index d318480..0000000 --- a/lib/payme/methods/cancel_transaction.py +++ /dev/null @@ -1,54 +0,0 @@ -import time - -from django.db import transaction - -from payme.utils.logging import logger -from payme.models import MerchatTransactionsModel -from payme.errors.exceptions import PerformTransactionDoesNotExist -from payme.serializers import MerchatTransactionsModelSerializer as MTMS - - -class CancelTransaction: - """ - CancelTransaction class - That is used to cancel a transaction. - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-merchant-api/canceltransaction - """ - - @transaction.atomic - def __call__(self, params: dict): - clean_data: dict = MTMS.get_validated_data( - params=params - ) - try: - with transaction.atomic(): - transactions: MerchatTransactionsModel = \ - MerchatTransactionsModel.objects.filter( - _id=clean_data.get('_id'), - ).first() - if transactions.cancel_time == 0: - transactions.cancel_time = int(time.time() * 1000) - if transactions.perform_time == 0: - transactions.state = -1 - if transactions.perform_time != 0: - transactions.state = -2 - transactions.reason = clean_data.get("reason") - transactions.save() - - except PerformTransactionDoesNotExist as error: - logger.error("Paycom transaction does not exist: %s", error) - raise PerformTransactionDoesNotExist() from error - - response: dict = { - "result": { - "state": transactions.state, - "cancel_time": transactions.cancel_time, - "transaction": transactions.transaction_id, - "reason": int(transactions.reason), - } - } - - return transactions.order_id, response diff --git a/lib/payme/methods/check_perform_transaction.py b/lib/payme/methods/check_perform_transaction.py deleted file mode 100644 index 78a7411..0000000 --- a/lib/payme/methods/check_perform_transaction.py +++ /dev/null @@ -1,26 +0,0 @@ -from payme.utils.get_params import get_params -from payme.serializers import MerchatTransactionsModelSerializer - - -class CheckPerformTransaction: - """ - CheckPerformTransaction class - That's used to check perform transaction. - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-merchant-api/checktransaction - """ - def __call__(self, params: dict) -> dict: - serializer = MerchatTransactionsModelSerializer( - data=get_params(params) - ) - serializer.is_valid(raise_exception=True) - - response = { - "result": { - "allow": True, - } - } - - return None, response diff --git a/lib/payme/methods/check_transaction.py b/lib/payme/methods/check_transaction.py deleted file mode 100644 index 4768154..0000000 --- a/lib/payme/methods/check_transaction.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.db import DatabaseError - -from payme.utils.logging import logger -from payme.models import MerchatTransactionsModel -from payme.serializers import MerchatTransactionsModelSerializer as MTMS - - -class CheckTransaction: - """ - CheckTransaction class - That's used to check transaction - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-merchant-api/checkperformtransaction - """ - def __call__(self, params: dict) -> None: - clean_data: dict = MTMS.get_validated_data( - params=params - ) - - try: - transaction = \ - MerchatTransactionsModel.objects.get( - _id=clean_data.get("_id"), - ) - response = { - "result": { - "create_time": int(transaction.created_at_ms), - "perform_time": transaction.perform_time, - "cancel_time": transaction.cancel_time, - "transaction": transaction.transaction_id, - "state": transaction.state, - "reason": None, - } - } - if transaction.reason is not None: - response["result"]["reason"] = int(transaction.reason) - - except DatabaseError as error: - logger.error("Error getting transaction in database: %s", error) - - return None, response diff --git a/lib/payme/methods/create_transaction.py b/lib/payme/methods/create_transaction.py deleted file mode 100644 index a5f2c77..0000000 --- a/lib/payme/methods/create_transaction.py +++ /dev/null @@ -1,68 +0,0 @@ -import uuid -import time -import datetime - -from payme.utils.logging import logger -from payme.utils.get_params import get_params -from payme.models import MerchatTransactionsModel -from payme.errors.exceptions import TooManyRequests -from payme.serializers import MerchatTransactionsModelSerializer - - -class CreateTransaction: - """ - CreateTransaction class - That's used to create transaction - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-merchant-api/createtransaction - """ - def __call__(self, params: dict) -> dict: - serializer = MerchatTransactionsModelSerializer( - data=get_params(params) - ) - serializer.is_valid(raise_exception=True) - order_id = serializer.validated_data.get("order_id") - - try: - transaction = MerchatTransactionsModel.objects.filter( - order_id=order_id - ).last() - - if transaction is not None: - if transaction._id != serializer.validated_data.get("_id"): - raise TooManyRequests() - - except TooManyRequests as error: - logger.error("Too many requests for transaction %s", error) - raise TooManyRequests() from error - - if transaction is None: - transaction, _ = \ - MerchatTransactionsModel.objects.get_or_create( - _id=serializer.validated_data.get('_id'), - order_id=serializer.validated_data.get('order_id'), - transaction_id=uuid.uuid4(), - amount=serializer.validated_data.get('amount'), - created_at_ms=int(time.time() * 1000), - ) - - if transaction: - response: dict = { - "result": { - "create_time": int(transaction.created_at_ms), - "transaction": transaction.transaction_id, - "state": int(transaction.state), - } - } - - return order_id, response - - @staticmethod - def _convert_ms_to_datetime(time_ms: str) -> int: - """Use this format to convert from time ms to datetime format. - """ - readable_datetime = datetime.datetime.fromtimestamp(time_ms / 1000) - - return readable_datetime diff --git a/lib/payme/methods/generate_link.py b/lib/payme/methods/generate_link.py deleted file mode 100644 index 0738315..0000000 --- a/lib/payme/methods/generate_link.py +++ /dev/null @@ -1,74 +0,0 @@ -import base64 -from decimal import Decimal -from dataclasses import dataclass - -from django.conf import settings - -PAYME_ID = settings.PAYME.get('PAYME_ID') -PAYME_ACCOUNT = settings.PAYME.get('PAYME_ACCOUNT') -PAYME_CALL_BACK_URL = settings.PAYME.get('PAYME_CALL_BACK_URL') -PAYME_URL = settings.PAYME.get("PAYME_URL") - - -@dataclass -class GeneratePayLink: - """ - GeneratePayLink dataclass - That's used to generate pay lint for each order. - - Parameters - ---------- - order_id: int — The order_id for paying - amount: int — The amount belong to the order - - Returns str — pay link - ---------------------- - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/initsializatsiya-platezhey/ - """ - order_id: str - amount: Decimal - - def generate_link(self) -> str: - """ - GeneratePayLink for each order. - """ - generated_pay_link: str = "{payme_url}/{encode_params}" - params: str = 'm={payme_id};ac.{payme_account}={order_id};a={amount};c={call_back_url}' - - params = params.format( - payme_id=PAYME_ID, - payme_account=PAYME_ACCOUNT, - order_id=self.order_id, - amount=self.amount, - call_back_url=PAYME_CALL_BACK_URL - ) - encode_params = base64.b64encode(params.encode("utf-8")) - return generated_pay_link.format( - payme_url=PAYME_URL, - encode_params=str(encode_params, 'utf-8') - ) - - @staticmethod - def to_tiyin(amount: Decimal) -> Decimal: - """ - Convert from soum to tiyin. - - Parameters - ---------- - amount: Decimal -> order amount - """ - return amount * 100 - - @staticmethod - def to_soum(amount: Decimal) -> Decimal: - """ - Convert from tiyin to soum. - - Parameters - ---------- - amount: Decimal -> order amount - """ - return amount / 100 diff --git a/lib/payme/methods/get_statement.py b/lib/payme/methods/get_statement.py deleted file mode 100644 index ba41c7e..0000000 --- a/lib/payme/methods/get_statement.py +++ /dev/null @@ -1,65 +0,0 @@ -from django.db import DatabaseError - -from payme.utils.logging import logger -from payme.models import MerchatTransactionsModel -from payme.serializers import MerchatTransactionsModelSerializer as MTMS -from payme.utils.make_aware_datetime import make_aware_datetime as mad - - -class GetStatement: - """ - GetStatement class - Transaction information is used for reconciliation - of merchant and Payme Business transactions. - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-merchant-api/getstatement - """ - - def __call__(self, params: dict): - clean_data: dict = MTMS.get_validated_data( - params=params - ) - - start_date, end_date = mad( - int(clean_data.get("start_date")), - int(clean_data.get("end_date")) - ) - - try: - transactions = \ - MerchatTransactionsModel.objects.filter( - created_at__gte=start_date, - created_at__lte=end_date - ) - - if not transactions: # no transactions found for the period - return {"result": {"transactions": []}} - - statements = [ - { - 'id': t._id, - 'time': int(t.created_at.timestamp()), - 'amount': t.amount, - 'account': {'order_id': t.order_id}, - 'create_time': t.state, - 'perform_time': t.perform_time, - 'cancel_time': t.cancel_time, - 'transaction': t.order_id, - 'state': t.state, - 'reason': t.reason, - 'receivers': [] # not implemented - } for t in transactions - ] - - response: dict = { - "result": { - "transactions": statements - } - } - except DatabaseError as error: - logger.error("Error getting transaction in database: %s", error) - response = {"result": {"transactions": []}} - - return None, response diff --git a/lib/payme/methods/perform_transaction.py b/lib/payme/methods/perform_transaction.py deleted file mode 100644 index 5312065..0000000 --- a/lib/payme/methods/perform_transaction.py +++ /dev/null @@ -1,47 +0,0 @@ -import time - -from django.db import DatabaseError - -from payme.utils.logging import logger -from payme.utils.get_params import get_params -from payme.models import MerchatTransactionsModel -from payme.serializers import MerchatTransactionsModelSerializer - - -class PerformTransaction: - """ - PerformTransaction class - That's used to perform a transaction. - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-merchant-api/performtransaction - """ - def __call__(self, params: dict) -> dict: - serializer = MerchatTransactionsModelSerializer( - data=get_params(params) - ) - serializer.is_valid(raise_exception=True) - clean_data: dict = serializer.validated_data - response: dict = None - try: - transaction = \ - MerchatTransactionsModel.objects.get( - _id=clean_data.get("_id"), - ) - transaction.state = 2 - if transaction.perform_time == 0: - transaction.perform_time = int(time.time() * 1000) - - transaction.save() - response: dict = { - "result": { - "perform_time": int(transaction.perform_time), - "transaction": transaction.transaction_id, - "state": int(transaction.state), - } - } - except DatabaseError as error: - logger.error("error while getting transaction in db: %s", error) - - return transaction.order_id, response diff --git a/lib/payme/migrations/0001_initial.py b/lib/payme/migrations/0001_initial.py deleted file mode 100644 index eec730a..0000000 --- a/lib/payme/migrations/0001_initial.py +++ /dev/null @@ -1,48 +0,0 @@ -# pylint: disable=invalid-name -from django.db import migrations, models - - -class Migration(migrations.Migration): - # pylint: disable=missing-class-docstring - initial = True - dependencies = [] - - operations = [ - migrations.CreateModel( - name='MerchatTransactionsModel', - fields=[ - ('id', models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID') - ), - ('_id', models.CharField(max_length=255, null=True)), - ('transaction_id', models.CharField(max_length=255, null=True)), - ('order_id', models.BigIntegerField(blank=True, null=True)), - ('amount', models.FloatField(blank=True, null=True)), - ('time', models.BigIntegerField(blank=True, null=True)), - ('perform_time', models.BigIntegerField(default=0, null=True)), - ('cancel_time', models.BigIntegerField(default=0, null=True)), - ('state', models.IntegerField(default=1, null=True)), - ('reason', models.CharField(blank=True, max_length=255, null=True)), - ('created_at_ms', models.CharField(blank=True, max_length=255, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - ), - migrations.CreateModel( - name='Order', - fields=[ - ('id', models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID') - ), - ('amount', models.IntegerField(blank=True, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - ), - ] diff --git a/lib/payme/migrations/__init__.py b/lib/payme/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/lib/payme/models.py b/lib/payme/models.py deleted file mode 100644 index 52d46cf..0000000 --- a/lib/payme/models.py +++ /dev/null @@ -1,61 +0,0 @@ -from django.db import models -from django.conf import settings -from django.utils.module_loading import import_string -from django.core.exceptions import FieldError - -from payme.utils.logging import logger - - -class MerchatTransactionsModel(models.Model): - """ - MerchatTransactionsModel class \ - That's used for managing transactions in database. - """ - _id = models.CharField(max_length=255, null=True, blank=False) - transaction_id = models.CharField(max_length=255, null=True, blank=False) - order_id = models.BigIntegerField(null=True, blank=True) - amount = models.FloatField(null=True, blank=True) - time = models.BigIntegerField(null=True, blank=True) - perform_time = models.BigIntegerField(null=True, default=0) - cancel_time = models.BigIntegerField(null=True, default=0) - state = models.IntegerField(null=True, default=1) - reason = models.CharField(max_length=255, null=True, blank=True) - created_at_ms = models.CharField(max_length=255, null=True, blank=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return str(self._id) - - -try: - CUSTOM_ORDER = import_string(settings.ORDER_MODEL) - - if not isinstance(CUSTOM_ORDER, models.base.ModelBase): - raise TypeError("The input must be an instance of models.Model class") - - # pylint: disable=protected-access - if 'amount' not in [f.name for f in CUSTOM_ORDER._meta.fields]: - raise FieldError("Missing 'amount' field in your custom order model") - - Order = CUSTOM_ORDER -except (ImportError, AttributeError): - logger.warning("You have no payme custom order model") - - CUSTOM_ORDER = None - - class Order(models.Model): - """ - Order class \ - That's used for managing order process - """ - amount = models.IntegerField(null=True, blank=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return f"ORDER ID: {self.id} - AMOUNT: {self.amount}" - - class Meta: - # pylint: disable=missing-class-docstring - managed = False diff --git a/lib/payme/receipts/__init__.py b/lib/payme/receipts/__init__.py deleted file mode 100644 index 6cd8cf5..0000000 --- a/lib/payme/receipts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .import subscribe_receipts diff --git a/lib/payme/receipts/subscribe_receipts.py b/lib/payme/receipts/subscribe_receipts.py deleted file mode 100644 index a96b410..0000000 --- a/lib/payme/receipts/subscribe_receipts.py +++ /dev/null @@ -1,217 +0,0 @@ -from payme.utils.to_json import to_json -from payme.decorators.decorators import payme_request - - - -class PaymeSubscribeReceipts: - """ - The PaymeSubscribeReceipts class inclues - all paycom methods which are belongs receipts part. - - Parameters - ---------- - base_url string: The base url of the paycom api - paycom_id string: The paycom_id uses to identify - paycom_key string: The paycom_key uses to identify too - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-subscribe-api/ - """ - def __init__( - self, - base_url: str, - paycom_id: str, - paycom_key: str, - timeout: int = 5 - ) -> "PaymeSubscribeReceipts": - self.base_url: str = base_url - self.headers: dict = { - "X-Auth": f"{paycom_id}:{paycom_key}" - } - self.__methods: dict = { - "receipts_get": "receipts.get", - "receipts_pay": "receipts.pay", - "receipts_send": "receipts.send", - "receipts_check": "receipts.check", - "receipts_cancel": "receipts.cancel", - "receipts_create": "receipts.create", - "receipts_get_all": "receipts.get_all", - } - self.timeout = timeout - - @payme_request - def __request(self, data) -> dict: - """ - Use this private method to request. - On success,response will be OK with format JSON. - - Parameters - ---------- - data: dict — Includes request data. - - Returns dictionary Payme Response - --------------------------------- - """ - return data - - def receipts_create(self, amount: float, order_id: int) -> dict: - """ - Use this method to create a new payment receipt. - - Parameters - ---------- - amount: float — Payment amount in tiyins - order_id: int — Order object ID - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-subscribe-api/receipts.create - """ - data: dict = { - "method": self.__methods.get("receipts_create"), - "params": { - "amount": amount, - "account": { - "order_id": order_id, - } - } - } - return self.__request(to_json(**data)) - - def receipts_pay(self, invoice_id: str, token: str, phone: str) -> dict: - """ - Use this method to pay for an exist receipt. - - Parameters - ---------- - invoice_id: str — Invoice id for indentity transaction - token: str — The card's active token - phone: str —The payer's phone number - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-subscribe-api/receipts.pay - """ - data: dict = { - "method": self.__methods.get("receipts_pay"), - "params": { - "id": invoice_id, - "token": token, - "payer": { - "phone": phone, - } - } - } - return self.__request(to_json(**data)) - - def receipts_send(self, invoice_id: str, phone: str) -> dict: - """ - Use this method to send a receipt for payment in an SMS message. - - Parameters - ---------- - invoice_id: str — The invoice id for indentity transaction - phone: str — The payer's phone number - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-subscribe-api/receipts.send - """ - data: dict = { - "method": self.__methods.get('receipts_send'), - "params": { - "id": invoice_id, - "phone": phone - } - } - return self.__request(to_json(**data)) - - def receipts_cancel(self, invoice_id: str) -> dict: - """ - Use this method a paid check in the queue for cancellation. - - Parameters - ---------- - invoice_id: str — The invoice id for indentity transaction - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-subscribe-api/receipts.cancel - """ - data: dict = { - "method": self.__methods.get('receipts_cancel'), - "params": { - "id": invoice_id - } - } - - return self.__request(to_json(**data)) - - def receipts_check(self, invoice_id: str) -> dict: - """ - Use this method check for an exist receipt. - - Parameters - ---------- - invoice_id: str — The invoice id for indentity transaction - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-subscribe-api/receipts.check - """ - data: dict = { - "method": self.__methods.get('receipts_check'), - "params": { - "id": invoice_id - } - } - - return self.__request(to_json(**data)) - - def reciepts_get(self, invoice_id: str) -> dict: - """ - Use this method check status for an exist receipt. - - Parameters - ---------- - invoice_id: str — The invoice id for indentity transaction - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-subscribe-api/receipts.get - """ - data: dict = { - "method": self.__methods.get('receipts_get'), - "params": { - "id": invoice_id - } - } - - return self.__request(to_json(**data)) - - def reciepts_get_all(self, count: int, _from: int, _to: int, offset: int) -> dict: - """ - Use this method get all complete information, on checks for a certain period. - - Parameters - ---------- - count: int — The number of checks. Maximum value - 50 - _from: str — The date of the beginning - _to: int — The date of the ending - offset: str — The number of subsequent skipped checks. - - Full method documentation - ------------------------- - https://developer.help.paycom.uz/metody-subscribe-api/receipts.get_all - """ - data: str = { - "method": self.__methods.get('receipts_get_all'), - "params": { - "count": count, - "from": _from, - "to": _to, - "offset": offset - } - } - return self.__request(to_json(**data)) diff --git a/lib/payme/serializers.py b/lib/payme/serializers.py deleted file mode 100644 index 9fe8118..0000000 --- a/lib/payme/serializers.py +++ /dev/null @@ -1,86 +0,0 @@ -from django.conf import settings - -from rest_framework import serializers - -from payme.models import Order -from payme.utils.logging import logger -from payme.utils.get_params import get_params -from payme.models import MerchatTransactionsModel -from payme.errors.exceptions import IncorrectAmount -from payme.errors.exceptions import PerformTransactionDoesNotExist - - -class MerchatTransactionsModelSerializer(serializers.ModelSerializer): - """ - MerchatTransactionsModelSerializer class \ - That's used to serialize merchat transactions data. - """ - start_date = serializers.IntegerField(allow_null=True) - end_date = serializers.IntegerField(allow_null=True) - - class Meta: - # pylint: disable=missing-class-docstring - model: MerchatTransactionsModel = MerchatTransactionsModel - fields: str = "__all__" - extra_fields = ['start_date', 'end_date'] - - def validate(self, attrs) -> dict: - """ - Validate the data given to the MerchatTransactionsModel. - """ - if attrs.get("order_id") is not None: - try: - order = Order.objects.get( - id=attrs['order_id'] - ) - if order.amount != int(attrs['amount']): - raise IncorrectAmount() - - except IncorrectAmount as error: - logger.error("Invalid amount for order: %s", attrs['order_id']) - raise IncorrectAmount() from error - - return attrs - - def validate_amount(self, amount: int) -> int: - """ - Validator for Transactions Amount. - """ - if amount is not None: - if amount <= int(settings.PAYME.get("PAYME_MIN_AMOUNT")): - raise IncorrectAmount("Payment amount is less than allowed.") - - return amount - - def validate_order_id(self, order_id) -> int: - """ - Use this method to check if a transaction is allowed to be executed. - - Parameters - ---------- - order_id: str -> Order Indentation. - """ - try: - Order.objects.get(id=order_id) - except Order.DoesNotExist as error: - logger.error("Order does not exist order_id: %s", order_id) - raise PerformTransactionDoesNotExist() from error - - return order_id - - @staticmethod - def get_validated_data(params: dict) -> dict: - """ - This static method helps to get validated data. - - Parameters - ---------- - params: dict — Includes request params. - """ - serializer = MerchatTransactionsModelSerializer( - data=get_params(params) - ) - serializer.is_valid(raise_exception=True) - clean_data: dict = serializer.validated_data - - return clean_data diff --git a/lib/payme/urls.py b/lib/payme/urls.py deleted file mode 100644 index c9c7886..0000000 --- a/lib/payme/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.urls import path - -from payme.views import MerchantAPIView - - -urlpatterns = [ - path("merchant/", MerchantAPIView.as_view()) -] diff --git a/lib/payme/utils/__init__.py b/lib/payme/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/lib/payme/utils/get_params.py b/lib/payme/utils/get_params.py deleted file mode 100644 index 8005c86..0000000 --- a/lib/payme/utils/get_params.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.conf import settings - - -def get_params(params: dict) -> dict: - """ - Use this function to get the parameters from the payme. - """ - account: dict = params.get("account") - - clean_params: dict = {} - clean_params["_id"] = params.get("id") - clean_params["time"] = params.get("time") - clean_params["amount"] = params.get("amount") - clean_params["reason"] = params.get("reason") - - # get statement method params - clean_params["start_date"] = params.get("from") - clean_params["end_date"] = params.get("to") - - if account is not None: - account_name: str = settings.PAYME.get("PAYME_ACCOUNT") - clean_params["order_id"] = account[account_name] - - return clean_params diff --git a/lib/payme/utils/logging.py b/lib/payme/utils/logging.py deleted file mode 100644 index e602214..0000000 --- a/lib/payme/utils/logging.py +++ /dev/null @@ -1,9 +0,0 @@ -import logging - -logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s %(levelname)s %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' -) - -logger = logging.getLogger(__name__) diff --git a/lib/payme/utils/make_aware_datetime.py b/lib/payme/utils/make_aware_datetime.py deleted file mode 100644 index ef2f1e7..0000000 --- a/lib/payme/utils/make_aware_datetime.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.utils.timezone import datetime as dt -from django.utils.timezone import make_aware - - -def make_aware_datetime(start_date: int, end_date: int): - """ - Convert Unix timestamps to aware datetimes. - - :param start_date: Unix timestamp (milliseconds) - :param end_date: Unix timestamp (milliseconds) - - :return: A tuple of two aware datetimes - """ - return map( - lambda timestamp: make_aware( - dt.fromtimestamp( - timestamp / 1000 - ) - ), - [start_date, end_date] - ) diff --git a/lib/payme/utils/support.py b/lib/payme/utils/support.py deleted file mode 100644 index 3b7f813..0000000 --- a/lib/payme/utils/support.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -Author: Muhammadali Akbarov -Gmail: muhammadali17abc@gmail.com -Phone: +998888351717 -Telegram: @Muhammadalive -Twitter: https://twitter.com/muhammadali_abc -GitHub: https://github.com/Muhammadali-Akbarov/ -""" diff --git a/lib/payme/utils/to_json.py b/lib/payme/utils/to_json.py deleted file mode 100644 index cd29eba..0000000 --- a/lib/payme/utils/to_json.py +++ /dev/null @@ -1,12 +0,0 @@ -import json - -def to_json(**kwargs) -> dict: - """ - Use this static method to data dumps. - """ - data: dict = { - "method": kwargs.pop("method"), - "params": kwargs.pop("params"), - } - - return json.dumps(data) diff --git a/lib/payme/views.py b/lib/payme/views.py deleted file mode 100644 index 25db03c..0000000 --- a/lib/payme/views.py +++ /dev/null @@ -1,163 +0,0 @@ -import base64 -import binascii - -from django.conf import settings - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.exceptions import ValidationError - -from payme.utils.logging import logger - -from payme.errors.exceptions import MethodNotFound -from payme.errors.exceptions import PermissionDenied -from payme.errors.exceptions import PerformTransactionDoesNotExist - -from payme.methods.get_statement import GetStatement -from payme.methods.check_transaction import CheckTransaction -from payme.methods.cancel_transaction import CancelTransaction -from payme.methods.create_transaction import CreateTransaction -from payme.methods.perform_transaction import PerformTransaction -from payme.methods.check_perform_transaction import CheckPerformTransaction - - -class MerchantAPIView(APIView): - """ - MerchantAPIView class provides payme call back functionality. - """ - permission_classes = () - authentication_classes = () - - def post(self, request) -> Response: - """ - Payme sends post request to our call back url. - That methods are includes 5 methods - - CheckPerformTransaction - - CreateTransaction - - PerformTransaction - - CancelTransaction - - CheckTransaction - - GetStatement - """ - password = request.META.get('HTTP_AUTHORIZATION') - if self.authorize(password): - incoming_data: dict = request.data - incoming_method: str = incoming_data.get("method") - - logger.info("Call back data is incoming %s", incoming_data) - - try: - paycom_method = self.get_paycom_method_by_name( - incoming_method=incoming_method - ) - except ValidationError as error: - logger.error("Validation Error occurred: %s", error) - raise MethodNotFound() from error - - except PerformTransactionDoesNotExist as error: - logger.error("PerformTransactionDoesNotExist Error occurred: %s", error) - raise PerformTransactionDoesNotExist() from error - - order_id, action = paycom_method(incoming_data.get("params")) - - if isinstance(paycom_method, CreateTransaction): - self.create_transaction( - order_id=order_id, - action=action, - ) - - if isinstance(paycom_method, PerformTransaction): - self.perform_transaction( - order_id=order_id, - action=action, - ) - - if isinstance(paycom_method, CancelTransaction): - self.cancel_transaction( - order_id=order_id, - action=action, - ) - - return Response(data=action) - - def get_paycom_method_by_name(self, incoming_method: str) -> object: - """ - Use this static method to get the paycom method by name. - :param incoming_method: string -> incoming method name - """ - available_methods: dict = { - "CheckPerformTransaction": CheckPerformTransaction, - "CreateTransaction": CreateTransaction, - "PerformTransaction": PerformTransaction, - "CancelTransaction": CancelTransaction, - "CheckTransaction": CheckTransaction, - "GetStatement": GetStatement - } - - try: - merchant_method = available_methods[incoming_method] - except Exception as error: - error_message = "Unavailable method: %s", incoming_method - logger.error(error_message) - raise MethodNotFound(error_message=error_message) from error - - merchant_method = merchant_method() - - return merchant_method - - @staticmethod - def authorize(password: str) -> None: - """ - Authorize the Merchant. - :param password: string -> Merchant authorization password - """ - is_payme: bool = False - error_message: str = "" - - if not isinstance(password, str): - error_message = "Request from an unauthorized source!" - logger.error(error_message) - raise PermissionDenied(error_message=error_message) - - password = password.split()[-1] - - try: - password = base64.b64decode(password).decode('utf-8') - except (binascii.Error, UnicodeDecodeError) as error: - error_message = "Error when authorize request to merchant!" - logger.error(error_message) - - raise PermissionDenied(error_message=error_message) from error - - merchant_key = password.split(':')[-1] - - if merchant_key == settings.PAYME.get('PAYME_KEY'): - is_payme = True - - if merchant_key != settings.PAYME.get('PAYME_KEY'): - logger.error("Invalid key in request!") - - if is_payme is False: - raise PermissionDenied( - error_message="Unavailable data for unauthorized users!" - ) - - return is_payme - - def create_transaction(self, order_id, action) -> None: - """ - need implement in your view class - """ - pass - - def perform_transaction(self, order_id, action) -> None: - """ - need implement in your view class - """ - pass - - def cancel_transaction(self,order_id, action) -> None: - """ - need implement in your view class - """ - pass diff --git a/makefile b/makefile index 3f18e6c..d273c77 100644 --- a/makefile +++ b/makefile @@ -1,6 +1,3 @@ -lint: - pylint ./lib/* - upload: rm -rf ./dist/* python setup.py sdist diff --git a/payme/__init__.py b/payme/__init__.py new file mode 100644 index 0000000..5727c08 --- /dev/null +++ b/payme/__init__.py @@ -0,0 +1 @@ +from payme.classes.client import Payme # noqa diff --git a/payme/admin.py b/payme/admin.py new file mode 100644 index 0000000..9af0d26 --- /dev/null +++ b/payme/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin + + +from payme.models import PaymeTransactions + + +class PaymeTransactionsUI(admin.ModelAdmin): + """ + Custom admin interface for PaymeTransactions model. + """ + list_display = ('id', 'state', 'cancel_reason', 'created_at') + list_filter = ('state', 'cancel_reason', 'created_at') + search_fields = ('transaction_id', 'account__id') + ordering = ('-created_at',) + + +admin.site.register(PaymeTransactions, PaymeTransactionsUI) diff --git a/lib/payme/apps.py b/payme/apps.py similarity index 53% rename from lib/payme/apps.py rename to payme/apps.py index 52fd71f..f6266b6 100644 --- a/lib/payme/apps.py +++ b/payme/apps.py @@ -2,9 +2,5 @@ class PaymeConfig(AppConfig): - """ - PaymeConfig AppConfig \ - That is used to configure the payme application with django settings. - """ default_auto_field = 'django.db.models.BigAutoField' name = 'payme' diff --git a/lib/payme/__init__.py b/payme/classes/__init__.py similarity index 100% rename from lib/payme/__init__.py rename to payme/classes/__init__.py diff --git a/payme/classes/cards.py b/payme/classes/cards.py new file mode 100644 index 0000000..af0c424 --- /dev/null +++ b/payme/classes/cards.py @@ -0,0 +1,212 @@ +from typing import Optional + +from payme.util import input_type_checker +from payme.classes.http import HttpClient +from payme.types.response import cards as response + + +ALLOWED_METHODS = { + "cards.create": response.CardsCreateResponse, + "cards.get_verify_code": response.GetVerifyResponse, + "cards.verify": response.VerifyResponse, + "cards.remove": response.RemoveResponse, + "cards.check": response.CheckResponse +} + + +class Cards: + """ + The Cards class provides a simple interface to interact with Paycom card + services. It allows you to create new cards and retrieve verification + codes for existing cards. + """ + + @input_type_checker + def __init__(self, url: str, payme_id: str) -> "Cards": + """ + Initialize the Cards client. + + :param payme_id: The Paycom ID used for authentication. + :param url: The base URL for the Paycom card service API. + """ + headers = { + "X-Auth": payme_id, + "Content-Type": "application/json" + } + self.http = HttpClient(url, headers) + + @input_type_checker + def create(self, number: str, expire: str, save: bool = False, + timeout: int = 10) -> response.CardsCreateResponse: + """ + Create a new card. + + :param number: The card number. + :param expire: The expiration date of the card in MMYY format. + :param save: A boolean indicating whether to save the card for future + use (default is False). + :param timeout: The request timeout duration in seconds (default is + 10 seconds). + :return: A CardsCreateResponse object containing the response data. + """ + method = "cards.create" + params = {"card": {"number": number, "expire": expire}, "save": save} + return self._post_request(method, params, timeout) + + @input_type_checker + def get_verify_code(self, token: str, timeout: int = 10) -> \ + response.GetVerifyResponse: + """ + Retrieve a verification code for a specified token. + + :param token: The token associated with the card. + :param timeout: The request timeout duration in seconds (default is + 10 seconds). + :return: A GetVerifyResponse object containing the response data. + """ + method = "cards.get_verify_code" + params = {"token": token} + return self._post_request(method, params, timeout) + + @input_type_checker + def verify(self, token: str, code: str, timeout: int = 10) -> \ + response.VerifyResponse: + """ + Verify a verification code for a specified token. + + :param token: The token associated with the card. + :param code: The verification code to be verified. + :param timeout: The request timeout duration in seconds (default is + 10 seconds). + :return: A VerifyResponse object containing the response data. + """ + method = "cards.verify" + params = {"token": token, "code": code} + return self._post_request(method, params, timeout) + + @input_type_checker + def remove(self, token: str, timeout: int = 10) -> response.RemoveResponse: + """ + Remove a card from the Paycom system. + + :param token: The token associated with the card. + :param timeout: The request timeout duration in seconds (default is + 10 seconds). + :return: A RemoveResponse object containing the response data. + """ + method = "cards.remove" + params = {"token": token} + return self._post_request(method, params, timeout) + + @input_type_checker + def check(self, token: str, timeout: int = 10) -> response.CheckResponse: + """ + Check the status of a card. + + :param token: The token associated with the card. + :param timeout: The request timeout duration in seconds (default is + 10 seconds). + :return: A CheckResponse object containing the response data. + """ + method = "cards.check" + params = {"token": token} + return self._post_request(method, params, timeout) + + @input_type_checker + def _post_request(self, method: str, params: dict, + timeout: int = 10) -> response.Common: + """ + Helper method to post requests to the HTTP client. + + :param method: The API method to be called. + :param params: The parameters to be sent with the request. + :param timeout: The request timeout duration in seconds (default is + 10 seconds). + :return: A response object corresponding to the method called. + """ + json = {"method": method, "params": params} + dict_result = self.http.post(json, timeout) + response_class = ALLOWED_METHODS[method] + return response_class.from_dict(dict_result) + + def test(self): + """ + Run a comprehensive test suite for card functionalities including + creation, verification, status check, and removal. + """ + # Expected values for verification + number = "8600495473316478" + expire = "0399" + + expected_number = "860049******6478" + expected_expire = "03/99" + verify_code = "666666" + + # Step 1: Create Card + create_response = self.create(number=number, expire=expire) + token = create_response.result.card.token + + # Validate card creation response + self._assert_and_print( + create_response.result.card.number == expected_number, + "Card number matched.", + test_case="Card Creation - Number Validation" + ) + self._assert_and_print( + create_response.result.card.expire == expected_expire, + "Expiration date matched.", + test_case="Card Creation - Expiration Date Validation" + ) + + # Step 2: Get Verification Code + get_verify_response = self.get_verify_code(token=token) + self._assert_and_print( + get_verify_response.result.sent is True, + "Verification code requested successfully.", + test_case="Verification Code Request" + ) + + # Step 3: Verify Code + verify_response = self.verify(token=token, code=verify_code) + self._assert_and_print( + verify_response.result.card.verify is True, + "Verification code validated successfully.", + test_case="Code Verification" + ) + + # Step 4: Check Card Status + check_response = self.check(token=token) + self._assert_and_print( + check_response.result.card.verify is True, + "Card status verified successfully.", + test_case="Card Status Check" + ) + + # Step 5: Remove Card + remove_response = self.remove(token=token) + self._assert_and_print( + remove_response.result.success is True, + "Card removed successfully.", + test_case="Card Removal" + ) + + def _assert_and_print(self, condition: bool, success_message: str, + test_case: Optional[str] = None): + """ + Assertion helper that prints success or failure messages based on + test outcomes. + + :param condition: The test condition to check. + :param success_message: Message to print upon successful test. + :param test_case: A description of the test case (optional). + """ + try: + assert condition, "Assertion failed!" + print(f"Success: {success_message}") + except AssertionError as exc: + error_message = ( + f"Test Case Failed: {test_case or 'Unknown Test Case'}\n" + f"Error Details: {str(exc)}" + ) + print(error_message) + raise AssertionError(error_message) from exc diff --git a/payme/classes/client.py b/payme/classes/client.py new file mode 100644 index 0000000..f8d98bb --- /dev/null +++ b/payme/classes/client.py @@ -0,0 +1,31 @@ + +from typing import Union + +from payme.const import Networks +from payme.classes.cards import Cards +from payme.util import input_type_checker +from payme.classes.receipts import Receipts +from payme.classes.initializer import Initializer + + +class Payme: + """ + The payme class provides a simple interface + """ + @input_type_checker + def __init__( + self, + payme_id: str, + payme_key: Union[str, None] = None, + is_test_mode: bool = False + ): + + # initialize payme network + url = Networks.PROD_NET + + if is_test_mode is True: + url = Networks.TEST_NET + + self.cards = Cards(url=url, payme_id=payme_id) + self.initializer = Initializer(payme_id=payme_id) + self.receipts = Receipts(url=url, payme_id=payme_id, payme_key=payme_key) # noqa diff --git a/payme/classes/http.py b/payme/classes/http.py new file mode 100644 index 0000000..8fffd60 --- /dev/null +++ b/payme/classes/http.py @@ -0,0 +1,106 @@ +import requests + +from payme.exceptions import general as exc + + +networking_errors = ( + requests.exceptions.Timeout, + requests.exceptions.HTTPError, + requests.exceptions.ConnectionError, + requests.exceptions.TooManyRedirects, + requests.exceptions.URLRequired, + requests.exceptions.MissingSchema, + requests.exceptions.InvalidURL, + requests.exceptions.InvalidHeader, + requests.exceptions.JSONDecodeError, + requests.exceptions.ConnectTimeout, + requests.exceptions.ReadTimeout, + requests.exceptions.SSLError, + requests.exceptions.ProxyError, + requests.exceptions.ChunkedEncodingError, + requests.exceptions.StreamConsumedError, + requests.exceptions.RequestException +) + + +class HttpClient: + """ + A simple HTTP client to handle requests to a specified URL. + It provides methods for sending GET, POST, PUT, and DELETE requests + with error handling. + """ + + def __init__(self, url: str, headers: dict = None): + """ + Initialize the HttpClient. + + Parameters + ---------- + url : str + The base URL for the API (e.g., 'https://checkout.paycom.uz/api'). + headers : dict, optional + Optional default headers to include in all requests. + These headers will be sent with every request unless overridden. + """ + self.url = url + self.headers = headers + + def post(self, json: dict, timeout: int = 10): + """ + Send a POST request to the specified URL with the provided JSON data. + + Parameters + ---------- + json : dict + The JSON data payload for the POST request. This will be sent + as the request body. + timeout : int, optional + The request timeout duration in seconds (default is 10 seconds). + + Returns + ------- + dict + A dictionary containing the response data if the request was + successful, or an error message if an error occurred. + """ + try: + response = requests.post( + url=self.url, + headers=self.headers, + json=json, + timeout=timeout + ) + response.raise_for_status() + response_data = response.json() + + # Check if the response contains a specific error format + if "error" in response_data: + return self.handle_payme_error(response_data["error"]) + + return response_data + + except networking_errors as exc_data: + raise exc.PaymeNetworkError(data=exc_data) + + def handle_payme_error(self, error: dict): + """ + Handle Paycom-specific errors from the JSON-RPC error response. + + Parameters + ---------- + error : dict + The error dictionary from Paycom's response, typically containing + error details such as code, message, and data. + + Returns + ------- + None + Raises an exception based on the error code received from + Paycom's response. + """ + error_code = error.get("code", "Unknown code") + error_message = error.get("message", "Unknown error") + error_data = error.get("data", "") + + exception_class = exc.errors_map.get(error_code, exc.BaseError) + raise exception_class(message=error_message, data=error_data) diff --git a/payme/classes/initializer.py b/payme/classes/initializer.py new file mode 100644 index 0000000..8cc6119 --- /dev/null +++ b/payme/classes/initializer.py @@ -0,0 +1,77 @@ +import base64 + +from payme.util import input_type_checker + + +class Initializer: + """ + Initialize the Payme class with necessary details. + + Attributes + ---------- + payme_id: str + The Payme ID associated with your account + """ + + @input_type_checker + def __init__(self, payme_id: str = None): + self.payme_id = payme_id + + # pylint: disable=W0622 + @input_type_checker + def generate_pay_link( + self, + id: int, + amount: int, + return_url: str + ) -> str: + """ + Generate a payment link for a specific order. + + This method encodes the payment parameters into a base64 string and + constructs a URL for the Payme checkout. + + Parameters + ---------- + id : int + Unique identifier for the account. + amount : int + The amount associated with the order in currency units. + return_url : str + The URL to which the user will be redirected after the payment is + processed. + + Returns + ------- + str + A payment link formatted as a URL, ready to be used in the payment + process. + + References + ---------- + For full method documentation, visit: + https://developer.help.paycom.uz/initsializatsiya-platezhey/ + """ + amount = amount * 100 # Convert amount to the smallest currency unit + params = ( + f'm={self.payme_id};ac.id={id};a={amount};c={return_url}' + ) + params = base64.b64encode(params.encode("utf-8")).decode("utf-8") + return f"https://checkout.paycom.uz/{params}" + + def test(self): + """ + Test method for the Initializer class. + + This method generates a payment link for a sample order and checks + if the result is a valid string. If successful, it prints a + confirmation message. + """ + result = self.generate_pay_link( + id=12345, + amount=7000, + return_url="https://example.com" + ) + + assert isinstance(result, str), "Failed to generate payment link" + print("Success: Payment link generated successfully.") diff --git a/payme/classes/receipts.py b/payme/classes/receipts.py new file mode 100644 index 0000000..808ba65 --- /dev/null +++ b/payme/classes/receipts.py @@ -0,0 +1,310 @@ +from typing import Union, Optional + +from payme.classes.cards import Cards +from payme.util import input_type_checker +from payme.classes.http import HttpClient +from payme.types.response import receipts as response +from payme.const import PaymeTransactionStateEnum as Status + + +ALLOWED_METHODS = { + "receipts.create": response.CreateResponse, + "receipts.pay": response.PayResponse, + "receipts.send": response.SendResponse, + "receipts.cancel": response.CancelResponse, + "receipts.check": response.CheckResponse, + "receipts.get": response.GetResponse, + "receipts.get_all": response.GetAllResponse, +} + + +class Receipts: + """ + The Receipts class provides methods to interact with the Payme Receipts. + """ + + @input_type_checker + def __init__(self, payme_id: str, payme_key: str, url: str) -> "Receipts": + """ + Initialize the Receipts client. + + :param payme_id: The Payme ID associated with your account. + :param payme_key: The Payme API key associated with your account. + :param url: The base URL for the Payme Receipts API. + """ + self.__cards = Cards(url, payme_id) + + headers = { + "X-Auth": f"{payme_id}:{payme_key}", + "Content-Type": "application/json" + } + self.http = HttpClient(url, headers) + + @input_type_checker + def create( + self, + account: dict, + amount: Union[float, int], + description: Optional[str] = None, + detail: Optional[dict] = None, + timeout: int = 10 + ) -> response.CreateResponse: + """ + Create a new receipt. + + :param account: The account details for the receipt. + :param amount: The amount of the receipt. + :param description: Optional description for the receipt. + :param detail: Optional additional details for the receipt. + :param timeout: The request timeout duration in seconds (default 10). + """ + method = "receipts.create" + params = { + "amount": amount, + "account": account, + "description": description, + "detail": detail + } + return self._post_request(method, params, timeout) + + @input_type_checker + def pay( + self, receipts_id: str, token: str, timeout: int = 10 + ) -> response.PayResponse: + """ + Pay the receipt using a cheque. + + :param receipts_id: The ID of the cheque used for payment. + :param token: The token associated with the cheque. + :param timeout: + The request timeout duration in seconds (default is 10). + """ + method = "receipts.pay" + params = { + "id": receipts_id, + "token": token + } + return self._post_request(method, params, timeout) + + @input_type_checker + def send( + self, receipts_id: str, phone: str, timeout: int = 10 + ) -> response.SendResponse: + """ + Send the receipt to a mobile phone. + + :param receipts_id: The ID of the cheque used for payment. + :param phone: The phone number to send the receipt to. + :param timeout: The request timeout duration in seconds (default 10). + """ + method = "receipts.send" + params = { + "id": receipts_id, + "phone": phone + } + return self._post_request(method, params, timeout) + + @input_type_checker + def cancel( + self, receipts_id: str, timeout: int = 10 + ) -> response.CancelResponse: + """ + Cancel the receipt. + + :param receipts_id: The ID of the cheque used for payment. + :param timeout: The request timeout duration in seconds (default 10). + """ + method = "receipts.cancel" + params = { + "id": receipts_id + } + return self._post_request(method, params, timeout) + + @input_type_checker + def check( + self, receipts_id: str, timeout: int = 10 + ) -> response.CheckResponse: + """ + Check the status of a cheque. + + :param receipts_id: The ID of the cheque used for payment. + :param timeout: The request timeout duration in seconds (default 10). + """ + method = "receipts.check" + params = { + "id": receipts_id + } + return self._post_request(method, params, timeout) + + @input_type_checker + def get( + self, receipts_id: str, timeout: int = 10 + ) -> response.GetResponse: + """ + Get the details of a specific cheque. + + :param receipts_id: The ID of the cheque used for payment. + :param timeout: The request timeout duration in seconds (default 10). + """ + method = "receipts.get" + params = { + "id": receipts_id + } + return self._post_request(method, params, timeout) + + @input_type_checker + def get_all( + self, count: int, from_: int, to: int, offset: int, timeout: int = 10 + ) -> response.GetAllResponse: + """ + Get all cheques for a specific account. + + :param count: The number of cheques to retrieve. + :param from_: The start index of the cheques to retrieve. + :param to: The end index of the cheques to retrieve. + :param offset: The offset for pagination. + :param timeout: The request timeout duration in seconds (default 10). + """ + method = "receipts.get_all" + params = { + "count": count, + "from": from_, + "to": to, + "offset": offset + } + return self._post_request(method, params, timeout) + + @input_type_checker + def _post_request( + self, method: str, params: dict, timeout: int = 10 + ) -> response.Common: + """ + Helper method to post requests to the HTTP client. + + :param method: The API method to be called. + :param params: The parameters to be sent with the request. + :param timeout: The request timeout duration in seconds (default 10). + :return: A response object corresponding to the method called. + """ + json = {"method": method, "params": params} + dict_result = self.http.post(json, timeout) + response_class = ALLOWED_METHODS[method] + return response_class.from_dict(dict_result) + + def test(self): + """ + Run a comprehensive suite of tests for the Receipts class, + covering creation, payment, sending, cancellation, status checks, + retrieval of a single receipt, and retrieval of multiple receipts. + """ + # Helper to assert conditions with messaging + def assert_condition(condition, message, test_case): + self._assert_and_print(condition, message, test_case=test_case) + + # Helper to create a receipt for reuse + def create_sample_receipt(): + return self.create( + account={"id": 12345}, + amount=1000, + description="Test receipt", + detail={"key": "value"} + ) + + # Test 1: Initialization check + assert_condition( + isinstance(self, Receipts), + "Initialized Receipts class successfully.", + test_case="Initialization Test" + ) + + # Test 2: Create and Pay Receipt + create_response = create_sample_receipt() + assert_condition( + isinstance(create_response, response.CreateResponse), + "Created a new receipt successfully.", + test_case="Receipt Creation Test" + ) + + # pylint: disable=W0212 + assert_condition( + isinstance(create_response.result.receipt._id, str), + "Created a valid receipt ID.", + test_case="Receipt ID Test" + ) + + # Prepare card and verification + cards_create_response = self.__cards.create( + number="8600495473316478", + expire="0399", + save=True + ) + token = cards_create_response.result.card.token + self.__cards.get_verify_code(token=token) + self.__cards.verify(token=token, code="666666") + + # Pay receipt and verify payment state + receipt_id = create_response.result.receipt._id + pay_response = self.pay(receipts_id=receipt_id, token=token) + assert_condition( + pay_response.result.receipt.state == Status.SUCCESS, + "Paid the receipt successfully.", + test_case="Payment Test" + ) + + # Test 3: Create and Send Receipt + create_response = create_sample_receipt() + receipt_id = create_response.result.receipt._id + send_response = self.send(receipts_id=receipt_id, phone="998901304527") + assert_condition( + send_response.result.success is True, + "Sent the receipt successfully.", + test_case="Send Test" + ) + + # Test 4: Create and Cancel Receipt + create_response = create_sample_receipt() + receipt_id = create_response.result.receipt._id + cancel_response = self.cancel(receipts_id=receipt_id) + assert_condition( + cancel_response.result.receipt.state == Status.CANCELED, + "Cancelled the receipt successfully.", + test_case="Cancel Test" + ) + + # Test 5: Check Receipt Status + check_response = self.check(receipts_id=receipt_id) + assert_condition( + check_response.result.state == Status.CANCELED, + "Checked the receipt status successfully.", + test_case="Check Test" + ) + + # Test 6: Get Receipt Details + get_response = self.get(receipts_id=receipt_id) + assert_condition( + get_response.result.receipt._id == receipt_id, + "Retrieved the receipt details successfully.", + test_case="Get Test" + ) + + # Test 7: Retrieve All Receipts + get_all_response = self.get_all( + count=1, + from_=1730322122000, + to=1730398982000, + offset=0 + ) + assert_condition( + isinstance(get_all_response.result, list), + "Retrieved all receipts successfully.", + test_case="Get All Test" + ) + + # pylint: disable=W0212 + def _assert_and_print( + self, + condition: bool, + success_message: str, + test_case: Optional[str] = None + ): + self.__cards._assert_and_print(condition, success_message, test_case) diff --git a/payme/const.py b/payme/const.py new file mode 100644 index 0000000..4273ab5 --- /dev/null +++ b/payme/const.py @@ -0,0 +1,116 @@ +""" +Payme enumerations +""" +from enum import StrEnum, IntEnum + + +class Methods(StrEnum): + """ + The enumeration of create transaction methods. + + Available Methods: + - GET_STATEMENT: Fetches transaction statement. + - CHECK_TRANSACTION: Checks a transaction. + - CREATE_TRANSACTION: Creates a new transaction. + - CANCEL_TRANSACTION: Cancels an existing transaction. + - PERFORM_TRANSACTION: Performs a transaction. + - CHECK_PERFORM_TRANSACTION: Checks if the transaction can be performed. + """ + GET_STATEMENT = "GetStatement" + CHECK_TRANSACTION = "CheckTransaction" + CREATE_TRANSACTION = "CreateTransaction" + CANCEL_TRANSACTION = "CancelTransaction" + PERFORM_TRANSACTION = "PerformTransaction" + CHECK_PERFORM_TRANSACTION = "CheckPerformTransaction" + + def __str__(self): + return str(self.value) + + +class PaymeTransactionStateEnum(IntEnum): + """ + The enumeration of possible payment states during the transaction process. + + States: + - CREATED (0): Check created. Awaiting payment confirmation. + - WITHDRAWAL_IN_PROGRESS_1 (1): + Creating a transaction in the provider's billing system. + - WITHDRAWAL_IN_PROGRESS_2 (2): Deducting money from the card. + - WITHDRAWAL_CLOSING (3): + Closing the transaction in the provider's billing system. + - PERFORMED (4): Check paid successfully. + - WAITING_TO_BE_CHECKED (20): Check is paused for manual intervention. + - WAITING_TO_BE_CANCELLED_1 (21): Check is queued for cancellation. + - WAITING_TO_BE_CANCELLED_2 (30): + Check is queued for closing the transaction in the provider's billing system. # Noqa + - CANCELED (50): Check canceled. + - CANCELLED_WITHDRAWAL_IN_PROGRESS_2 (-2): + Canceled after the second withdrawal stage. + - CANCELLED_AFTER_WITHDRAWAL_IN_PROGRESS_1 (-1): Canceled after the first withdrawal stage. + """ + # PENDING states + CREATED = 0 + WITHDRAWAL_IN_PROGRESS_1 = 1 + WITHDRAWAL_IN_PROGRESS_2 = 2 + WITHDRAWAL_CLOSING = 3 + + # SUCCESSFUL state + SUCCESS = 4 + + # States indicating the transaction will be cancelled or paused + WAITING_TO_BE_CHECKED = 20 + WAITING_TO_BE_CANCELLED_1 = 21 + WAITING_TO_BE_CANCELLED_2 = 30 + + # CANCELED state + CANCELED = 50 + CANCELLED_WITHDRAWAL_IN_PROGRESS_2 = -2 + CANCELLED_AFTER_WITHDRAWAL_IN_PROGRESS_1 = -1 + + def __str__(self): + return f"Payment state {self.name} (Code: {self.value})" + + @classmethod + def choices(cls): + """ + Provides choices for Django models' `choices` field argument. + """ + return [(state.value, state.name) for state in cls] + + +class PaymeReasonCodesEnum(IntEnum): + """ + Enum class for Payme error codes, + representing different error states that can occur during a transaction. + + Error Codes: + - RECIPIENT_NOT_FOUND (1): + One or more recipients not found or inactive in Payme Business. + - DEBIT_OPERATION_FAILED (2): + Error during debit operation in the processing center. + - TRANSACTION_FAILED (3): Error executing the transaction. + - TRANSACTION_TIMEOUT (4): Transaction canceled due to timeout. + - REFUND (5): Refund issued. + - UNKNOWN_ERROR (10): Unknown error. + """ + RECIPIENT_NOT_FOUND = 1 + DEBIT_OPERATION_FAILED = 2 + TRANSACTION_FAILED = 3 + TRANSACTION_TIMEOUT = 4 + REFUND = 5 + UNKNOWN_ERROR = 10 + + @classmethod + def choices(cls): + """ + Provides choices for Django models' `choices` field argument. + """ + return [(state.value, state.name) for state in cls] + + +class Networks(StrEnum): + """ + Payme networks + """ + PROD_NET = "https://checkout.paycom.uz/api" + TEST_NET = "https://checkout.test.paycom.uz/api" diff --git a/payme/exceptions/__init__.py b/payme/exceptions/__init__.py new file mode 100644 index 0000000..82f93f4 --- /dev/null +++ b/payme/exceptions/__init__.py @@ -0,0 +1,5 @@ +""" +init all payme exceptions +""" +from .general import * # noqa +from .webhook import * # noqa diff --git a/payme/exceptions/general.py b/payme/exceptions/general.py new file mode 100644 index 0000000..376c9eb --- /dev/null +++ b/payme/exceptions/general.py @@ -0,0 +1,250 @@ +import logging + + +class BaseError(Exception): + """Base class for all errors in the payment system.""" + logger = logging.getLogger(__name__) + + def __init__(self, code, message, data=None): + super().__init__(message) + self.code = code + self.data = data + + # pylint: disable=W1203 + self.logger.error(f"Error {code}: {message}. Data: {data}") + + +class CardError(BaseError): + """Base class for card-related errors.""" + + +class TransportError(CardError): + """Transport error occurred during card operation.""" + message = "Transport error." + + def __init__(self, data=None): + super().__init__(-32300, self.message, data) + + +class ParseError(CardError): + """Parse error occurred during card operation.""" + message = "Parse error." + + def __init__(self, data=None): + super().__init__(-32700, self.message, data) + + +class InvalidRequestError(CardError): + """Invalid request made during card operation.""" + message = "Invalid Request." + + def __init__(self, data=None): + super().__init__(-32600, self.message, data) + + +class InvalidResponseError(CardError): + """Invalid response received during card operation.""" + message = "Invalid Response." + + def __init__(self, data=None): + super().__init__(-32600, self.message, data) + + +class SystemError(CardError): + """System error occurred during card operation.""" + message = "System error." + + def __init__(self, data=None): + super().__init__(-32400, self.message, data) + + +class MethodNotFoundError(CardError): + """Method not found during card operation.""" + message = "Method not found." + + def __init__(self, data=None): + super().__init__(-32601, self.message, data) + + +class InvalidParamsError(CardError): + """Invalid parameters provided during card operation.""" + message = "Invalid Params." + + def __init__(self, data=None): + super().__init__(-32602, self.message, data) + + +class AccessDeniedError(CardError): + """Access denied for the card operation.""" + message = "Access denied." + + def __init__(self, data=None): + super().__init__(-32504, self.message, data) + + +class CardNotFoundError(CardError): + """Card not found during operation.""" + message = "Card not found." + + def __init__(self, data=None): + super().__init__(-31400, self.message, data) + + +class SmsNotConnectedError(CardError): + """SMS notification not connected.""" + message = "SMS notification not connected." + + def __init__(self, data=None): + super().__init__(-31301, self.message, data) + + +class CardExpiredError(CardError): + """Card has expired.""" + message = "Card has expired." + + def __init__(self, data=None): + super().__init__(-31301, self.message, data) + + +class CardBlockedError(CardError): + """Card is blocked.""" + message = "Card is blocked." + + def __init__(self, data=None): + super().__init__(-31301, self.message, data) + + +class CorporateCardError(CardError): + """Financial operations with corporate cards are not allowed.""" + message = "Financial operations with corporate cards are not allowed." + + def __init__(self, data=None): + super().__init__(-31300, self.message, data) + + +class BalanceError(CardError): + """Unable to retrieve card balance. Please try again later.""" + message = "Unable to retrieve card balance. Please try again later." + + def __init__(self, data=None): + super().__init__(-31302, self.message, data) + + +class InsufficientFundsError(CardError): + """Insufficient funds on the card.""" + message = "Insufficient funds on the card." + + def __init__(self, data=None): + super().__init__(-31303, self.message, data) + + +class InvalidCardNumberError(CardError): + """Invalid card number provided.""" + message = "Invalid card number." + + def __init__(self, data=None): + super().__init__(-31300, self.message, data) + + +class CardNotFoundWithNumberError(CardError): + """Card with the provided number not found.""" + message = "Card with this number not found." + + def __init__(self, data=None): + super().__init__(-31300, self.message, data) + + +class InvalidExpiryDateError(CardError): + """Invalid expiry date provided for the card.""" + message = "Invalid expiry date for the card." + + def __init__(self, data=None): + super().__init__(-31300, self.message, data) + + +class ProcessingServerError(CardError): + """Processing center server is unavailable. Please try again later.""" + message = \ + "Processing center server is unavailable. Please try again later." + + def __init__(self, data=None): + super().__init__(-31002, self.message, data) + + +# OTP Module Errors + +class OtpError(BaseError): + """Base class for OTP-related errors.""" + + +class OtpSendError(OtpError): + """Error occurred while sending OTP.""" + message = "Error occurred while sending SMS. Please try again." + + def __init__(self, data=None): + super().__init__(-31110, self.message, data) + + +class OtpCheckError(OtpError): + """Base class for OTP check errors.""" + + +class OtpExpiredError(OtpCheckError): + """OTP code has expired. Request a new code.""" + message = "OTP code has expired. Request a new code." + + def __init__(self, data=None): + super().__init__(-31101, self.message, data) + + +class OtpAttemptsExceededError(OtpCheckError): + """ + Number of attempts to enter the code has been exceeded. Request a new code. + """ + message = "Number of attempts to enter the code has been exceeded." + + def __init__(self, data=None): + super().__init__(-31102, self.message, data) + + +class OtpInvalidCodeError(OtpCheckError): + """Invalid OTP code entered.""" + message = "Invalid OTP code." + + def __init__(self, data=None): + super().__init__(-31103, self.message, data) + + +class PaymeNetworkError(BaseError): + """Network error occurred during request to Payme server.""" + message = "Network error occurred during request to Payme server." + + def __init__(self, data=None): + super().__init__(self.message, data) + + +class ReceiptsNotFoundError(BaseException): + """No receipts found for the given transaction ID.""" + def __init__(self, message="No receipts found for the given transaction ID.", data=None): + super().__init__(message, data) + + +errors_map = { + -32300: TransportError, + -32700: ParseError, + -32600: InvalidRequestError, + -32601: MethodNotFoundError, + -32602: InvalidParamsError, + -32504: AccessDeniedError, + -31400: CardNotFoundError, + -31301: SmsNotConnectedError, + -31302: BalanceError, + -31303: InsufficientFundsError, + -31300: InvalidCardNumberError, + -31002: ProcessingServerError, + -31110: OtpSendError, + -31101: OtpExpiredError, + -31102: OtpAttemptsExceededError, + -31103: OtpInvalidCodeError, + -31602: ReceiptsNotFoundError +} diff --git a/payme/exceptions/webhook.py b/payme/exceptions/webhook.py new file mode 100644 index 0000000..6b841ff --- /dev/null +++ b/payme/exceptions/webhook.py @@ -0,0 +1,125 @@ +""" +Init Payme base exception. +""" +import logging +from rest_framework.exceptions import APIException + +logger = logging.getLogger(__name__) + + +class BasePaymeException(APIException): + """ + BasePaymeException inherits from APIException. + """ + status_code = 200 + error_code = None + message = None + + # pylint: disable=super-init-not-called + def __init__(self, message: str = None): + detail: dict = { + "error": { + "code": self.error_code, + "message": self.message, + "data": message + } + } + logger.error(f"Payme error detail: {detail}") + self.detail = detail + + +class PermissionDenied(BasePaymeException): + """ + PermissionDenied APIException. + + Raised when the client is not allowed to access the server. + """ + status_code = 200 + error_code = -32504 + message = "Permission denied." + + +class InternalServiceError(BasePaymeException): + """ + InternalServiceError APIException. + + Raised when a transaction fails to perform. + """ + status_code = 200 + error_code = -32400 + message = { + "uz": "Tizimda xatolik yuzaga keldi.", + "ru": "Внутренняя ошибка сервиса.", + "en": "Internal service error." + } + + +class MethodNotFound(BasePaymeException): + """ + MethodNotFound APIException. + + Raised when the requested method does not exist. + """ + status_code = 405 + error_code = -32601 + message = "Method not found." + + +class AccountDoesNotExist(BasePaymeException): + """ + AccountDoesNotExist APIException. + + Raised when an account does not exist or has been deleted. + """ + status_code = 200 + error_code = -31050 + message = { + "uz": "Hisob topilmadi.", + "ru": "Счет не найден.", + "en": "Account does not exist." + } + + +class IncorrectAmount(BasePaymeException): + """ + IncorrectAmount APIException. + + Raised when the provided amount is incorrect. + """ + status_code = 200 + error_code = -31001 + message = { + 'ru': 'Неверная сумма.', + 'uz': "Noto'g'ri summa.", + 'en': 'Incorrect amount.' + } + + +class TransactionAlreadyExists(BasePaymeException): + """ + TransactionAlreadyExists APIException. + + Raised when a transaction already exists in the system, + preventing the creation of a new transaction with the same identifier. + + Attributes: + status_code (int): The HTTP status code for the response. + error_code (int): The specific error code for this exception. + message (dict): A dictionary containing localized error messages. + """ + status_code = 200 + error_code = -31099 + message = { + "uz": "Tranzaksiya allaqachon mavjud.", + "ru": "Транзакция уже существует.", + "en": "Transaction already exists." + } + + +exception_whitelist = ( + IncorrectAmount, + MethodNotFound, + PermissionDenied, + AccountDoesNotExist, + TransactionAlreadyExists +) diff --git a/payme/migrations/0001_initial.py b/payme/migrations/0001_initial.py new file mode 100644 index 0000000..a09fabc --- /dev/null +++ b/payme/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 5.1.2 on 2024-10-18 17:06 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='PaymeTransactions', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('transaction_id', models.CharField(max_length=50)), + ('amount', models.DecimalField(decimal_places=2, max_digits=10)), + ('state', models.IntegerField(choices=[(0, 'CREATED'), (1, 'WITHDRAWAL_IN_PROGRESS_1'), (2, 'WITHDRAWAL_IN_PROGRESS_2'), (3, 'WITHDRAWAL_CLOSING'), (4, 'SUCCESS'), (20, 'WAITING_TO_BE_CHECKED'), (21, 'WAITING_TO_BE_CANCELLED_1'), (30, 'WAITING_TO_BE_CANCELLED_2'), (50, 'CANCELED'), (-2, 'CANCELLED_WITHDRAWAL_IN_PROGRESS_2'), (-1, 'CANCELLED_AFTER_WITHDRAWAL_IN_PROGRESS_1')], default=0)), + ('cancel_reason', models.IntegerField(blank=True, choices=[(1, 'RECIPIENT_NOT_FOUND'), (2, 'DEBIT_OPERATION_FAILED'), (3, 'TRANSACTION_FAILED'), (4, 'TRANSACTION_TIMEOUT'), (5, 'REFUND'), (10, 'UNKNOWN_ERROR')], null=True)), + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True, db_index=True)), + ('performed_at', models.DateTimeField(blank=True, db_index=True, null=True)), + ('cancelled_at', models.DateTimeField(blank=True, db_index=True, null=True)), + ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payme_transactions', to='users.client')), + ], + options={ + 'verbose_name': 'Payme Transaction', + 'verbose_name_plural': 'Payme Transactions', + 'db_table': 'payme_transactions', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/lib/payme/decorators/__init__.py b/payme/migrations/__init__.py similarity index 100% rename from lib/payme/decorators/__init__.py rename to payme/migrations/__init__.py diff --git a/payme/models.py b/payme/models.py new file mode 100644 index 0000000..ab970e4 --- /dev/null +++ b/payme/models.py @@ -0,0 +1,156 @@ +from django.db import models +from django.conf import settings +from django.utils import timezone + +from django.utils.module_loading import import_string + +from payme.const import PaymeReasonCodesEnum +from payme.const import PaymeTransactionStateEnum as Status + + +AccountModel = import_string(settings.PAYME_ACCOUNT_MODEL) + + +class PaymeTransactions(models.Model): + """ + Model to store payment transactions. + """ + account_related_name = "payme_transactions" + state_choices = Status.choices() + cancel_reason_choices = PaymeReasonCodesEnum.choices() + state_default = Status.CREATED.value + + transaction_id = models.CharField(max_length=50) + account = models.ForeignKey( + AccountModel, + related_name=account_related_name, + on_delete=models.CASCADE + ) + amount = models.DecimalField(max_digits=10, decimal_places=2) + state = models.IntegerField(choices=state_choices, default=state_default) + cancel_reason = models.IntegerField( + choices=cancel_reason_choices, null=True, blank=True + ) + created_at = models.DateTimeField(auto_now_add=True, db_index=True) + updated_at = models.DateTimeField(auto_now=True, db_index=True) + performed_at = models.DateTimeField(null=True, blank=True, db_index=True) + cancelled_at = models.DateTimeField(null=True, blank=True, db_index=True) + + class Meta: + """ + Model Meta options. + """ + verbose_name = "Payme Transaction" + verbose_name_plural = "Payme Transactions" + ordering = ["-created_at"] + db_table = "payme_transactions" + + def __str__(self): + """ + String representation of the PaymentTransaction model. + """ + return f"Payme Transaction #{self.transaction_id} Account: {self.account} - {self.state}" # noqa + + @classmethod + def get_or_create( + cls, transaction_id, + amount, state, account + ) -> tuple["PaymeTransactions", bool]: + """ + Class method to get or create a PaymentTransaction instance. + + :param transaction_id: The unique ID of the transaction. + :param amount: The amount for the transaction. + :param state: The current state of the transaction. + :param account: The related account instance for the transaction. + :return: (instance, created) tuple where 'instance' is the + PaymentTransaction instance and 'created' is a boolean indicating + if it was created or not. + """ + transaction, created = cls.objects.get_or_create( + account=account, + defaults={ + 'amount': amount, + 'state': state, + 'transaction_id': transaction_id + } + ) + return transaction, created + + @classmethod + def get_by_transaction_id(cls, transaction_id): + """ + Class method to get a PaymentTransaction instance by its transaction ID. # noqa + + :param transaction_id: The unique ID of the transaction. + :return: The PaymentTransaction instance or None if not found. + """ + return cls.objects.get(transaction_id=transaction_id) + + def is_performed(self) -> bool: + """ + Check if the transaction is completed. + + :return: True if the transaction is completed, False otherwise. + """ + return self.state == Status.WITHDRAWAL_IN_PROGRESS_2.value + + def is_cancelled(self) -> bool: + """ + Check if the transaction is cancelled. + + :return: True if the transaction is cancelled, False otherwise. + """ + return self.state in [ + Status.CANCELED.value, + Status.CANCELLED_WITHDRAWAL_IN_PROGRESS_2.value, + Status.CANCELLED_AFTER_WITHDRAWAL_IN_PROGRESS_1.value + ] + + def is_created(self) -> bool: + """ + Check if the transaction is created. + + :return: True if the transaction is created, False otherwise. + """ + return self.state == Status.CREATED.value + + def is_created_in_payme(self) -> bool: + """ + Check if the transaction was created in Payme. + + :return: True if the transaction was created in Payme, False otherwise. + """ + return self.state == Status.WITHDRAWAL_IN_PROGRESS_1.value + + def mark_as_cancelled( + self, cancel_reason: int, state: int + ) -> "PaymeTransactions": + """ + Mark the transaction as cancelled. + + :param cancel_reason: The reason for cancelling the transaction. + :return:True if the transaction was successfully marked as cancelled, False otherwise. # noqa + """ + if self.state == state: + return self + + self.state = state + self.cancel_reason = cancel_reason + self.cancelled_at = timezone.now() + self.save() + return self + + def mark_as_performed(self) -> bool: + """ + Mark the transaction as performed. + + :return: True if the transaction was successfully marked as performed, False otherwise. # noqa + """ + if self.state != Status.WITHDRAWAL_IN_PROGRESS_1.value: + return False + + self.state = Status.WITHDRAWAL_IN_PROGRESS_2.value + self.performed_at = timezone.now() + self.save() + return True diff --git a/lib/payme/errors/__init__.py b/payme/types/__init__.py similarity index 100% rename from lib/payme/errors/__init__.py rename to payme/types/__init__.py diff --git a/lib/payme/methods/__init__.py b/payme/types/request/__init__.py similarity index 100% rename from lib/payme/methods/__init__.py rename to payme/types/request/__init__.py diff --git a/payme/types/response/__init__.py b/payme/types/response/__init__.py new file mode 100644 index 0000000..5e2e988 --- /dev/null +++ b/payme/types/response/__init__.py @@ -0,0 +1,4 @@ +""" +init all response typing of payme provider +""" +from .webhook import * # noqa diff --git a/payme/types/response/cards.py b/payme/types/response/cards.py new file mode 100644 index 0000000..36c89c2 --- /dev/null +++ b/payme/types/response/cards.py @@ -0,0 +1,110 @@ +from typing import Dict, Optional +from dataclasses import dataclass + + +class Common: + """ + The common response structure. + """ + + @classmethod + def from_dict(cls, data: Dict): + """ + Prepare fields for nested dataclasses + """ + field_values = {} + for field in cls.__dataclass_fields__: + field_type = cls.__dataclass_fields__[field].type + field_data = data.get(field) + + if isinstance(field_data, dict) and issubclass(field_type, Common): + field_values[field] = field_type.from_dict(field_data) + else: + field_values[field] = field_data + + return cls(**field_values) + + +@dataclass +class Card(Common): + """ + The card object represents a credit card. + """ + number: str + expire: str + token: str + recurrent: bool + verify: bool + type: str + number_hash: Optional[str] = None + + +@dataclass +class Result(Common): + """ + The result object contains the created card. + """ + card: Card + + +@dataclass +class CardsCreateResponse(Common): + """ + The cards.create response. + """ + jsonrpc: str + result: Result + + +@dataclass +class VerifyResult(Common): + """ + The result object for the verification response. + """ + sent: bool + phone: str + wait: int + + +@dataclass +class GetVerifyResponse(Common): + """ + The verification response structure. + """ + jsonrpc: str + result: VerifyResult + + +@dataclass +class VerifyResponse(Common): + """ + The verification response structure. + """ + jsonrpc: str + result: Result + + +@dataclass +class RemoveCardResult(Common): + """ + The result object for the removal response. + """ + success: bool + + +@dataclass +class RemoveResponse(Common): + """ + The remove response structure. + """ + jsonrpc: str + result: RemoveCardResult + + +@dataclass +class CheckResponse(Common): + """ + The check response structure. + """ + jsonrpc: str + result: Result diff --git a/payme/types/response/receipts.py b/payme/types/response/receipts.py new file mode 100644 index 0000000..6fe5014 --- /dev/null +++ b/payme/types/response/receipts.py @@ -0,0 +1,216 @@ +from dataclasses import dataclass +from typing import Dict, Optional, Union + + +class Common: + """ + The common response structure. + """ + jsonrpc: str + id: int + + @classmethod + def from_dict(cls, data: Dict): + """ + Prepare fields for nested dataclasses + """ + field_values = {} + for field in cls.__dataclass_fields__: + field_type = cls.__dataclass_fields__[field].type + field_data = data.get(field) + + if isinstance(field_data, dict) and issubclass(field_type, Common): + field_values[field] = field_type.from_dict(field_data) + else: + field_values[field] = field_data + + return cls(**field_values) + + +@dataclass +class Account(Common): + """ + The account object represents a user's banking account. + """ + _id: str + account_number: str + account_name: str + account_type: str + bank_name: str + currency: str + status: str + + +@dataclass +class PaymentMethod(Common): + """ + The payment method object represents a user's payment method. + """ + name: str + title: str + value: str + main: Optional[bool] = None + + +@dataclass +class Detail(Common): + """ + The detail object represents additional details for a receipt. + """ + discount: Optional[str] = None + shipping: Optional[str] = None + items: Optional[str] = None + + +# pylint: disable=C0103 +@dataclass +class MerchantEpos(Common): + """ + The merchantEpos object represents a user's ePOS. + """ + eposId: str + eposName: str + eposType: str + eposTerminalId: str + + +@dataclass +class Meta(Common): + """ + The meta object represents additional metadata for a receipt. + """ + source: any = None + owner: any = None + host: any = None + + +@dataclass +class Merchant: + """ + The merchant object represents a user's merchant. + """ + _id: str + name: str + organization: str + address: Optional[str] = None + business_id: Optional[str] = None + epos: Optional[MerchantEpos] = None + restrictions: Optional[str] = None + date: Optional[int] = None + logo: Optional[str] = None + type: Optional[str] = None + terms: Optional[str] = None + + +@dataclass +class Payer(Common): + """ + The payer object represents a user's payer. + """ + phone: str + + +@dataclass +class Receipt(Common): + """ + The receipt object represents a payment receipt. + """ + _id: str + create_time: int + pay_time: int + cancel_time: int + state: int + type: int + external: bool + operation: int + category: any = None + error: any = None + description: str = None + detail: Detail = None + currency: int = None + commission: int = None + card: str = None + creator: str = None + payer: Payer = None + amount: Union[float, int] = None + account: list[Account] = None + merchant: Merchant = None + processing_id: str = None + meta: Meta = None + + +@dataclass +class CreateResult(Common): + """ + The result object for the create response. + """ + receipt: Receipt + + +@dataclass +class CreateResponse(Common): + """ + The create response structure. + """ + result: CreateResult + + +@dataclass +class PayResponse(CreateResponse): + """ + The pay response structure. + """ + + +@dataclass +class SendResult(Common): + """ + The result object for the send response. + """ + success: bool + + +@dataclass +class SendResponse(Common): + """ + The send response structure. + """ + result: SendResult + + +@dataclass +class CancelResponse(CreateResponse): + """ + The cancel response structure. + """ + + +@dataclass +class CheckResult(Common): + """ + The result object for the check response. + """ + state: int + + +@dataclass +class CheckResponse(Common): + """ + The check response structure. + """ + result: CheckResult + + +@dataclass +class GetResponse(CreateResponse): + """ + The result object for the get response. + """ + + +@dataclass +class GetAllResponse(Common): + """ + The result object for the get all response. + """ + result: list[Receipt] = None diff --git a/payme/types/response/webhook.py b/payme/types/response/webhook.py new file mode 100644 index 0000000..d081ecf --- /dev/null +++ b/payme/types/response/webhook.py @@ -0,0 +1,136 @@ +from dataclasses import dataclass, field +from typing import List, Optional, Dict + + +class CommonResponse: + """ + The common response structure + """ + def as_resp(self): + response = {'result': {}} + for key, value in self.__dict__.items(): + response['result'][key] = value + return response + + +@dataclass +class Shipping(CommonResponse): + """ + Shipping information response structure + """ + title: str + price: int + + +@dataclass +class Item(CommonResponse): + """ + Item information response structure + """ + discount: int + title: str + price: int + count: int + code: str + units: int + vat_percent: int + package_code: str + + def as_resp(self): + return { + "discount": self.discount, + "title": self.title, + "price": self.price, + "count": self.count, + "code": self.code, + "units": self.units, + "vat_percent": self.vat_percent, + "package_code": self.package_code + } + + +@dataclass +class CheckPerformTransaction(CommonResponse): + """ + Receipt information response structure for transaction checks. + """ + allow: bool + additional: Optional[Dict[str, str]] = None + receipt_type: Optional[int] = None + shipping: Optional[Shipping] = None + items: List[Item] = field(default_factory=list) + + def add_item(self, item: Item): + self.items.append(item) + + def as_resp(self): + detail_dict = {} + receipt_dict = {"allow": self.allow} + + if self.additional: + receipt_dict["additional"] = self.additional + + if isinstance(self.receipt_type, int): + detail_dict["receipt_type"] = self.receipt_type + + if self.shipping: + detail_dict["shipping"] = self.shipping.as_resp() + + if self.items: + detail_dict["items"] = [item.as_resp() for item in self.items] + + if detail_dict: + receipt_dict["detail"] = detail_dict + + return {"result": receipt_dict} + + +@dataclass +class CreateTransaction(CommonResponse): + """ + The create transaction request + """ + transaction: str + state: str + create_time: str + + +@dataclass +class PerformTransaction(CommonResponse): + """ + The perform transaction response + """ + transaction: str + state: str + perform_time: str + + +@dataclass +class CancelTransaction(CommonResponse): + """ + The cancel transaction request + """ + transaction: str + state: str + cancel_time: str + + +@dataclass +class CheckTransaction(CommonResponse): + """ + The check transaction request + """ + transaction: str + state: str + reason: str + create_time: str + perform_time: Optional[str] = None + cancel_time: Optional[str] = None + + +@dataclass +class GetStatement(CommonResponse): + """ + The check perform transactions response + """ + transactions: List[str] diff --git a/payme/urls.py b/payme/urls.py new file mode 100644 index 0000000..2c3e96e --- /dev/null +++ b/payme/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from payme.views import PaymeWebHookAPIView + + +urlpatterns = [ + path("update/", PaymeWebHookAPIView.as_view()) +] diff --git a/payme/util.py b/payme/util.py new file mode 100644 index 0000000..11cb81b --- /dev/null +++ b/payme/util.py @@ -0,0 +1,53 @@ +from functools import wraps +from datetime import datetime +from typing import get_type_hints + + +def time_to_payme(datatime) -> int: + """ + Convert datetime object to Payme's datetime format. + + Payme's datetime format is in the format: YYYY-MM-DD HH:MM:SS.ssssss + + Args: + datatime (datetime): The datetime object to convert. + + Returns: + str: The datetime object in Payme's datetime format. + """ + if not datatime: + return 0 + + return int(datatime.timestamp() * 1000) + + +def time_to_service(milliseconds: int) -> datetime: + """ + Converts milliseconds since the epoch to a datetime object. + """ + return datetime.fromtimestamp(milliseconds / 1000) + + +def input_type_checker(func): + """ + input type checker decorator helps to + validate the input types of the function before executing it. + """ + @wraps(func) + def wrapper(*args, **kwargs): + """ + Get the type hints of the function + """ + hints = get_type_hints(func) + + all_args = kwargs.copy() + all_args.update(zip(func.__code__.co_varnames, args)) + + for arg_name, arg_type in hints.items(): + if arg_name in all_args and not isinstance(all_args[arg_name], arg_type): # noqa + raise TypeError( + f"Argument '{arg_name}' in {func.__name__} must be of type {arg_type.__name__}, " # noqa + f"but got {type(all_args[arg_name]).__name__}." + ) + return func(*args, **kwargs) + return wrapper diff --git a/payme/views.py b/payme/views.py new file mode 100644 index 0000000..ef99387 --- /dev/null +++ b/payme/views.py @@ -0,0 +1,325 @@ +import base64 +import logging +import binascii +from decimal import Decimal + +from django.conf import settings +from django.utils.module_loading import import_string + +from rest_framework import views +from rest_framework.response import Response + +from payme import exceptions +from payme.types import response +from payme.models import PaymeTransactions +from payme.const import PaymeTransactionStateEnum +from payme.util import time_to_payme, time_to_service + +logger = logging.getLogger(__name__) +AccountModel = import_string(settings.PAYME_ACCOUNT_MODEL) + + +def handle_exceptions(func): + """ + Decorator to handle exceptions and raise appropriate Payme exceptions. + """ + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except KeyError as exc: + message = "Invalid parameters received." + logger.error(f"{message}: {exc}s {exc} {args} {kwargs}") + raise exceptions.InternalServiceError(message) from exc + + except AccountModel.DoesNotExist as exc: + logger.error(f"Account does not exist: {exc} {args} {kwargs}") + raise exceptions.AccountDoesNotExist(str(exc)) from exc + + except PaymeTransactions.DoesNotExist as exc: + logger.error(f"Transaction does not exist: {exc} {args} {kwargs}") + raise exceptions.TransactionNotFound(str(exc)) from exc + + except exceptions.exception_whiltelist as exc: + # No need to raise exception for exception whitelist + raise exc + except Exception as exc: + logger.error(f"Unexpected error: {exc} {args} {kwargs}") + raise exceptions.InternalServiceError(str(exc)) from exc + + return wrapper + + +class PaymeWebHookAPIView(views.APIView): + """ + A webhook view for Payme. + """ + authentication_classes = () + + def post(self, request, *args, **kwargs): + """ + Handle the incoming webhook request. + """ + self.__check_authorize(request) + + payme_methods = { + "GetStatement": self.get_statement, + "CancelTransaction": self.cancel_transaction, + "PerformTransaction": self.perform_transaction, + "CreateTransaction": self.create_transaction, + "CheckTransaction": self.check_transaction, + "CheckPerformTransaction": self.check_perform_transaction, + } + + try: + method = request.data["method"] + params = request.data["params"] + except KeyError as exc: + message = f"Error processing webhook: {exc}" + raise exceptions.InternalServiceError(message) from exc + + if method in payme_methods: + result = payme_methods[method](params) + return Response(result) + + raise exceptions.MethodNotFound("Method not supported yet!") + + @staticmethod + def __check_authorize(request): + """ + Verify the integrity of the request using the merchant key. + """ + password = request.META.get('HTTP_AUTHORIZATION') + if not password: + raise exceptions.PermissionDenied("Missing authentication credentials") + + password = password.split()[-1] + + try: + password = base64.b64decode(password).decode('utf-8') + except (binascii.Error, UnicodeDecodeError) as exc: + raise exceptions.PermissionDenied("Decoding error in authentication credentials") from exc + + try: + payme_key = password.split(':')[-1] + except IndexError as exc: + message = "Invalid merchant key format in authentication credentials" + raise exceptions.PermissionDenied(message) from exc + + if payme_key != settings.PAYME_KEY: + raise exceptions.PermissionDenied("Invalid merchant key specified") + + @handle_exceptions + def fetch_account(self, params: dict): + """ + Fetch account based on settings and params. + """ + account_field = settings.PAYME_ACCOUNT_FIELD + account_value = params['account'].get(account_field) + if not account_value: + raise exceptions.InvalidAccount("Missing account field in parameters.") + + account = AccountModel.objects.get(**{account_field: account_value}) + + return account + + @handle_exceptions + def validate_amount(self, account, amount): + """ + Validates if the amount matches for one-time payment accounts. + """ + if settings.PAYME_ONE_TIME_PAYMENT: + account_amount = getattr(account, settings.PAYME_AMOUNT_FIELD) + if Decimal(account_amount) != Decimal(amount): + message = f"Invalid amount. Expected: {account_amount}, received: {amount}" + raise exceptions.IncorrectAmount(message) + + @handle_exceptions + def check_perform_transaction(self, params) -> response.CheckPerformTransaction: + """ + Handle the pre_create_transaction action. + """ + account = self.fetch_account(params) + self.validate_amount(account, params.get('amount')) + + result = response.CheckPerformTransaction(allow=True) + return result.as_resp() + + @handle_exceptions + def create_transaction(self, params) -> response.CreateTransaction: + """ + Handle the create_transaction action. + """ + transaction_id = params["id"] + amount = Decimal(params.get('amount', 0)) + account = self.fetch_account(params) + + self.validate_amount(account, amount) + + defaults = { + "amount": amount, + "state": PaymeTransactionStateEnum.WITHDRAWAL_IN_PROGRESS_1.value, + "account": account, + } + + transaction, created = PaymeTransactions.objects.get_or_create( + transaction_id=transaction_id, + defaults=defaults + ) + + # Handle already existing transaction with the same ID for one-time payments + if not created and settings.PAYME_ONE_TIME_PAYMENT: + if transaction.transaction_id != transaction_id: + message = f"Transaction {transaction_id} already exists (Payme)." + logger.warning(message) + raise exceptions.TransactionAlreadyExists(message) + + result = response.CreateTransaction( + transaction=transaction.transaction_id, + state=transaction.state, + create_time=time_to_payme(transaction.created_at), + ) + result = result.as_resp() + + # callback event + self.handle_created_payment(params, result) + + return result + + @handle_exceptions + def perform_transaction(self, params) -> response.PerformTransaction: + """ + Handle the successful payment. + """ + transaction = PaymeTransactions.get_by_transaction_id(transaction_id=params["id"]) + + if transaction.is_performed(): + result = response.PerformTransaction( + transaction=transaction.transaction_id, + state=transaction.state, + perform_time=time_to_payme(transaction.performed_at), + ) + return result.as_resp() + + transaction.mark_as_performed() + + result = response.PerformTransaction( + transaction=transaction.transaction_id, + state=transaction.state, + perform_time=time_to_payme(transaction.performed_at), + ) + result = result.as_resp() + + # callback successfully event + self.handle_successfully_payment(params, result) + + return result + + @handle_exceptions + def check_transaction(self, params: dict) -> dict | str | response.CheckPerformTransaction: + """ + Handle check transaction request. + """ + transaction = PaymeTransactions.get_by_transaction_id(transaction_id=params["id"]) + + result = response.CheckTransaction( + transaction=transaction.transaction_id, + state=transaction.state, + reason=transaction.cancel_reason, + create_time=time_to_payme(transaction.created_at), + perform_time=time_to_payme(transaction.performed_at), + cancel_time=time_to_payme(transaction.cancelled_at), + ) + + return result.as_resp() + + @handle_exceptions + def cancel_transaction(self, params) -> response.CancelTransaction: + """ + Handle the cancelled payment. + """ + transaction = PaymeTransactions.get_by_transaction_id(transaction_id=params["id"]) + + if transaction.is_cancelled(): + return self._cancel_response(transaction) + + if transaction.is_performed(): + transaction.mark_as_cancelled( + cancel_reason=params["reason"], + state=PaymeTransactionStateEnum.CANCELLED_WITHDRAWAL_IN_PROGRESS_2.value + ) + elif transaction.is_created_in_payme(): + transaction.mark_as_cancelled( + cancel_reason=params["reason"], + state=PaymeTransactionStateEnum.CANCELLED_AFTER_WITHDRAWAL_IN_PROGRESS_1.value + ) + + result = self._cancel_response(transaction) + + # callback cancelled transaction event + self.handle_cancelled_payment(params, result) + + return result + + @handle_exceptions + def get_statement(self, params) -> response.GetStatement: + """ + Retrieves a statement of transactions. + """ + date_range = [time_to_service(params['from']), time_to_service(params['to'])] + + transactions = PaymeTransactions.objects.filter( + created_at__range=date_range + ).order_by('-created_at') + + result = response.GetStatement(transactions=[]) + + for transaction in transactions: + result.transactions.append({ + "transaction": transaction.transaction_id, + "amount": transaction.amount, + "account": { + settings.PAYME_ACCOUNT_FIELD: transaction.account.id + }, + "reason": transaction.cancel_reason, + "state": transaction.state, + "create_time": time_to_payme(transaction.created_at), + "perform_time": time_to_payme(transaction.performed_at), + "cancel_time": time_to_payme(transaction.cancelled_at), + }) + + return result.as_resp() + + def _cancel_response(self, transaction): + """ + Helper method to generate cancel transaction response. + """ + result = response.CancelTransaction( + transaction=transaction.transaction_id, + state=transaction.state, + cancel_time=time_to_payme(transaction.cancelled_at), + ) + return result.as_resp() + + def handle_pre_payment(self, params, result, *args, **kwargs): + """ + Handle the pre_create_transaction action. You can override this method + """ + print(f"Transaction pre_created for this params: {params} and pre_created_result: {result}") + + def handle_created_payment(self, params, result, *args, **kwargs): + """ + Handle the successful payment. You can override this method + """ + print(f"Transaction created for this params: {params} and cr_result: {result}") + + def handle_successfully_payment(self, params, result, *args, **kwargs): + """ + Handle the successful payment. You can override this method + """ + print(f"Transaction successfully performed for this params: {params} and performed_result: {result}") + + def handle_cancelled_payment(self, params, result, *args, **kwargs): + """ + Handle the cancelled payment. You can override this method + """ + print(f"Transaction cancelled for this params: {params} and cancelled_result: {result}") diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt index bd8a17e..c611aae 100644 --- a/requirements/dev-requirements.txt +++ b/requirements/dev-requirements.txt @@ -8,3 +8,5 @@ twine==3.8.0 mypy==0.961 pip-upgrader==1.4.15 wheel==0.38.1 +djangorestframework==3.14.0 +environs==9.5.0 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 0739c50..d963332 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,3 +1,3 @@ requests==2.* dataclasses==0.* -djangorestframework==3.14.0 +djangorestframework>=3.15.1,<3.16.0 diff --git a/setup.py b/setup.py index 289e665..55fe519 100644 --- a/setup.py +++ b/setup.py @@ -3,17 +3,16 @@ setup( name='payme-pkg', - version='2.5.3', + version='3.0.0b1', license='MIT', author="Muhammadali Akbarov", author_email='muhammadali17abc@gmail.com', - packages=find_packages('lib'), - package_dir={'': 'lib'}, + packages=find_packages(), url='https://github.com/Muhammadali-Akbarov/payme-pkg', keywords='paymeuz paycomuz payme-merchant merchant-api subscribe-api payme-pkg payme-api', install_requires=[ 'requests==2.*', - "dataclasses==0.*;python_version<'3.7'", # will only install on py3.6 - 'djangorestframework==3.14.0' + "dataclasses==0.*;python_version<'3.7'", # will only install on py3.6 + 'djangorestframework==3.*' ], ) diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..45efc3a --- /dev/null +++ b/tests.py @@ -0,0 +1,42 @@ +import os + +import unittest +from payme import Payme + + +class TestPaymeAPI(unittest.TestCase): + """ + Test Payme API methods. + """ + @classmethod + def setUpClass(cls): + """ + Initialize the Payme client in test mode + """ + cls.payme = Payme( + payme_id=os.getenv("PAYME_ID"), + payme_key=os.getenv("PAYME_KEY"), + is_test_mode=True + ) + + def test_cards_all_methods(self): + """ + Verify that the cards test method works without errors + """ + return self.payme.cards.test() + + def test_receipts_all_methods(self): + """ + Verify that the receipts test method works without errors + """ + return self.payme.receipts.test() + + def test_initializer_all_methods(self): + """ + Verify that the initializer test method works without errors + """ + return self.payme.initializer.test() + + +if __name__ == '__main__': + unittest.main()