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 .models import ChargeRequest
from .models import PaymentRequest
class ChargeRequestAdmin(admin.ModelAdmin):
list_display = ['uuid', 'id']
class PaymentRequestAdmin(admin.ModelAdmin):
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):
CREATED = 1 , _('created')
DELAYED = 2 , _('delayed') # delayed as user wish
PENDING = 3 , _('pending') # wait for external service response (or action)
INCOMPLETE = 4 , _('incomplete') # started and wait for internal progress to complete
SUCCESS = 5 , _('success')
FAILED = 6 , _('failed')
EXPECTED_FAILURE = 7 , _('expected_failure') # no exact data available but guessed to be failed
IPG_PENDING = 2 , _('ipg pending')
IPG_SUCCESS = 3 , _('ipg success')
WALLET_SUBMIT_PENDING = 11 , _('wallet submit pending')
WALLET_SUBMIT_SUCCESS = 12 , _('wallet submit success')
FAILED = 18 , _('failed')
EXPECTED_FAILURE = 19 , _('expected failure') # no exact data available but guessed to be failed
SUCCESS = 20 , _('success')
class TypeChoices(IntegerChoices):

View file

@ -1,10 +1,10 @@
from django import forms
from .models import ChargeRequest
from .models import PaymentRequest
class ChargeRequestForm(forms.ModelForm):
class PaymentRequestForm(forms.ModelForm):
class Meta:
model = ChargeRequest
model = PaymentRequest
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,45 +1,68 @@
import urllib.parse
from mimetypes import guess_all_extensions
from django.conf import settings
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
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, withdraw_from_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):
uuid = models.UUIDField(editable=False, default=uuid.uuid4, unique=True, db_index=True)
class GatewayChoise(TextChoices):
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,
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)
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)
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
def change_state(self, from_state, to_state, error_message=None, **data):
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,
**data
)
@ -50,44 +73,13 @@ class ChargeRequest(models.Model):
else:
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):
if self.state in [StateChoices.CREATED]:
to_pending = ChargeRequest.objects.filter(
if self.state in [StateChoices.IPG_SUCCESS]:
to_pending = PaymentRequest.objects.filter(
pk=self.pk,
state=StateChoices.CREATED.value,
state=StateChoices.IPG_SUCCESS.value,
).update(
state=StateChoices.PENDING.value)
state=StateChoices.WALLET_SUBMIT_PENDING.value)
if not to_pending:
raise Exception('Cannot charge balance because state')
@ -107,14 +99,29 @@ class ChargeRequest(models.Model):
if not submit_response.uuid:
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)
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')
to_pay_success = ChargeRequest.objects.filter(
to_pay_success = PaymentRequest.objects.filter(
pk=self.pk,
state=StateChoices.PENDING.value
state=StateChoices.WALLET_SUBMIT_SUCCESS.value
).update(
state=StateChoices.SUCCESS.value,
)
@ -123,3 +130,56 @@ class ChargeRequest(models.Model):
raise Exception('Failed to update state payment')
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 .models import ChargeRequest
from .models import PaymentRequest
class ChargeRequestSerializer(serializers.ModelSerializer):
class PaymentRequestSerializer(serializers.ModelSerializer):
# uuid = serializers.UUIDField(required=True)
class Meta:
model = ChargeRequest
model = PaymentRequest
fields = (
'uuid',
'user',

View file

@ -1,10 +1,10 @@
from django.urls import path, include
from .views import ChargeRequestView, ChargeRequestCreateApiView, ChargeConfirmView
from .views import PaymentRequestView, PaymentRequestCreateApiView, PaymentConfirmView
app_name = "pg"
urlpatterns = [
path('page/charge/<uuid>/', ChargeRequestView.as_view(), name='charge-request-view'),
path('page/pna_callback/<uuid>/', ChargeConfirmView.as_view(), name='charge-confirm-view'),
path('api/charge_create/', ChargeRequestCreateApiView.as_view(), name='charge-request-create-api-view'),
path('page/charge/<uuid>/', PaymentRequestView.as_view(), name='payment-request-view'),
path('page/pna_callback/<uuid>/', PaymentConfirmView.as_view(), name='payment-confirm-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.views import APIView
from .forms import ChargeRequestForm
from .models import ChargeRequest
from .serializers import ChargeRequestSerializer
from .forms import PaymentRequestForm
from .models import PaymentRequest, GatewayChoise
from .serializers import PaymentRequestSerializer
from .constans import StateChoices
from ..gooyal_oauth2.utils import get_application
class ChargeRequestCreateApiView(generics.CreateAPIView):
class PaymentRequestCreateApiView(generics.CreateAPIView):
permission_classes = [IsAuthenticatedOrTokenHasScope]
serializer_class = ChargeRequestSerializer
serializer_class = PaymentRequestSerializer
required_scopes = ['wallet.wallet:get_balance'] # TODO: is it good scope?
model = ChargeRequest
model = PaymentRequest
def perform_create(self, serializer):
user = self.request.user
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
# charge_request.charge_wallet_balance()
@method_decorator(csrf_exempt, name='dispatch')
class ChargeRequestView(UpdateView):
template_name = 'pg/charge-request.html'
form_class = ChargeRequestForm
class PaymentRequestView(UpdateView):
template_name = 'pg/payment-request.html'
form_class = PaymentRequestForm
# pk_url_kwarg = 'uuid'
slug_field = 'uuid'
slug_url_kwarg = 'uuid'
@ -42,36 +42,42 @@ class ChargeRequestView(UpdateView):
def get_queryset(self):
# return ChargeRequest.objects.all()
# 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):
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:
charge_request.request_sale()
pna_redirect_url = charge_request.get_pna_redirect_url()
if pna_redirect_url:
return HttpResponseRedirect(pna_redirect_url)
tool.make_request()
redirect_url = tool.get_redirect_url()
if redirect_url:
return HttpResponseRedirect(redirect_url)
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
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')
class ChargeConfirmView(UpdateView):
template_name = 'pg/charge-confirm.html'
form_class = ChargeRequestForm
class PaymentConfirmView(UpdateView):
template_name = 'pg/payment-confirm.html'
form_class = PaymentRequestForm
# pk_url_kwarg = 'uuid'
slug_field = 'uuid'
slug_url_kwarg = 'uuid'
def get_queryset(self):
return ChargeRequest.objects.all()
return PaymentRequest.objects.all()
# return Charge.objects.filter(state__in=[StateChoices.CREATED.value, StateChoices.SUCCESS.value])
def get(self, request, *args, **kwargs):
@ -99,16 +105,16 @@ class ChargeConfirmView(UpdateView):
return response
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': ['NDI2MDY3ODI0MTg1OA=='], 'OrderId': ['47'], 'status': ['-138']}>
print(dict(request.POST))
post_dict = dict(request.POST)
if post_dict.get('status') in [0, '0', [0], ['0']]:
try:
charge_request: ChargeRequest = self.get_object()
charge_request.charge_wallet_balance()
charge_request.confirm_sale()
payment_request: PaymentRequest = self.get_object()
tool = payment_request.get_gateway_tools()
tool.confirm_request()
payment_request.charge_wallet_balance()
messages.success(self.request, 'شارژ انجام شد')
except Exception as e:

View file

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