separate payment gateways model

This commit is contained in:
Sayyid Hamid Mahdavi 2025-05-26 20:23:26 +03:30
parent 12a490393b
commit b37a11fdc2
12 changed files with 268 additions and 103 deletions

View file

@ -17,6 +17,22 @@ class BaseModel(models.Model):
class BasePGModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True, db_index=True)
def make_request(self):
pass
def confirm_request(self):
pass
def reverse_request(self):
pass
class Meta:
abstract = True

View file

@ -1,8 +1,8 @@
from django.contrib import admin from django.contrib import admin
from .models import ChargeRequest from .models import PaymentRequest
class ChargeRequestAdmin(admin.ModelAdmin): class PaymentRequestAdmin(admin.ModelAdmin):
list_display = ['uuid', 'id'] list_display = ['uuid', 'amount', 'user_id']
admin.site.register(ChargeRequest, ChargeRequestAdmin) admin.site.register(PaymentRequest, PaymentRequestAdmin)

View file

@ -4,12 +4,15 @@ from django.utils.translation import gettext_lazy as _
class StateChoices(IntegerChoices): class StateChoices(IntegerChoices):
CREATED = 1 , _('created') CREATED = 1 , _('created')
DELAYED = 2 , _('delayed') # delayed as user wish IPG_PENDING = 2 , _('ipg pending')
PENDING = 3 , _('pending') # wait for external service response (or action) IPG_SUCCESS = 3 , _('ipg success')
INCOMPLETE = 4 , _('incomplete') # started and wait for internal progress to complete
SUCCESS = 5 , _('success') WALLET_SUBMIT_PENDING = 11 , _('wallet submit pending')
FAILED = 6 , _('failed') WALLET_SUBMIT_SUCCESS = 12 , _('wallet submit success')
EXPECTED_FAILURE = 7 , _('expected_failure') # no exact data available but guessed to be failed
FAILED = 18 , _('failed')
EXPECTED_FAILURE = 19 , _('expected failure') # no exact data available but guessed to be failed
SUCCESS = 20 , _('success')
class TypeChoices(IntegerChoices): class TypeChoices(IntegerChoices):

View file

@ -1,10 +1,10 @@
from django import forms from django import forms
from .models import ChargeRequest from .models import PaymentRequest
class ChargeRequestForm(forms.ModelForm): class PaymentRequestForm(forms.ModelForm):
class Meta: class Meta:
model = ChargeRequest model = PaymentRequest
fields = [] fields = []

View file

