import logging import urllib.parse import jdatetime from django.conf import settings from django.contrib import messages from django.db import models from django.db.models import TextChoices from django.urls import reverse from oauth2_provider.settings import oauth2_settings from apps.core.models import BaseModel, BasePGModel from apps.pg.constans import StateChoices, TypeChoices import uuid from utils.clients.wallet_client import deposit_to_user_wallet_submit, deposit_to_user_wallet_verify from utils.commons import calculate_pna_commission from main import settings from utils.pna import PNAClient from utils.sep import SEPClient from django.utils.translation import gettext_lazy as _ logger = logging.getLogger(__name__) class GatewayChoise(TextChoices): # FAKE = "fake", _("fake ipg") PNA = 'pna', _('pna ipg') SEP = 'sep', _('sep ipg') def get_gateway_class(gateway_label): return { # "fake": Fake, "pna": PNA, "sep": SEP, }[gateway_label] # TODO: use generic relation class PaymentRequest(models.Model): id = None uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True) application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, related_name='+', null=True, blank=True) gateway = models.CharField(max_length=255, choices=GatewayChoise.choices, blank=True) state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) amount = models.PositiveBigIntegerField(null=True, blank=True) commission = models.PositiveBigIntegerField(null=True) phone_number = models.CharField(max_length=255, null=True, blank=True) callback_url = models.CharField(max_length=255, null=True, blank=True) is_registered = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True, db_index=True) updated_at = models.DateTimeField(auto_now=True, db_index=True) @property def splited_amount(self): def amount_split_3digit(value): text = str(value)[::-1] return ','.join([l[::-1] for l in [text[i:i + 3] for i in range(0, len(text), 3)][::-1]]) if self.amount: amount_str = amount_split_3digit(str(self.amount)) return amount_str return @property def jajali_datetime(self): def to_jalali(value): if value: return jdatetime.datetime.fromgregorian(date=value, locale='fa_IR').strftime("%Y/%m/%d-%H:%M:%S") else: return value return to_jalali(self.created_at) @property def income_uuid(self): return uuid.uuid5(self.uuid, 'income') def save_commission(self): self.commission = calculate_pna_commission(self.amount) self.save(update_fields=['commission']) def get_policy(self): application_gateway, created = ApplicationGatewayPolicy.objects.get_or_create(application=self.application) return application_gateway def get_preferred_gateways(self): application_policy = self.get_policy() return application_policy.get_preferred_gateways() def get_preferred_gateway_classes(self): preferred_gateways = self.get_preferred_gateways() return [get_gateway_class(gateway_label) for gateway_label in preferred_gateways] def get_gateway_tools(self): gateway_class = get_gateway_class(self.gateway) return gateway_class.objects.get(payment_request=self) def set_gateway(self, gateway_label): preferred_gateways = self.get_preferred_gateways() if gateway_label in preferred_gateways: self.gateway = gateway_label gateway_class = get_gateway_class(gateway_label) pg = gateway_class.objects.create(payment_request=self) pg.payment_request = self self.save() if not self.gateway: raise Exception('No gateway') @property def reference_number(self): gateway_tools = self.get_gateway_tools() if gateway_tools: return f'{self.gateway}{gateway_tools.pk}' else: return '00000000' # TODO: complete and use this def change_state(self, from_state, to_state, error_message=None, **data): if self.state == from_state: updated = PaymentRequest.objects.filter(pk=self.pk, state=from_state).update( state=to_state, **data ) if updated: # double check in db level self.state = to_state else: raise Exception(error_message) else: raise Exception(error_message) def deposit_submit(self): data = { "uuid": str(self.uuid), "payee": str(self.user.uuid), "payee_type": 1, "payee_wallet": settings.WALLET_RIAL_DEPOSIT, "amount": self.amount, "details": { 'description': str(_("charge via ipg")), 'reference_id': str(self.uuid), 'application_details_url': '' }, } submit_response = deposit_to_user_wallet_submit(settings.WALLET_RIAL_DEPOSIT, data) if not submit_response or not submit_response.uuid: logger.error(f"[Deposit Submit] Wallet submission failed | payment_id={self.pk}") raise Exception('Failed to submit payment in wallet') to_success = PaymentRequest.objects.filter( pk=self.pk, state=StateChoices.WALLET_SUBMIT_PENDING.value, ).update( state=StateChoices.WALLET_SUBMIT_SUCCESS.value) if not to_success: logger.error(f"[Deposit Submit] Failed to update state to WALLET_SUBMIT_SUCCESS | payment_id={self.pk}") raise Exception('Cannot charge balance because state') return to_success def deposit_verify(self): verify_response = deposit_to_user_wallet_verify(settings.WALLET_RIAL_DEPOSIT, self.uuid) if not verify_response: logger.error(f"[Deposit Verify] Failed to get response from wallet service | payment_id={self.pk}") raise Exception('Failed to get response from wallet service') if not verify_response.uuid: logger.error(f"[Deposit Verify] Failed to get response uuid from wallet service | payment_id={self.pk}") raise Exception('Failed to get response uuid from wallet service') if verify_response.state != 5: logger.error( f"[Deposit Verify] Wallet verification failed | payment_id={self.pk}, response={verify_response}") # to_pay_failed = PaymentRequest.objects.filter( # pk=self.pk, # state=StateChoices.WALLET_SUBMIT_SUCCESS.value # ).update( # state=StateChoices.FAILED.value, # ) raise Exception('Failed to verify payment in wallet') to_pay_success = PaymentRequest.objects.filter( pk=self.pk, state=StateChoices.WALLET_SUBMIT_SUCCESS.value ).update( state=StateChoices.SUCCESS.value, ) if not to_pay_success: logger.error(f"[Deposit Verify] Failed to update state to SUCCESS | payment_id={self.pk}") raise Exception('Failed to update state payment') return to_pay_success def deposit_income_submit(self): data = { "uuid": str(self.income_uuid), "payee": str(self.application_id), "payee_type": TypeChoices.APPLICATION.value, "payee_wallet": settings.WALLET_IPG_TRANSIT, "amount": self.commission, "details": { 'description': str(_("ipg commission income")), 'reference_id': str(self.uuid), 'application_details_url': '' }, } submit_response = deposit_to_user_wallet_submit(settings.WALLET_IPG_TRANSIT, data) if not submit_response or not submit_response.uuid: raise Exception('Failed to submit income in wallet') return submit_response def deposit_income_verify(self): verify_response = deposit_to_user_wallet_verify(settings.WALLET_IPG_TRANSIT, self.income_uuid) if not verify_response or not verify_response.uuid: raise Exception('Failed to verify income in wallet') if verify_response.state != 5: raise Exception('Failed to verify income in wallet') return True def charge_wallet_income(self): if not self.application_id or not self.commission: return try: self.deposit_income_submit() self.deposit_income_verify() logger.info(f"[Charge Wallet] Income charged successfully | payment_id={self.pk}") except Exception: logger.exception(f"[Charge Wallet] Failed to charge income | payment_id={self.pk}") def charge_wallet_balance(self): logger.info(f"[Charge Wallet] Starting wallet charge process | payment_id={self.pk}") if self.state in [StateChoices.IPG_SUCCESS]: to_pending = PaymentRequest.objects.filter( pk=self.pk, state=StateChoices.IPG_SUCCESS.value, ).update( state=StateChoices.WALLET_SUBMIT_PENDING.value) if not to_pending: logger.error(f"[Charge Wallet] Failed to update state to WALLET_SUBMIT_PENDING | payment_id={self.pk}") raise Exception('Cannot charge balance because state') logger.info(f"[Charge Wallet] State updated to WALLET_SUBMIT_PENDING | payment_id={self.pk}") self.deposit_submit() to_pay_success = self.deposit_verify() if to_pay_success: logger.info(f"[Charge Wallet] Wallet charged successfully | payment_id={self.pk}") self.charge_wallet_income() else: logger.warning(f"[Charge Wallet] Wallet charge verification failed | payment_id={self.pk}") return bool(to_pay_success) class PNA(BasePGModel): label = GatewayChoise.PNA.value image = f'image/{GatewayChoise.PNA.value}.png' payment_request = models.OneToOneField(PaymentRequest, on_delete=models.PROTECT, related_name='pna') details = models.JSONField(null=True, blank=True, default=dict) pna_token = models.CharField(null=True, blank=True, max_length=255, db_index=True) pna_order_id = models.IntegerField(null=True, blank=True, db_index=True) pna_originator = models.CharField(null=True, blank=True, max_length=255) pna_status = models.IntegerField(choices=StateChoices.choices, null=True, blank=True, db_index=True) pna_status_code = models.IntegerField(null=True, blank=True) pna_status_message = models.CharField(null=True, blank=True, max_length=255) client = PNAClient() def make_request(self): self.payment_request.change_state(StateChoices.CREATED, StateChoices.IPG_PENDING, 'can not change state') amount = self.payment_request.amount + self.payment_request.commission description = 'from addwin ipg' callback_url = urllib.parse.urljoin( settings.IPG_BASE_PUBLIC_URL, reverse("pg:payment-confirm-view", kwargs={"uuid": self.payment_request.uuid})) order_id = self.id phone_number = self.payment_request.phone_number try: response = self.client.request_normal_sale(amount, description, callback_url, order_id, phone_number) self.details['make_request'] = response self.pna_status_code = response['status'] self.pna_status_message = response.get('title') if response['status'] == 0: self.pna_token = response['token'] else: self.payment_request.change_state(self.payment_request.state, StateChoices.FAILED, 'error in pardakht novin gateway') except Exception as e: self.payment_request.change_state(self.payment_request.state, StateChoices.FAILED, 'error in pardakht novin gateway') self.save() def get_pg_page_url(self): if self.pna_token: return self.client.pg_base_url.format(token=self.pna_token) def confirm_request(self, request=None): if self.payment_request.state not in [StateChoices.IPG_PENDING]: messages.error(request, "وضعیت تراکنش در حالت مناسبی نیست.") return False, "وضعیت تراکنش در حالت مناسبی نیست." print(dict(request.POST)) post_dict = dict(request.POST) if post_dict.get('status') in [0, '0', [0], ['0']]: try: pna_token = self.pna_token response = self.client.request_normal_sale_confirm(pna_token) self.details['confirm_request'] = response self.save() # response = {'status': 0, 'cardNumberMasked': '504172******9210', 'rrn': 511629210215, 'appToken': 4101434578524, 'token': 'NDEwMTQzNDU3ODUyNA=='} # post = # post = if response['status'] == 0: self.pna_status = StateChoices.SUCCESS.value self.payment_request.change_state(StateChoices.IPG_PENDING, StateChoices.IPG_SUCCESS, 'error in change state') self.save() self.payment_request.charge_wallet_balance() messages.success(request, 'شارژ انجام شد') return True, 'شارژ انجام شد' except Exception as e: print(e) messages.error(request, str(e)) return False, "خطایی رخ داده است." else: print(request.POST) messages.error(request, "نتیجه تراکنش از سمت درگاه دریافت نشد.") return False, "نتیجه تراکنش از سمت درگاه دریافت نشد." def reverse_request(self): raise NotImplementedError() class SEP(BasePGModel): label = GatewayChoise.SEP.value image = f'image/{GatewayChoise.SEP.value}.png' redirect_method = 'GET' payment_request = models.OneToOneField(PaymentRequest, on_delete=models.PROTECT, related_name='sep') details = models.JSONField(null=True, blank=True, default=dict) sep_token = models.CharField(null=True, blank=True, max_length=255, db_index=True) sep_ref_number = models.CharField(null=True, blank=True, max_length=255, db_index=True) # sep_originator = models.CharField(null=True, blank=True, max_length=255) sep_status = models.IntegerField(choices=StateChoices.choices, null=True, blank=True, db_index=True) sep_status_code = models.IntegerField(null=True, blank=True) sep_status_message = models.CharField(null=True, blank=True, max_length=255) client = SEPClient() def make_request(self): self.payment_request.change_state(StateChoices.CREATED, StateChoices.IPG_PENDING, 'can not change state') amount = self.payment_request.amount + self.payment_request.commission description = 'from addwin ipg' callback_url = urllib.parse.urljoin( settings.IPG_BASE_PUBLIC_URL, reverse("pg:payment-confirm-view", kwargs={"uuid": self.payment_request.uuid})) order_id = self.id phone_number = self.payment_request.phone_number try: response = self.client.request_token(amount, callback_url, order_id, phone_number) self.details['make_request'] = response self.sep_status_code = response['status'] self.sep_status_message = response.get('title') if response['status'] == 1: self.sep_token = response['token'] else: self.payment_request.change_state(self.payment_request.state, StateChoices.FAILED, 'error in sep gateway') except Exception as e: self.payment_request.change_state(self.payment_request.state, StateChoices.FAILED, 'error in sep gateway') self.save() def get_pg_page_url(self): if self.sep_token: return self.client.pg_base_url.format(token=self.sep_token) def confirm_request(self, request=None): if self.payment_request.state not in [StateChoices.IPG_PENDING]: messages.error(request, "وضعیت تراکنش در حالت مناسبی نیست.") return False, "وضعیت تراکنش در حالت مناسبی نیست." print(dict(request.POST)) post_dict = dict(request.POST) self.details['post_dict'] = post_dict self.save() # post = {'MID': ['0'], 'TerminalId': ['15336754'], 'RefNum': ['wSLVxHt51y3BjY9tkFXieaQAxHg6cmGp2X7B506RZs'], 'ResNum': ['10'], 'State': ['OK'], 'TraceNo': ['393594'], 'Amount': ['20000'], 'AffectiveAmount': ['20000'], 'Wage': [''], 'Rrn': ['224413541909'], 'SecurePan': ['504172******9210'], 'Status': ['2'], 'Token': ['dd830117c88a4e8284230430ccf6c553'], 'HashedCardNumber': ['6008617e6bab76997499446356f5c8b60288445650dd551fdf32007bcf2b14af']} try: ref_number = post_dict.get('RefNum')[0] except Exception as e: print(e) ref_number = None if post_dict.get('Status') in [2, '2', [2], ['2']]: try: self.sep_ref_number = ref_number self.save() response = self.client.request_confirm(ref_number) # response = {'status': 0, 'cardNumberMasked': '504172******9210', 'rrn': 511629210215, 'appToken': 4101434578524, 'token': 'NDEwMTQzNDU3ODUyNA=='} # post = # post = self.details['verify_response'] = response self.save() if response['ResultCode'] == 0: self.sep_status_status = StateChoices.SUCCESS.value self.payment_request.change_state(StateChoices.IPG_PENDING, StateChoices.IPG_SUCCESS, 'error in change state') self.save() self.payment_request.charge_wallet_balance() messages.success(request, 'شارژ انجام شد') return True, 'شارژ انجام شد' except Exception as e: print(e) messages.error(request, str(e)) return False, "خطایی رخ داده است." else: print(request.POST) messages.error(request, "نتیجه تراکنش از سمت درگاه دریافت نشد.") return False, "نتیجه تراکنش از سمت درگاه دریافت نشد." def reverse_request(self): raise NotImplementedError() class Fake(BasePGModel): # label = GatewayChoise.FAKE.value # image = f'image/{GatewayChoise.FAKE.value}.png' payment_request = models.OneToOneField(PaymentRequest, on_delete=models.PROTECT, related_name='+') def make_request(self): self.payment_request.change_state(StateChoices.CREATED, StateChoices.IPG_PENDING, 'can not change state') def get_pg_page_url(self): return reverse('pg:payment-confirm-view', kwargs={'uuid': self.payment_request.uuid}) def confirm_request(self, request=None): self.payment_request.change_state(StateChoices.IPG_PENDING, StateChoices.IPG_SUCCESS, 'can not change state') self.payment_request.charge_wallet_balance() messages.success(request, 'شارژ انجام شد') self.save() def reverse_request(self): raise NotImplementedError() class ApplicationGatewayPolicy(BaseModel): class PolicyChoise(TextChoices): RANDOM = "random", _("random") SELECTABLE = 'selectable', _('selectable') application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, related_name='+', null=True, blank=True, unique=True) preferred_gateways = models.CharField(max_length=100, null=True, blank=True, default='') policy = models.CharField(max_length=20, choices=PolicyChoise.choices, default=PolicyChoise.SELECTABLE) def get_preferred_gateways(self): return [p.strip() for p in self.preferred_gateways.split() if p.strip()]