ipg/apps/pg/models.py
2025-10-29 13:36:50 +03:30

315 lines
12 KiB
Python

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.wallet_client import deposit_to_user_wallet_submit, \
deposit_to_user_wallet_verify
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)
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 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
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 = <QueryDict: {'Token': ['NDEwMTI0Nzc0MDMzMA=='], 'OrderId': ['7'], 'TerminalNo': ['31127029'], 'RRN': ['511629156251'], 'status': ['0'], 'TspToken': ['00000000-0000-0000-0000-000000000000'], 'HashCardNumber': ['6008617E6BAB76997499446356F5C8B60288445650DD551FDF32007BCF2B14AF'], 'Amount': ['21,000'], 'SwAmount': [''], 'STraceNo': ['123232']}>
# post = <QueryDict: {'Token': ['NDI2MDY3ODI0MTg1OA=='], 'OrderId': ['47'], 'status': ['-138']}>
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()