ipg/apps/pg/models.py
Ali Asadi 7dcb730507 FEATURE(wallets): rename WALLET_RIAL and add WALLET_INCOME_FROM_IPG
WALLET_RIAL -> WALLET_RIAL_DEPOSIT to match the naming used for the same
wallet-service UUID in the advertising/settlement/promotions repos. Also
adds WALLET_INCOME_FROM_IPG and wires it in: after a successful wallet
charge, the commission portion is deposited separately into it (attributed
to the requesting OAuth2 application) instead of never entering the wallet
ledger. The income deposit is best-effort and logs on failure rather than
failing an already-successful charge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 15:09:49 +03:30

503 lines
21 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, 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_INCOME_FROM_IPG,
"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_INCOME_FROM_IPG, 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_INCOME_FROM_IPG, 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 = <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']}>
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 = <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()]