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 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 django.utils.translation import gettext_lazy as _ logger = logging.getLogger(__name__) class GatewayChoise(TextChoices): FAKE = "fake", _("fake") PNA = 'pna', _('pna') # 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) 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) def save_commission(self): self.commission = calculate_pna_commission(self.amount) self.save(update_fields=['commission']) def get_gateway_tools(self): if self.gateway == GatewayChoise.PNA: return PNA.objects.get(payment_request=self) if self.gateway == GatewayChoise.FAKE: return Fake.objects.get(payment_request=self) def set_gateway(self, data): # if 'fake' in data: # self.gateway = GatewayChoise.FAKE # fake = Fake.objects.create(payment_request=self) # fake.payment_request = self # if 'pna' in data: self.gateway = GatewayChoise.PNA pna = PNA.objects.create(payment_request=self) pna.payment_request = self else: raise Exception('Unknown gateway') self.save() @property def reference_number(self): pna = PNA.objects.filter(payment_request=self).first() if pna: return pna.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, "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, 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, 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 or 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 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}") else: logger.warning(f"[Charge Wallet] Wallet charge verification failed | payment_id={self.pk}") return bool(to_pay_success) class PNA(BasePGModel): 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() @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) 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.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) # response = {'status': 0, 'cardNumberMasked': '504172******9210', 'rrn': 511629210215, 'appToken': 4101434578524, 'token': 'NDEwMTQzNDU3ODUyNA=='} # post = # post = self.details = response 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 Fake(BasePGModel): 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()