Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -14,4 +14,5 @@ urlpatterns = [
|
||||
path('address/<int:pk>', views.GetIDUserAddressView.as_view(), name='get-ID-address'),
|
||||
path('subscribe', views.SubscribeView.as_view(), name='subscibe'),
|
||||
path('attack/view/<int:pk>', views.ChangeViewAttack.as_view(), name='attack-view'),
|
||||
path('logout', views.LogoutView.as_view(), name='logout'),
|
||||
]
|
||||
@@ -195,4 +195,27 @@ class ChangeViewAttack(View):
|
||||
attack = get_object_or_404(SecurityBreachAttemptModel, pk=pk)
|
||||
attack.viewd = not attack.viewd
|
||||
attack.save()
|
||||
return redirect('admin:account_securitybreachattemptmodel_changelist')
|
||||
return redirect('admin:account_securitybreachattemptmodel_changelist')
|
||||
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
class LogoutSerializer(serializers.Serializer):
|
||||
refresh_token = serializers.CharField(help_text="Refresh token to be blacklisted")
|
||||
|
||||
class LogoutView(APIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
@extend_schema(
|
||||
request=LogoutSerializer,
|
||||
responses={205: None, 400: "Bad request (invalid token or missing data)"},
|
||||
)
|
||||
def post(self, request):
|
||||
try:
|
||||
refresh_token = request.data["refresh_token"]
|
||||
token = RefreshToken(refresh_token)
|
||||
token.blacklist()
|
||||
return Response(status=status.HTTP_205_RESET_CONTENT)
|
||||
except Exception as e:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -0,0 +1,2 @@
|
||||
__version__ = "v2.0.5"
|
||||
default_app_config = "azbankgateways.apps.AZIranianBankGatewaysConfig"
|
||||
@@ -0,0 +1,72 @@
|
||||
from django.contrib import admin
|
||||
from utils.admin import ModelAdmin
|
||||
from .models import Bank
|
||||
|
||||
|
||||
class BankAdmin(ModelAdmin):
|
||||
fields = [
|
||||
"pk",
|
||||
"status",
|
||||
"bank_type",
|
||||
"tracking_code",
|
||||
"amount",
|
||||
"reference_number",
|
||||
"response_result",
|
||||
"callback_url",
|
||||
"extra_information",
|
||||
"bank_choose_identifier",
|
||||
"created_at",
|
||||
"update_at",
|
||||
'order'
|
||||
]
|
||||
list_display = [
|
||||
"pk",
|
||||
"status",
|
||||
"bank_type",
|
||||
"tracking_code",
|
||||
"amount",
|
||||
"reference_number",
|
||||
"response_result",
|
||||
"callback_url",
|
||||
"extra_information",
|
||||
"bank_choose_identifier",
|
||||
"created_at",
|
||||
"update_at",
|
||||
'order'
|
||||
]
|
||||
list_filter = [
|
||||
"status",
|
||||
"bank_type",
|
||||
"created_at",
|
||||
"update_at",
|
||||
]
|
||||
search_fields = [
|
||||
"status",
|
||||
"bank_type",
|
||||
"tracking_code",
|
||||
"amount",
|
||||
"reference_number",
|
||||
"response_result",
|
||||
"callback_url",
|
||||
"extra_information",
|
||||
"created_at",
|
||||
"update_at",
|
||||
]
|
||||
exclude = []
|
||||
dynamic_raw_id_fields = []
|
||||
readonly_fields = [
|
||||
"pk",
|
||||
"status",
|
||||
"bank_type",
|
||||
"tracking_code",
|
||||
"amount",
|
||||
"reference_number",
|
||||
"response_result",
|
||||
"callback_url",
|
||||
"extra_information",
|
||||
"created_at",
|
||||
"update_at",
|
||||
]
|
||||
|
||||
|
||||
admin.site.register(Bank, BankAdmin)
|
||||
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class AZIranianBankGatewaysConfig(AppConfig):
|
||||
name = "azbankgateways"
|
||||
verbose_name = _("Iranian bank gateway")
|
||||
verbose_name_plural = _("Iranian bank gateways")
|
||||
# compatible with django >= 3.2
|
||||
default_auto_field = "django.db.models.AutoField"
|
||||
@@ -0,0 +1,63 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
from . import default_settings as settings
|
||||
from .banks import BaseBank
|
||||
from .exceptions.exceptions import BankGatewayAutoConnectionFailed
|
||||
from .models import BankType
|
||||
|
||||
|
||||
class BankFactory:
|
||||
def __init__(self):
|
||||
logging.debug("Create bank factory")
|
||||
self._secret_value_reader = self._import(settings.SETTING_VALUE_READER_CLASS)()
|
||||
|
||||
@staticmethod
|
||||
def _import(path):
|
||||
package, attr = path.rsplit(".", 1)
|
||||
klass = getattr(importlib.import_module(package), attr)
|
||||
return klass
|
||||
|
||||
def _import_bank(self, bank_type: BankType, identifier: str):
|
||||
"""
|
||||
helper to import bank aliases from string paths.
|
||||
|
||||
raises an AttributeError if a bank can't be found by it's alias
|
||||
"""
|
||||
bank_class = self._import(self._secret_value_reader.klass(bank_type=bank_type, identifier=identifier))
|
||||
logging.debug("Import bank class")
|
||||
|
||||
return bank_class, self._secret_value_reader.read(bank_type=bank_type, identifier=identifier)
|
||||
|
||||
def create(self, bank_type: BankType = None, identifier: str = "1") -> BaseBank:
|
||||
"""Build bank class"""
|
||||
if not bank_type:
|
||||
bank_type = self._secret_value_reader.default(identifier)
|
||||
logging.debug("Request create bank", extra={"bank_type": bank_type})
|
||||
|
||||
bank_klass, bank_settings = self._import_bank(bank_type, identifier)
|
||||
bank = bank_klass(**bank_settings, identifier=identifier)
|
||||
bank.set_currency(self._secret_value_reader.currency(identifier))
|
||||
|
||||
logging.debug("Create bank")
|
||||
return bank
|
||||
|
||||
def auto_create(self, identifier: str = "1", amount=None) -> BaseBank:
|
||||
logging.debug("Request create bank automatically")
|
||||
bank_list = self._secret_value_reader.get_bank_priorities(identifier)
|
||||
errors = []
|
||||
for bank_type in bank_list:
|
||||
try:
|
||||
bank = self.create(bank_type, identifier)
|
||||
bank.check_gateway(amount)
|
||||
return bank
|
||||
except Exception as e:
|
||||
logging.debug(str(e))
|
||||
logging.debug("Try to connect another bank...")
|
||||
errors.append(e)
|
||||
continue
|
||||
logging.debug("All banks failed to connect")
|
||||
errors_msg = "\n".join([str(e) for e in errors])
|
||||
raise BankGatewayAutoConnectionFailed(errors_msg)
|
||||
@@ -0,0 +1,8 @@
|
||||
from .bahamta import Bahamta # noqa
|
||||
from .banks import BaseBank # noqa
|
||||
from .bmi import BMI # noqa
|
||||
from .idpay import IDPay # noqa
|
||||
from .mellat import Mellat # noqa
|
||||
from .sep import SEP # noqa
|
||||
from .zarinpal import Zarinpal # noqa
|
||||
from .zibal import Zibal # noqa
|
||||
@@ -0,0 +1,134 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from azbankgateways.exceptions import BankGatewayConnectionError, SettingDoesNotExist
|
||||
from azbankgateways.exceptions.exceptions import BankGatewayRejectPayment
|
||||
from azbankgateways.models import BankType, CurrencyEnum, PaymentStatus
|
||||
from azbankgateways.utils import append_querystring, get_json, split_to_dict_querystring
|
||||
|
||||
from .banks import BaseBank
|
||||
|
||||
|
||||
class Bahamta(BaseBank):
|
||||
_merchant_code = None
|
||||
_params = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Bahamta, self).__init__(**kwargs)
|
||||
self.set_gateway_currency(CurrencyEnum.IRR)
|
||||
self._token_api_url = "https://webpay.bahamta.com/api/create_request"
|
||||
self._payment_url = None
|
||||
self._verify_api_url = "https://webpay.bahamta.com/api/confirm_payment"
|
||||
|
||||
def get_bank_type(self):
|
||||
return BankType.BAHAMTA
|
||||
|
||||
def set_default_settings(self):
|
||||
for item in ["MERCHANT_CODE"]:
|
||||
if item not in self.default_setting_kwargs:
|
||||
raise SettingDoesNotExist()
|
||||
setattr(self, f"_{item.lower()}", self.default_setting_kwargs[item])
|
||||
|
||||
"""
|
||||
Gateway
|
||||
"""
|
||||
|
||||
def _get_gateway_payment_url_parameter(self):
|
||||
return self._payment_url
|
||||
|
||||
def _get_gateway_payment_parameter(self):
|
||||
params = {}
|
||||
params.update(self._params)
|
||||
return params
|
||||
|
||||
def _get_gateway_payment_method_parameter(self):
|
||||
return "GET"
|
||||
|
||||
"""
|
||||
pay
|
||||
"""
|
||||
|
||||
def get_pay_data(self):
|
||||
data = {
|
||||
"api_key": self._merchant_code,
|
||||
"reference": self.get_tracking_code(),
|
||||
"amount_irr": self.get_gateway_amount(),
|
||||
"payer_mobile": self.get_mobile_number(),
|
||||
"callback_url": self._get_gateway_callback_url(),
|
||||
}
|
||||
return data
|
||||
|
||||
def prepare_pay(self):
|
||||
super(Bahamta, self).prepare_pay()
|
||||
|
||||
def pay(self):
|
||||
super(Bahamta, self).pay()
|
||||
data = self.get_pay_data()
|
||||
response_json = self._send_data(self._token_api_url, data)
|
||||
if response_json["ok"]:
|
||||
# در این سیستم رفرنس برای ذخیره سازی بر نمی گردد!
|
||||
token = self.get_tracking_code()
|
||||
self._payment_url, self._params = split_to_dict_querystring(response_json["result"]["payment_url"])
|
||||
self._set_reference_number(token)
|
||||
else:
|
||||
logging.critical("Bahamta gateway reject payment")
|
||||
raise BankGatewayRejectPayment(self.get_transaction_status_text())
|
||||
|
||||
"""
|
||||
verify gateway
|
||||
"""
|
||||
|
||||
def prepare_verify_from_gateway(self):
|
||||
super(Bahamta, self).prepare_verify_from_gateway()
|
||||
token = self.get_request().GET.get("reference", None)
|
||||
self._set_reference_number(token)
|
||||
self._set_bank_record()
|
||||
|
||||
def verify_from_gateway(self, request):
|
||||
super(Bahamta, self).verify_from_gateway(request)
|
||||
|
||||
"""
|
||||
verify
|
||||
"""
|
||||
|
||||
def get_verify_data(self):
|
||||
super(Bahamta, self).get_verify_data()
|
||||
data = {
|
||||
"api_key": self._merchant_code,
|
||||
"reference": self.get_reference_number(),
|
||||
"amount_irr": self.get_gateway_amount(),
|
||||
}
|
||||
return data
|
||||
|
||||
def prepare_verify(self, tracking_code):
|
||||
super(Bahamta, self).prepare_verify(tracking_code)
|
||||
|
||||
def verify(self, transaction_code):
|
||||
super(Bahamta, self).verify(transaction_code)
|
||||
data = self.get_verify_data()
|
||||
response_json = self._send_data(self._verify_api_url, data)
|
||||
if response_json.get("ok", False) and response_json.get("result", {}).get("state", None) == "paid":
|
||||
self._set_payment_status(PaymentStatus.COMPLETE)
|
||||
extra_information = json.dumps(response_json.get("result", {}))
|
||||
self._bank.extra_information = extra_information
|
||||
self._bank.save()
|
||||
else:
|
||||
self._set_payment_status(PaymentStatus.CANCEL_BY_USER)
|
||||
logging.debug("Bahamta gateway unapprove payment")
|
||||
|
||||
def _send_data(self, api, data):
|
||||
try:
|
||||
url = append_querystring(api, data)
|
||||
response = requests.get(url, timeout=5)
|
||||
except requests.Timeout:
|
||||
logging.exception("Bahamta time out gateway {}".format(data))
|
||||
raise BankGatewayConnectionError()
|
||||
except requests.ConnectionError:
|
||||
logging.exception("Bahamta time out gateway {}".format(data))
|
||||
raise BankGatewayConnectionError()
|
||||
|
||||
response_json = get_json(response)
|
||||
self._set_transaction_status_text(response_json.get("error"))
|
||||
return response_json
|
||||
@@ -0,0 +1,370 @@
|
||||
import abc
|
||||
import logging
|
||||
import uuid
|
||||
from urllib import parse
|
||||
|
||||
import six
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from .. import default_settings as settings
|
||||
from ..exceptions import (
|
||||
AmountDoesNotSupport,
|
||||
BankGatewayStateInvalid,
|
||||
BankGatewayTokenExpired,
|
||||
CurrencyDoesNotSupport,
|
||||
SafeSettingsEnabled,
|
||||
)
|
||||
from ..models import Bank, CurrencyEnum, PaymentStatus
|
||||
from ..utils import append_querystring
|
||||
|
||||
|
||||
# TODO: handle and expire record after 15 minutes
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseBank:
|
||||
"""Base bank for sending to gateway."""
|
||||
|
||||
_gateway_currency: str = CurrencyEnum.IRR
|
||||
_currency: str = CurrencyEnum.IRR
|
||||
_amount: int = 0
|
||||
_gateway_amount: int = 0
|
||||
_mobile_number: str = None
|
||||
_tracking_code: int = None
|
||||
_reference_number: str = ""
|
||||
_transaction_status_text: str = ""
|
||||
_client_callback_url: str = ""
|
||||
_bank: Bank = None
|
||||
_request = None
|
||||
|
||||
def __init__(self, identifier: str, **kwargs):
|
||||
self.identifier = identifier
|
||||
self.default_setting_kwargs = kwargs
|
||||
self.set_default_settings()
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_default_settings(self):
|
||||
"""default setting, like fetch merchant code, terminal id and etc"""
|
||||
pass
|
||||
|
||||
def prepare_amount(self):
|
||||
"""prepare amount"""
|
||||
if self._currency == self._gateway_currency:
|
||||
self._gateway_amount = self._amount
|
||||
elif self._currency == CurrencyEnum.IRR and self._gateway_currency == CurrencyEnum.IRT:
|
||||
self._gateway_amount = CurrencyEnum.rial_to_toman(self._amount)
|
||||
elif self._currency == CurrencyEnum.IRT and self._gateway_currency == CurrencyEnum.IRR:
|
||||
self._gateway_amount = CurrencyEnum.toman_to_rial(self._amount)
|
||||
else:
|
||||
self._gateway_amount = self._amount
|
||||
|
||||
if not self.check_amount():
|
||||
raise AmountDoesNotSupport()
|
||||
|
||||
def check_amount(self):
|
||||
return self.get_gateway_amount() >= self.get_minimum_amount()
|
||||
|
||||
@classmethod
|
||||
def get_minimum_amount(cls):
|
||||
return 1000
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_bank_type(self):
|
||||
pass
|
||||
|
||||
def get_amount(self):
|
||||
"""get the amount"""
|
||||
return self._amount
|
||||
|
||||
def set_amount(self, amount):
|
||||
"""set amount"""
|
||||
if int(amount) <= 0:
|
||||
raise AmountDoesNotSupport()
|
||||
self._amount = int(amount)
|
||||
|
||||
@abc.abstractmethod
|
||||
def prepare_pay(self):
|
||||
logging.debug("Prepare pay method")
|
||||
self.prepare_amount()
|
||||
tracking_code = int(str(uuid.uuid4().int)[-1 * settings.TRACKING_CODE_LENGTH :])
|
||||
self._set_tracking_code(tracking_code)
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_pay_data(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def pay(self):
|
||||
logging.debug("Pay method")
|
||||
self.prepare_pay()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_verify_data(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def prepare_verify(self, tracking_code):
|
||||
logging.debug("Prepare verify method")
|
||||
self._set_tracking_code(tracking_code)
|
||||
self._set_bank_record()
|
||||
self.prepare_amount()
|
||||
|
||||
@abc.abstractmethod
|
||||
def verify(self, tracking_code):
|
||||
logging.debug("Verify method")
|
||||
self.prepare_verify(tracking_code)
|
||||
|
||||
def ready(self) -> Bank:
|
||||
self.pay()
|
||||
bank = Bank.objects.create(
|
||||
bank_choose_identifier=self.identifier,
|
||||
bank_type=self.get_bank_type(),
|
||||
amount=self.get_amount(),
|
||||
reference_number=self.get_reference_number(),
|
||||
response_result=self.get_transaction_status_text(),
|
||||
tracking_code=self.get_tracking_code(),
|
||||
)
|
||||
self._bank = bank
|
||||
self._set_payment_status(PaymentStatus.WAITING)
|
||||
if self._client_callback_url:
|
||||
self._bank.callback_url = self._client_callback_url
|
||||
return bank
|
||||
|
||||
@abc.abstractmethod
|
||||
def prepare_verify_from_gateway(self):
|
||||
pass
|
||||
|
||||
def verify_from_gateway(self, request):
|
||||
"""زمانی که کاربر از گیت وی بانک باز میگردد این متد فراخوانی می شود."""
|
||||
self.set_request(request)
|
||||
self.prepare_verify_from_gateway()
|
||||
self._set_payment_status(PaymentStatus.RETURN_FROM_BANK)
|
||||
self.verify(self.get_tracking_code())
|
||||
|
||||
def get_client_callback_url(self):
|
||||
"""این متد پس از وریفای شدن استفاده خواهد شد. لینک برگشت را بر میگرداند.حال چه وریفای موفقیت آمیز باشد چه با
|
||||
لغو کاربر مواجه شده باشد"""
|
||||
return append_querystring(
|
||||
self._bank.callback_url,
|
||||
{settings.TRACKING_CODE_QUERY_PARAM: self.get_tracking_code()},
|
||||
)
|
||||
|
||||
def redirect_client_callback(self):
|
||||
""" "این متد کاربر را به مسیری که نرم افزار میخواهد هدایت خواهد کرد و پس از وریفای شدن استفاده می شود."""
|
||||
logging.debug("Redirect to client")
|
||||
return redirect(self.get_client_callback_url())
|
||||
|
||||
def set_mobile_number(self, mobile_number):
|
||||
"""شماره موبایل کاربر را جهت ارسال به درگاه برای فتچ کردن شماره کارت ها و ... ارسال خواهد کرد."""
|
||||
self._mobile_number = mobile_number
|
||||
|
||||
def get_mobile_number(self):
|
||||
return self._mobile_number
|
||||
|
||||
def set_client_callback_url(self, callback_url):
|
||||
"""ذخیره کال بک از طریق نرم افزار برای بازگردانی کاربر پس از بازگشت درگاه بانک به پکیج و سپس از پکیج به نرم
|
||||
افزار."""
|
||||
if not self._bank:
|
||||
self._client_callback_url = callback_url
|
||||
else:
|
||||
logging.critical(
|
||||
"You are change the call back url in invalid situation.",
|
||||
extra={
|
||||
"bank_id": self._bank.pk,
|
||||
"status": self._bank.status,
|
||||
},
|
||||
)
|
||||
raise BankGatewayStateInvalid(
|
||||
"Bank state not equal to waiting. Probably finish "
|
||||
f"or redirect to bank gateway. status is {self._bank.status}"
|
||||
)
|
||||
|
||||
def _set_reference_number(self, reference_number):
|
||||
"""reference number get from bank"""
|
||||
self._reference_number = reference_number
|
||||
|
||||
def _set_bank_record(self):
|
||||
try:
|
||||
self._bank = Bank.objects.get(
|
||||
Q(Q(reference_number=self.get_reference_number()) | Q(tracking_code=self.get_tracking_code())),
|
||||
Q(bank_type=self.get_bank_type()),
|
||||
)
|
||||
logging.debug("Set reference find bank object.")
|
||||
except Bank.DoesNotExist:
|
||||
logging.debug("Cant find bank record object.")
|
||||
raise BankGatewayStateInvalid(
|
||||
"Cant find bank record with reference number reference number is {}".format(
|
||||
self.get_reference_number()
|
||||
)
|
||||
)
|
||||
self._set_tracking_code(self._bank.tracking_code)
|
||||
self._set_reference_number(self._bank.reference_number)
|
||||
self.set_amount(self._bank.amount)
|
||||
|
||||
def get_reference_number(self):
|
||||
return self._reference_number
|
||||
|
||||
"""
|
||||
ترنزکشن تکست متنی است که از طرف درگاه بانک به عنوان پیام باز میگردد.
|
||||
"""
|
||||
|
||||
def _set_transaction_status_text(self, txt):
|
||||
self._transaction_status_text = txt
|
||||
|
||||
def get_transaction_status_text(self):
|
||||
return self._transaction_status_text
|
||||
|
||||
def _set_payment_status(self, payment_status):
|
||||
if payment_status == PaymentStatus.RETURN_FROM_BANK and self._bank.status != PaymentStatus.REDIRECT_TO_BANK:
|
||||
logging.debug(
|
||||
"Payment status is not status suitable.",
|
||||
extra={"status": self._bank.status},
|
||||
)
|
||||
raise BankGatewayStateInvalid(
|
||||
"You change the status bank record before/after this record change status from redirect to bank. "
|
||||
"current status is {}".format(self._bank.status)
|
||||
)
|
||||
self._bank.status = payment_status
|
||||
self._bank.save()
|
||||
logging.debug("Change bank payment status", extra={"status": payment_status})
|
||||
|
||||
def set_gateway_currency(self, currency: CurrencyEnum):
|
||||
"""واحد پولی درگاه بانک"""
|
||||
if currency not in [CurrencyEnum.IRR, CurrencyEnum.IRT]:
|
||||
raise CurrencyDoesNotSupport()
|
||||
self._gateway_currency = currency
|
||||
|
||||
def get_gateway_currency(self):
|
||||
return self._gateway_currency
|
||||
|
||||
def set_currency(self, currency: CurrencyEnum):
|
||||
""" "واحد پولی نرم افزار"""
|
||||
if currency not in [CurrencyEnum.IRR, CurrencyEnum.IRT]:
|
||||
raise CurrencyDoesNotSupport()
|
||||
self._currency = currency
|
||||
|
||||
def get_currency(self):
|
||||
return self._currency
|
||||
|
||||
def get_gateway_amount(self):
|
||||
return self._gateway_amount
|
||||
|
||||
"""
|
||||
ترکینگ کد توسط برنامه تولید شده و برای استفاده های بعدی کاربرد خواهد داشت.
|
||||
"""
|
||||
|
||||
def _set_tracking_code(self, tracking_code):
|
||||
self._tracking_code = tracking_code
|
||||
|
||||
def get_tracking_code(self):
|
||||
return self._tracking_code
|
||||
|
||||
"""ًRequest"""
|
||||
|
||||
def set_request(self, request):
|
||||
self._request = request
|
||||
|
||||
def get_request(self):
|
||||
return self._request
|
||||
|
||||
"""gateway"""
|
||||
|
||||
def _prepare_check_gateway(self, amount=None):
|
||||
"""ست کردن داده های اولیه"""
|
||||
if amount:
|
||||
self.set_amount(amount)
|
||||
else:
|
||||
self.set_amount(10000)
|
||||
self.set_client_callback_url("/")
|
||||
|
||||
def check_gateway(self, amount=None):
|
||||
"""با این متد از صحت و سلامت گیت وی برای اتصال اطمینان حاصل می کنیم."""
|
||||
self._prepare_check_gateway(amount)
|
||||
self.pay()
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_gateway_payment_url_parameter(self):
|
||||
"""این متد بسته به بانک متفاوت پر می شود."""
|
||||
"""
|
||||
:return
|
||||
url: str
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_gateway_payment_parameter(self):
|
||||
"""این متد بسته به بانک متفاوت پر می شود."""
|
||||
"""
|
||||
:return
|
||||
params: dict
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_gateway_payment_method_parameter(self):
|
||||
"""این متد بسته به بانک متفاوت پر می شود."""
|
||||
"""
|
||||
:return
|
||||
method: POST, GET
|
||||
"""
|
||||
pass
|
||||
|
||||
def _verify_payment_expiry(self):
|
||||
"""برسی میکند درگاه ساخته شده اعتبار دارد یا خیر"""
|
||||
if (timezone.now() - self._bank.created_at).seconds > 120:
|
||||
self._set_payment_status(PaymentStatus.EXPIRE_GATEWAY_TOKEN)
|
||||
logging.debug("Redirect to bank expire!")
|
||||
raise BankGatewayTokenExpired()
|
||||
|
||||
def redirect_gateway(self):
|
||||
"""کاربر را به درگاه بانک هدایت می کند"""
|
||||
self._verify_payment_expiry()
|
||||
if settings.IS_SAFE_GET_GATEWAY_PAYMENT:
|
||||
raise SafeSettingsEnabled()
|
||||
logging.debug("Redirect to bank")
|
||||
self._set_payment_status(PaymentStatus.REDIRECT_TO_BANK)
|
||||
return redirect(self.get_gateway_payment_url())
|
||||
|
||||
def get_gateway(self):
|
||||
"""اطلاعات درگاه پرداخت را برمیگرداند"""
|
||||
self._verify_payment_expiry()
|
||||
logging.debug("Redirect to bank")
|
||||
self._set_payment_status(PaymentStatus.REDIRECT_TO_BANK)
|
||||
return self.safe_get_gateway_payment_url()
|
||||
|
||||
def safe_get_gateway_payment_url(self):
|
||||
url = self._get_gateway_payment_url_parameter()
|
||||
params = self._get_gateway_payment_parameter()
|
||||
method = self._get_gateway_payment_method_parameter()
|
||||
context = {"params": params, "url": url, "method": method}
|
||||
return context
|
||||
|
||||
def get_gateway_payment_url(self):
|
||||
redirect_url = reverse(settings.GO_TO_BANK_GATEWAY_NAMESPACE)
|
||||
url = self._get_gateway_payment_url_parameter()
|
||||
params = self._get_gateway_payment_parameter()
|
||||
method = self._get_gateway_payment_method_parameter()
|
||||
params.update(
|
||||
{
|
||||
"url": url,
|
||||
"method": method,
|
||||
}
|
||||
)
|
||||
redirect_url = append_querystring(redirect_url, params)
|
||||
if self.get_request():
|
||||
redirect_url = self.get_request().build_absolute_uri(redirect_url)
|
||||
return redirect_url
|
||||
|
||||
def _get_gateway_callback_url(self):
|
||||
url = reverse(settings.CALLBACK_NAMESPACE)
|
||||
if self.get_request():
|
||||
url_parts = list(parse.urlparse(url))
|
||||
if not (url_parts[0] and url_parts[1]):
|
||||
url = self.get_request().build_absolute_uri(url)
|
||||
query = dict(parse.parse_qsl(self.get_request().GET.urlencode()))
|
||||
query.update({"bank_type": self.get_bank_type()})
|
||||
query.update({"identifier": self.identifier})
|
||||
url = append_querystring(url, query)
|
||||
|
||||
return url
|
||||
@@ -0,0 +1,155 @@
|
||||
import base64
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from Crypto.Cipher import DES3
|
||||
|
||||
from azbankgateways.banks import BaseBank
|
||||
from azbankgateways.exceptions import BankGatewayConnectionError, SettingDoesNotExist
|
||||
from azbankgateways.exceptions.exceptions import (
|
||||
BankGatewayRejectPayment,
|
||||
BankGatewayStateInvalid,
|
||||
)
|
||||
from azbankgateways.models import BankType, CurrencyEnum, PaymentStatus
|
||||
from azbankgateways.utils import get_json
|
||||
|
||||
|
||||
class BMI(BaseBank):
|
||||
_merchant_code = None
|
||||
_terminal_code = None
|
||||
_secret_key = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(BMI, self).__init__(**kwargs)
|
||||
self.set_gateway_currency(CurrencyEnum.IRR)
|
||||
self._token_api_url = "https://sadad.shaparak.ir/vpg/api/v0/Request/PaymentRequest"
|
||||
self._payment_url = "https://sadad.shaparak.ir/VPG/Purchase"
|
||||
self._verify_api_url = "https://sadad.shaparak.ir/vpg/api/v0/Advice/Verify"
|
||||
|
||||
def get_bank_type(self):
|
||||
return BankType.BMI
|
||||
|
||||
def set_default_settings(self):
|
||||
for item in ["MERCHANT_CODE", "TERMINAL_CODE", "SECRET_KEY"]:
|
||||
if item not in self.default_setting_kwargs:
|
||||
raise SettingDoesNotExist()
|
||||
setattr(self, f"_{item.lower()}", self.default_setting_kwargs[item])
|
||||
|
||||
def get_pay_data(self):
|
||||
time_now = datetime.datetime.now().strftime("%m/%d/%Y %H:%M:%S %p")
|
||||
data = {
|
||||
"TerminalId": self._terminal_code,
|
||||
"MerchantId": self._merchant_code,
|
||||
"Amount": self.get_gateway_amount(),
|
||||
"SignData": self._encrypt_des3(
|
||||
"{};{};{}".format(
|
||||
self._terminal_code,
|
||||
self.get_tracking_code(),
|
||||
self.get_gateway_amount(),
|
||||
)
|
||||
),
|
||||
"ReturnUrl": self._get_gateway_callback_url(),
|
||||
"LocalDateTime": time_now,
|
||||
"OrderId": self.get_tracking_code(),
|
||||
"AdditionalData": "oi:%s-ou:%s" % (self.get_tracking_code(), self.get_mobile_number()),
|
||||
}
|
||||
return data
|
||||
|
||||
def prepare_pay(self):
|
||||
super(BMI, self).prepare_pay()
|
||||
|
||||
def pay(self):
|
||||
super(BMI, self).pay()
|
||||
data = self.get_pay_data()
|
||||
response_json = self._send_data(self._token_api_url, data)
|
||||
if response_json["ResCode"] == "0":
|
||||
token = response_json["Token"]
|
||||
self._set_reference_number(token)
|
||||
else:
|
||||
logging.critical("BMI gateway reject payment")
|
||||
raise BankGatewayRejectPayment(self.get_transaction_status_text())
|
||||
|
||||
"""
|
||||
: gateway
|
||||
"""
|
||||
|
||||
def _get_gateway_payment_method_parameter(self):
|
||||
return "GET"
|
||||
|
||||
def _get_gateway_payment_url_parameter(self):
|
||||
return self._payment_url
|
||||
|
||||
def _get_gateway_payment_parameter(self):
|
||||
params = {"Token": self.get_reference_number()}
|
||||
return params
|
||||
|
||||
def get_verify_data(self):
|
||||
super(BMI, self).get_verify_data()
|
||||
data = {
|
||||
"Token": self.get_reference_number(),
|
||||
"SignData": self._encrypt_des3(self.get_reference_number()),
|
||||
}
|
||||
return data
|
||||
|
||||
def prepare_verify(self, tracking_code):
|
||||
super(BMI, self).prepare_verify(tracking_code)
|
||||
|
||||
def verify(self, transaction_code):
|
||||
super(BMI, self).verify(transaction_code)
|
||||
data = self.get_verify_data()
|
||||
response_json = self._send_data(self._verify_api_url, data)
|
||||
if response_json["ResCode"] == "0":
|
||||
self._set_payment_status(PaymentStatus.COMPLETE)
|
||||
extra_information = (
|
||||
f"RetrivalRefNo={response_json['RetrivalRefNo']},SystemTraceNo={response_json['SystemTraceNo']}"
|
||||
)
|
||||
self._bank.extra_information = extra_information
|
||||
self._bank.save()
|
||||
else:
|
||||
self._set_payment_status(PaymentStatus.CANCEL_BY_USER)
|
||||
logging.debug("BMI gateway unapprove payment")
|
||||
|
||||
def prepare_verify_from_gateway(self):
|
||||
super(BMI, self).prepare_verify_from_gateway()
|
||||
request = self.get_request()
|
||||
for method in ["POST", "GET", "data", "PUT"]:
|
||||
token = getattr(request, method, {}).get("token", None)
|
||||
if token:
|
||||
break
|
||||
if not token:
|
||||
raise BankGatewayStateInvalid
|
||||
self._set_reference_number(token)
|
||||
self._set_bank_record()
|
||||
|
||||
def verify_from_gateway(self, request):
|
||||
super(BMI, self).verify_from_gateway(request)
|
||||
|
||||
@classmethod
|
||||
def _pad(cls, text, pad_size=16):
|
||||
text_length = len(text)
|
||||
last_block_size = text_length % pad_size
|
||||
remaining_space = pad_size - last_block_size
|
||||
text = text + (remaining_space * chr(remaining_space))
|
||||
return text
|
||||
|
||||
def _encrypt_des3(self, text):
|
||||
secret_key_bytes = base64.b64decode(self._secret_key)
|
||||
text = self._pad(text, 8)
|
||||
cipher = DES3.new(secret_key_bytes, DES3.MODE_ECB)
|
||||
cipher_text = cipher.encrypt(str.encode(text))
|
||||
return base64.b64encode(cipher_text).decode("utf-8")
|
||||
|
||||
def _send_data(self, api, data):
|
||||
try:
|
||||
response = requests.post(api, json=data, timeout=5)
|
||||
except requests.Timeout:
|
||||
logging.exception("BMI time out gateway {}".format(data))
|
||||
raise BankGatewayConnectionError()
|
||||
except requests.ConnectionError:
|
||||
logging.exception("BMI time out gateway {}".format(data))
|
||||
raise BankGatewayConnectionError()
|
||||
|
||||
response_json = get_json(response)
|
||||
self._set_transaction_status_text(response_json["Description"])
|
||||
return response_json
|
||||
@@ -0,0 +1,141 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from azbankgateways.banks import BaseBank
|
||||
from azbankgateways.exceptions import BankGatewayConnectionError, SettingDoesNotExist
|
||||
from azbankgateways.exceptions.exceptions import BankGatewayRejectPayment
|
||||
from azbankgateways.models import BankType, CurrencyEnum, PaymentStatus
|
||||
from azbankgateways.utils import get_json, split_to_dict_querystring
|
||||
|
||||
|
||||
class IDPay(BaseBank):
|
||||
_merchant_code = None
|
||||
_method = None
|
||||
_x_sandbox = None
|
||||
_payment_url = None
|
||||
_params = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(IDPay, self).__init__(**kwargs)
|
||||
self.set_gateway_currency(CurrencyEnum.IRR)
|
||||
self._token_api_url = "https://api.idpay.ir/v1.1/payment"
|
||||
self._verify_api_url = "https://api.idpay.ir/v1.1/payment/verify"
|
||||
|
||||
def get_bank_type(self):
|
||||
return BankType.IDPAY
|
||||
|
||||
def set_default_settings(self):
|
||||
for item in ["MERCHANT_CODE", "METHOD", "X_SANDBOX"]:
|
||||
if item not in self.default_setting_kwargs:
|
||||
raise SettingDoesNotExist()
|
||||
setattr(self, f"_{item.lower()}", self.default_setting_kwargs[item])
|
||||
|
||||
self._x_sandbox = str(self._x_sandbox)
|
||||
|
||||
"""
|
||||
gateway
|
||||
"""
|
||||
|
||||
def _get_gateway_payment_url_parameter(self):
|
||||
return self._payment_url
|
||||
|
||||
def _get_gateway_payment_parameter(self):
|
||||
params = {}
|
||||
params.update(self._params)
|
||||
return params
|
||||
|
||||
def _get_gateway_payment_method_parameter(self):
|
||||
return "GET"
|
||||
|
||||
"""
|
||||
pay
|
||||
"""
|
||||
|
||||
def get_pay_data(self):
|
||||
data = {
|
||||
"order_id": self.get_tracking_code(),
|
||||
"amount": self.get_gateway_amount(),
|
||||
"phone": self.get_mobile_number(),
|
||||
"callback": self._get_gateway_callback_url(),
|
||||
}
|
||||
return data
|
||||
|
||||
def prepare_pay(self):
|
||||
super(IDPay, self).prepare_pay()
|
||||
|
||||
def pay(self):
|
||||
super(IDPay, self).pay()
|
||||
data = self.get_pay_data()
|
||||
response_json = self._send_data(self._token_api_url, data)
|
||||
if "id" in response_json and "link" in response_json and response_json["link"] and response_json["id"]:
|
||||
token = response_json["id"]
|
||||
self._payment_url, self._params = split_to_dict_querystring(response_json["link"])
|
||||
self._set_reference_number(token)
|
||||
else:
|
||||
logging.critical("IDPay gateway reject payment")
|
||||
raise BankGatewayRejectPayment(self.get_transaction_status_text())
|
||||
|
||||
"""
|
||||
verify gateway
|
||||
"""
|
||||
|
||||
def prepare_verify_from_gateway(self):
|
||||
super(IDPay, self).prepare_verify_from_gateway()
|
||||
for method in ["GET", "POST", "data"]:
|
||||
token = getattr(self.get_request(), method).get("id", None)
|
||||
if token:
|
||||
self._set_reference_number(token)
|
||||
self._set_bank_record()
|
||||
break
|
||||
|
||||
def verify_from_gateway(self, request):
|
||||
super(IDPay, self).verify_from_gateway(request)
|
||||
|
||||
"""
|
||||
verify
|
||||
"""
|
||||
|
||||
def get_verify_data(self):
|
||||
super(IDPay, self).get_verify_data()
|
||||
data = {
|
||||
"id": self.get_reference_number(),
|
||||
"order_id": self.get_tracking_code(),
|
||||
}
|
||||
return data
|
||||
|
||||
def prepare_verify(self, tracking_code):
|
||||
super(IDPay, self).prepare_verify(tracking_code)
|
||||
|
||||
def verify(self, transaction_code):
|
||||
super(IDPay, self).verify(transaction_code)
|
||||
data = self.get_verify_data()
|
||||
response_json = self._send_data(self._verify_api_url, data, timeout=10)
|
||||
if response_json.get("verify", {}).get("date", None):
|
||||
self._set_payment_status(PaymentStatus.COMPLETE)
|
||||
extra_information = json.dumps(response_json)
|
||||
self._bank.extra_information = extra_information
|
||||
self._bank.save()
|
||||
else:
|
||||
self._set_payment_status(PaymentStatus.CANCEL_BY_USER)
|
||||
logging.debug("IDPay gateway unapprove payment")
|
||||
|
||||
def _send_data(self, api, data, timeout=5):
|
||||
headers = {
|
||||
"X-API-KEY": self._merchant_code,
|
||||
"X-SANDBOX": self._x_sandbox,
|
||||
}
|
||||
try:
|
||||
response = requests.post(api, headers=headers, json=data, timeout=timeout)
|
||||
except requests.Timeout:
|
||||
logging.exception("IDPay time out gateway {}".format(data))
|
||||
raise BankGatewayConnectionError()
|
||||
except requests.ConnectionError:
|
||||
logging.exception("IDPay time out gateway {}".format(data))
|
||||
raise BankGatewayConnectionError()
|
||||
|
||||
response_json = get_json(response)
|
||||
if "error_message" in response_json:
|
||||
self._set_transaction_status_text(response_json["error_message"])
|
||||
return response_json
|
||||
@@ -0,0 +1,267 @@
|
||||
import logging
|
||||
from json import dumps, loads
|
||||
from time import gmtime, strftime
|
||||
|
||||
from zeep import Client, Transport
|
||||
|
||||
from azbankgateways.banks import BaseBank
|
||||
from azbankgateways.exceptions import SettingDoesNotExist
|
||||
from azbankgateways.exceptions.exceptions import BankGatewayRejectPayment
|
||||
from azbankgateways.models import BankType, CurrencyEnum, PaymentStatus
|
||||
|
||||
|
||||
class Mellat(BaseBank):
|
||||
_terminal_code = None
|
||||
_username = None
|
||||
_password = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Mellat, self).__init__(**kwargs)
|
||||
self.set_gateway_currency(CurrencyEnum.IRR)
|
||||
self._payment_url = "https://bpm.shaparak.ir/pgwchannel/startpay.mellat"
|
||||
|
||||
def get_bank_type(self):
|
||||
return BankType.MELLAT
|
||||
|
||||
def set_default_settings(self):
|
||||
for item in ["TERMINAL_CODE", "USERNAME", "PASSWORD"]:
|
||||
if item not in self.default_setting_kwargs:
|
||||
raise SettingDoesNotExist()
|
||||
setattr(self, f"_{item.lower()}", self.default_setting_kwargs[item])
|
||||
|
||||
"""
|
||||
gateway
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_minimum_amount(cls):
|
||||
return 1000
|
||||
|
||||
def _get_gateway_payment_url_parameter(self):
|
||||
return self._payment_url
|
||||
|
||||
def _get_gateway_payment_parameter(self):
|
||||
params = {
|
||||
"RefId": self.get_reference_number(),
|
||||
"MobileNo": self.get_mobile_number(),
|
||||
}
|
||||
return params
|
||||
|
||||
def _get_gateway_payment_method_parameter(self):
|
||||
return "GET"
|
||||
|
||||
"""
|
||||
pay
|
||||
"""
|
||||
|
||||
def get_pay_data(self):
|
||||
description = "خرید با شماره پیگیری - {}".format(self.get_tracking_code())
|
||||
data = {
|
||||
"terminalId": int(self._terminal_code),
|
||||
"userName": self._username,
|
||||
"userPassword": self._password,
|
||||
"orderId": int(self.get_tracking_code()),
|
||||
"amount": int(self.get_gateway_amount()),
|
||||
"localDate": self._get_current_date(),
|
||||
"localTime": self._get_current_time(),
|
||||
"additionalData": description,
|
||||
"callBackUrl": self._get_gateway_callback_url(),
|
||||
"payerId": 0,
|
||||
}
|
||||
return data
|
||||
|
||||
def prepare_pay(self):
|
||||
super(Mellat, self).prepare_pay()
|
||||
|
||||
def pay(self):
|
||||
super(Mellat, self).pay()
|
||||
|
||||
data = self.get_pay_data()
|
||||
client = self._get_client()
|
||||
response = client.service.bpPayRequest(**data)
|
||||
try:
|
||||
status, token = response.split(",")
|
||||
if status == "0":
|
||||
self._set_reference_number(token)
|
||||
except ValueError:
|
||||
status_text = "Unknown error"
|
||||
if response == "11":
|
||||
status_text = "Card number is invalid"
|
||||
elif response == "12":
|
||||
status_text = "Insufficient inventory"
|
||||
elif response == "13":
|
||||
status_text = "Password is incorrect"
|
||||
elif response == "14":
|
||||
status_text = "Max try reached"
|
||||
elif response == "15":
|
||||
status_text = "Card is invalid"
|
||||
elif response == "16":
|
||||
status_text = "The number of withdrawals is more than allowed"
|
||||
elif response == "17":
|
||||
status_text = "The user has abandoned the transaction"
|
||||
elif response == "18":
|
||||
status_text = "The card has expired"
|
||||
elif response == "19":
|
||||
status_text = "The withdrawal amount is over the limit"
|
||||
elif response == "21":
|
||||
status_text = "Invalid service"
|
||||
elif response == "23":
|
||||
status_text = "A security error has occurred"
|
||||
elif response == "24":
|
||||
status_text = "The recipient's user information is invalid"
|
||||
elif response == "25":
|
||||
status_text = "The amount is invalid"
|
||||
elif response == "31":
|
||||
status_text = "The response is invalid"
|
||||
elif response == "32":
|
||||
status_text = "The format of the entered information is not correct"
|
||||
elif response == "33":
|
||||
status_text = "The account is invalid"
|
||||
elif response == "34":
|
||||
status_text = "System error"
|
||||
elif response == "35":
|
||||
status_text = "Date is invalid"
|
||||
elif response == "41":
|
||||
status_text = "The request number is duplicate"
|
||||
elif response == "42":
|
||||
status_text = "Sale transaction not found"
|
||||
elif response == "43":
|
||||
status_text = "Verify has already been requested"
|
||||
elif response == "44":
|
||||
status_text = "Verify request not found"
|
||||
elif response == "45":
|
||||
status_text = "The transaction has been settled"
|
||||
elif response == "46":
|
||||
status_text = "The transaction has not been settled"
|
||||
elif response == "47":
|
||||
status_text = "Settle transaction not found"
|
||||
elif response == "48":
|
||||
status_text = "The transaction has been reversed"
|
||||
elif response == "49":
|
||||
status_text = "Refund transaction not found"
|
||||
elif response == "51":
|
||||
status_text = "The transaction is repeated"
|
||||
elif response == "54":
|
||||
status_text = "The reference transaction does not exist"
|
||||
elif response == "55":
|
||||
status_text = "The transaction is invalid"
|
||||
elif response == "61":
|
||||
status_text = "Error in deposit"
|
||||
elif response == "111":
|
||||
status_text = "Card issuer is invalid"
|
||||
elif response == "112":
|
||||
status_text = "Card issuing switch error"
|
||||
elif response == "113":
|
||||
status_text = "No response was received from the card issuer"
|
||||
elif response == "114":
|
||||
status_text = "The cardholder is not authorized to perform this transaction"
|
||||
elif response == "113":
|
||||
status_text = "No response was received from the card issuer"
|
||||
elif response == "412":
|
||||
status_text = "The invoice ID is incorrect"
|
||||
elif response == "413":
|
||||
status_text = "Payment ID is incorrect"
|
||||
elif response == "414":
|
||||
status_text = "The organization issuing the bill is invalid"
|
||||
elif response == "415":
|
||||
status_text = "The working session has ended"
|
||||
elif response == "416":
|
||||
status_text = "The working session has ended"
|
||||
elif response == "417":
|
||||
status_text = "Payer ID is invalid"
|
||||
elif response == "418":
|
||||
status_text = "Problems in defining customer information"
|
||||
elif response == "419":
|
||||
status_text = "The number of data entries has exceeded the limit"
|
||||
elif response == "421":
|
||||
status_text = "Invalid IP address"
|
||||
|
||||
self._set_transaction_status_text(status_text)
|
||||
logging.critical(status_text)
|
||||
raise BankGatewayRejectPayment(self.get_transaction_status_text())
|
||||
|
||||
"""
|
||||
verify from gateway
|
||||
"""
|
||||
|
||||
def prepare_verify_from_gateway(self):
|
||||
super(Mellat, self).prepare_verify_from_gateway()
|
||||
post = self.get_request().POST
|
||||
token = post.get("RefId", None)
|
||||
if not token:
|
||||
return
|
||||
self._set_reference_number(token)
|
||||
self._set_bank_record()
|
||||
self._bank.extra_information = dumps(dict(zip(post.keys(), post.values())))
|
||||
self._bank.save()
|
||||
|
||||
def verify_from_gateway(self, request):
|
||||
super(Mellat, self).verify_from_gateway(request)
|
||||
|
||||
"""
|
||||
verify
|
||||
"""
|
||||
|
||||
def get_verify_data(self):
|
||||
super(Mellat, self).get_verify_data()
|
||||
data = {
|
||||
"terminalId": self._terminal_code,
|
||||
"userName": self._username,
|
||||
"userPassword": self._password,
|
||||
"orderId": self.get_tracking_code(),
|
||||
"saleOrderId": self.get_tracking_code(),
|
||||
"saleReferenceId": self._get_sale_reference_id(),
|
||||
}
|
||||
return data
|
||||
|
||||
def prepare_verify(self, tracking_code):
|
||||
super(Mellat, self).prepare_verify(tracking_code)
|
||||
|
||||
def verify(self, transaction_code):
|
||||
super(Mellat, self).verify(transaction_code)
|
||||
data = self.get_verify_data()
|
||||
client = self._get_client()
|
||||
|
||||
verify_result = client.service.bpVerifyRequest(**data)
|
||||
if verify_result == "0":
|
||||
self._settle_transaction()
|
||||
else:
|
||||
verify_result = client.service.bpInquiryRequest(**data)
|
||||
if verify_result == "0":
|
||||
self._settle_transaction()
|
||||
else:
|
||||
logging.debug("Not able to verify the transaction, Making reversal request")
|
||||
reversal_result = client.service.bpReversalRequest(**data)
|
||||
|
||||
if reversal_result != "0":
|
||||
logging.debug("Reversal request was not successfull")
|
||||
|
||||
self._set_payment_status(PaymentStatus.CANCEL_BY_USER)
|
||||
logging.debug("Mellat gateway unapproved the payment")
|
||||
|
||||
def _settle_transaction(self):
|
||||
data = self.get_verify_data()
|
||||
client = self._get_client()
|
||||
settle_result = client.service.bpSettleRequest(**data)
|
||||
if settle_result == "0":
|
||||
self._set_payment_status(PaymentStatus.COMPLETE)
|
||||
else:
|
||||
logging.debug("Mellat gateway did not settle the payment")
|
||||
|
||||
@staticmethod
|
||||
def _get_client():
|
||||
transport = Transport(timeout=5, operation_timeout=5)
|
||||
client = Client("https://bpm.shaparak.ir/pgwchannel/services/pgw?wsdl", transport=transport)
|
||||
return client
|
||||
|
||||
@staticmethod
|
||||
def _get_current_time():
|
||||
return strftime("%H%M%S")
|
||||
|
||||
@staticmethod
|
||||
def _get_current_date():
|
||||
return strftime("%Y%m%d", gmtime())
|
||||
|
||||
def _get_sale_reference_id(self):
|
||||
extra_information = loads(getattr(self._bank, "extra_information", "{}"))
|
||||
return extra_information.get("SaleReferenceId", "1")
|
||||
@@ -0,0 +1,152 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from azbankgateways.banks import BaseBank
|
||||
from azbankgateways.default_settings import TRACKING_CODE_QUERY_PARAM
|
||||
from azbankgateways.exceptions import BankGatewayConnectionError, SettingDoesNotExist
|
||||
from azbankgateways.exceptions.exceptions import (
|
||||
BankGatewayRejectPayment,
|
||||
BankGatewayStateInvalid,
|
||||
)
|
||||
from azbankgateways.models import BankType, CurrencyEnum, PaymentStatus
|
||||
|
||||
|
||||
class PayV1(BaseBank):
|
||||
_merchant_code = None
|
||||
_x_sandbox = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(PayV1, self).__init__(**kwargs)
|
||||
self.set_gateway_currency(CurrencyEnum.IRR)
|
||||
self._token_api_url = "https://pay.ir/pg/send"
|
||||
self._payment_url = "https://pay.ir/pg/{}"
|
||||
self._verify_api_url = "https://pay.ir/pg/verify"
|
||||
|
||||
def get_bank_type(self):
|
||||
return BankType.PAYV1
|
||||
|
||||
def set_default_settings(self):
|
||||
for item in ["MERCHANT_CODE", "X_SANDBOX"]:
|
||||
if item not in self.default_setting_kwargs:
|
||||
raise SettingDoesNotExist()
|
||||
setattr(self, f"_{item.lower()}", self.default_setting_kwargs[item])
|
||||
|
||||
self._merchant_code = self._merchant_code if not self._x_sandbox else "test"
|
||||
|
||||
"""
|
||||
gateway
|
||||
"""
|
||||
|
||||
def _get_gateway_payment_url_parameter(self):
|
||||
return self._payment_url.format(self._reference_number)
|
||||
|
||||
def _get_gateway_payment_parameter(self):
|
||||
return {}
|
||||
|
||||
def _get_gateway_payment_method_parameter(self):
|
||||
return "GET"
|
||||
|
||||
"""
|
||||
pay
|
||||
"""
|
||||
|
||||
def get_pay_data(self):
|
||||
data = {
|
||||
"api": self._merchant_code,
|
||||
"amount": self.get_gateway_amount(),
|
||||
"redirect": self._get_gateway_callback_url(),
|
||||
"mobile": self.get_mobile_number(),
|
||||
"factorNumber": self.get_tracking_code(),
|
||||
}
|
||||
return data
|
||||
|
||||
def prepare_pay(self):
|
||||
super(PayV1, self).prepare_pay()
|
||||
|
||||
def pay(self):
|
||||
super(PayV1, self).pay()
|
||||
data = self.get_pay_data()
|
||||
response = self._send_data(self._token_api_url, data)
|
||||
response_json = response.json()
|
||||
if response.status_code == 200 and int(response_json["status"]) == 1:
|
||||
token = response_json["token"]
|
||||
self._set_reference_number(token)
|
||||
else:
|
||||
logging.critical(
|
||||
"PayV1 gateway reject payment with error code {0} and status code {1}".format(
|
||||
response_json["errorCode"], response.status_code
|
||||
)
|
||||
)
|
||||
raise BankGatewayRejectPayment(self.get_transaction_status_text())
|
||||
|
||||
"""
|
||||
verify gateway
|
||||
"""
|
||||
|
||||
def prepare_verify_from_gateway(self):
|
||||
super(PayV1, self).prepare_verify_from_gateway()
|
||||
for method in ["GET", "POST", "data"]:
|
||||
token = getattr(self.get_request(), method).get(TRACKING_CODE_QUERY_PARAM, None)
|
||||
if token:
|
||||
self._set_reference_number(token)
|
||||
self._set_bank_record()
|
||||
break
|
||||
else:
|
||||
raise BankGatewayStateInvalid
|
||||
|
||||
def verify_from_gateway(self, request):
|
||||
super(PayV1, self).verify_from_gateway(request)
|
||||
|
||||
"""
|
||||
verify
|
||||
"""
|
||||
|
||||
def get_verify_data(self):
|
||||
super(PayV1, self).get_verify_data()
|
||||
data = {
|
||||
"api": self._merchant_code(),
|
||||
"token": self.get_reference_number(),
|
||||
}
|
||||
return data
|
||||
|
||||
def prepare_verify(self, tracking_code):
|
||||
super(PayV1, self).prepare_verify(tracking_code)
|
||||
|
||||
def verify(self, tracking_code):
|
||||
super(PayV1, self).verify(tracking_code)
|
||||
|
||||
data = self.get_verify_data()
|
||||
response = self._send_data(self._verify_api_url, data, timeout=10)
|
||||
response_json = response.json()
|
||||
status = PaymentStatus.COMPLETE
|
||||
if int(response_json["status"]) != 1:
|
||||
if int(response_json["errorCode"]) == -5:
|
||||
status = PaymentStatus.ERROR
|
||||
elif int(response_json["errorCode"]) == -9:
|
||||
status = PaymentStatus.EXPIRE_VERIFY_PAYMENT
|
||||
elif int(response_json["errorCode"]) == -15:
|
||||
status = PaymentStatus.CANCEL_BY_USER
|
||||
elif int(response_json["errorCode"]) == -27:
|
||||
status = PaymentStatus.RETURN_FROM_BANK
|
||||
else:
|
||||
status = PaymentStatus.ERROR
|
||||
|
||||
self._set_payment_status(status)
|
||||
extra_information = json.dumps(response_json)
|
||||
self._bank.extra_information = extra_information
|
||||
self._bank.save()
|
||||
|
||||
def _send_data(self, url, data, timeout=5) -> requests.post:
|
||||
try:
|
||||
logging.debug("Sending POST request to {} with data {}".format(url, data))
|
||||
response = requests.post(url, json=data, timeout=timeout)
|
||||
except requests.Timeout:
|
||||
logging.exception("PayV1 time out gateway {}".format(data))
|
||||
raise BankGatewayConnectionError()
|
||||
except requests.ConnectionError:
|
||||
logging.exception("PayV1 time out gateway {}".format(data))
|
||||
raise BankGatewayConnectionError()
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,146 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from zeep import Client, Transport
|
||||
|
||||
from azbankgateways.banks import BaseBank
|
||||
from azbankgateways.exceptions import BankGatewayConnectionError, SettingDoesNotExist
|
||||
from azbankgateways.exceptions.exceptions import BankGatewayRejectPayment
|
||||
from azbankgateways.models import BankType, CurrencyEnum, PaymentStatus
|
||||
from azbankgateways.utils import get_json
|
||||
|
||||
|
||||
class SEP(BaseBank):
|
||||
_merchant_code = None
|
||||
_terminal_code = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(SEP, self).__init__(**kwargs)
|
||||
self.set_gateway_currency(CurrencyEnum.IRR)
|
||||
self._token_api_url = "https://sep.shaparak.ir/MobilePG/MobilePayment"
|
||||
self._payment_url = "https://sep.shaparak.ir/OnlinePG/OnlinePG"
|
||||
self._verify_api_url = "https://verify.sep.ir/Payments/ReferencePayment.asmx?WSDL"
|
||||
|
||||
def get_bank_type(self):
|
||||
return BankType.SEP
|
||||
|
||||
def set_default_settings(self):
|
||||
for item in ["MERCHANT_CODE", "TERMINAL_CODE"]:
|
||||
if item not in self.default_setting_kwargs:
|
||||
raise SettingDoesNotExist()
|
||||
setattr(self, f"_{item.lower()}", self.default_setting_kwargs[item])
|
||||
|
||||
def get_pay_data(self):
|
||||
data = {
|
||||
"Action": "Token",
|
||||
"Amount": self.get_gateway_amount(),
|
||||
"Wage": 0,
|
||||
"TerminalId": self._merchant_code,
|
||||
"ResNum": self.get_tracking_code(),
|
||||
"RedirectURL": self._get_gateway_callback_url(),
|
||||
"CellNumber": self.get_mobile_number(),
|
||||
}
|
||||
return data
|
||||
|
||||
def prepare_pay(self):
|
||||
super(SEP, self).prepare_pay()
|
||||
|
||||
def pay(self):
|
||||
super(SEP, self).pay()
|
||||
data = self.get_pay_data()
|
||||
response_json = self._send_data(self._token_api_url, data)
|
||||
if str(response_json["status"]) == "1":
|
||||
token = response_json["token"]
|
||||
self._set_reference_number(token)
|
||||
else:
|
||||
logging.critical("SEP gateway reject payment")
|
||||
raise BankGatewayRejectPayment(self.get_transaction_status_text())
|
||||
|
||||
"""
|
||||
: gateway
|
||||
"""
|
||||
|
||||
def _get_gateway_payment_url_parameter(self):
|
||||
return self._payment_url
|
||||
|
||||
def _get_gateway_payment_method_parameter(self):
|
||||
return "POST"
|
||||
|
||||
def _get_gateway_payment_parameter(self):
|
||||
params = {
|
||||
"Token": self.get_reference_number(),
|
||||
"GetMethod": "true",
|
||||
}
|
||||
return params
|
||||
|
||||
"""
|
||||
verify from gateway
|
||||
"""
|
||||
|
||||
def prepare_verify_from_gateway(self):
|
||||
super(SEP, self).prepare_verify_from_gateway()
|
||||
request = self.get_request()
|
||||
tracking_code = request.GET.get("ResNum", None)
|
||||
token = request.GET.get("Token", None)
|
||||
self._set_tracking_code(tracking_code)
|
||||
self._set_bank_record()
|
||||
ref_num = request.GET.get("RefNum", None)
|
||||
if request.GET.get("State", "NOK") == "OK" and ref_num:
|
||||
self._set_reference_number(ref_num)
|
||||
self._bank.reference_number = ref_num
|
||||
extra_information = f"TRACENO={request.GET.get('TRACENO', None)}, RefNum={ref_num}, Token={token}"
|
||||
self._bank.extra_information = extra_information
|
||||
self._bank.save()
|
||||
|
||||
def verify_from_gateway(self, request):
|
||||
super(SEP, self).verify_from_gateway(request)
|
||||
|
||||
"""
|
||||
verify
|
||||
"""
|
||||
|
||||
def get_verify_data(self):
|
||||
super(SEP, self).get_verify_data()
|
||||
data = self.get_reference_number(), self._merchant_code
|
||||
return data
|
||||
|
||||
def prepare_verify(self, tracking_code):
|
||||
super(SEP, self).prepare_verify(tracking_code)
|
||||
|
||||
def verify(self, transaction_code):
|
||||
super(SEP, self).verify(transaction_code)
|
||||
data = self.get_verify_data()
|
||||
client = self._get_client(self._verify_api_url)
|
||||
result = client.service.verifyTransaction(*data)
|
||||
if result == self.get_gateway_amount():
|
||||
self._set_payment_status(PaymentStatus.COMPLETE)
|
||||
else:
|
||||
self._set_payment_status(PaymentStatus.CANCEL_BY_USER)
|
||||
logging.debug("SEP gateway unapprove payment")
|
||||
|
||||
def _send_data(self, api, data):
|
||||
try:
|
||||
response = requests.post(api, json=data, timeout=5)
|
||||
except requests.Timeout:
|
||||
logging.exception("SEP time out gateway {}".format(data))
|
||||
raise BankGatewayConnectionError()
|
||||
except requests.ConnectionError:
|
||||
logging.exception("SEP time out gateway {}".format(data))
|
||||
raise BankGatewayConnectionError()
|
||||
|
||||
response_json = get_json(response)
|
||||
self._set_transaction_status_text(response_json.get("errorDesc"))
|
||||
return response_json
|
||||
|
||||
@staticmethod
|
||||
def _get_client(url):
|
||||
headers = {
|
||||
"Accept": "*/*",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Connection": "keep-alive",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0",
|
||||
}
|
||||
transport = Transport(timeout=5, operation_timeout=5)
|
||||
transport.session.headers = headers
|
||||
client = Client(url, transport=transport)
|
||||
return client
|
||||
@@ -0,0 +1,131 @@
|
||||
import logging
|
||||
|
||||
from zeep import Client, Transport
|
||||
|
||||
from azbankgateways.banks import BaseBank
|
||||
from azbankgateways.exceptions import SettingDoesNotExist
|
||||
from azbankgateways.exceptions.exceptions import BankGatewayRejectPayment
|
||||
from azbankgateways.models import BankType, CurrencyEnum, PaymentStatus
|
||||
|
||||
|
||||
class Zarinpal(BaseBank):
|
||||
_merchant_code = None
|
||||
_sandbox = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs.setdefault("SANDBOX", 0)
|
||||
super(Zarinpal, self).__init__(**kwargs)
|
||||
self.set_gateway_currency(CurrencyEnum.IRT)
|
||||
self._payment_url = "https://www.zarinpal.com/pg/StartPay/{}/ZarinGate"
|
||||
self._sandbox_url = "https://sandbox.zarinpal.com/pg/StartPay/{}/ZarinGate"
|
||||
|
||||
def get_bank_type(self):
|
||||
return BankType.ZARINPAL
|
||||
|
||||
def set_default_settings(self):
|
||||
for item in ["MERCHANT_CODE", "SANDBOX"]:
|
||||
if item not in self.default_setting_kwargs:
|
||||
raise SettingDoesNotExist(f"{item} does not exist in default_setting_kwargs")
|
||||
setattr(self, f"_{item.lower()}", self.default_setting_kwargs[item])
|
||||
|
||||
"""
|
||||
gateway
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_minimum_amount(cls):
|
||||
return 1000
|
||||
|
||||
def _get_gateway_payment_url_parameter(self):
|
||||
if self._sandbox:
|
||||
return self._sandbox_url.format(self.get_reference_number())
|
||||
return self._payment_url.format(self.get_reference_number())
|
||||
|
||||
def _get_gateway_payment_parameter(self):
|
||||
return {}
|
||||
|
||||
def _get_gateway_payment_method_parameter(self):
|
||||
return "GET"
|
||||
|
||||
"""
|
||||
pay
|
||||
"""
|
||||
|
||||
def get_pay_data(self):
|
||||
description = "خرید با شماره پیگیری - {}".format(self.get_tracking_code())
|
||||
|
||||
return {
|
||||
"Description": description,
|
||||
"MerchantID": self._merchant_code,
|
||||
"Amount": self.get_gateway_amount(),
|
||||
"Email": None,
|
||||
"Mobile": self.get_mobile_number(),
|
||||
"CallbackURL": self._get_gateway_callback_url(),
|
||||
}
|
||||
|
||||
def prepare_pay(self):
|
||||
super(Zarinpal, self).prepare_pay()
|
||||
|
||||
def pay(self):
|
||||
super(Zarinpal, self).pay()
|
||||
data = self.get_pay_data()
|
||||
client = self._get_client()
|
||||
result = client.service.PaymentRequest(**data)
|
||||
if result.Status == 100:
|
||||
token = result.Authority
|
||||
self._set_reference_number(token)
|
||||
else:
|
||||
logging.critical("Zarinpal gateway reject payment")
|
||||
raise BankGatewayRejectPayment(self.get_transaction_status_text())
|
||||
|
||||
"""
|
||||
verify from gateway
|
||||
"""
|
||||
|
||||
def prepare_verify_from_gateway(self):
|
||||
super(Zarinpal, self).prepare_verify_from_gateway()
|
||||
token = self.get_request().GET.get("Authority", None)
|
||||
self._set_reference_number(token)
|
||||
self._set_bank_record()
|
||||
|
||||
def verify_from_gateway(self, request):
|
||||
super(Zarinpal, self).verify_from_gateway(request)
|
||||
|
||||
"""
|
||||
verify
|
||||
"""
|
||||
|
||||
def get_verify_data(self):
|
||||
super(Zarinpal, self).get_verify_data()
|
||||
return {
|
||||
"MerchantID": self._merchant_code,
|
||||
"Authority": self.get_reference_number(),
|
||||
"Amount": self.get_gateway_amount(),
|
||||
}
|
||||
|
||||
def prepare_verify(self, tracking_code):
|
||||
super(Zarinpal, self).prepare_verify(tracking_code)
|
||||
|
||||
def verify(self, transaction_code):
|
||||
super(Zarinpal, self).verify(transaction_code)
|
||||
data = self.get_verify_data()
|
||||
client = self._get_client(timeout=10)
|
||||
result = client.service.PaymentVerification(**data)
|
||||
if result.Status in [100, 101]:
|
||||
self._set_payment_status(PaymentStatus.COMPLETE)
|
||||
else:
|
||||
self._set_payment_status(PaymentStatus.CANCEL_BY_USER)
|
||||
logging.debug("Zarinpal gateway unapprove payment")
|
||||
|
||||
def _get_client(self, timeout=5):
|
||||
transport = Transport(timeout=timeout, operation_timeout=timeout)
|
||||
if self._sandbox:
|
||||
return Client(
|
||||
"https://sandbox.zarinpal.com/pg/services/WebGate/wsdl",
|
||||
transport=transport,
|
||||
)
|
||||
|
||||
return Client(
|
||||
"https://www.zarinpal.com/pg/services/WebGate/wsdl",
|
||||
transport=transport,
|
||||
)
|
||||
@@ -0,0 +1,127 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from azbankgateways.banks import BaseBank
|
||||
from azbankgateways.exceptions import BankGatewayConnectionError, SettingDoesNotExist
|
||||
from azbankgateways.exceptions.exceptions import BankGatewayRejectPayment
|
||||
from azbankgateways.models import BankType, CurrencyEnum, PaymentStatus
|
||||
from azbankgateways.utils import get_json
|
||||
|
||||
|
||||
class Zibal(BaseBank):
|
||||
_merchant_code = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Zibal, self).__init__(**kwargs)
|
||||
self.set_gateway_currency(CurrencyEnum.IRR)
|
||||
self._token_api_url = "https://gateway.zibal.ir/v1/request"
|
||||
self._payment_url = "https://gateway.zibal.ir/start/{}"
|
||||
self._verify_api_url = "https://gateway.zibal.ir/v1/verify"
|
||||
|
||||
def get_bank_type(self):
|
||||
return BankType.ZIBAL
|
||||
|
||||
def set_default_settings(self):
|
||||
for item in ["MERCHANT_CODE"]:
|
||||
if item not in self.default_setting_kwargs:
|
||||
raise SettingDoesNotExist()
|
||||
setattr(self, f"_{item.lower()}", self.default_setting_kwargs[item])
|
||||
|
||||
"""
|
||||
gateway
|
||||
"""
|
||||
|
||||
def _get_gateway_payment_url_parameter(self):
|
||||
return self._payment_url.format(self.get_reference_number())
|
||||
|
||||
def _get_gateway_payment_parameter(self):
|
||||
params = {}
|
||||
return params
|
||||
|
||||
def _get_gateway_payment_method_parameter(self):
|
||||
return "GET"
|
||||
|
||||
"""
|
||||
pay
|
||||
"""
|
||||
|
||||
def get_pay_data(self):
|
||||
data = {
|
||||
"merchant": self._merchant_code,
|
||||
"amount": self.get_gateway_amount(),
|
||||
"callbackUrl": self._get_gateway_callback_url(),
|
||||
"orderId": self.get_tracking_code(),
|
||||
"mobile": self.get_mobile_number(),
|
||||
}
|
||||
return data
|
||||
|
||||
def prepare_pay(self):
|
||||
super(Zibal, self).prepare_pay()
|
||||
|
||||
def pay(self):
|
||||
super(Zibal, self).pay()
|
||||
data = self.get_pay_data()
|
||||
response_json = self._send_data(self._token_api_url, data)
|
||||
if response_json["result"] == 100:
|
||||
token = response_json["trackId"]
|
||||
self._set_reference_number(token)
|
||||
else:
|
||||
logging.critical("Zibal gateway reject payment")
|
||||
raise BankGatewayRejectPayment(self.get_transaction_status_text())
|
||||
|
||||
"""
|
||||
verify from gateway
|
||||
"""
|
||||
|
||||
def prepare_verify_from_gateway(self):
|
||||
super(Zibal, self).prepare_verify_from_gateway()
|
||||
token = self.get_request().GET.get("trackId", None)
|
||||
self._set_reference_number(token)
|
||||
self._set_bank_record()
|
||||
|
||||
def verify_from_gateway(self, request):
|
||||
super(Zibal, self).verify_from_gateway(request)
|
||||
|
||||
"""
|
||||
verify
|
||||
"""
|
||||
|
||||
def get_verify_data(self):
|
||||
super(Zibal, self).get_verify_data()
|
||||
data = {
|
||||
"trackId": self.get_reference_number(),
|
||||
"merchant": self._merchant_code,
|
||||
}
|
||||
return data
|
||||
|
||||
def prepare_verify(self, tracking_code):
|
||||
super(Zibal, self).prepare_verify(tracking_code)
|
||||
|
||||
def verify(self, transaction_code):
|
||||
super(Zibal, self).verify(transaction_code)
|
||||
data = self.get_verify_data()
|
||||
response_json = self._send_data(self._verify_api_url, data)
|
||||
if response_json["result"] == 100 and response_json["status"] == 1:
|
||||
self._set_payment_status(PaymentStatus.COMPLETE)
|
||||
extra_information = json.dumps(response_json)
|
||||
self._bank.extra_information = extra_information
|
||||
self._bank.save()
|
||||
else:
|
||||
self._set_payment_status(PaymentStatus.CANCEL_BY_USER)
|
||||
logging.debug("Zibal gateway unapprove payment")
|
||||
|
||||
def _send_data(self, api, data):
|
||||
try:
|
||||
response = requests.post(api, json=data, timeout=5)
|
||||
except requests.Timeout:
|
||||
logging.exception("Zibal time out gateway {}".format(data))
|
||||
raise BankGatewayConnectionError()
|
||||
except requests.ConnectionError:
|
||||
logging.exception("Zibal time out gateway {}".format(data))
|
||||
raise BankGatewayConnectionError()
|
||||
|
||||
response_json = get_json(response)
|
||||
self._set_transaction_status_text(response_json["message"])
|
||||
return response_json
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Default settings for messaging."""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from azbankgateways.apps import AZIranianBankGatewaysConfig
|
||||
|
||||
|
||||
BANK_CLASS = getattr(
|
||||
settings,
|
||||
"CLASS",
|
||||
{
|
||||
"BMI": "azbankgateways.banks.BMI",
|
||||
"SEP": "azbankgateways.banks.SEP",
|
||||
"ZARINPAL": "azbankgateways.banks.Zarinpal",
|
||||
"IDPAY": "azbankgateways.banks.IDPay",
|
||||
"ZIBAL": "azbankgateways.banks.Zibal",
|
||||
"BAHAMTA": "azbankgateways.banks.Bahamta",
|
||||
"MELLAT": "azbankgateways.banks.Mellat",
|
||||
"PAYV1": "azbankgateways.banks.PayV1",
|
||||
},
|
||||
)
|
||||
_AZ_IRANIAN_BANK_GATEWAYS = getattr(settings, "AZ_IRANIAN_BANK_GATEWAYS", {})
|
||||
BANK_PRIORITIES = _AZ_IRANIAN_BANK_GATEWAYS.get("BANK_PRIORITIES", [])
|
||||
BANK_GATEWAYS = _AZ_IRANIAN_BANK_GATEWAYS.get("GATEWAYS", {})
|
||||
BANK_DEFAULT = _AZ_IRANIAN_BANK_GATEWAYS.get("DEFAULT", "BMI")
|
||||
SETTING_VALUE_READER_CLASS = _AZ_IRANIAN_BANK_GATEWAYS.get(
|
||||
"SETTING_VALUE_READER_CLASS", "azbankgateways.readers.DefaultReader"
|
||||
)
|
||||
CURRENCY = _AZ_IRANIAN_BANK_GATEWAYS.get("CURRENCY", "IRR")
|
||||
TRACKING_CODE_QUERY_PARAM = _AZ_IRANIAN_BANK_GATEWAYS.get("TRACKING_CODE_QUERY_PARAM", "tc")
|
||||
TRACKING_CODE_LENGTH = _AZ_IRANIAN_BANK_GATEWAYS.get("TRACKING_CODE_LENGTH", 16)
|
||||
IS_SAMPLE_FORM_ENABLE = _AZ_IRANIAN_BANK_GATEWAYS.get("IS_SAMPLE_FORM_ENABLE", False)
|
||||
IS_SAFE_GET_GATEWAY_PAYMENT = _AZ_IRANIAN_BANK_GATEWAYS.get("IS_SAFE_GET_GATEWAY_PAYMENT", False)
|
||||
CUSTOM_APP = _AZ_IRANIAN_BANK_GATEWAYS.get("CUSTOM_APP")
|
||||
if CUSTOM_APP:
|
||||
CALLBACK_NAMESPACE = f"{CUSTOM_APP}:{AZIranianBankGatewaysConfig.name}:callback"
|
||||
GO_TO_BANK_GATEWAY_NAMESPACE = f"{CUSTOM_APP}:{AZIranianBankGatewaysConfig.name}:go-to-bank-gateway"
|
||||
SAMPLE_RESULT_NAMESPACE = f"{CUSTOM_APP}:{AZIranianBankGatewaysConfig.name}:sample-result"
|
||||
else:
|
||||
CALLBACK_NAMESPACE = _AZ_IRANIAN_BANK_GATEWAYS.get(
|
||||
"CALLBACK_NAMESPACE", f"{AZIranianBankGatewaysConfig.name}:callback"
|
||||
)
|
||||
GO_TO_BANK_GATEWAY_NAMESPACE = _AZ_IRANIAN_BANK_GATEWAYS.get(
|
||||
"GO_TO_BANK_GATEWAY_NAMESPACE", f"{AZIranianBankGatewaysConfig.name}:go-to-bank-gateway"
|
||||
)
|
||||
SAMPLE_RESULT_NAMESPACE = _AZ_IRANIAN_BANK_GATEWAYS.get(
|
||||
"SAMPLE_RESULT_NAMESPACE", f"{AZIranianBankGatewaysConfig.name}:sample-result"
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
from .exceptions import ( # noqa
|
||||
AmountDoesNotSupport,
|
||||
AZBankGatewaysException,
|
||||
BankGatewayConnectionError,
|
||||
BankGatewayStateInvalid,
|
||||
BankGatewayTokenExpired,
|
||||
BankGatewayUnclear,
|
||||
CurrencyDoesNotSupport,
|
||||
SettingDoesNotExist,
|
||||
SafeSettingsEnabled,
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
class AZBankGatewaysException(Exception):
|
||||
"""AZ bank gateways exception"""
|
||||
|
||||
|
||||
class SettingDoesNotExist(AZBankGatewaysException):
|
||||
"""The requested setting does not exist"""
|
||||
|
||||
|
||||
class CurrencyDoesNotSupport(AZBankGatewaysException):
|
||||
"""The requested currency does not support"""
|
||||
|
||||
|
||||
class AmountDoesNotSupport(AZBankGatewaysException):
|
||||
"""The requested amount does not support"""
|
||||
|
||||
|
||||
class BankGatewayConnectionError(AZBankGatewaysException):
|
||||
"""The requested gateway connection error"""
|
||||
|
||||
|
||||
class BankGatewayRejectPayment(AZBankGatewaysException):
|
||||
"""The requested bank reject payment"""
|
||||
|
||||
|
||||
class BankGatewayTokenExpired(AZBankGatewaysException):
|
||||
"""The requested bank token expire"""
|
||||
|
||||
|
||||
class BankGatewayUnclear(AZBankGatewaysException):
|
||||
"""The requested bank unclear"""
|
||||
|
||||
|
||||
class BankGatewayStateInvalid(AZBankGatewaysException):
|
||||
"""The requested bank unclear"""
|
||||
|
||||
|
||||
class BankGatewayAutoConnectionFailed(AZBankGatewaysException):
|
||||
"""The auto connection cant find bank"""
|
||||
|
||||
|
||||
class SafeSettingsEnabled(AZBankGatewaysException):
|
||||
"""This feature is disabled when the safe gateway is active"""
|
||||
@@ -0,0 +1,6 @@
|
||||
from django import forms
|
||||
|
||||
|
||||
class PaymentSampleForm(forms.Form):
|
||||
amount = forms.IntegerField(label="Amount", initial=10000)
|
||||
mobile_number = forms.CharField(label="Mobile", max_length=13, initial="+989112223344")
|
||||
@@ -0,0 +1,135 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-06-11 23:03+0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: apps.py:8
|
||||
msgid "Iranian bank gateway"
|
||||
msgstr ""
|
||||
|
||||
#: apps.py:9
|
||||
msgid "Iranian bank gateways"
|
||||
msgstr ""
|
||||
|
||||
#: models/banks.py:50
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: models/banks.py:55
|
||||
msgid "Bank"
|
||||
msgstr ""
|
||||
|
||||
#: models/banks.py:62
|
||||
msgid "Tracking code"
|
||||
msgstr ""
|
||||
|
||||
#: models/banks.py:68
|
||||
msgid "Amount"
|
||||
msgstr ""
|
||||
|
||||
#: models/banks.py:76
|
||||
msgid "Reference number"
|
||||
msgstr ""
|
||||
|
||||
#: models/banks.py:81
|
||||
msgid "Bank result"
|
||||
msgstr ""
|
||||
|
||||
#: models/banks.py:86
|
||||
msgid "Callback url"
|
||||
msgstr ""
|
||||
|
||||
#: models/banks.py:91
|
||||
msgid "Extra information"
|
||||
msgstr ""
|
||||
|
||||
#: models/banks.py:97
|
||||
msgid "Bank choose identifier"
|
||||
msgstr ""
|
||||
|
||||
#: models/banks.py:112
|
||||
msgid "Bank gateway"
|
||||
msgstr ""
|
||||
|
||||
#: models/banks.py:113
|
||||
msgid "Bank gateways"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:7
|
||||
msgid "BMI"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:8
|
||||
msgid "SEP"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:9
|
||||
msgid "Zarinpal"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:10
|
||||
msgid "IDPay"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:11
|
||||
msgid "Zibal"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:12
|
||||
msgid "Bahamta"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:13
|
||||
msgid "Mellat"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:17
|
||||
msgid "Rial"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:18
|
||||
msgid "Toman"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:30
|
||||
msgid "Waiting"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:31
|
||||
msgid "Redirect to bank"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:32
|
||||
msgid "Return from bank"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:33
|
||||
msgid "Cancel by user"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:34
|
||||
msgid "Expire gateway token"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:35
|
||||
msgid "Expire verify payment"
|
||||
msgstr ""
|
||||
|
||||
#: models/enum.py:36
|
||||
msgid "Complete"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,147 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-06-11 23:04+0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: apps.py:8
|
||||
msgid "Iranian bank gateway"
|
||||
msgstr "ماژول درگاه پرداخت"
|
||||
|
||||
#: apps.py:9
|
||||
msgid "Iranian bank gateways"
|
||||
msgstr "ماژول درگاه های پرداخت"
|
||||
|
||||
#: models/banks.py:50
|
||||
msgid "Status"
|
||||
msgstr "وضعیت"
|
||||
|
||||
#: models/banks.py:55
|
||||
msgid "Bank"
|
||||
msgstr "بانک"
|
||||
|
||||
#: models/banks.py:62
|
||||
msgid "Tracking code"
|
||||
msgstr "کد پیگیری"
|
||||
|
||||
#: models/banks.py:68
|
||||
msgid "Amount"
|
||||
msgstr "مبلغ"
|
||||
|
||||
#: models/banks.py:76
|
||||
msgid "Reference number"
|
||||
msgstr "رفرنس"
|
||||
|
||||
#: models/banks.py:81
|
||||
msgid "Bank result"
|
||||
msgstr "نتیجه بانک"
|
||||
|
||||
#: models/banks.py:86
|
||||
msgid "Callback url"
|
||||
msgstr "آدرس کال بک"
|
||||
|
||||
#: models/banks.py:91
|
||||
msgid "Extra information"
|
||||
msgstr "توضیحات"
|
||||
|
||||
#: models/banks.py:97
|
||||
msgid "Bank choose identifier"
|
||||
msgstr "مشخصه درگاه"
|
||||
|
||||
#: models/banks.py:112
|
||||
msgid "Bank gateway"
|
||||
msgstr "پرداخت"
|
||||
|
||||
#: models/banks.py:113
|
||||
msgid "Bank gateways"
|
||||
msgstr "پرداخت ها"
|
||||
|
||||
#: models/banks.py:75
|
||||
msgid "Created at"
|
||||
msgstr "تاریخ ایجاد"
|
||||
|
||||
#: models/banks.py:76
|
||||
msgid "Updated at"
|
||||
msgstr "تاریخ بروزرسانی"
|
||||
|
||||
#: models/enum.py:7
|
||||
msgid "BMI"
|
||||
msgstr "بانک ملی ایران"
|
||||
|
||||
#: models/enum.py:8
|
||||
msgid "SEP"
|
||||
msgstr "بانک سامان"
|
||||
|
||||
#: models/enum.py:9
|
||||
msgid "Zarinpal"
|
||||
msgstr "زرین پال"
|
||||
|
||||
#: models/enum.py:10
|
||||
msgid "IDPay"
|
||||
msgstr "آی دی پی"
|
||||
|
||||
#: models/enum.py:11
|
||||
msgid "Zibal"
|
||||
msgstr "زیبال"
|
||||
|
||||
#: models/enum.py:12
|
||||
msgid "Bahamta"
|
||||
msgstr "باهمتا"
|
||||
|
||||
#: models/enum.py:13
|
||||
msgid "Mellat"
|
||||
msgstr "بانک ملت"
|
||||
|
||||
#: models/enum.py:17
|
||||
msgid "Rial"
|
||||
msgstr "ریال"
|
||||
|
||||
#: models/enum.py:18
|
||||
msgid "Toman"
|
||||
msgstr "تومان"
|
||||
|
||||
#: models/enum.py:30
|
||||
msgid "Waiting"
|
||||
msgstr "در انتظار"
|
||||
|
||||
#: models/enum.py:31
|
||||
msgid "Redirect to bank"
|
||||
msgstr "هدایت شده به بانک"
|
||||
|
||||
#: models/enum.py:32
|
||||
msgid "Return from bank"
|
||||
msgstr "بازگشته از بانک"
|
||||
|
||||
#: models/enum.py:33
|
||||
msgid "Cancel by user"
|
||||
msgstr "لغو توسط کاربر"
|
||||
|
||||
#: models/enum.py:34
|
||||
msgid "Expire gateway token"
|
||||
msgstr "توکن منقضی شده"
|
||||
|
||||
#: models/enum.py:35
|
||||
msgid "Expire verify payment"
|
||||
msgstr "مهلت پرداخت به اتمام رسیده"
|
||||
|
||||
#: models/enum.py:36
|
||||
msgid "Complete"
|
||||
msgstr "تکمیل شده"
|
||||
|
||||
#: models/enum.py:38
|
||||
msgid "Unknown error acquired"
|
||||
msgstr "مشکلی پیش آمده"
|
||||
@@ -0,0 +1,74 @@
|
||||
# Generated by Django 3.1.4 on 2020-12-06 13:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Bank",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("Waiting", "Waiting"),
|
||||
("Redirect to bank", "Redirect To Bank"),
|
||||
("Return from bank", "Return From Bank"),
|
||||
("Cancel by user", "Cancel By User"),
|
||||
("Expire gateway token", "Expire Gateway Token"),
|
||||
("Complete", "Complete"),
|
||||
],
|
||||
max_length=50,
|
||||
verbose_name="Status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"bank_type",
|
||||
models.CharField(
|
||||
choices=[("BMI", "BMI"), ("ZARINPAL", "Zarinpal")],
|
||||
max_length=50,
|
||||
verbose_name="Bank",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tracking_code",
|
||||
models.CharField(max_length=255, verbose_name="Tracking code"),
|
||||
),
|
||||
("amount", models.CharField(max_length=10, verbose_name="Amount")),
|
||||
(
|
||||
"reference_number",
|
||||
models.CharField(max_length=255, unique=True, verbose_name="Reference number"),
|
||||
),
|
||||
(
|
||||
"response_result",
|
||||
models.TextField(blank=True, null=True, verbose_name="Bank result"),
|
||||
),
|
||||
("callback_url", models.TextField(verbose_name="Callback url")),
|
||||
(
|
||||
"extra_information",
|
||||
models.TextField(blank=True, null=True, verbose_name="Extra information"),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("update_at", models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Bank gateway",
|
||||
"verbose_name_plural": "Bank gateways",
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 3.1.4 on 2021-01-02 07:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("azbankgateways", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="bank",
|
||||
name="bank_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("BMI", "BMI"),
|
||||
("SEP", "SEP"),
|
||||
("ZARINPAL", "Zarinpal"),
|
||||
("IDPAY", "IDPay"),
|
||||
("ZIBAL", "Zibal"),
|
||||
("BAHAMTA", "Bahamta"),
|
||||
],
|
||||
max_length=50,
|
||||
verbose_name="Bank",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="bank",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("Waiting", "Waiting"),
|
||||
("Redirect to bank", "Redirect To Bank"),
|
||||
("Return from bank", "Return From Bank"),
|
||||
("Cancel by user", "Cancel By User"),
|
||||
("Expire gateway token", "Expire Gateway Token"),
|
||||
("Expire verify payment", "Expire Verify Payment"),
|
||||
("Complete", "Complete"),
|
||||
],
|
||||
max_length=50,
|
||||
verbose_name="Status",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.4 on 2021-01-04 03:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("azbankgateways", "0002_auto_20210102_0721"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="bank",
|
||||
name="bank_choose_identifier",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="Bank choose identifier",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 3.2 on 2021-11-15 15:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("azbankgateways", "0003_bank_bank_choose_identifier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="bank",
|
||||
name="bank_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("BMI", "BMI"),
|
||||
("SEP", "SEP"),
|
||||
("ZARINPAL", "Zarinpal"),
|
||||
("IDPAY", "IDPay"),
|
||||
("ZIBAL", "Zibal"),
|
||||
("BAHAMTA", "Bahamta"),
|
||||
("MELLAT", "Mellat"),
|
||||
],
|
||||
max_length=50,
|
||||
verbose_name="Bank",
|
||||
),
|
||||
),
|
||||
]
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
# Generated by Django 5.0.3 on 2024-03-28 13:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('azbankgateways', '0004_auto_20211115_1500'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='bank',
|
||||
name='bank_type',
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
('BMI', 'BMI'),
|
||||
('SEP', 'SEP'),
|
||||
('ZARINPAL', 'Zarinpal'),
|
||||
('IDPAY', 'IDPay'),
|
||||
('ZIBAL', 'Zibal'),
|
||||
('BAHAMTA', 'Bahamta'),
|
||||
('MELLAT', 'Mellat'),
|
||||
('PAYV1', 'PayV1'),
|
||||
],
|
||||
max_length=50,
|
||||
verbose_name='Bank',
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='bank',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='bank',
|
||||
name='status',
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
('WAITING', 'Waiting'),
|
||||
('REDIRECT_TO_BANK', 'Redirect to bank'),
|
||||
('RETURN_FROM_BANK', 'Return from bank'),
|
||||
('CANCEL_BY_USER', 'Cancel by user'),
|
||||
('EXPIRE_GATEWAY_TOKEN', 'Expire gateway token'),
|
||||
('EXPIRE_VERIFY_PAYMENT', 'Expire verify payment'),
|
||||
('COMPLETE', 'Complete'),
|
||||
('ERROR', 'Unknown error acquired'),
|
||||
],
|
||||
max_length=50,
|
||||
verbose_name='Status',
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='bank',
|
||||
name='update_at',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Updated at'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.1.2 on 2025-03-18 13:30
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('azbankgateways', '0005_alter_bank_bank_type_alter_bank_created_at_and_more'),
|
||||
('order', '0023_remove_ordermodel_bank_records'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bank',
|
||||
name='order',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='order.ordermodel'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,2 @@
|
||||
from .banks import Bank # noqa
|
||||
from .enum import BankType, CurrencyEnum, PaymentStatus # noqa
|
||||
@@ -0,0 +1,92 @@
|
||||
import datetime
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .enum import BankType, PaymentStatus
|
||||
|
||||
from order.models import OrderModel
|
||||
|
||||
class BankQuerySet(models.QuerySet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BankQuerySet, self).__init__(*args, **kwargs)
|
||||
|
||||
def active(self):
|
||||
return self.filter()
|
||||
|
||||
|
||||
class BankManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return BankQuerySet(self.model, using=self._db)
|
||||
|
||||
def active(self):
|
||||
return self.get_queryset().active()
|
||||
|
||||
def update_expire_records(self):
|
||||
count = (
|
||||
self.active()
|
||||
.filter(
|
||||
status=PaymentStatus.RETURN_FROM_BANK,
|
||||
update_at__lte=datetime.datetime.now() - datetime.timedelta(minutes=15),
|
||||
)
|
||||
.update(status=PaymentStatus.EXPIRE_VERIFY_PAYMENT)
|
||||
)
|
||||
|
||||
count = count + self.active().filter(
|
||||
status=PaymentStatus.REDIRECT_TO_BANK,
|
||||
update_at__lt=datetime.datetime.now() - datetime.timedelta(minutes=15),
|
||||
).update(status=PaymentStatus.EXPIRE_GATEWAY_TOKEN)
|
||||
return count
|
||||
|
||||
def filter_return_from_bank(self):
|
||||
return self.active().filter(status=PaymentStatus.RETURN_FROM_BANK)
|
||||
|
||||
|
||||
class Bank(models.Model):
|
||||
status = models.CharField(
|
||||
max_length=50,
|
||||
null=False,
|
||||
blank=False,
|
||||
choices=PaymentStatus.choices,
|
||||
verbose_name=_("Status"),
|
||||
)
|
||||
bank_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=BankType.choices,
|
||||
verbose_name=_("Bank"),
|
||||
)
|
||||
# It's local and generate locally
|
||||
tracking_code = models.CharField(max_length=255, null=False, blank=False, verbose_name=_("Tracking code"))
|
||||
amount = models.CharField(max_length=10, null=False, blank=False, verbose_name=_("Amount"))
|
||||
# Reference number return from bank
|
||||
reference_number = models.CharField(
|
||||
unique=True,
|
||||
max_length=255,
|
||||
null=False,
|
||||
blank=False,
|
||||
verbose_name=_("Reference number"),
|
||||
)
|
||||
response_result = models.TextField(null=True, blank=True, verbose_name=_("Bank result"))
|
||||
callback_url = models.TextField(null=False, blank=False, verbose_name=_("Callback url"))
|
||||
extra_information = models.TextField(null=True, blank=True, verbose_name=_("Extra information"))
|
||||
bank_choose_identifier = models.CharField(
|
||||
max_length=255, blank=True, null=True, verbose_name=_("Bank choose identifier")
|
||||
)
|
||||
|
||||
order = models.ForeignKey(OrderModel, on_delete=models.SET_NULL, null=True, blank=True ,related_name='bank_records')
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_("Created at"))
|
||||
update_at = models.DateTimeField(auto_now=True, editable=False, verbose_name=_("Updated at"))
|
||||
|
||||
objects = BankManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Bank gateway")
|
||||
verbose_name_plural = _("Bank gateways")
|
||||
|
||||
def __str__(self):
|
||||
return "{}-{}".format(self.pk, self.tracking_code)
|
||||
|
||||
@property
|
||||
def is_success(self):
|
||||
return self.status == PaymentStatus.COMPLETE
|
||||
@@ -0,0 +1,37 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class BankType(models.TextChoices):
|
||||
BMI = "BMI", _("BMI")
|
||||
SEP = "SEP", _("SEP")
|
||||
ZARINPAL = "ZARINPAL", _("Zarinpal")
|
||||
IDPAY = "IDPAY", _("IDPay")
|
||||
ZIBAL = "ZIBAL", _("Zibal")
|
||||
BAHAMTA = "BAHAMTA", _("Bahamta")
|
||||
MELLAT = "MELLAT", _("Mellat")
|
||||
PAYV1 = "PAYV1", _("PayV1")
|
||||
|
||||
|
||||
class CurrencyEnum(models.TextChoices):
|
||||
IRR = "IRR", _("Rial")
|
||||
IRT = "IRT", _("Toman")
|
||||
|
||||
@classmethod
|
||||
def rial_to_toman(cls, amount):
|
||||
return amount / 10
|
||||
|
||||
@classmethod
|
||||
def toman_to_rial(cls, amount):
|
||||
return amount * 10
|
||||
|
||||
|
||||
class PaymentStatus(models.TextChoices):
|
||||
WAITING = "WAITING", _("Waiting")
|
||||
REDIRECT_TO_BANK = "REDIRECT_TO_BANK", _("Redirect to bank")
|
||||
RETURN_FROM_BANK = "RETURN_FROM_BANK", _("Return from bank")
|
||||
CANCEL_BY_USER = "CANCEL_BY_USER", _("Cancel by user")
|
||||
EXPIRE_GATEWAY_TOKEN = "EXPIRE_GATEWAY_TOKEN", _("Expire gateway token")
|
||||
EXPIRE_VERIFY_PAYMENT = "EXPIRE_VERIFY_PAYMENT", _("Expire verify payment")
|
||||
COMPLETE = "COMPLETE", _("Complete")
|
||||
ERROR = "ERROR", _("Unknown error acquired")
|
||||
@@ -0,0 +1,2 @@
|
||||
from .bases import Reader # noqa
|
||||
from .defaults import DefaultReader # noqa
|
||||
@@ -0,0 +1,40 @@
|
||||
import abc
|
||||
|
||||
import six
|
||||
|
||||
from azbankgateways import default_settings as settings
|
||||
from azbankgateways.models import BankType
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Reader:
|
||||
@abc.abstractmethod
|
||||
def read(self, bank_type: BankType, identifier: str) -> dict:
|
||||
"""
|
||||
|
||||
:param bank_type:
|
||||
:param identifier:
|
||||
:return:
|
||||
base on bank type for example for BMI:
|
||||
{
|
||||
'MERCHANT_CODE': '<YOUR INFO>',
|
||||
'TERMINAL_CODE': '<YOUR INFO>',
|
||||
'SECRET_KEY': '<YOUR INFO>',
|
||||
}
|
||||
"""
|
||||
pass
|
||||
|
||||
def klass(self, bank_type: BankType, identifier: str) -> dict:
|
||||
return settings.BANK_CLASS[bank_type]
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_bank_priorities(self, identifier: str) -> list:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def default(self, identifier: str):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def currency(self, identifier: str):
|
||||
pass
|
||||
@@ -0,0 +1,32 @@
|
||||
from azbankgateways import default_settings as settings
|
||||
from azbankgateways.models import BankType
|
||||
|
||||
from .bases import Reader
|
||||
|
||||
|
||||
class DefaultReader(Reader):
|
||||
def read(self, bank_type: BankType, identifier: str) -> dict:
|
||||
"""
|
||||
|
||||
:param bank_type:
|
||||
:param identifier:
|
||||
:return:
|
||||
base on bank type for example for BMI:
|
||||
{
|
||||
'MERCHANT_CODE': '<YOUR INFO>',
|
||||
'TERMINAL_CODE': '<YOUR INFO>',
|
||||
'SECRET_KEY': '<YOUR INFO>',
|
||||
}
|
||||
"""
|
||||
return settings.BANK_GATEWAYS[bank_type]
|
||||
|
||||
def default(self, identifier: str):
|
||||
return settings.BANK_DEFAULT
|
||||
|
||||
def currency(self, identifier: str):
|
||||
return settings.CURRENCY
|
||||
|
||||
def get_bank_priorities(self, identifier: str) -> list:
|
||||
priorities = [self.default(identifier)]
|
||||
priorities = list(dict.fromkeys(priorities + settings.BANK_PRIORITIES))
|
||||
return priorities
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<form id='id_form' action="{{ url }}" method="{{ method }}">
|
||||
{% csrf_token %}
|
||||
{% for key, value in params.items %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}">
|
||||
{% endfor %}
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.onload = function () {
|
||||
|
||||
function submitForm() {
|
||||
document.forms['id_form'].submit();
|
||||
}
|
||||
|
||||
submitForm();
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
{% load i18n static %}
|
||||
<!DOCTYPE html>
|
||||
{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %}
|
||||
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
|
||||
<head>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
{% block extrastyle %}{% endblock %}
|
||||
|
||||
{% if LANGUAGE_BIDI %}
|
||||
{% endif %}
|
||||
|
||||
{% block extrahead %}{% endblock %}
|
||||
{% block extrameta %}{% endblock %}
|
||||
{% block blockbots %}
|
||||
<meta name="robots" content="index, follow">
|
||||
{% endblock %}
|
||||
<meta charset="utf-8">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
|
||||
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{% block content %} {% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends "azbankgateways/samples/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block extrameta %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "azbankgateways/samples/base_site.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% block title %}پرداخت{% endblock %}
|
||||
{% block content %}
|
||||
<div class="mt-5">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="form-group">
|
||||
<label>{{ field.label_tag }}</label>
|
||||
<input type="{{ field.type }}" class="form-control" id="{{ field.name }}" name="{{ field.name }}"
|
||||
placeholder="{{ field.label_name }}" value="{{ field.initial }}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "azbankgateways/samples/base_site.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% block title %}نتیجه پرداخت{% endblock %}
|
||||
{% block content %}
|
||||
<div class="mt-5">
|
||||
<div class="card text-center">
|
||||
<div class="card-header">
|
||||
{{ bank_record.bank_type }} - نتیجه پرداخت
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title {% if bank_record.is_success %}bg-success{% else %}bg-danger{% endif %}">{% if bank_record.is_success %}پرداخت موفق{% else %}پرداخت نا موفق{% endif %}</h5>
|
||||
<p class="card-text">{{ bank_record.tracking_code }} - {{ bank_record.reference_number }} - {{ bank_record.response_result }} - {{ bank_record.extra_information }}</p>
|
||||
<a href="{{ bank_record.callback_url|cut:'/sample-result/'|add:'/sample-payment/' }}" class="btn btn-primary">Go somewhere</a>
|
||||
</div>
|
||||
<div class="card-footer text-muted">
|
||||
{{ bank_record.created_at }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,3 @@
|
||||
import typing
|
||||
|
||||
DictQuerystring = typing.Tuple[str, dict]
|
||||
@@ -0,0 +1,31 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import default_settings as settings
|
||||
from .apps import AZIranianBankGatewaysConfig
|
||||
from .views import (
|
||||
callback_view,
|
||||
go_to_bank_gateway,
|
||||
sample_payment_view,
|
||||
sample_result_view,
|
||||
)
|
||||
|
||||
app_name = AZIranianBankGatewaysConfig.name
|
||||
|
||||
_urlpatterns = [
|
||||
path("callback/", callback_view, name="callback"),
|
||||
]
|
||||
|
||||
if not settings.IS_SAFE_GET_GATEWAY_PAYMENT:
|
||||
_urlpatterns += [
|
||||
path("go-to-bank-gateway/", go_to_bank_gateway, name="go-to-bank-gateway"),
|
||||
]
|
||||
|
||||
if settings.IS_SAMPLE_FORM_ENABLE:
|
||||
_urlpatterns += [
|
||||
path("sample-payment/", sample_payment_view, name="sample-payment"),
|
||||
path("sample-result/", sample_result_view, name="sample-result"),
|
||||
]
|
||||
|
||||
|
||||
def az_bank_gateways_urls():
|
||||
return _urlpatterns, app_name, app_name
|
||||
@@ -0,0 +1,35 @@
|
||||
import json
|
||||
from urllib import parse
|
||||
|
||||
from azbankgateways.types import DictQuerystring
|
||||
|
||||
|
||||
def get_json(resp):
|
||||
"""
|
||||
:param response:returned response as json when sending a request
|
||||
using 'requests' module.
|
||||
|
||||
:return:response's content with json format
|
||||
"""
|
||||
|
||||
return json.loads(resp.content.decode("utf-8"))
|
||||
|
||||
|
||||
def append_querystring(url: str, params: dict) -> str:
|
||||
url_parts = list(parse.urlparse(url))
|
||||
query = dict(parse.parse_qsl(url_parts[4]))
|
||||
query.update(params)
|
||||
|
||||
url_parts[4] = parse.urlencode(query)
|
||||
|
||||
return parse.urlunparse(url_parts)
|
||||
|
||||
|
||||
def split_to_dict_querystring(url: str) -> DictQuerystring:
|
||||
url_parts = list(parse.urlparse(url))
|
||||
query = dict(parse.parse_qsl(url_parts[4]))
|
||||
|
||||
url_parts[4] = ""
|
||||
url_parts[5] = ""
|
||||
|
||||
return parse.urlunparse(url_parts), query
|
||||
@@ -0,0 +1,2 @@
|
||||
from .banks import callback_view, go_to_bank_gateway # noqa
|
||||
from .samples import sample_payment_view, sample_result_view # noqa
|
||||
@@ -0,0 +1,39 @@
|
||||
import logging
|
||||
from urllib.parse import unquote
|
||||
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from azbankgateways.bankfactories import BankFactory
|
||||
from azbankgateways.exceptions import AZBankGatewaysException
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def callback_view(request):
|
||||
bank_type = request.GET.get("bank_type", None)
|
||||
identifier = request.GET.get("identifier", None)
|
||||
|
||||
if not bank_type:
|
||||
logging.critical("Bank type is required. but it doesnt send.")
|
||||
raise Http404
|
||||
|
||||
factory = BankFactory()
|
||||
bank = factory.create(bank_type, identifier=identifier)
|
||||
try:
|
||||
bank.verify_from_gateway(request)
|
||||
except AZBankGatewaysException:
|
||||
logging.exception("Verify from gateway failed.", stack_info=True)
|
||||
return bank.redirect_client_callback()
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def go_to_bank_gateway(request):
|
||||
context = {"params": {}}
|
||||
for key, value in request.GET.items():
|
||||
if key == "url" or key == "method":
|
||||
context[key] = unquote(value)
|
||||
else:
|
||||
context["params"][key] = unquote(value)
|
||||
|
||||
return render(request, "azbankgateways/redirect_to_bank.html", context=context)
|
||||
@@ -0,0 +1,68 @@
|
||||
import logging
|
||||
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
|
||||
from azbankgateways import bankfactories
|
||||
from azbankgateways import default_settings as settings
|
||||
from azbankgateways import models as bank_models
|
||||
from azbankgateways.apps import AZIranianBankGatewaysConfig
|
||||
from azbankgateways.exceptions import AZBankGatewaysException
|
||||
|
||||
from ..forms import PaymentSampleForm
|
||||
|
||||
|
||||
def sample_payment_view(request):
|
||||
# if this is a POST request we need to process the form data
|
||||
if request.method == "POST":
|
||||
# create a form instance and populate it with data from the request:
|
||||
form = PaymentSampleForm(request.POST)
|
||||
# check whether it's valid:
|
||||
if form.is_valid():
|
||||
amount = form.cleaned_data["amount"]
|
||||
mobile_number = form.cleaned_data["mobile_number"]
|
||||
factory = bankfactories.BankFactory()
|
||||
try:
|
||||
bank = factory.auto_create()
|
||||
bank.set_request(request)
|
||||
bank.set_amount(amount)
|
||||
# یو آر ال بازگشت به نرم افزار برای ادامه فرآیند
|
||||
bank.set_client_callback_url(reverse(settings.SAMPLE_RESULT_NAMESPACE))
|
||||
bank.set_mobile_number(mobile_number) # اختیاری
|
||||
|
||||
# در صورت تمایل اتصال این رکورد به رکورد فاکتور یا هر چیزی که
|
||||
# بعدا بتوانید ارتباط بین محصول یا خدمات را با این
|
||||
# پرداخت برقرار کنید.
|
||||
|
||||
bank_record = bank.ready() # noqa
|
||||
|
||||
# هدایت کاربر به درگاه بانک
|
||||
if settings.IS_SAMPLE_FORM_ENABLE:
|
||||
return render(request, 'azbankgateways/redirect_to_bank.html', context=bank.get_gateway())
|
||||
return bank.redirect_gateway()
|
||||
except AZBankGatewaysException as e:
|
||||
logging.critical(e)
|
||||
# TODO: redirect to failed result.
|
||||
raise e
|
||||
|
||||
# if a GET (or any other method) we'll create a blank form
|
||||
else:
|
||||
form = PaymentSampleForm()
|
||||
|
||||
return render(request, "azbankgateways/samples/gateway.html", {"form": form})
|
||||
|
||||
|
||||
def sample_result_view(request):
|
||||
tracking_code = request.GET.get(settings.TRACKING_CODE_QUERY_PARAM, None)
|
||||
if not tracking_code:
|
||||
logging.debug("این لینک معتبر نیست.")
|
||||
raise Http404
|
||||
|
||||
try:
|
||||
bank_record = bank_models.Bank.objects.get(tracking_code=tracking_code)
|
||||
except bank_models.Bank.DoesNotExist:
|
||||
logging.debug("این لینک معتبر نیست.")
|
||||
raise Http404
|
||||
|
||||
return render(request, "azbankgateways/samples/result.html", {"bank_record": bank_record})
|
||||
@@ -65,4 +65,8 @@ CELERY_BEAT_SCHEDULE = {
|
||||
'task': 'product.tasks.update_product_prices',
|
||||
'schedule': crontab(minute='*'),
|
||||
},
|
||||
'update-bank-record-every-minute': {
|
||||
'task': 'order.tasks.udpate_bank_status',
|
||||
'schedule': crontab(minute='*'),
|
||||
},
|
||||
}
|
||||
@@ -288,6 +288,5 @@ class FakeAdminLoginView(View):
|
||||
hacker, created = SecurityBreachAttemptModel.objects.get_or_create(ip_address=ip)
|
||||
hacker.trys += 1
|
||||
hacker.save()
|
||||
messages.error(request, "Please correct the error below.")
|
||||
messages.error(request, "Please enter the correct شماره تماس and password for a staff account. Note that both fields may be case-sensitive.")
|
||||
messages.error(request, "لطفا شماره تماس و گذرواژه را برای یک حساب کارمند وارد کنید. توجه داشته باشید که ممکن است هر دو به کوچکی و بزرگی حروف حساس باشند.")
|
||||
return render(request, 'admin/fake_login.html', self.get_context(request))
|
||||
+3
-2
@@ -5,9 +5,10 @@ def main():
|
||||
settings_module = "core.settings.production"
|
||||
|
||||
|
||||
if "--develop" in sys.argv:
|
||||
if "--develop" in sys.argv or '-d' in sys.argv:
|
||||
settings_module = "core.settings.development"
|
||||
sys.argv.remove("--develop")
|
||||
dev_flag = '--develop' if "--develop" in sys.argv else '-d'
|
||||
sys.argv.remove(dev_flag)
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module)
|
||||
|
||||
|
||||
+55
-16
@@ -1,4 +1,4 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib import admin, messages
|
||||
from .models import *
|
||||
from unfold.admin import TabularInline, StackedInline
|
||||
|
||||
@@ -8,6 +8,11 @@ from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from utils.admin import ModelAdmin
|
||||
from django.utils.html import format_html, format_html_join
|
||||
from azbankgateways.models.banks import Bank
|
||||
from unfold.decorators import action
|
||||
from django.shortcuts import redirect
|
||||
|
||||
|
||||
class OrderItemModelInline(StackedInline):
|
||||
model = OrderItemModel
|
||||
extra = 0
|
||||
@@ -26,34 +31,68 @@ class DiscountCodeAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
list_display = ['code', 'expiration_date', 'percent', 'quantity']
|
||||
|
||||
|
||||
class BankRecordInline(StackedInline):
|
||||
model = Bank
|
||||
extra = 0
|
||||
max_num = 0
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
return [field.name for field in self.model._meta.fields]
|
||||
|
||||
|
||||
|
||||
@admin.register(OrderModel)
|
||||
class OrderAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
import_form_class = ImportForm
|
||||
export_form_class = ExportForm
|
||||
|
||||
list_filter = ['is_paid', 'status']
|
||||
|
||||
list_display = ['user', 'is_paid', 'status', 'discount_code', 'address', ]
|
||||
readonly_fields = ('created_at', 'bank_links')
|
||||
actions_list = ['redirect_to_learn', 'udpate_bank_status']
|
||||
list_display = ['user', 'is_paid', 'status', 'discount_code', 'address',]
|
||||
readonly_fields = ('created_at', )
|
||||
compressed_fields = True
|
||||
warn_unsaved_form = True
|
||||
exclude = ('bank_records',)
|
||||
# exclude = ('bank_records',)
|
||||
formfield_overrides = {
|
||||
ArrayField: {
|
||||
"widget": ArrayWidget,
|
||||
}
|
||||
}
|
||||
inlines = [OrderItemModelInline]
|
||||
def bank_links(self, obj):
|
||||
banks = obj.bank_records.all()
|
||||
inlines = [OrderItemModelInline, BankRecordInline]
|
||||
# def bank_links(self, obj):
|
||||
# banks = obj.bank_records.all()
|
||||
|
||||
if not banks.exists():
|
||||
return "-"
|
||||
# if not banks.exists():
|
||||
# return "-"
|
||||
|
||||
return format_html_join(
|
||||
"",
|
||||
'<a style="padding-bottom:10px;display:block;" href="/secret-admin/azbankgateways/bank/{}/change/" class="text-primary-600 dark:text-primary-500">{}</a>',
|
||||
[(bank.id, bank.tracking_code) for bank in banks]
|
||||
) or "-"
|
||||
# return format_html_join(
|
||||
# "",
|
||||
# '<a style="padding-bottom:10px;display:block;" href="/secret-admin/azbankgateways/bank/{}/change/" class="text-primary-600 dark:text-primary-500">{}</a>',
|
||||
# [(bank.id, bank.tracking_code) for bank in banks]
|
||||
# ) or "-"
|
||||
|
||||
bank_links.short_description = "Bank Records"
|
||||
# bank_links.short_description = "Bank Records"
|
||||
|
||||
@action(description='اپدیت وضعیت رکورد های بانکی')
|
||||
def udpate_bank_status(self, request):
|
||||
import logging
|
||||
from azbankgateways import (
|
||||
bankfactories,
|
||||
models as bank_models,
|
||||
default_settings as settings,
|
||||
)
|
||||
factory = bankfactories.BankFactory()
|
||||
|
||||
bank_models.Bank.objects.update_expire_records()
|
||||
|
||||
for item in bank_models.Bank.objects.filter_return_from_bank():
|
||||
bank = factory.create(
|
||||
bank_type=item.bank_type, identifier=item.bank_choose_identifier
|
||||
)
|
||||
bank.verify(item.tracking_code)
|
||||
bank_record = bank_models.Bank.objects.get(tracking_code=item.tracking_code)
|
||||
if bank_record.is_success:
|
||||
logging.debug("This record is verify now.", extra={"pk": bank_record.pk})
|
||||
messages.success(request, f"با موفقیت اپدیت شد")
|
||||
return redirect("admin:order_ordermodel_changelist")
|
||||
@@ -1,2 +0,0 @@
|
||||
class DiscountNotAvailableError(Exception):
|
||||
pass
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.2 on 2025-03-17 14:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0021_ordermodel_discount_ordermodel_final_price_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='orderitemmodel',
|
||||
name='price',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='قیمت'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.1.2 on 2025-03-18 13:30
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0022_alter_orderitemmodel_price'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='ordermodel',
|
||||
name='bank_records',
|
||||
),
|
||||
]
|
||||
@@ -2,9 +2,7 @@ from django.db import models
|
||||
from account.models import User, UserAddressModel, PushSubscription
|
||||
from product.models import ProductModel, ProductVariant, ProductImageModel
|
||||
from django.utils import timezone
|
||||
from .execptions import DiscountNotAvailableError
|
||||
from django_jalali.db import models as jmodels
|
||||
from azbankgateways.models.banks import Bank
|
||||
|
||||
|
||||
class DiscountCode(models.Model):
|
||||
@@ -56,7 +54,7 @@ class OrderModel(models.Model):
|
||||
tax = models.BigIntegerField(null=True, blank=True, verbose_name='مالیات')
|
||||
final_price = models.BigIntegerField(null=True, blank=True, verbose_name='قیمت نهایی')
|
||||
cart_total = models.BigIntegerField(null=True, blank=True, verbose_name='کل سبد خرید')
|
||||
bank_records = models.ManyToManyField(Bank, max_length=100, verbose_name='رکورد بانکی', null=True, blank=True)
|
||||
# bank_records = models.ManyToManyField(Bank, max_length=100, verbose_name='رکورد بانکی', null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f'سفارش: {self.id + 1000}'
|
||||
@@ -108,7 +106,7 @@ class OrderModel(models.Model):
|
||||
class OrderItemModel(models.Model):
|
||||
order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش')
|
||||
quantity = models.PositiveSmallIntegerField(verbose_name="تعداد")
|
||||
price = models.PositiveIntegerField(verbose_name='قیمت', blank=True, null=True)
|
||||
price = models.PositiveIntegerField(verbose_name='قیمت', default=0)
|
||||
product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول")
|
||||
class Meta:
|
||||
verbose_name = 'ایتم سبد خرید'
|
||||
|
||||
+17
-10
@@ -5,15 +5,22 @@ from azbankgateways import (
|
||||
default_settings as settings,
|
||||
)
|
||||
|
||||
# factory = bankfactories.BankFactory()
|
||||
|
||||
# bank_models.Bank.objects.update_expire_records()
|
||||
from celery import shared_task
|
||||
|
||||
# for item in bank_models.Bank.objects.filter_return_from_bank():
|
||||
# bank = factory.create(
|
||||
# bank_type=item.bank_type, identifier=item.bank_choose_identifier
|
||||
# )
|
||||
# bank.verify(item.tracking_code)
|
||||
# bank_record = bank_models.Bank.objects.get(tracking_code=item.tracking_code)
|
||||
# if bank_record.is_success:
|
||||
# logging.debug("This record is verify now.", extra={"pk": bank_record.pk})
|
||||
@shared_task
|
||||
def udpate_bank_status():
|
||||
factory = bankfactories.BankFactory()
|
||||
|
||||
bank_models.Bank.objects.update_expire_records()
|
||||
|
||||
for item in bank_models.Bank.objects.filter_return_from_bank():
|
||||
bank = factory.create(
|
||||
bank_type=item.bank_type, identifier=item.bank_choose_identifier
|
||||
)
|
||||
bank.verify(item.tracking_code)
|
||||
bank_record = bank_models.Bank.objects.get(tracking_code=item.tracking_code)
|
||||
if bank_record.is_success:
|
||||
logging.debug("This record is verify now.", extra={"pk": bank_record.pk})
|
||||
|
||||
print('update bank record is done')
|
||||
+9
-16
@@ -1,5 +1,4 @@
|
||||
from django.shortcuts import render
|
||||
from .execptions import DiscountNotAvailableError
|
||||
from rest_framework.views import APIView, Response
|
||||
from django.shortcuts import get_object_or_404
|
||||
from product.models import ProductVariant
|
||||
@@ -13,19 +12,12 @@ from azbankgateways import bankfactories, models as bank_models
|
||||
from azbankgateways.exceptions import AZBankGatewaysException
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
|
||||
from utils.pagination import StructurePagination
|
||||
try:
|
||||
pass
|
||||
except DiscountNotAvailableError:
|
||||
pass
|
||||
from order.models import OrderModel
|
||||
from django.urls import reverse
|
||||
"""
|
||||
|
||||
add post
|
||||
remove delete
|
||||
show get
|
||||
|
||||
pay
|
||||
"""
|
||||
# try:
|
||||
# pass
|
||||
# except DiscountNotAvailableError:
|
||||
# pass
|
||||
|
||||
|
||||
|
||||
@@ -204,9 +196,10 @@ class PaymentView(APIView):
|
||||
bank.set_mobile_number(user_mobile_number)
|
||||
|
||||
bank_record = bank.ready()
|
||||
cart_order.bank_records.add(bank_record)
|
||||
cart_order.save()
|
||||
print(bank.redirect_gateway().url)
|
||||
# cart_order.bank_records.add(bank_record)
|
||||
# cart_order.save()
|
||||
bank_record.order = cart_order
|
||||
bank_record.save()
|
||||
return Response(bank.redirect_gateway().url)
|
||||
except AZBankGatewaysException as e:
|
||||
print(e)
|
||||
|
||||
@@ -65,7 +65,12 @@ class ProductVariantSerialzier(serializers.ModelSerializer):
|
||||
request = self.context.get('request')
|
||||
if not request or not request.user.is_authenticated:
|
||||
return 0
|
||||
return 1
|
||||
cart_items = self.context.get('cart_items', [])
|
||||
if cart_items:
|
||||
for item in cart_items:
|
||||
if item['product']['id'] == obj.id:
|
||||
return item['quantity']
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ from rest_framework.permissions import IsAuthenticatedOrReadOnly
|
||||
from utils.pagination import StructurePagination
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
|
||||
from order.serializers import OrderItemSerailzier
|
||||
from order.models import OrderModel
|
||||
# class APIView(APIView):
|
||||
# def __init__(self, *args, **kwargs):
|
||||
# super().__init__(*args, **kwargs)
|
||||
@@ -54,7 +54,15 @@ class ProductView(APIView):
|
||||
# authentication_classes = []
|
||||
def get(self, request, pk):
|
||||
product = get_object_or_404(ProductModel, id=pk)
|
||||
product_ser = self.serializer_class(instance=product, many=False, context={'request': request, 'view_type': 'instance'})
|
||||
if request.user.is_authenticated:
|
||||
cart_obj, _ = OrderModel.objects.get_or_create(user=request.user, status='CART')
|
||||
cart_items = cart_obj.items.all()
|
||||
cart_items_ser = OrderItemSerailzier(cart_items, many=True, context={'request': request})
|
||||
product_ser_context = {'request': request, 'view_type': 'instance', 'cart_items': cart_items_ser.data}
|
||||
else:
|
||||
product_ser_context = {'request': request, 'view_type': 'instance'}
|
||||
|
||||
product_ser = self.serializer_class(instance=product, many=False, context=product_ser_context)
|
||||
return Response(product_ser.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ annotated-types==0.7.0
|
||||
anyio==4.6.0
|
||||
asgiref==3.8.1
|
||||
attrs==24.2.0
|
||||
az-iranian-bank-gateways==2.0.5
|
||||
beautifulsoup4==4.12.3
|
||||
billiard==4.2.1
|
||||
boto3==1.36.26
|
||||
|
||||
@@ -76,12 +76,12 @@ const handleDeleteAddress = (id: number) => {
|
||||
</div>
|
||||
<span class="flex items-center justify-between w-full gap-3">
|
||||
<div
|
||||
class="flex items-center gap-3 typo-sub-h-lg font-semibold text-slate-900"
|
||||
class="flex items-center gap-3 lg:text-[1.125rem] font-semibold text-slate-900"
|
||||
>
|
||||
{{ !!address ? address.name : "آدرس" }}
|
||||
<span
|
||||
v-if="isSelected"
|
||||
class="bg-black rounded-xl px-3 py-2 text-slate-200 text-xs"
|
||||
class="bg-black rounded-lg px-3 py-2 text-slate-200 text-[10px] lg:text-xs"
|
||||
>
|
||||
انتخاب شده
|
||||
</span>
|
||||
@@ -97,7 +97,7 @@ const handleDeleteAddress = (id: number) => {
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-center justify-between w-full gap-8 lg:flex-row"
|
||||
class="flex flex-col items-center justify-between w-full gap-5 lg:gap-8 lg:flex-row"
|
||||
>
|
||||
<div class="w-full lg:w-9/12 overflow-hidden">
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useDeleteDiscountCode from "~/composables/api/orders/useDeleteDiscountCode";
|
||||
import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
|
||||
import useSubmitDiscountCode from "~/composables/api/orders/useSubmitDiscountCode";
|
||||
import { useToast } from "~/composables/global/useToast";
|
||||
@@ -10,25 +11,31 @@ import { QUERY_KEYS } from "~/constants";
|
||||
|
||||
const route = useRoute();
|
||||
const { $queryClient: queryClient } = useNuxtApp();
|
||||
|
||||
const discountCode = ref("");
|
||||
const { addToast } = useToast();
|
||||
|
||||
// queries
|
||||
|
||||
const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
|
||||
|
||||
const { addToast } = useToast();
|
||||
const discountCode = ref(cart.value?.discount_code?.code || "");
|
||||
|
||||
const {
|
||||
mutateAsync: submitDiscountCode,
|
||||
isPending: submitDiscountCodeIsPending,
|
||||
} = useSubmitDiscountCode();
|
||||
|
||||
const {
|
||||
mutateAsync: deleteDiscountCode,
|
||||
isPending: deleteDiscountCodeIsPending,
|
||||
} = useDeleteDiscountCode();
|
||||
|
||||
// computed
|
||||
|
||||
const nextPage: ComputedRef<{ name: string; label: string } | undefined> =
|
||||
computed(() => route.meta.nextPage);
|
||||
|
||||
const hasSubmittedDiscountCode = computed(() => !!cart.value?.discount_code);
|
||||
|
||||
// methods
|
||||
|
||||
const handleSubmitDiscountCode = () => {
|
||||
@@ -50,33 +57,49 @@ const handleSubmitDiscountCode = () => {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteDiscountCode = () => {
|
||||
deleteDiscountCode(undefined, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.cart] });
|
||||
discountCode.value = "";
|
||||
},
|
||||
onError: () => {
|
||||
addToast({
|
||||
message: "خطایی در حذف کد تخفیف رخ داد",
|
||||
options: {
|
||||
status: "error",
|
||||
},
|
||||
});
|
||||
discountCode.value = "";
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col bg-slate-50 sticky top-44 w-full lg:w-3/12 transition-all border border-slate-200 rounded-xl"
|
||||
class="flex flex-col bg-slate-50 w-full lg:w-3/12 transition-all border border-slate-200 rounded-xl"
|
||||
>
|
||||
<div
|
||||
class="w-full flex items-center justify-between p-5 border-b border-slate-200"
|
||||
class="w-full flex items-center justify-between py-5 px-4 border-b border-slate-200"
|
||||
>
|
||||
<span class="typo-sub-h-xl text-black">فاکتور خرید</span>
|
||||
<Icon name="ci:cart" class="**:stroke-black" size="24" />
|
||||
</div>
|
||||
|
||||
<div v-if="cartIsLoading" class="flex flex-col p-5 gap-4">
|
||||
<div v-if="cartIsLoading" class="flex flex-col p-4 gap-4 !rounded-lg">
|
||||
<Skeleton
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
class="w-full !h-7"
|
||||
:class="{
|
||||
'!h-12': [4, 5].includes(i),
|
||||
'!rounded-full': i == 5,
|
||||
'!rounded-lg': i != 5,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col p-5 gap-4">
|
||||
<div v-else class="flex flex-col p-4 gap-4">
|
||||
<div
|
||||
class="flex items-center justify-between w-full text-slate-800"
|
||||
>
|
||||
@@ -96,7 +119,7 @@ const handleSubmitDiscountCode = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!!cart?.discount_code"
|
||||
v-if="hasSubmittedDiscountCode"
|
||||
class="flex items-center justify-between w-full text-status-error-primary text-red-700"
|
||||
>
|
||||
<span class="max-w-1/2 text-sm"> تخفیف: </span>
|
||||
@@ -116,23 +139,43 @@ const handleSubmitDiscountCode = () => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
v-model="discountCode"
|
||||
placeholder="کد تخفیف"
|
||||
class="!py-2 !pe-2 ps-2.5"
|
||||
>
|
||||
<template #endItem>
|
||||
<button
|
||||
@click="handleSubmitDiscountCode"
|
||||
class="text-xs px-3 rounded-[7px] py-1.5 text-white bg-black hover:invert border border-white transition-all"
|
||||
>
|
||||
ثبت
|
||||
</button>
|
||||
</template>
|
||||
</Input>
|
||||
<div class="w-full flex justify-between">
|
||||
<Input
|
||||
v-model="discountCode"
|
||||
placeholder="کد تخفیف"
|
||||
class="!py-3 !pe-2 ps-2.5 w-full !rounded-none !border-e-[0px] !rounded-s-100"
|
||||
:disabled="hasSubmittedDiscountCode"
|
||||
/>
|
||||
<button
|
||||
@click="
|
||||
hasSubmittedDiscountCode
|
||||
? handleDeleteDiscountCode()
|
||||
: handleSubmitDiscountCode()
|
||||
"
|
||||
class="text-xs px-5 rounded-e-100 py-1.5 text-white bg-black hover:invert border-[1.5px] border-black hover:border-white transition-all disabled:cursor-not-allowed"
|
||||
:disabled="
|
||||
!discountCode.length ||
|
||||
submitDiscountCodeIsPending ||
|
||||
deleteDiscountCodeIsPending
|
||||
"
|
||||
>
|
||||
<Icon
|
||||
v-if="
|
||||
submitDiscountCodeIsPending ||
|
||||
deleteDiscountCodeIsPending
|
||||
"
|
||||
name="svg-spinners:180-ring-with-bg"
|
||||
size="20"
|
||||
class="**:fill-white"
|
||||
/>
|
||||
<span v-else>
|
||||
{{ hasSubmittedDiscountCode ? "حذف" : "ثبت" }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<NuxtLink :to="{ name: nextPage?.name }">
|
||||
<Button start-icon="bi:arrow-right" class="w-full rounded-full">
|
||||
<Button start-icon="bi:arrow-right" class="w-full rounded-100">
|
||||
{{ nextPage?.label }}
|
||||
</Button>
|
||||
</NuxtLink>
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import { useImage } from "@vueuse/core";
|
||||
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
image: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
// props
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { image } = toRefs(props);
|
||||
|
||||
const { isLoading: cartImageIsLoading } = useImage({
|
||||
src: image.value,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center w-full gap-4">
|
||||
<div
|
||||
v-if="!cartImageIsLoading"
|
||||
class="size-[3.5rem] shrink-0 rounded-100 border border-gray-300 overflow-hidden"
|
||||
>
|
||||
<img src="/img/product-1.jpg" alt="product" class="object-conver" />
|
||||
<img :src="image" alt="product" class="object-conver" />
|
||||
</div>
|
||||
<Skeleton
|
||||
v-else
|
||||
class="!size-[3.5rem] aspect-square shrink-0 !rounded-100 border border-slate-200"
|
||||
/>
|
||||
<span
|
||||
class="text-xs font-semibold lg:text-sm text-gray-800 line-clamp-2"
|
||||
>
|
||||
فشارسنج بازویی امرن Omron M3
|
||||
{{ title }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
|
||||
|
||||
// state
|
||||
|
||||
const showMore = ref(false);
|
||||
|
||||
// queries
|
||||
|
||||
const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col items-center w-full gap-4 p-4 border lg:gap-6 border-gray-300 rounded-xl bg-gray-50"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-gray-900"
|
||||
>
|
||||
خلاصه سفارش
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="grid w-full grid-cols-1 gap-4 lg:gap-6 lg:grid-cols-3 md:grid-cols-2 lg:h-max transition-all"
|
||||
:class="showMore ? 'h-[calc(100px)]' : 'h-max'"
|
||||
>
|
||||
<template v-if="cartIsLoading">
|
||||
<Skeleton
|
||||
v-for="i in 6"
|
||||
class="w-full !h-14 !rounded-100"
|
||||
></Skeleton>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="w-full overflow-hidden gap-4 flex flex-col">
|
||||
<MinimalCartItem
|
||||
v-for="(cartItem, index) in cart?.items"
|
||||
:key="index"
|
||||
:image="cartItem.product.image"
|
||||
:title="cartItem.product.title"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="cart?.items.length > 5"
|
||||
class="h-7 flex-center col-span-full lg:hidden"
|
||||
>
|
||||
<button
|
||||
@click="showMore = !showMore"
|
||||
class="gap-2 flex-center"
|
||||
>
|
||||
<span class="text-sm text-black"> مشاهده بیشتر </span>
|
||||
<Icon name="bi:chevron-down" class="**:stroke-black" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -92,7 +92,7 @@ watch(
|
||||
() => debouncedCounter.value,
|
||||
(nv) => {
|
||||
addCartItem(
|
||||
{ id: data.value.id, quantity: nv },
|
||||
{ id: data.value.product.id, quantity: nv },
|
||||
{
|
||||
onSuccess: () => {
|
||||
invalidateCart();
|
||||
@@ -116,20 +116,10 @@ watch(
|
||||
<li
|
||||
class="flex flex-col items-center w-full gap-4 p-4 border lg:flex-row border-slate-200 rounded-xl bg-slate-50 overflow-hidden relative"
|
||||
>
|
||||
<img
|
||||
src="/logo.png"
|
||||
class="absolute -top-5 -left-5 rotate-[135deg] size-28"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-start w-full gap-2.5 lg:gap-4">
|
||||
<Skeleton
|
||||
v-if="cartImageIsLoading"
|
||||
class="!size-[12rem] aspect-square shrink-0 !rounded-xl border border-slate-200"
|
||||
/>
|
||||
|
||||
<div class="flex items-start justify-start w-full gap-2.5 lg:gap-4">
|
||||
<div
|
||||
v-else
|
||||
class="size-[12rem] aspect-square shrink-0 rounded-xl border border-slate-200 overflow-hidden"
|
||||
v-if="!cartImageIsLoading"
|
||||
class="size-[4rem] lg:size-[12rem] aspect-square shrink-0 rounded-xl border border-slate-200 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
:src="data.product.image"
|
||||
@@ -137,34 +127,42 @@ watch(
|
||||
alt="product"
|
||||
/>
|
||||
</div>
|
||||
<Skeleton
|
||||
v-else
|
||||
class="!size-[12rem] aspect-square shrink-0 !rounded-xl border border-slate-200"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<span class="font-semibold typo-sub-h-sm text-slate-600">
|
||||
{{ data.product.category }}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center justify-start gap-3">
|
||||
<span class="font-semibold typo-sub-h-xl text-black">
|
||||
{{ data.product.title }}
|
||||
<div class="flex flex-col w-full gap-3 lg:gap-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span
|
||||
class="font-semibold typo-sub-h-xs lg:typo-sub-h-sm text-slate-600"
|
||||
>
|
||||
{{ data.product.category }}
|
||||
</span>
|
||||
<div
|
||||
v-if="data.product.discount > 0"
|
||||
class="text-white bg-blue-500 px-4 py-2 text-xs rounded-full flex items-center gap-1"
|
||||
v-if="data.discount > 0"
|
||||
class="text-white bg-blue-500 px-3 lg:px-4 py-1.5 lg:py-2 text-[10px] lg:text-xs rounded-full flex items-center gap-1"
|
||||
>
|
||||
<Icon name="bi:percent" class="size-4" />
|
||||
{{ data.product.discount }}
|
||||
% تخفیف
|
||||
{{ data.discount }}
|
||||
تخفیف
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="font-semibold typo-sub-h-sm lg:typo-sub-h-xl text-black"
|
||||
>
|
||||
{{ data.product.title }}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center justify-start gap-1.5">
|
||||
<div
|
||||
v-if="!!data.product.color"
|
||||
class="px-3 py-1 rounded-full border border-slate-200 text-sm flex-center gap-1.5"
|
||||
class="px-3 py-1 rounded-full border border-slate-200 text-xs lg:text-sm flex-center gap-1.5"
|
||||
>
|
||||
<span> رنگ </span>
|
||||
<span
|
||||
class="!size-4 shadow-black/30 shadow-inner rounded-full"
|
||||
class="size-3 lg:!size-4 shadow-black/30 shadow-inner rounded-full"
|
||||
:style="{
|
||||
backgroundColor: `${data.product.color}`,
|
||||
}"
|
||||
@@ -176,7 +174,7 @@ watch(
|
||||
v-for="(variant, index) in data.product
|
||||
.product_attributes"
|
||||
:index="index"
|
||||
class="px-3 py-1 rounded-full border border-slate-200 text-sm"
|
||||
class="px-3 py-1 rounded-full border border-slate-200 text-xs lg:text-sm"
|
||||
>
|
||||
{{ variant.value }}
|
||||
</span>
|
||||
@@ -229,15 +227,15 @@ watch(
|
||||
<div class="flex items-end gap-2">
|
||||
<div class="flex flex-col">
|
||||
<span
|
||||
v-if="data.product.discount > 0"
|
||||
v-if="data.discount > 0"
|
||||
class="typo-p-sm relative flex-center w-fit line-through"
|
||||
>
|
||||
{{ data.product.price }}
|
||||
{{ data.price }}
|
||||
</span>
|
||||
<span
|
||||
class="typo-p-xl relative flex-center w-fit font-medium"
|
||||
>
|
||||
{{ data.product.final_price }}
|
||||
{{ data.final_price }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -249,7 +247,7 @@ watch(
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
@click="handleIncreaseQuantity"
|
||||
class="border size-10 flex-center rounded-100 border-slate-300"
|
||||
class="border size-7 p-1 lg:size-10 flex-center rounded-50 border-slate-300"
|
||||
:class="
|
||||
deleteCartItemIsPending ? 'pointer-events-none' : ''
|
||||
"
|
||||
@@ -257,11 +255,13 @@ watch(
|
||||
<Icon name="bi:plus" class="**:stroke-slate-800" />
|
||||
</button>
|
||||
|
||||
<div class="size-10 text-[1.125rem] flex-center">1</div>
|
||||
<div class="size-10 text-sm flex-center">
|
||||
{{ counter }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="handleDecreaseQuantity"
|
||||
class="border size-10 flex-center rounded-100 border-slate-300"
|
||||
class="border size-7 lg:size-10 p-1 flex-center rounded-50 border-slate-300"
|
||||
:class="
|
||||
deleteCartItemIsPending ? 'pointer-events-none' : ''
|
||||
"
|
||||
@@ -279,9 +279,17 @@ watch(
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span class="text-[1.125rem] text-slate-900 font-semibold">
|
||||
{{ data.product.price }}
|
||||
</span>
|
||||
<div class="flex flex-col">
|
||||
<span
|
||||
v-if="data.discount > 0"
|
||||
class="typo-p-xs relative flex-center w-fit line-through"
|
||||
>
|
||||
{{ data.price }}
|
||||
</span>
|
||||
<span class="typo-p-md relative flex-center w-fit font-medium">
|
||||
{{ data.final_price }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -31,9 +31,6 @@ const handleSubmit = () => {
|
||||
status: "success",
|
||||
},
|
||||
});
|
||||
setTimeout(() => {
|
||||
router.push({ name: "index" });
|
||||
}, 1000);
|
||||
},
|
||||
onError: () => {
|
||||
addToast({
|
||||
@@ -56,7 +53,11 @@ const handleSubmit = () => {
|
||||
@close="isShow = false"
|
||||
>
|
||||
<template #trigger>
|
||||
<Button class="rounded-full" end-icon="bi:trash" size="md">
|
||||
<Button
|
||||
class="rounded-full shrink-0 whitespace-pre"
|
||||
end-icon="bi:trash"
|
||||
size="md"
|
||||
>
|
||||
حذف همه
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
@@ -18,7 +18,7 @@ const { variant, size, loading } = toRefs(props);
|
||||
// computed
|
||||
const classes = computed(() => {
|
||||
return [
|
||||
"flex items-center justify-center transition-all cursor-pointer",
|
||||
"flex items-center justify-center transition-all cursor-pointer max-lg:text-xs",
|
||||
{
|
||||
"btn-solid": variant.value === "solid",
|
||||
"btn-secondary": variant.value === "secondary",
|
||||
|
||||
@@ -15,7 +15,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
placeholder: "وارد نشده",
|
||||
});
|
||||
const { variant, error, modelValue } = toRefs(props);
|
||||
const { variant, error, modelValue, disabled } = toRefs(props);
|
||||
|
||||
// emits
|
||||
|
||||
@@ -38,6 +38,7 @@ const classes = computed(() => {
|
||||
{
|
||||
"input-solid": variant.value === "solid",
|
||||
"input-outlined": variant.value === "outlined",
|
||||
"pointer-events-none opacity-80 text-slate-500": disabled.value,
|
||||
"input-effects": !error.value,
|
||||
[variant.value === "solid"
|
||||
? "input-solid-error"
|
||||
|
||||
@@ -1,31 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
products: ProductListItem[]
|
||||
}
|
||||
title?: string;
|
||||
products: ProductListItem[];
|
||||
withHeader?: boolean;
|
||||
};
|
||||
|
||||
// props
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
withHeader: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="w-full flex flex-col gap-10 md:gap-[4rem] py-[5rem] container">
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<span class="text-black typo-h-6 max-sm:text-xl md:typo-h-5 lg:typo-h-4">
|
||||
<section
|
||||
class="w-full flex flex-col gap-10 md:gap-[4rem] py-[5rem] container"
|
||||
>
|
||||
<div v-if="withHeader" class="w-full flex justify-between items-center">
|
||||
<span
|
||||
class="text-black typo-h-6 max-sm:text-xl md:typo-h-5 lg:typo-h-4"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
<NuxtLink to="/products">
|
||||
<Button variant="outlined" class="rounded-full max-sm:typo-label-sm max-sm:py-2" end-icon="ci:arrow-left">
|
||||
<Button
|
||||
variant="outlined"
|
||||
class="rounded-full max-sm:typo-label-sm max-sm:py-2"
|
||||
end-icon="ci:arrow-left"
|
||||
>
|
||||
نمایش همه
|
||||
</Button>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-y-8 gap-5 sm:gap-8">
|
||||
<ul
|
||||
class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-y-8 gap-5 sm:gap-8"
|
||||
>
|
||||
<ProductCard
|
||||
v-for="product in products"
|
||||
:key="product.id"
|
||||
@@ -37,6 +48,6 @@ defineProps<Props>();
|
||||
:rate="product.rating"
|
||||
:dark-layer="true"
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -13,48 +13,57 @@ const highlights = ref<Highlight[]>([
|
||||
{
|
||||
icon: "/img/footer-support.svg",
|
||||
title: "خدمات مشتری",
|
||||
description: "پشتیبانی استثنایی، راهحلهای پایدار برای شما"
|
||||
description: "پشتیبانی استثنایی، راهحلهای پایدار برای شما",
|
||||
},
|
||||
{
|
||||
icon: "/img/footer-send.svg",
|
||||
title: "ارسال سریع و رایگان",
|
||||
description: "ارسال رایگان برای سفارشهای بالای ۱۵۰ دلار"
|
||||
description: "ارسال رایگان برای سفارشهای بالای ۱۵۰ دلار",
|
||||
},
|
||||
{
|
||||
icon: "/img/footer-share.svg",
|
||||
title: "معرفی به دوستان",
|
||||
description: "ما را به دوستان خود معرفی کنید"
|
||||
description: "ما را به دوستان خود معرفی کنید",
|
||||
},
|
||||
{
|
||||
icon: "/img/footer-security.svg",
|
||||
title: "پرداخت امن",
|
||||
description: "پرداخت شما بهصورت امن پردازش میشود"
|
||||
}
|
||||
description: "پرداخت شما بهصورت امن پردازش میشود",
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="w-full border-t-[0.5px] border-slate-200">
|
||||
<div class="w-full py-[5rem] gap-12 xs:gap-8 sm:gap-12 xl:gap-0 container grid grid-cols-1 xs:grid-cols-2 lg:grid-cols-4">
|
||||
<div
|
||||
class="w-full py-[5rem] gap-8 sm:gap-12 xl:gap-0 container grid grid-cols-2 lg:grid-cols-4"
|
||||
>
|
||||
<template v-for="(highlight, index) in highlights" :key="index">
|
||||
<div class="flex flex-col-center gap-[.75rem] px-5">
|
||||
|
||||
<img :src="highlight.icon" class="size-[70px] md:size-[90px]" alt="" />
|
||||
<img
|
||||
:src="highlight.icon"
|
||||
class="size-[70px] md:size-[90px]"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<div class="w-full flex-col-center gap-[.25rem]">
|
||||
<span class="typo-sub-h-md text-black text-center">
|
||||
<span
|
||||
class="typo-sub-h-sm lg:typo-sub-h-md text-black text-center"
|
||||
>
|
||||
{{ highlight.title }}
|
||||
</span>
|
||||
<p class="text-slate-500 typo-p-sm mt-1 text-center">
|
||||
<p
|
||||
class="text-slate-500 typo-p-xs lg:typo-p-sm mt-1 text-center"
|
||||
>
|
||||
{{ highlight.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div-->
|
||||
<!-- class="w-[1px] h-[5rem] bg-slate-200"-->
|
||||
<!-- v-if="index + 1 != highlights.length"-->
|
||||
<!-- />-->
|
||||
<!-- <div-->
|
||||
<!-- class="w-[1px] h-[5rem] bg-slate-200"-->
|
||||
<!-- v-if="index + 1 != highlights.length"-->
|
||||
<!-- />-->
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -43,7 +43,7 @@ const closeSideDrawer = () => {
|
||||
<div
|
||||
@click.stop
|
||||
:class="modelValue ? 'translate-x-0' : 'translate-x-[100%]'"
|
||||
class="md:hidden cursor-default flex top-0 right-0 fixed z-999 transition-all flex-col bg-white w-[350px] h-full gap-8 pt-12"
|
||||
class="md:hidden cursor-default flex top-0 right-0 fixed z-999 transition-all duration-500 rounded-e-xl flex-col bg-white w-[300px] h-full gap-8 pt-12"
|
||||
>
|
||||
<div class="flex items-center flex-col justify-end gap-[1.5rem]">
|
||||
<Tooltip v-if="!!account && !!token" title="حساب کاربری">
|
||||
|
||||
@@ -30,11 +30,10 @@ await suspense();
|
||||
const onSwiper = (swiper: SwiperClass) => {
|
||||
swiper_instance.value = swiper;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="w-full flex flex-col gap-10 md:gap-[4rem] py-[5rem] lg:container">
|
||||
<section class="w-full flex flex-col gap-10 md:gap-[4rem]">
|
||||
<div class="w-full flex justify-between items-center max-lg:container">
|
||||
<span class="text-black typo-h-6 md:typo-h-5 lg:typo-h-4">
|
||||
{{ title }}
|
||||
@@ -92,12 +91,12 @@ const onSwiper = (swiper: SwiperClass) => {
|
||||
:breakpoints="{
|
||||
640: {
|
||||
centeredSlides: true,
|
||||
slidesPerView : 2.5
|
||||
slidesPerView: 2.5,
|
||||
},
|
||||
1024 : {
|
||||
1024: {
|
||||
centeredSlides: false,
|
||||
slidesPerView : 3
|
||||
}
|
||||
slidesPerView: 3,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<SwiperSlide
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
// import
|
||||
|
||||
import Tag from "~/components/global/Tag.vue";
|
||||
@@ -10,7 +9,7 @@ import { useImageColor } from "~/composables/global/useImageColor";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
id: number,
|
||||
id: number;
|
||||
title: string;
|
||||
colors: string[];
|
||||
price: string;
|
||||
@@ -28,77 +27,89 @@ const { id } = toRefs(props);
|
||||
// state
|
||||
|
||||
const { colorObject } = useImageColor(`#product-image-${id.value}`);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink :to="'/product/' + id">
|
||||
<div class="@container">
|
||||
<div
|
||||
class="group relative size-full aspect-square rounded-xl @[280px]:rounded-2xl bg-white brightness-[98%] overflow-hidden p-6"
|
||||
>
|
||||
|
||||
<img
|
||||
:id="`product-image-${id}`"
|
||||
:src="picture"
|
||||
class="group-hover:scale-105 transition-transform duration-200 size-full object-contain absolute inset-0"
|
||||
alt="product-background"
|
||||
/>
|
||||
|
||||
<li class="w-full">
|
||||
<NuxtLink :to="'/product/' + id">
|
||||
<div class="@container">
|
||||
<div
|
||||
v-if="darkLayer"
|
||||
class="bg-linear-to-t inset-0 from-black/50 to-transparent to-55% absolute z-10 size-full"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex justify-between items-center absolute px-4 @[280px]:px-6 pt-4 @[280px]:pt-6 top-0 w-full inset-x-0"
|
||||
>
|
||||
<Rate v-if="rate" :rate="rate" />
|
||||
<Tag v-if="tag">
|
||||
{{ tag }}
|
||||
</Tag>
|
||||
</div>
|
||||
<div
|
||||
:class="
|
||||
colorObject?.isLight && !darkLayer
|
||||
? 'text-black'
|
||||
: 'text-white'
|
||||
"
|
||||
class="absolute inset-x-0 bottom-0 pb-4 @[280px]:pb-6 px-4 @[280px]:px-6 flex flex-row-reverse justify-between items-end z-10"
|
||||
class="group relative size-full aspect-square rounded-xl @[280px]:rounded-2xl bg-white brightness-[98%] overflow-hidden p-6"
|
||||
>
|
||||
<img
|
||||
:id="`product-image-${id}`"
|
||||
:src="picture"
|
||||
class="group-hover:scale-105 transition-transform duration-200 size-full object-contain absolute inset-0"
|
||||
alt="product-background"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2 items-start w-full">
|
||||
<span class="@max-[280px]:hidden typo-sub-h-md @[280px]:typo-sub-h-lg truncate w-full">
|
||||
{{ title }}
|
||||
</span>
|
||||
<div class="flex items-center justify-between w-full mt-1">
|
||||
<div class="flex items-center gap-2 @[280px]:mt-1">
|
||||
<ColorCircle
|
||||
v-for="color in colors"
|
||||
:key="color"
|
||||
:style="{ backgroundColor: color }"
|
||||
class="!size-5 @[280px]:!size-6"
|
||||
/>
|
||||
</div>
|
||||
<span class="@max-[280px]:hidden typo-p-xs @[280px]:typo-p-md !font-semibold whitespace-nowrap">
|
||||
{{ price }}
|
||||
<div
|
||||
v-if="darkLayer"
|
||||
class="bg-linear-to-t inset-0 from-black/50 to-transparent to-55% absolute z-10 size-full"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex justify-between items-center absolute px-4 @[280px]:px-6 pt-4 @[280px]:pt-6 top-0 w-full inset-x-0"
|
||||
>
|
||||
<Rate v-if="rate" :rate="rate" />
|
||||
<Tag v-if="tag">
|
||||
{{ tag }}
|
||||
</Tag>
|
||||
</div>
|
||||
<div
|
||||
:class="
|
||||
colorObject?.isLight && !darkLayer
|
||||
? 'text-black'
|
||||
: 'text-white'
|
||||
"
|
||||
class="absolute inset-x-0 bottom-0 pb-4 @[280px]:pb-6 px-4 @[280px]:px-6 flex flex-row-reverse justify-between items-end z-10"
|
||||
>
|
||||
<div class="flex flex-col gap-2 items-start w-full">
|
||||
<span
|
||||
class="@max-[280px]:hidden typo-sub-h-md @[280px]:typo-sub-h-lg truncate w-full"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
<div
|
||||
class="flex items-center justify-between w-full mt-1"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 @[280px]:mt-1"
|
||||
>
|
||||
<ColorCircle
|
||||
v-for="color in colors"
|
||||
:key="color"
|
||||
:style="{ backgroundColor: color }"
|
||||
class="!size-5 @[280px]:!size-6"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="@max-[280px]:hidden typo-p-xs @[280px]:typo-p-md !font-semibold whitespace-nowrap"
|
||||
>
|
||||
{{ price }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 px-2 items-start w-full text-black mt-4 @[280px]:hidden">
|
||||
<span class="typo-sub-h-sm w-full truncate">
|
||||
{{ title }}
|
||||
</span>
|
||||
<div class="@[280px]:hidden flex items-center justify-between w-full mt-1">
|
||||
<span class="typo-p-xs !font-semibold whitespace-nowrap">
|
||||
{{ price }}
|
||||
<div
|
||||
class="flex flex-col gap-1 px-2 items-start w-full text-black mt-4 @[280px]:hidden"
|
||||
>
|
||||
<span class="typo-sub-h-sm w-full truncate">
|
||||
{{ title }}
|
||||
</span>
|
||||
<div
|
||||
class="@[280px]:hidden flex items-center justify-between w-full mt-1"
|
||||
>
|
||||
<span
|
||||
class="typo-p-xs !font-semibold whitespace-nowrap"
|
||||
>
|
||||
{{ price }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -1,49 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useDeleteCartAll from "~/composables/api/orders/useDeleteCartAll";
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
import useSignOut from "~/composables/api/auth/useSignOut";
|
||||
import { useToast } from "~/composables/global/useToast";
|
||||
import { QUERY_KEYS } from "~/constants";
|
||||
|
||||
// state
|
||||
|
||||
const { $queryClient: queryClient } = useNuxtApp();
|
||||
const router = useRouter();
|
||||
const { refreshToken, logout } = useAuth();
|
||||
const { addToast } = useToast();
|
||||
|
||||
const isShow = ref(false);
|
||||
|
||||
// queries
|
||||
|
||||
const { mutateAsync: deleteCartAll, isPending: deleteCartAllIsPending } =
|
||||
useDeleteCartAll();
|
||||
const { mutateAsync: signout, isPending: signoutIsPending } = useSignOut();
|
||||
|
||||
// methods
|
||||
|
||||
const handleSubmit = () => {
|
||||
deleteCartAll(undefined, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.cart] });
|
||||
isShow.value = false;
|
||||
addToast({
|
||||
message: "سبد با موفقیت حذف شد",
|
||||
options: {
|
||||
status: "success",
|
||||
},
|
||||
});
|
||||
setTimeout(() => {
|
||||
router.push({ name: "index" });
|
||||
}, 1000);
|
||||
},
|
||||
onError: () => {
|
||||
addToast({
|
||||
message: "خطایی در حذف سبد رخ داد",
|
||||
options: {
|
||||
status: "error",
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
signout(
|
||||
{ refresh_token: refreshToken.value! },
|
||||
{
|
||||
onSuccess: () => {
|
||||
addToast({
|
||||
message: "با موفقیت از حساب خارج شدید",
|
||||
});
|
||||
logout(true);
|
||||
},
|
||||
onError: () => {
|
||||
addToast({
|
||||
message: "خطایی در خروج از حساب رخ داد",
|
||||
});
|
||||
isShow.value = false;
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -78,10 +70,10 @@ const handleSubmit = () => {
|
||||
size="md"
|
||||
>
|
||||
<Icon
|
||||
v-if="deleteCartAllIsPending"
|
||||
v-if="signoutIsPending"
|
||||
name="svg-spinners:3-dots-bounce"
|
||||
/>
|
||||
<span v-else>آره; دارم میرم</span>
|
||||
<span v-else>آره دارم میرم</span>
|
||||
</Button>
|
||||
<DialogClose aria-label="Close">
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type SignOutRequest = {
|
||||
refresh_token: string;
|
||||
};
|
||||
|
||||
const useSignOut = () => {
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleSignOut = async (params: SignOutRequest) => {
|
||||
const { data } = await axios.post(
|
||||
`${API_ENDPOINTS.auth.signout}`,
|
||||
params
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (params: SignOutRequest) => handleSignOut(params),
|
||||
});
|
||||
};
|
||||
|
||||
export default useSignOut;
|
||||
@@ -28,7 +28,8 @@ const useAddCartItem = () => {
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (itemData: AddCartItemRequest) => handleAddCartItem(itemData),
|
||||
mutationFn: (itemData: AddCartItemRequest) =>
|
||||
handleAddCartItem(itemData),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
const useDeleteDiscountCode = () => {
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleDeleteDiscountCode = async () => {
|
||||
const { data } = await axios.delete(
|
||||
API_ENDPOINTS.orders.cart.delete_discount
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => handleDeleteDiscountCode(),
|
||||
});
|
||||
};
|
||||
|
||||
export default useDeleteDiscountCode;
|
||||
@@ -0,0 +1,19 @@
|
||||
export const usePWA = () => {
|
||||
const isInstalledAsPWA = ref(false);
|
||||
|
||||
const checkPWAInstallation = () => {
|
||||
const isStandalone = window.matchMedia(
|
||||
"(display-mode: standalone)"
|
||||
).matches;
|
||||
const isIOSPWA = (window.navigator as any).standalone;
|
||||
isInstalledAsPWA.value = isStandalone || isIOSPWA;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkPWAInstallation();
|
||||
});
|
||||
|
||||
return {
|
||||
isInstalledAsPWA,
|
||||
};
|
||||
};
|
||||
@@ -24,7 +24,7 @@ export const API_ENDPOINTS = {
|
||||
refresh: "/token/refresh",
|
||||
verify: "/accounts/verify",
|
||||
signin: "/token",
|
||||
logout: "/accounts/logout",
|
||||
signout: "/accounts/logout",
|
||||
},
|
||||
chat: {
|
||||
messages: "/chat/product",
|
||||
@@ -50,6 +50,7 @@ export const API_ENDPOINTS = {
|
||||
delete_all: "/order/cart/all",
|
||||
add_one: "/order/cart/item",
|
||||
add_discount: "/order/cart/discount",
|
||||
delete_discount: "/order/cart/discount",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,7 +12,9 @@ const prevPage = computed(() => route.meta.prevPage);
|
||||
|
||||
// queries
|
||||
|
||||
const { data: cart } = useGetCartOrders();
|
||||
const { data: cart, isPending: cartIsPending, suspense } = useGetCartOrders();
|
||||
|
||||
await suspense();
|
||||
|
||||
// computed
|
||||
|
||||
@@ -27,12 +29,13 @@ const hasCartItem = computed(
|
||||
dir="rtl"
|
||||
>
|
||||
<Header />
|
||||
|
||||
<main
|
||||
class="w-full overflow-x-hidden container flex flex-col gap-[5rem] max-w-[80vw]"
|
||||
class="w-full overflow-x-hidden flex flex-col gap-[5rem] lg:max-w-[85vw]"
|
||||
>
|
||||
<div class="w-full flex flex-col">
|
||||
<div class="w-full flex flex-col container">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center gap-4 py-[5rem] lg:gap-0 lg:flex-row"
|
||||
class="flex flex-col items-center justify-center py-[3.5rem] lg:py-[5rem] gap-5 lg:gap-0 lg:flex-row"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-start w-full lg:w-3/12"
|
||||
@@ -53,7 +56,7 @@ const hasCartItem = computed(
|
||||
</div>
|
||||
|
||||
<h1
|
||||
class="w-full text-center typo-h-3 lg:w-6/12 title-large"
|
||||
class="w-full text-center lg:w-6/12 typo-h-5 lg:typo-h-4"
|
||||
>
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
@@ -61,7 +64,7 @@ const hasCartItem = computed(
|
||||
<div class="hidden w-3/12 shrink-0 lg:block"> </div>
|
||||
</div>
|
||||
<div
|
||||
class="w-full flex flex-col items-center relative justify-between gap-8 lg:gap-6 lg:flex-row lg:items-start"
|
||||
class="w-full flex flex-col items-center relative justify-between gap-4 lg:gap-6 lg:flex-row lg:items-start"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col w-full gap-4 lg:gap-6 shrink-0"
|
||||
@@ -69,11 +72,10 @@ const hasCartItem = computed(
|
||||
>
|
||||
<NuxtPage />
|
||||
</div>
|
||||
|
||||
<CartSummary v-if="hasCartItem" />
|
||||
<CartSummary v-if="hasCartItem && !cartIsPending" />
|
||||
</div>
|
||||
</div>
|
||||
<ProductsSlider title="دیگران این محصولات را هم خریدهاند" />
|
||||
<ProductsSlider title="دیگر محصولات" />
|
||||
</main>
|
||||
<div class="w-full flex-col flex mt-20">
|
||||
<ServiceHighlights />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<main class="w-full h-[100svh] font-iran-yekan-x">
|
||||
<main class="w-full h-[100svh] font-iran-yekan-x overflow-x-hidden">
|
||||
<NuxtPage />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -84,28 +84,7 @@ const selectedGateway = ref<PaymentGateway>(paymentGateways.value[0]);
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center w-full gap-4 p-4 border lg:gap-6 border-gray-300 rounded-xl bg-gray-50"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-gray-900"
|
||||
>
|
||||
خلاصه سفارش
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="grid w-full grid-cols-1 gap-6 lg:grid-cols-3 md:grid-cols-2"
|
||||
>
|
||||
<MinimalCartItem v-for="i in 9" />
|
||||
|
||||
<div class="h-7 flex-center col-span-full lg:hidden">
|
||||
<button class="gap-2 flex-center">
|
||||
<span class="text-sm text-black"> مشاهده بیشتر </span>
|
||||
<Icon name="bi:chevron-down" class="**:stroke-black" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<OrderSummary />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -142,52 +142,9 @@ const handleSelectAddress = (address: Address) => {
|
||||
۱۵۰٬۰۰۰ تومان
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label
|
||||
class="flex items-center opacity-50 select-none pointer-events-none justify-between w-full p-3 transition-all border cursor-pointer delivery-option focus-within:ring-2 ring-black ring-offset-2 focus-within:border-black rounded-100 border-slate-200 bg-slate-50"
|
||||
>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<SwitchRoot
|
||||
v-model="deliveryData.deliveryMethod.peyk"
|
||||
class="w-[3rem] h-[1.8rem] shrink-0 flex data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-black border border-slate-200 data-[state=checked]:border-black/20 rounded-full relative transition-all focus-within:outline-none"
|
||||
>
|
||||
<SwitchThumb
|
||||
class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]"
|
||||
/>
|
||||
</SwitchRoot>
|
||||
<span class="w-full text-slate-800 text-sm lg:text-[1rem]"
|
||||
>ارسال با پیک (فقط ارسال درون شهری شیراز)</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<span class="text-slate-800 text-sm lg:text-[1rem]">
|
||||
۱۵۰٬۰۰۰ تومان
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-center w-full gap-4 p-4 border lg:gap-6 border-slate-200 rounded-xl bg-slate-50"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-slate-900"
|
||||
>
|
||||
خلاصه سفارش
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="grid w-full grid-cols-1 gap-6 lg:grid-cols-3 md:grid-cols-2"
|
||||
>
|
||||
<MinimalCartItem v-for="i in 9" />
|
||||
|
||||
<div class="h-7 flex-center col-span-full lg:hidden">
|
||||
<button class="gap-2 flex-center">
|
||||
<span class="text-sm text-black"> مشاهده بیشتر </span>
|
||||
<Icon name="bi:chevron-down" class="**:stroke-black" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<OrderSummary />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const hasCartItem = computed(
|
||||
<div class="w-full flex flex-col gap-4 lg:gap-6">
|
||||
<div
|
||||
v-if="hasCartItem"
|
||||
class="flex items-center justify-between w-full gap-3 px-5 py-4 rounded-xl bg-slate-50 border border-slate-200"
|
||||
class="flex items-center justify-between w-full gap-3 px-4 py-4 rounded-xl bg-slate-50 border border-slate-200"
|
||||
>
|
||||
<Skeleton
|
||||
v-if="cartIsLoading"
|
||||
|
||||
+20
-22
@@ -105,34 +105,32 @@ watch(
|
||||
</div>
|
||||
<ul
|
||||
v-if="productsIsLoading"
|
||||
class="w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-[1.5rem]"
|
||||
class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-y-8 gap-5 sm:gap-8 w-full"
|
||||
>
|
||||
<Skeleton
|
||||
v-for="i in 9"
|
||||
:key="i"
|
||||
class="w-full !h-[25rem] lg:!h-[22.5rem] !rounded-2xl"
|
||||
/>
|
||||
<div class="w-full flex flex-col gap-3" v-for="i in 8" :key="i">
|
||||
<Skeleton
|
||||
v-for="i in 3"
|
||||
:key="i"
|
||||
class="w-full"
|
||||
:class="{
|
||||
'!h-[11.75rem] lg:!h-[22.5rem] !rounded-2xl': i == 1,
|
||||
'!h-[1.35rem] lg:!h-[1.5rem] !rounded-sm lg:!hidden': [
|
||||
2, 3,
|
||||
].includes(i),
|
||||
'!w-1/2': i == 2,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
<div v-else class="w-full h-max">
|
||||
<div v-if="!products?.length" class="flex flex-grow w-full">
|
||||
<Placeholder title="محصولی یافت نشد :(" icon="bi:search" />
|
||||
</div>
|
||||
<ul
|
||||
v-else
|
||||
class="w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-[1.5rem]"
|
||||
>
|
||||
<li v-for="(product, index) in products" :key="index">
|
||||
<ProductCard
|
||||
:id="product.id"
|
||||
:title="product.name"
|
||||
:picture="product.variants[0].images[0].image"
|
||||
:colors="product.colors"
|
||||
:price="product.variants[0].price"
|
||||
:rate="product.rating"
|
||||
:dark-layer="true"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<ProductsGrid
|
||||
:with-header="false"
|
||||
:products="products"
|
||||
class="!p-0"
|
||||
/>
|
||||
<div v-if="data?.count > 10" class="w-full flex-center py-10">
|
||||
<Pagination :items="paginationData" :total="data?.count" />
|
||||
</div>
|
||||
|
||||
@@ -150,7 +150,7 @@ const resetForm = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-svh items-center relative">
|
||||
<div class="w-full flex h-svh items-center relative container">
|
||||
<div
|
||||
class="bg-[url(/img/pattern-1.png)] -z-10 size-full absolute opacity-70"
|
||||
:style="{
|
||||
|
||||
@@ -1,50 +1,39 @@
|
||||
import { Workbox } from "workbox-window";
|
||||
import { usePWA } from "~/composables/global/usePwa";
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const updateAvailable = ref(false);
|
||||
let wb: Workbox | null = null;
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
const { isInstalledAsPWA } = usePWA();
|
||||
|
||||
if ("serviceWorker" in navigator && isInstalledAsPWA.value) {
|
||||
wb = new Workbox("/sw.js");
|
||||
|
||||
const isStandalone = window.matchMedia(
|
||||
"(display-mode: standalone)"
|
||||
).matches;
|
||||
const isIOSPWA = (window.navigator as any).standalone;
|
||||
const isInstalledAsPWA = isStandalone || isIOSPWA;
|
||||
|
||||
// Listen for messages from the service worker
|
||||
navigator.serviceWorker.addEventListener("message", (event) => {
|
||||
if (
|
||||
event.data &&
|
||||
event.data.type === "VERSION_CHECK"
|
||||
) {
|
||||
checkForUpdate(event.data.version);
|
||||
}
|
||||
wb.addEventListener("waiting", () => {
|
||||
checkForUpdate();
|
||||
});
|
||||
|
||||
// Register the service worker and check if there's already a waiting one
|
||||
wb.register().then((registration: any) => {
|
||||
if (registration.waiting) {
|
||||
checkForUpdate();
|
||||
}
|
||||
});
|
||||
wb.register()
|
||||
.then((registration: any) => {
|
||||
if (registration.waiting) {
|
||||
checkForUpdate();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Service worker registration failed:", error);
|
||||
});
|
||||
}
|
||||
|
||||
// 🔹 Function to compare versions and show update modal if needed
|
||||
const checkForUpdate = (newVersion?: string) => {
|
||||
const currentVersion = localStorage.getItem("pwa_version");
|
||||
|
||||
if (newVersion && currentVersion !== newVersion) {
|
||||
updateAvailable.value = true;
|
||||
localStorage.setItem("pwa_version", newVersion);
|
||||
}
|
||||
const checkForUpdate = () => {
|
||||
updateAvailable.value = true;
|
||||
};
|
||||
|
||||
// 🔹 Function to apply the update
|
||||
const handleUpdate = () => {
|
||||
wb?.messageSW({ type: "SKIP_WAITING" });
|
||||
window.location.reload();
|
||||
if (wb) {
|
||||
wb.messageSW({ type: "SKIP_WAITING" }).then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
Vendored
+5
-1
@@ -71,7 +71,7 @@ declare global {
|
||||
discount: number;
|
||||
color: string;
|
||||
video: string | null;
|
||||
cart_quantity : number;
|
||||
cart_quantity: number;
|
||||
};
|
||||
|
||||
type Product = {
|
||||
@@ -215,6 +215,10 @@ declare global {
|
||||
discount_amount: string;
|
||||
final_price: string;
|
||||
};
|
||||
discount: number;
|
||||
discount_amount: string;
|
||||
price: string;
|
||||
final_price: string;
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user