diff --git a/apps/pg/migrations/0002_chargerequest_pna_order_id_and_more.py b/apps/pg/migrations/0002_chargerequest_pna_order_id_and_more.py new file mode 100644 index 0000000..47f5391 --- /dev/null +++ b/apps/pg/migrations/0002_chargerequest_pna_order_id_and_more.py @@ -0,0 +1,51 @@ +# Generated by Django 5.1.7 on 2025-05-13 16:08 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pg', '0001_initial'), + migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='chargerequest', + name='pna_order_id', + field=models.IntegerField(blank=True, db_index=True, null=True), + ), + migrations.AddField( + model_name='chargerequest', + name='pna_originator', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='chargerequest', + name='pna_status', + field=models.IntegerField(blank=True, choices=[(1, 'created'), (2, 'delayed'), (3, 'pending'), (4, 'incomplete'), (5, 'success'), (6, 'failed'), (7, 'expected_failure')], db_index=True, null=True), + ), + migrations.AddField( + model_name='chargerequest', + name='pna_status_code', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='chargerequest', + name='pna_status_message', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='chargerequest', + name='pna_token', + field=models.CharField(blank=True, db_index=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='chargerequest', + name='application', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL), + ), + ] diff --git a/apps/pg/models.py b/apps/pg/models.py index ec2feee..e193f08 100644 --- a/apps/pg/models.py +++ b/apps/pg/models.py @@ -9,6 +9,8 @@ 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 + class ChargeRequest(BaseModel): application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, @@ -19,6 +21,13 @@ class ChargeRequest(BaseModel): callback_url = models.URLField(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) + # TODO: complete and use this def change_state(self, from_state, to_state, error_message=None, **data): if self.state == from_state: @@ -33,6 +42,32 @@ class ChargeRequest(BaseModel): else: raise Exception(error_message) + def request_sale(self): + pna_client = PNAClient() + description = 'from addwin ipg' + pna_callback_url = f'https://ipg.addwin.ir/page/pna_callback/{self.pk}/' + response = pna_client.request_normal_sale(self.amount, description, pna_callback_url, order_id=1) + + 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) + 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( diff --git a/apps/pg/urls.py b/apps/pg/urls.py index cf46be5..525e72f 100644 --- a/apps/pg/urls.py +++ b/apps/pg/urls.py @@ -1,9 +1,10 @@ from django.urls import path, include -from .views import ChargeRequestView, ChargeRequestCreateApiView +from .views import ChargeRequestView, ChargeRequestCreateApiView, ChargeConfirmView app_name = "pg" urlpatterns = [ - path('page/charge//', ChargeRequestView.as_view(), name='charge-request-view'), + path('page/charge//', ChargeRequestView.as_view(), name='charge-request-view'), + path('page/pna_callback//', ChargeConfirmView.as_view(), name='charge-confirm-view'), path('api/charge_create/', ChargeRequestCreateApiView.as_view(), name='charge-request-create-api-view'), ] diff --git a/apps/pg/views.py b/apps/pg/views.py index afadf52..13a8e91 100644 --- a/apps/pg/views.py +++ b/apps/pg/views.py @@ -4,7 +4,7 @@ from django.http import HttpResponseRedirect from django.shortcuts import render from django.urls import reverse from django.utils.decorators import method_decorator -from django.views.generic import UpdateView +from django.views.generic import UpdateView, DetailView from oauth2_provider.contrib.rest_framework import TokenHasScope, IsAuthenticatedOrTokenHasScope from rest_framework import generics from rest_framework.views import APIView @@ -34,6 +34,9 @@ class ChargeRequestCreateApiView(generics.CreateAPIView): class ChargeRequestView(UpdateView): template_name = 'pg/charge-request.html' form_class = ChargeRequestForm + # pk_url_kwarg = 'uuid' + slug_field = 'uuid' + slug_url_kwarg = 'uuid' def get_queryset(self): return ChargeRequest.objects.all() @@ -42,28 +45,83 @@ class ChargeRequestView(UpdateView): def form_valid(self, form): self.object = form.save() charge_request: ChargeRequest = self.get_object() - try: - charge_request.charge_wallet_balance() - # transaction = Transaction.objects.create( - # uuid=charge.uuid, - # payee_id=charge.user.pk, - # payee_type=1, - # payer_id=payer_pk, - # payer_type=2, - # amount=amount, - # details={'description': 'شارژ مجازی کیف پول'} - # ) - # transaction.submit() - # transaction.verify() - # charge.change_state(from_state=StateChoices.PENDING.value, to_state=StateChoices.SUCCESS.value) - messages.success(self.request, 'شارژ انجام شد') - except Exception as e: - print(e) - messages.error(self.request, 'خطایی رخ داده است') - charge_request.change_state(from_state=StateChoices.PENDING.value, to_state=StateChoices.EXPECTED_FAILURE.value) + # try: + charge_request.request_sale() + return HttpResponseRedirect(get_pna_redirect_url) - return HttpResponseRedirect(self.get_success_url()) + charge_request.charge_wallet_balance() + # transaction = Transaction.objects.create( + # uuid=charge.uuid, + # payee_id=charge.user.pk, + # payee_type=1, + # payer_id=payer_pk, + # payer_type=2, + # amount=amount, + # details={'description': 'شارژ مجازی کیف پول'} + # ) + # transaction.submit() + # transaction.verify() + # charge.change_state(from_state=StateChoices.PENDING.value, to_state=StateChoices.SUCCESS.value) + messages.success(self.request, 'شارژ انجام شد') + + # except Exception as e: + # print(e) + # messages.error(self.request, 'خطایی رخ داده است') + # charge_request.change_state(from_state=StateChoices.PENDING.value, to_state=StateChoices.EXPECTED_FAILURE.value) + # + # return HttpResponseRedirect(self.get_success_url()) + + def get_success_url(self): + return self.object.callback_url + # return reverse('pg:charge-request-view', kwargs={'pk': self.object.pk}) + + +class ChargeConfirmView(UpdateView): + template_name = 'pg/charge-request.html' + form_class = ChargeRequestForm + # pk_url_kwarg = 'uuid' + slug_field = 'uuid' + slug_url_kwarg = 'uuid' + + def get_queryset(self): + return ChargeRequest.objects.all() + # return Charge.objects.filter(state__in=[StateChoices.CREATED.value, StateChoices.SUCCESS.value]) + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + charge_request: ChargeRequest = self.get_object() + charge_request.charge_wallet_balance() + charge_request.confirm_sale() + context = self.get_context_data(object=self.object) + messages.success(self.request, 'شارژ انجام شد') + + return self.render_to_response(context) + + # def form_valid(self, form): + # self.object = form.save() + # charge_request: ChargeRequest = self.get_object() + # try: + # + # # transaction = Transaction.objects.create( + # # uuid=charge.uuid, + # # payee_id=charge.user.pk, + # # payee_type=1, + # # payer_id=payer_pk, + # # payer_type=2, + # # amount=amount, + # # details={'description': 'شارژ مجازی کیف پول'} + # # ) + # # transaction.submit() + # # transaction.verify() + # # charge.change_state(from_state=StateChoices.PENDING.value, to_state=StateChoices.SUCCESS.value) + # + # except Exception as e: + # print(e) + # messages.error(self.request, 'خطایی رخ داده است') + # charge_request.change_state(from_state=StateChoices.PENDING.value, to_state=StateChoices.EXPECTED_FAILURE.value) + # + # return HttpResponseRedirect(self.get_success_url()) def get_success_url(self): return self.object.callback_url diff --git a/utils/pna.py b/utils/pna.py index b72fe31..f0e2d78 100644 --- a/utils/pna.py +++ b/utils/pna.py @@ -10,14 +10,14 @@ class PNAClient: corporation_pin = settings.PNA_CORPORATION_PIN - def request_normal_sale(self, amount, phone_number, discription, callback_url, order_id: int): + def request_normal_sale(self, amount, description, callback_url, order_id: int, phone_number): data = { "CorporationPin": self.corporation_pin, "Amount": amount, "OrderId": order_id, "CallBackUrl": callback_url, - "AdditionalData": discription, # optional - "Originator": phone_number, # optional + "AdditionalData": description, # optional + # "Originator": phone_number, # optional } response = requests.post(self.normal_sale_request_url, json=data)