@ -0,0 +1,51 @@
# Generated by Django 5.1.7 on 2025-05-26 15:28
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pg', '0002_alter_chargerequest_callback_url'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
]
operations = [
migrations.CreateModel(
name='PaymentRequest',
fields=[
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('gateway', models.CharField(choices=[('fake', 'fake'), ('pna', 'pna')], max_length=255)),
('state', models.IntegerField(choices=[(1, 'created'), (2, 'ipg pending'), (3, 'ipg success'), (11, 'wallet submit pending'), (12, 'wallet submit success'), (18, 'failed'), (19, 'expected failure'), (20, 'success')], default=1)),
('amount', models.PositiveBigIntegerField(blank=True, null=True)),
('callback_url', models.CharField(blank=True, max_length=255, null=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='PNA',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('details', models.JSONField(blank=True, default=dict, null=True)),
('pna_token', models.CharField(blank=True, db_index=True, max_length=255, null=True)),
('pna_order_id', models.IntegerField(blank=True, db_index=True, null=True)),
('pna_originator', models.CharField(blank=True, max_length=255, null=True)),
('pna_status', models.IntegerField(blank=True, choices=[(1, 'created'), (2, 'ipg pending'), (3, 'ipg success'), (11, 'wallet submit pending'), (12, 'wallet submit success'), (18, 'failed'), (19, 'expected failure'), (20, 'success')], db_index=True, null=True)),
('pna_status_code', models.IntegerField(blank=True, null=True)),
('pna_status_message', models.CharField(blank=True, max_length=255, null=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('payment_request', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='pg.paymentrequest')),
],
),
migrations.DeleteModel(
name='ChargeRequest',
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 5.1.7 on 2025-05-26 16:52
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pg', '0003_paymentrequest_pna_delete_chargerequest'),
]
operations = [
migrations.AddField(
model_name='paymentrequest',
name='phone_number',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='pna',
name='payment_request',
field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='pna', to='pg.paymentrequest'),
),
]

View file

@ -1,11 +1,13 @@
import urllib.parse import urllib.parse
from mimetypes import guess_all_extensions
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.db.models import TextChoices
from django.urls import reverse from django.urls import reverse
from oauth2_provider.settings import oauth2_settings from oauth2_provider.settings import oauth2_settings
from apps.core.models import BaseModel from apps.core.models import BaseModel, BasePGModel
from apps.pg.constans import StateChoices from apps.pg.constans import StateChoices
import uuid import uuid
@ -13,33 +15,54 @@ from utils.wallet_client import deposit_to_user_wallet_submit, withdraw_from_use
deposit_to_user_wallet_verify deposit_to_user_wallet_verify
from utils.pna import PNAClient from utils.pna import PNAClient
from django.utils.translation import gettext_lazy as _
class ChargeRequest(models.Model): class GatewayChoise(TextChoices):
uuid = models.UUIDField(editable=False, default=uuid.uuid4, unique=True, db_index=True) FAKE = "fake", _("fake")
PNA = 'pna' , _('pna')
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, application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
related_name='+', null=True, blank=True) related_name='+', null=True, blank=True)
gateway = models.CharField(max_length=255, choices=GatewayChoise.choices)
state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED) state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
amount = models.PositiveBigIntegerField(null=True, blank=True) 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) callback_url = models.CharField(max_length=255, null=True, blank=True)
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)
created_at = models.DateTimeField(auto_now_add=True, db_index=True) created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True, db_index=True) updated_at = models.DateTimeField(auto_now=True, db_index=True)
def get_gateway_tools(self):
if self.gateway == 'pna':
return self.pna
def set_gateway(self, gateway):
self.gateway = gateway
if gateway == 'pna':
pna = PNA.objects.create()
pna.payment_request = self
if gateway == 'fake':
pass
self.save()
# TODO: complete and use this # TODO: complete and use this
def change_state(self, from_state, to_state, error_message=None, **data): def change_state(self, from_state, to_state, error_message=None, **data):
if self.state == from_state: if self.state == from_state:
updated = ChargeRequest.objects.filter(pk=self.pk, state=from_state).update( updated = PaymentRequest.objects.filter(pk=self.pk, state=from_state).update(
state=to_state, state=to_state,
**data **data
) )
@ -50,44 +73,13 @@ class ChargeRequest(models.Model):
else: else:
raise Exception(error_message) raise Exception(error_message)
def request_sale(self):
pna_client = PNAClient()
description = 'from addwin ipg'
pna_callback_url = urllib.parse.urljoin(
settings.IPG_BASE_PUBLIC_URL,
reverse("pg:charge-confirm-view", kwargs={"uuid": self.uuid}))
response = pna_client.request_normal_sale(self.amount, description, pna_callback_url, order_id=self.id)
self.pna_status_code = response['status']
self.pna_status_message = response.get('title')
if response['status'] == 0:
self.pna_token = response['token']
self.save()
def get_pna_redirect_url(self):
if self.pna_token:
return PNAClient.pg_base_url.format(token=self.pna_token)
def confirm_sale(self):
pna_client = PNAClient()
description = 'from addwin ipg'
response = pna_client.request_normal_sale_confirm(self.pna_token)
# response = {'status': 0, 'cardNumberMasked': '504172******9210', 'rrn': 511629210215, 'appToken': 4101434578524, 'token': 'NDEwMTQzNDU3ODUyNA=='}
if response['status'] == 0:
self.pna_status = StateChoices.SUCCESS.value
def charge_wallet_balance(self): def charge_wallet_balance(self):
if self.state in [StateChoices.CREATED]: if self.state in [StateChoices.IPG_SUCCESS]:
to_pending = ChargeRequest.objects.filter( to_pending = PaymentRequest.objects.filter(
pk=self.pk, pk=self.pk,
state=StateChoices.CREATED.value, state=StateChoices.IPG_SUCCESS.value,
).update( ).update(
state=StateChoices.PENDING.value) state=StateChoices.WALLET_SUBMIT_PENDING.value)
if not to_pending: if not to_pending:
raise Exception('Cannot charge balance because state') raise Exception('Cannot charge balance because state')
@ -107,14 +99,29 @@ class ChargeRequest(models.Model):
if not submit_response.uuid: if not submit_response.uuid:
raise Exception('Failed to submit payment in wallet') 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:
raise Exception('Cannot charge balance because state')
verify_response = deposit_to_user_wallet_verify(self.uuid) verify_response = deposit_to_user_wallet_verify(self.uuid)
if not verify_response.uuid or verify_response.state != 5: if not verify_response.uuid or verify_response.state != 5:
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') raise Exception('Failed to verify payment in wallet')
to_pay_success = ChargeRequest.objects.filter( to_pay_success = PaymentRequest.objects.filter(
pk=self.pk, pk=self.pk,
state=StateChoices.PENDING.value state=StateChoices.WALLET_SUBMIT_SUCCESS.value
).update( ).update(
state=StateChoices.SUCCESS.value, state=StateChoices.SUCCESS.value,
) )
@ -123,3 +130,56 @@ class ChargeRequest(models.Model):
raise Exception('Failed to update state payment') raise Exception('Failed to update state payment')
return bool(to_pay_success) 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()
def make_request(self):
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
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']
self.save()
def get_redirect_url(self):
if self.pna_token:
return self.client.pg_base_url.format(token=self.pna_token)
def confirm_request(self):
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=='}
self.details = response
if response['status'] == 0:
self.pna_status = StateChoices.SUCCESS.value
self.save()
def reverse_request(self):
raise NotImplementedError()

