315 lines
12 KiB
Python
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()
|