ipg/apps/pg/models.py
Sayyid Hamid Mahdavi a318185147 admin
2026-04-22 19:59:44 +03:30

455 lines
20 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.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)
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, data):
preferred_gateways = self.get_preferred_gateways()
for gateway_label in preferred_gateways:
if gateway_label in data:
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.value}{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,
"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:
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 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):
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.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 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['get_token_response'] = 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['redirect_post_data'] = post_dict
# 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 = <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['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()]