View file

@ -1,13 +1,13 @@
from rest_framework import serializers from rest_framework import serializers
from .models import ChargeRequest from .models import PaymentRequest
class ChargeRequestSerializer(serializers.ModelSerializer): class PaymentRequestSerializer(serializers.ModelSerializer):
# uuid = serializers.UUIDField(required=True) # uuid = serializers.UUIDField(required=True)
class Meta: class Meta:
model = ChargeRequest model = PaymentRequest
fields = ( fields = (
'uuid', 'uuid',
'user', 'user',

View file

@ -1,10 +1,10 @@
from django.urls import path, include from django.urls import path, include
from .views import ChargeRequestView, ChargeRequestCreateApiView, ChargeConfirmView from .views import PaymentRequestView, PaymentRequestCreateApiView, PaymentConfirmView
app_name = "pg" app_name = "pg"
urlpatterns = [ urlpatterns = [
path('page/charge/<uuid>/', ChargeRequestView.as_view(), name='charge-request-view'), path('page/charge/<uuid>/', PaymentRequestView.as_view(), name='payment-request-view'),
path('page/pna_callback/<uuid>/', ChargeConfirmView.as_view(), name='charge-confirm-view'), path('page/pna_callback/<uuid>/', PaymentConfirmView.as_view(), name='payment-confirm-view'),
path('api/charge_create/', ChargeRequestCreateApiView.as_view(), name='charge-request-create-api-view'), path('api/charge_create/', PaymentRequestCreateApiView.as_view(), name='charge-request-create-api-view'),
] ]

View file

@ -10,31 +10,31 @@ from oauth2_provider.contrib.rest_framework import TokenHasScope, IsAuthenticate
from rest_framework import generics from rest_framework import generics
from rest_framework.views import APIView from rest_framework.views import APIView
from .forms import ChargeRequestForm from .forms import PaymentRequestForm
from .models import ChargeRequest from .models import PaymentRequest, GatewayChoise
from .serializers import ChargeRequestSerializer from .serializers import PaymentRequestSerializer
from .constans import StateChoices from .constans import StateChoices
from ..gooyal_oauth2.utils import get_application from ..gooyal_oauth2.utils import get_application
class ChargeRequestCreateApiView(generics.CreateAPIView): class PaymentRequestCreateApiView(generics.CreateAPIView):
permission_classes = [IsAuthenticatedOrTokenHasScope] permission_classes = [IsAuthenticatedOrTokenHasScope]
serializer_class = ChargeRequestSerializer serializer_class = PaymentRequestSerializer
required_scopes = ['wallet.wallet:get_balance'] # TODO: is it good scope? required_scopes = ['wallet.wallet:get_balance'] # TODO: is it good scope?
model = ChargeRequest model = PaymentRequest
def perform_create(self, serializer): def perform_create(self, serializer):
user = self.request.user user = self.request.user
application = get_application(self.request) application = get_application(self.request)
charge_request: ChargeRequest = serializer.save(user=user, application=application) charge_request: PaymentRequest = serializer.save(user=user, application=application)
# TODO: web view # TODO: web view
# charge_request.charge_wallet_balance() # charge_request.charge_wallet_balance()
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
class ChargeRequestView(UpdateView): class PaymentRequestView(UpdateView):
template_name = 'pg/charge-request.html' template_name = 'pg/payment-request.html'
form_class = ChargeRequestForm form_class = PaymentRequestForm
# pk_url_kwarg = 'uuid' # pk_url_kwarg = 'uuid'
slug_field = 'uuid' slug_field = 'uuid'
slug_url_kwarg = 'uuid' slug_url_kwarg = 'uuid'
@ -42,36 +42,42 @@ class ChargeRequestView(UpdateView):
def get_queryset(self): def get_queryset(self):
# return ChargeRequest.objects.all() # return ChargeRequest.objects.all()
# return Charge.objects.filter(state__in=[StateChoices.CREATED.value, StateChoices.SUCCESS.value]) # return Charge.objects.filter(state__in=[StateChoices.CREATED.value, StateChoices.SUCCESS.value])
return ChargeRequest.objects.filter(state__in=[StateChoices.CREATED.value]) return PaymentRequest.objects.filter(state__in=[StateChoices.CREATED.value])
def form_valid(self, form): def form_valid(self, form):
self.object = form.save() self.object = form.save()
charge_request: ChargeRequest = self.get_object() charge_request: PaymentRequest = self.get_object()
if 'fake' in form.data:
print('fake')
if 'pna' in form.data:
charge_request.set_gateway('pna')
tool = charge_request.get_gateway_tools()
# try: # try:
charge_request.request_sale() tool.make_request()
pna_redirect_url = charge_request.get_pna_redirect_url() redirect_url = tool.get_redirect_url()
if pna_redirect_url: if redirect_url:
return HttpResponseRedirect(pna_redirect_url) return HttpResponseRedirect(redirect_url)
else: else:
messages.error(self.request, 'can not redirect to pna') messages.error(self.request, 'can not redirect to payment gateway')
# TODO: redirect to error page # TODO: redirect to error page
return HttpResponseRedirect(reverse('pg:charge-confirm-view', kwargs={'uuid': charge_request.uuid})) return HttpResponseRedirect(reverse('pg:payment-confirm-view', kwargs={'uuid': charge_request.uuid}))
def get_success_url(self):
return self.object.callback_url
# return reverse('pg:charge-request-view', kwargs={'pk': self.object.pk})
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
class ChargeConfirmView(UpdateView): class PaymentConfirmView(UpdateView):
template_name = 'pg/charge-confirm.html' template_name = 'pg/payment-confirm.html'
form_class = ChargeRequestForm form_class = PaymentRequestForm
# pk_url_kwarg = 'uuid' # pk_url_kwarg = 'uuid'
slug_field = 'uuid' slug_field = 'uuid'
slug_url_kwarg = 'uuid' slug_url_kwarg = 'uuid'
def get_queryset(self): def get_queryset(self):
return ChargeRequest.objects.all() return PaymentRequest.objects.all()
# return Charge.objects.filter(state__in=[StateChoices.CREATED.value, StateChoices.SUCCESS.value]) # return Charge.objects.filter(state__in=[StateChoices.CREATED.value, StateChoices.SUCCESS.value])
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -99,16 +105,16 @@ class ChargeConfirmView(UpdateView):
return response return response
else: else:
# 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': ['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']}> # post = <QueryDict: {'Token': ['NDI2MDY3ODI0MTg1OA=='], 'OrderId': ['47'], 'status': ['-138']}>
print(dict(request.POST)) print(dict(request.POST))
post_dict = dict(request.POST) post_dict = dict(request.POST)
if post_dict.get('status') in [0, '0', [0], ['0']]: if post_dict.get('status') in [0, '0', [0], ['0']]:
try: try:
charge_request: ChargeRequest = self.get_object() payment_request: PaymentRequest = self.get_object()
charge_request.charge_wallet_balance() tool = payment_request.get_gateway_tools()
charge_request.confirm_sale() tool.confirm_request()
payment_request.charge_wallet_balance()
messages.success(self.request, 'شارژ انجام شد') messages.success(self.request, 'شارژ انجام شد')
except Exception as e: except Exception as e:

View file

@ -20,11 +20,16 @@
{# </div>#} {# </div>#}
</div> </div>
<h6 class="h6">پرداخت با:</h6> <h6 class="h6">پرداخت با:</h6>
<div class="row"> <div class="row justify-content-center">
<div class="col"> <div class="col">
<button type="submit" class="btn mx-auto d-block"> {# <div class="">#}
<button type="submit" class="btn mx-auto center-block" name="pna">
<img class="img-thumbnail" width="128" height="128" src="{% static "image/pna.png" %}"/> <img class="img-thumbnail" width="128" height="128" src="{% static "image/pna.png" %}"/>
</button> </button>
<button type="submit" class="btn mx-auto center-block" name="fake">
<img class="img-thumbnail" width="128" height="128" src="{% static "image/fake.png" %}"/>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>