diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..6011155 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,16 @@ +name: Documentation + +on: + push: + branches: + - payme-pkg-3.0b +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.x + - run: pip install -r requirements/docs-requirements.txt + - run: mkdocs gh-deploy --force diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 4046520..054b0a2 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -21,4 +21,4 @@ jobs: pip install pylint - name: Analysing the code with pylint run: | - pylint ./lib/* + pylint ./lib/* \ No newline at end of file diff --git a/.gitignore b/.gitignore index 998cacb..56dbef2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,19 @@ +# virtual env +env/ +venv/ + +# 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 diff --git a/.pylintrc b/.pylintrc index 70226f1..1df69b9 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,5 +1,5 @@ [MASTER] -disable=no-member, +disable=no-member,invalid-name, unnecessary-pass, useless-option-value, too-few-public-methods, diff --git a/docs/src/assets/logo.png b/docs/src/assets/logo.png new file mode 100644 index 0000000..35d3c11 Binary files /dev/null and b/docs/src/assets/logo.png differ diff --git a/docs/src/en/index.md b/docs/src/en/index.md new file mode 100644 index 0000000..1d1a380 --- /dev/null +++ b/docs/src/en/index.md @@ -0,0 +1,65 @@ +--- +title: Introduction +--- + +# **Welcome!** + +### Uzbekistan Payment Providers Packages + +[PayTechUz](https://pay-tech.uz) + +Welcome to the PayTechUz organization! This project aims to provide developers with seamless integration and implementation of Payment Providers APIs in Uzbekistan. Whether you're a Python enthusiast or a Go guru, we've got you covered with our versatile tech stack. + +## Table of Contents + +- [Introduction](#introduction) +- [Website](#website) +- [Telegram Group](#telegram-group) +- [Tech Stack](#tech-stack) +- [Usage](#usage) +- [Contributing](#contributing) +- [Support](#support) +- [License](#license) + +## Introduction + +Integrating payment providers into your applications is crucial in today's digital landscape. This project simplifies the process of integrating Payment Providers APIs in Uzbekistan, allowing you to focus on building amazing products while leaving the payment processing worries behind. + +## Website + +Visit our website [pay-tech.uz](https://pay-tech.uz) to explore detailed documentation, guides, and API references. We regularly update the documentation to ensure you have the most up-to-date information at your disposal. + +## Telegram Group + +Join our vibrant developer community on Telegram to connect with fellow developers, ask questions, share experiences, and stay updated with the latest news and announcements. + +[Telegram Group](https://t.me/+krHlGUizJrI1Zjli) + +## Tech Stack + +We have carefully chosen the following tech stack to ensure flexibility and ease of use: + +- [![Python](https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white)](https://python.org/) +- [![Go](https://img.shields.io/badge/Go-00ADD8?style=for-the-badge&logo=go&logoColor=white)](https://go.dev/) + +The combination of Python and Go offers a wide range of possibilities to cater to the preferences of different developers. + +## Usage + +Our packages are designed to streamline the integration process. Follow the detailed instructions in the documentation for each package to integrate the desired Payment Providers API seamlessly. + +## Contributing + +We welcome contributions from the community to make this project even better. If you find any issues or have suggestions for improvements, feel free to open an issue or submit a pull request. Please ensure you follow our [contribution guidelines](https://github.com/PayTechUz/.github/blob/main/profile/CONTRIBUTING.md). + +## Support + +If you encounter any problems, have questions, or need assistance with the integration, our team is here to help. Reach out to us on our [Telegram Group](https://t.me/+ydVV_9B3Xh02NGEy) or email us at paytechuz@gmail.com. + +## License + +This project is licensed under the [MIT License](https://github.com/PayTechUz/.github/blob/main/profile/LICENSE.txt), making it open and free for everyone to use, modify, and distribute. + +--- + +We hope our PayTechUz packages make your payment integration process a delightful experience. Happy coding! diff --git a/docs/src/en/merchant-api.md b/docs/src/en/merchant-api.md new file mode 100644 index 0000000..83c1c74 --- /dev/null +++ b/docs/src/en/merchant-api.md @@ -0,0 +1,6 @@ +--- +title: Merchant API methods +description: Merchant API methods +--- + +# Merchant API methods diff --git a/docs/src/en/sandbox.md b/docs/src/en/sandbox.md new file mode 100644 index 0000000..da2d56d --- /dev/null +++ b/docs/src/en/sandbox.md @@ -0,0 +1,6 @@ +--- +title: Sandbox (Testing) +description: Sandbox (Testing) +--- + +# Sandbox (Testing) diff --git a/docs/src/en/setup.md b/docs/src/en/setup.md new file mode 100644 index 0000000..12f0f53 --- /dev/null +++ b/docs/src/en/setup.md @@ -0,0 +1,6 @@ +--- +title: Interaction setup +description: Interaction setup +--- + +# Interaction setup diff --git a/docs/src/en/subscribe-api.md b/docs/src/en/subscribe-api.md new file mode 100644 index 0000000..95b8128 --- /dev/null +++ b/docs/src/en/subscribe-api.md @@ -0,0 +1,6 @@ +--- +title: Subscribe API methods +description: Subscribe API methods +--- + +# Subscribe API methods diff --git a/docs/src/en/support.md b/docs/src/en/support.md new file mode 100644 index 0000000..386243f --- /dev/null +++ b/docs/src/en/support.md @@ -0,0 +1,6 @@ +--- +title: Technical support +description: Technical support +--- + +# Technical support diff --git a/docs/src/overrides/partials/nav.html b/docs/src/overrides/partials/nav.html new file mode 100644 index 0000000..f4025e5 --- /dev/null +++ b/docs/src/overrides/partials/nav.html @@ -0,0 +1,23 @@ + diff --git a/docs/src/ru/index.md b/docs/src/ru/index.md new file mode 100644 index 0000000..5554065 --- /dev/null +++ b/docs/src/ru/index.md @@ -0,0 +1,5 @@ +--- +title: Введение +--- + +# **Добро пожаловать!** diff --git a/docs/src/ru/merchant-api.md b/docs/src/ru/merchant-api.md new file mode 100644 index 0000000..1398766 --- /dev/null +++ b/docs/src/ru/merchant-api.md @@ -0,0 +1,6 @@ +--- +title: Методы Merchant API +description: Методы Merchant API +--- + +# Методы Merchant API diff --git a/docs/src/ru/sandbox.md b/docs/src/ru/sandbox.md new file mode 100644 index 0000000..4631217 --- /dev/null +++ b/docs/src/ru/sandbox.md @@ -0,0 +1,6 @@ +--- +title: Песочница (Тестирование) +description: Песочница (Тестирование) +--- + +# Песочница (Тестирование) diff --git a/docs/src/ru/setup.md b/docs/src/ru/setup.md new file mode 100644 index 0000000..ac946b7 --- /dev/null +++ b/docs/src/ru/setup.md @@ -0,0 +1,6 @@ +--- +title: Настройка взаимодействия +description: Настройка взаимодействия +--- + +# Настройка взаимодействия diff --git a/docs/src/ru/subscribe-api.md b/docs/src/ru/subscribe-api.md new file mode 100644 index 0000000..b1fe530 --- /dev/null +++ b/docs/src/ru/subscribe-api.md @@ -0,0 +1,6 @@ +--- +title: Методы Subscribe API +description: Методы Subscribe API +--- + +# Методы Subscribe API diff --git a/docs/src/ru/support.md b/docs/src/ru/support.md new file mode 100644 index 0000000..060c771 --- /dev/null +++ b/docs/src/ru/support.md @@ -0,0 +1,6 @@ +--- +title: Техническая поддержка +description: Техническая поддержка +--- + +# Техническая поддержка diff --git a/docs/src/uz/index.md b/docs/src/uz/index.md new file mode 100644 index 0000000..7905467 --- /dev/null +++ b/docs/src/uz/index.md @@ -0,0 +1,5 @@ +--- +title: Kirish +--- + +# **Xush kelibsiz!** diff --git a/docs/src/uz/merchant-api.md b/docs/src/uz/merchant-api.md new file mode 100644 index 0000000..8e5f324 --- /dev/null +++ b/docs/src/uz/merchant-api.md @@ -0,0 +1,6 @@ +--- +title: Merchant API metodlari +description: Merchant API metodlari +--- + +# Merchant API metodlari diff --git a/docs/src/uz/sandbox.md b/docs/src/uz/sandbox.md new file mode 100644 index 0000000..99c75ae --- /dev/null +++ b/docs/src/uz/sandbox.md @@ -0,0 +1,6 @@ +--- +title: Qumdon (Sandbox) +description: Qumdon (Sandbox) +--- + +# Qumdon (Sandbox) diff --git a/docs/src/uz/setup.md b/docs/src/uz/setup.md new file mode 100644 index 0000000..fb18afd --- /dev/null +++ b/docs/src/uz/setup.md @@ -0,0 +1,6 @@ +--- +title: Sozlash (Setup) +description: Sozlash (Setup) +--- + +# Sozlash (Setup) diff --git a/docs/src/uz/subscribe-api.md b/docs/src/uz/subscribe-api.md new file mode 100644 index 0000000..cce1674 --- /dev/null +++ b/docs/src/uz/subscribe-api.md @@ -0,0 +1,6 @@ +--- +title: Subscribe API metodlari +description: Subscribe API metodlari +--- + +# Subscribe API metodlari diff --git a/docs/src/uz/support.md b/docs/src/uz/support.md new file mode 100644 index 0000000..a00db19 --- /dev/null +++ b/docs/src/uz/support.md @@ -0,0 +1,6 @@ +--- +title: Texnik yordam +description: Texnik yordam +--- + +# Texnik yordam diff --git a/lib/payme/admin.py b/lib/payme/admin.py index c5cf4b1..c07bd87 100644 --- a/lib/payme/admin.py +++ b/lib/payme/admin.py @@ -1,11 +1,20 @@ from django.contrib import admin -from payme.models import CUSTOM_ORDER -from payme.models import Order as DefaultOrderModel +from payme.models import ( + Item, MerchatTransactionsModel, + OrderDetail, ShippingDetail +) +# pylint: disable=fixme +# TODO: order Payme models in admin panel +# 1. OrderDetail +# 2. Item +# 3. ShippingDetail +# 4. MerchatTransactionsModel -from payme.models import MerchatTransactionsModel - -if not CUSTOM_ORDER: - admin.site.register(DefaultOrderModel) - -admin.site.register(MerchatTransactionsModel) +admin.site.register( + [ + OrderDetail, Item, + ShippingDetail, + MerchatTransactionsModel, + ] +) diff --git a/lib/payme/errors/exceptions.py b/lib/payme/errors/exceptions.py index feb2e78..faadbb9 100644 --- a/lib/payme/errors/exceptions.py +++ b/lib/payme/errors/exceptions.py @@ -87,3 +87,10 @@ class PaymeTimeoutException(Exception): """ Payme timeout exception that means that payme is working slowly. """ + + +class QRCodeError(Exception): + """ + QRCodeError Exception \ + Raises when an error occurs while generating a QR code. + """ diff --git a/lib/payme/methods/cancel_transaction.py b/lib/payme/methods/cancel_transaction.py index d318480..a92f3af 100644 --- a/lib/payme/methods/cancel_transaction.py +++ b/lib/payme/methods/cancel_transaction.py @@ -2,10 +2,10 @@ from django.db import transaction -from payme.utils.logging import logger -from payme.models import MerchatTransactionsModel from payme.errors.exceptions import PerformTransactionDoesNotExist +from payme.models import MerchatTransactionsModel as MTM from payme.serializers import MerchatTransactionsModelSerializer as MTMS +from payme.utils.logging import logger class CancelTransaction: @@ -25,16 +25,17 @@ def __call__(self, params: dict): ) try: with transaction.atomic(): - transactions: MerchatTransactionsModel = \ - MerchatTransactionsModel.objects.filter( - _id=clean_data.get('_id'), - ).first() + transactions = MTM.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() diff --git a/lib/payme/methods/check_perform_transaction.py b/lib/payme/methods/check_perform_transaction.py index 78a7411..a7d17ec 100644 --- a/lib/payme/methods/check_perform_transaction.py +++ b/lib/payme/methods/check_perform_transaction.py @@ -1,5 +1,9 @@ +from payme.serializers import ( + MerchatTransactionsModelSerializer, + OrderModelSerializer +) from payme.utils.get_params import get_params -from payme.serializers import MerchatTransactionsModelSerializer +from payme.utils.order_finder import Order class CheckPerformTransaction: @@ -11,16 +15,27 @@ class CheckPerformTransaction: ------------------------- 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) + order = OrderModelSerializer( + instance=Order.objects.get( + id=serializer.validated_data.get('order_id') + ) + ) + response = { "result": { "allow": True, - } + "detail": order.data.get("detail") } + } + + if not order.data.get("detail"): + del response["result"]["detail"] return None, response diff --git a/lib/payme/methods/check_transaction.py b/lib/payme/methods/check_transaction.py index 4768154..33ad8cf 100644 --- a/lib/payme/methods/check_transaction.py +++ b/lib/payme/methods/check_transaction.py @@ -14,10 +14,9 @@ class CheckTransaction: ------------------------- https://developer.help.paycom.uz/metody-merchant-api/checkperformtransaction """ + def __call__(self, params: dict) -> None: - clean_data: dict = MTMS.get_validated_data( - params=params - ) + clean_data: dict = MTMS.get_validated_data(params=params) try: transaction = \ diff --git a/lib/payme/methods/create_transaction.py b/lib/payme/methods/create_transaction.py index a5f2c77..84bc0ca 100644 --- a/lib/payme/methods/create_transaction.py +++ b/lib/payme/methods/create_transaction.py @@ -1,12 +1,12 @@ -import uuid -import time import datetime +import time +import uuid -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.models import MerchatTransactionsModel from payme.serializers import MerchatTransactionsModelSerializer +from payme.utils.get_params import get_params +from payme.utils.logging import logger class CreateTransaction: @@ -18,6 +18,7 @@ class CreateTransaction: ------------------------- https://developer.help.paycom.uz/metody-merchant-api/createtransaction """ + def __call__(self, params: dict) -> dict: serializer = MerchatTransactionsModelSerializer( data=get_params(params) @@ -61,8 +62,7 @@ def __call__(self, params: dict) -> dict: @staticmethod def _convert_ms_to_datetime(time_ms: str) -> int: - """Use this format to convert from time ms to datetime format. - """ + """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 index 0738315..454fa04 100644 --- a/lib/payme/methods/generate_link.py +++ b/lib/payme/methods/generate_link.py @@ -1,9 +1,17 @@ import base64 -from decimal import Decimal +import json +import os +import typing +import uuid from dataclasses import dataclass +import websocket +from websocket import WebSocketApp + from django.conf import settings +from payme.errors.exceptions import QRCodeError + 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') @@ -19,7 +27,7 @@ class GeneratePayLink: Parameters ---------- order_id: int — The order_id for paying - amount: int — The amount belong to the order + amount: float — The amount belong to the order Returns str — pay link ---------------------- @@ -29,11 +37,16 @@ class GeneratePayLink: https://developer.help.paycom.uz/initsializatsiya-platezhey/ """ order_id: str - amount: Decimal + amount: float def generate_link(self) -> str: """ GeneratePayLink for each order. + + Full method documentation + ---------- + https://developer.help.paycom.uz/initsializatsiya-platezhey/otpravka-cheka-po-metodu-get + """ generated_pay_link: str = "{payme_url}/{encode_params}" params: str = 'm={payme_id};ac.{payme_account}={order_id};a={amount};c={call_back_url}' @@ -51,24 +64,90 @@ def generate_link(self) -> str: encode_params=str(encode_params, 'utf-8') ) + def __send_and_receive_data(self, data) -> typing.Union[str, None]: + # pylint: disable=missing-function-docstring + + message = None + + def on_message(ws: WebSocketApp, _message): + nonlocal message + message = _message + + ws.close() + + websocket.enableTrace(False) + + ws = WebSocketApp( + url="wss://checkout.paycom.uz/", + keep_running=False, + on_message=on_message + ) + + ws.on_open = lambda ws: ws.send(json.dumps(data)) + ws.run_forever(ping_interval=0.1) + + return message + + def to_qrcode(self, path: str = 'qr-codes', filename: str = None, **kwargs): + """ + Generate qr-code for order. + + Full method documentation + ---------- + https://developer.help.paycom.uz/initsializatsiya-platezhey/generatsiya-knopki-oplaty-i-qr-koda + + Parameters + ---------- + path: str -> output path (folder) name + filename: str -> output image name without suffix + lang: str -> user language. available values: ru, uz, en. + callback: str -> return url after payment or payment cancellation. + + Returns + ---------- + str -> path of qr code svg + """ + data = { + "lang": kwargs.get("lang", "ru"), + "merchant": PAYME_ID, + "amount": self.amount, + "account": {PAYME_ACCOUNT: self.order_id}, + "callback": kwargs.get("callback", PAYME_CALL_BACK_URL) + } + message = self.__send_and_receive_data(data) + + if message is None: + raise QRCodeError + + if not os.path.exists(path): + os.makedirs(path) + + image_name = uuid.uuid4().hex if not filename else filename + image_output_path = f'{path}/{image_name}.svg' + + with open(image_output_path, 'w') as svg: + svg.write(message.split(',')[-1]) + + return image_output_path + @staticmethod - def to_tiyin(amount: Decimal) -> Decimal: + def to_tiyin(amount: float) -> float: """ Convert from soum to tiyin. Parameters ---------- - amount: Decimal -> order amount + amount: float -> order amount """ return amount * 100 @staticmethod - def to_soum(amount: Decimal) -> Decimal: + def to_soum(amount: float) -> float: """ Convert from tiyin to soum. Parameters ---------- - amount: Decimal -> order amount + amount: float -> order amount """ return amount / 100 diff --git a/lib/payme/methods/get_statement.py b/lib/payme/methods/get_statement.py index ba41c7e..18cf213 100644 --- a/lib/payme/methods/get_statement.py +++ b/lib/payme/methods/get_statement.py @@ -1,8 +1,8 @@ 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.logging import logger from payme.utils.make_aware_datetime import make_aware_datetime as mad diff --git a/lib/payme/methods/perform_transaction.py b/lib/payme/methods/perform_transaction.py index 5312065..8d381b4 100644 --- a/lib/payme/methods/perform_transaction.py +++ b/lib/payme/methods/perform_transaction.py @@ -2,10 +2,10 @@ 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 +from payme.utils.get_params import get_params +from payme.utils.logging import logger class PerformTransaction: @@ -17,6 +17,7 @@ class PerformTransaction: ------------------------- https://developer.help.paycom.uz/metody-merchant-api/performtransaction """ + def __call__(self, params: dict) -> dict: serializer = MerchatTransactionsModelSerializer( data=get_params(params) diff --git a/lib/payme/migrations/0001_initial.py b/lib/payme/migrations/0001_initial.py index eec730a..7c7c6f9 100644 --- a/lib/payme/migrations/0001_initial.py +++ b/lib/payme/migrations/0001_initial.py @@ -1,4 +1,5 @@ # pylint: disable=invalid-name +import django.db.models.deletion from django.db import migrations, models @@ -8,15 +9,30 @@ class Migration(migrations.Migration): dependencies = [] operations = [ + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.BigAutoField( + auto_created=True, primary_key=True, + serialize=False, verbose_name='ID' + )), + ('discount', models.FloatField(blank=True, null=True)), + ('title', models.CharField(max_length=255)), + ('price', models.FloatField(blank=True, null=True)), + ('count', models.IntegerField(default=1)), + ('code', models.CharField(max_length=17)), + ('units', models.IntegerField(blank=True, null=True)), + ('package_code', models.CharField(max_length=255)), + ('vat_percent', models.IntegerField(blank=True, default=0, null=True)), + ], + ), migrations.CreateModel( name='MerchatTransactionsModel', fields=[ ('id', models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID') - ), + 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)), @@ -32,17 +48,30 @@ class Migration(migrations.Migration): ], ), migrations.CreateModel( - name='Order', + name='ShippingDetail', 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)), + auto_created=True, primary_key=True, + serialize=False, verbose_name='ID' + )), + ('title', models.CharField(max_length=255)), + ('price', models.FloatField(default=0)), + ], + ), + migrations.CreateModel( + name='OrderDetail', + fields=[ + ('id', models.BigAutoField( + auto_created=True, primary_key=True, + serialize=False, verbose_name='ID' + )), + ('receipt_type', models.IntegerField(default=0)), + ('items', models.ManyToManyField(to='payme.item')), + ('shipping', models.ForeignKey( + blank=True, null=True, + on_delete=django.db.models.deletion.CASCADE, + to='payme.shippingdetail' + )), ], ), ] diff --git a/lib/payme/models.py b/lib/payme/models.py index 52d46cf..75c2c99 100644 --- a/lib/payme/models.py +++ b/lib/payme/models.py @@ -1,9 +1,4 @@ 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): @@ -28,34 +23,92 @@ def __str__(self): return str(self._id) -try: - CUSTOM_ORDER = import_string(settings.ORDER_MODEL) +class ShippingDetail(models.Model): + """ + ShippingDetail class \ + That's used for managing shipping + """ + title = models.CharField(max_length=255) + price = models.FloatField(default=0) + + def __str__(self) -> str: + return f"[{self.pk}] {self.title} {self.price}" + + +class Item(models.Model): + """ + Item class \ + That's used for managing order items + """ + discount = models.FloatField(null=True, blank=True) + title = models.CharField(max_length=255) + price = models.FloatField(null=True, blank=True) + count = models.IntegerField(default=1) + code = models.CharField(max_length=17) + units = models.IntegerField(null=True, blank=True) + package_code = models.CharField(max_length=255) + vat_percent = models.IntegerField(default=0, null=True, blank=True) + + def __str__(self) -> str: + return f"[{self.id}] {self.title} #{self.code}" + + +class OrderDetail(models.Model): + """ + OrderDetail class \ + That's used for managing order details + """ + receipt_type = models.IntegerField(default=0) + shipping = models.ForeignKey( + to=ShippingDetail, + null=True, blank=True, + on_delete=models.CASCADE + ) + items = models.ManyToManyField(Item) + + def get_items_display(self): + # pylint: disable=missing-function-docstring + return ', '.join([items.title for items in self.items.all()]) + + def __str__(self) -> str: + return f"[{self.pk}] {self.get_items_display()}" + + +class DisallowOverrideMetaclass(models.base.ModelBase): + # pylint: disable=missing-class-docstring + def __new__(mcs, name, bases, attrs: dict, **kwargs): + disallowed_fields = ['amount', 'detail'] + + if name != 'BaseOrder': + for field_name in disallowed_fields: + if not attrs.get(field_name): + continue - if not isinstance(CUSTOM_ORDER, models.base.ModelBase): - raise TypeError("The input must be an instance of models.Model class") + raise TypeError( + f"Field '{field_name}' in '{name}' cannot be overridden." + ) - # 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") + return super().__new__(mcs, name, bases, attrs, **kwargs) - Order = CUSTOM_ORDER -except (ImportError, AttributeError): - logger.warning("You have no payme custom order model") - CUSTOM_ORDER = None +class BaseOrder(models.Model, metaclass=DisallowOverrideMetaclass): + """ + Order class \ + That's used for managing order process + """ + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) - 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) + amount = models.FloatField(null=True, blank=True) + detail = models.ForeignKey( + OrderDetail, + null=True, blank=True, + on_delete=models.CASCADE + ) - def __str__(self): - return f"ORDER ID: {self.id} - AMOUNT: {self.amount}" + def __str__(self): + return f"ORDER ID: {self.id} - AMOUNT: {self.amount}" - class Meta: - # pylint: disable=missing-class-docstring - managed = False + class Meta: + # pylint: disable=missing-class-docstring + abstract = True diff --git a/lib/payme/serializers.py b/lib/payme/serializers.py index 8f9b3f3..3d36ed4 100644 --- a/lib/payme/serializers.py +++ b/lib/payme/serializers.py @@ -2,12 +2,15 @@ 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 +from payme.utils.get_params import get_params +from payme.utils.logging import logger +from payme.utils.order_finder import Order + +from payme.errors.exceptions import ( + PerformTransactionDoesNotExist, + IncorrectAmount +) class MerchatTransactionsModelSerializer(serializers.ModelSerializer): @@ -33,7 +36,7 @@ def validate(self, attrs) -> dict: order = Order.objects.get( id=attrs['order_id'] ) - if order.amount != int(attrs['amount']): + if order.amount != float(attrs['amount']): raise IncorrectAmount() except IncorrectAmount as error: @@ -42,19 +45,19 @@ def validate(self, attrs) -> dict: return attrs - def validate_amount(self, amount) -> int: + def validate_amount(self, amount: float) -> float: """ Validator for Transactions Amount. """ if amount is None: raise IncorrectAmount() - if int(amount) <= int(settings.PAYME.get("PAYME_MIN_AMOUNT", 0)): + if amount <= float(settings.PAYME.get("PAYME_MIN_AMOUNT", 0)): raise IncorrectAmount("Payment amount is less than allowed.") return amount - def validate_order_id(self, order_id) -> int: + def validate_order_id(self, order_id: int) -> int: """ Use this method to check if a transaction is allowed to be executed. @@ -86,3 +89,34 @@ def get_validated_data(params: dict) -> dict: clean_data: dict = serializer.validated_data return clean_data + + +class OrderModelSerializer(serializers.ModelSerializer): + """ + OrderModelSerializer class \ + That's used to serialize orders detail data. + """ + class Meta: + # pylint: disable=missing-class-docstring + model = Order + depth = 2 + exclude = ["id"] + read_only_fields = ["__all__"] + + def clean_empty(self, data): + # pylint: disable=missing-function-docstring + if isinstance(data, dict): + return { + k: v + for k, v in ((k, self.clean_empty(v)) for k, v in data.items()) + if v is not None and k != 'id' + } + if isinstance(data, list): + return [v for v in map(self.clean_empty, data) if v] + + return data + + def to_representation(self, instance): + ret = super().to_representation(instance) + + return self.clean_empty(ret) diff --git a/lib/payme/utils/order_finder.py b/lib/payme/utils/order_finder.py new file mode 100644 index 0000000..53e9aac --- /dev/null +++ b/lib/payme/utils/order_finder.py @@ -0,0 +1,41 @@ +from django.conf import settings +from django.db import models +from django.utils.module_loading import import_string + + +def _custom_order_model(): + """ + Get custom order model class from settings. + + Returns the custom order model class + defined in 'PAYME' or main settings. + + raise ImportError if both are undefined. + """ + order_model_paths = [] + + if hasattr(settings, 'ORDER_MODEL'): + order_model_paths.append(settings.ORDER_MODEL) + + if hasattr(settings, 'PAYME'): + order_model_paths.append(settings.PAYME.get('ORDER_MODEL')) + + for model_path in order_model_paths: + try: + return import_string(model_path) + except (ImportError, AttributeError): + pass + + raise ImportError + + +try: + CUSTOM_ORDER = _custom_order_model() +except (ImportError, AttributeError): + # pylint: disable=raise-missing-from + raise NotImplementedError("Order model not implemented!") + +if not isinstance(CUSTOM_ORDER, models.base.ModelBase): + raise TypeError("The input must be an instance of models.Model class") + +Order = CUSTOM_ORDER diff --git a/lib/payme/views.py b/lib/payme/views.py index 25db03c..bdd3382 100644 --- a/lib/payme/views.py +++ b/lib/payme/views.py @@ -31,7 +31,7 @@ class MerchantAPIView(APIView): def post(self, request) -> Response: """ Payme sends post request to our call back url. - That methods are includes 5 methods + That methods are includes 6 methods - CheckPerformTransaction - CreateTransaction - PerformTransaction @@ -55,7 +55,9 @@ def post(self, request) -> Response: raise MethodNotFound() from error except PerformTransactionDoesNotExist as error: - logger.error("PerformTransactionDoesNotExist Error occurred: %s", error) + logger.error( + "PerformTransactionDoesNotExist Error occurred: %s", error + ) raise PerformTransactionDoesNotExist() from error order_id, action = paycom_method(incoming_data.get("params")) @@ -156,7 +158,7 @@ def perform_transaction(self, order_id, action) -> None: """ pass - def cancel_transaction(self,order_id, action) -> None: + def cancel_transaction(self, order_id, action) -> None: """ need implement in your view class """ diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..4413364 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,112 @@ +# Project information +site_name: "Payme-pkg uchun yo'riqnoma" +site_description: 🟢 Merchant API and Subscribe API Integration 2023—08 +site_author: Hoosnick +site_url: https://paytechuz.github.io/payme-pkg +docs_dir: docs/src +site_dir: payme + +# Repository +repo_name: PayTechUz/payme-pkg +repo_url: https://github.com/PayTechUz/payme-pkg +edit_uri: "" + +# Copyright +copyright: Copyright © 2023 PayTechUz + +# Configuration +theme: + name: material + custom_dir: docs/src/overrides + language: uz + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: teal + toggle: + icon: material/brightness-4 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: teal + toggle: + icon: material/brightness-7 + name: Switch to light mode + font: + text: Ubuntu + code: Roboto Mono + # icon: + # logo: material/cash-multiple + logo: assets/logo.png + +# Extras +extra: + social: + - icon: fontawesome/brands/telegram + link: https://t.me/+7Gn-JZ99TfgwZDNi + name: "Biz telegramda" + - icon: fontawesome/brands/github-alt + link: https://github.com/PayTechUz/payme-pkg + name: GitHub + +# Extensions +markdown_extensions: + - admonition + - pymdownx.highlight + - pymdownx.superfences + - pymdownx.details + - meta + - attr_list + - toc: + permalink: true + +nav: + - "home": index.md + - "interaction_setup": setup.md + - "merchant_api": merchant-api.md + - "subscribe_api": subscribe-api.md + - "sandbox": sandbox.md + - "support": support.md + +plugins: + - search + - i18n: + default_language: uz + default_language_only: false + docs_structure: folder + + languages: + en: + name: "en English" + build: true + site_name: "Documentation for payme-pkg" + uz: + name: "uz O'zbekcha" + build: true + ru: + name: "ru Русский" + build: true + site_name: "Документация для payme-pkg" + + nav_translations: + en: + "home": "Introduction" + "interaction_setup": "Interaction setup" + "merchant_api": "Merchant API" + "subscribe_api": "Subscribe API" + "sandbox": "Sandbox (Testing)" + "support": "Technical support" + ru: + "home": "Введение" + "interaction_setup": "Настройка взаимодействия" + "merchant_api": "Merchant API" + "subscribe_api": "Subscribe API" + "sandbox": "Песочница (Тестирование)" + "support": "Техническая поддержка" + uz: + "home": "Kirish" + "interaction_setup": "Sozlash (Setup)" + "merchant_api": "Merchant API" + "subscribe_api": "Subscribe API" + "sandbox": "Qumdon (Sandbox)" + "support": "Texnik yordam" diff --git a/requirements/docs-requirements.txt b/requirements/docs-requirements.txt new file mode 100644 index 0000000..55f78da --- /dev/null +++ b/requirements/docs-requirements.txt @@ -0,0 +1,2 @@ +mkdocs-material==9.1.21 +mkdocs-static-i18n==0.56 \ No newline at end of file diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 0739c50..752c05b 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,3 +1,4 @@ requests==2.* dataclasses==0.* djangorestframework==3.14.0 +websocket-client==1.6.1 diff --git a/setup.py b/setup.py index dce6ed4..caa7156 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ install_requires=[ 'requests==2.*', 'dataclasses==0.*', - 'djangorestframework==3.14.0' - ], + 'djangorestframework==3.14.0', + 'websocket-client==1.6.1' + ], )