Compare commits

...

10 commits

Author SHA1 Message Date
Sayyid Hamid Mahdavi
f5ce799a38 better exception 2026-04-25 02:11:59 +03:30
Sayyid Hamid Mahdavi
7a215dc457 bug fix 2026-04-22 21:41:23 +03:30
Sayyid Hamid Mahdavi
e77a711bbf bug fix 2026-04-22 21:38:24 +03:30
Sayyid Hamid Mahdavi
7ff91acb82 bug fix 2026-04-22 21:26:32 +03:30
Sayyid Hamid Mahdavi
a318185147 admin 2026-04-22 19:59:44 +03:30
Sayyid Hamid Mahdavi
82c35ffc4a comment hide 2026-04-22 19:54:25 +03:30
Sayyid Hamid Mahdavi
e114651dbe application gateway policy 2026-04-22 19:51:40 +03:30
Sayyid Hamid Mahdavi
556cdaf20a application gateway policy 2026-04-22 19:51:36 +03:30
Sayyid Hamid Mahdavi
82e35a98f7 hide sep 2026-04-17 14:17:57 +03:30
Sayyid Hamid Mahdavi
e7953b104b cleanup 2026-03-29 13:59:06 +03:30
12 changed files with 442 additions and 140 deletions

2
.gitignore vendored
View file

@ -3,3 +3,5 @@
media media
/clients/ /clients/
/delme.py /delme.py
/log/accounts.log
/log/errors.log

View file

@ -23,6 +23,14 @@ class BasePGModel(models.Model):
redirect_method = 'GET' redirect_method = 'GET'
@property
def label(self):
raise NotImplementedError()
@property
def image(self):
raise NotImplementedError()
def get_pg_page_url(self): def get_pg_page_url(self):
pass pass
@ -35,6 +43,17 @@ class BasePGModel(models.Model):
def reverse_request(self): def reverse_request(self):
pass pass
@property
def jajali_datetime(self):
def to_jalali(value):
if value:
import jdatetime
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)
class Meta: class Meta:
abstract = True abstract = True

View file

@ -12,16 +12,24 @@ from django.shortcuts import redirect, get_object_or_404
from django.urls import path from django.urls import path
from .constans import StateChoices from .constans import StateChoices
from .models import PaymentRequest, PNA, Fake from .forms import ApplicationGatewayPolicyForm
from .models import PaymentRequest, PNA, Fake, ApplicationGatewayPolicy, SEP
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class PNAAdmin(admin.ModelAdmin): class PNAAdmin(admin.ModelAdmin):
list_display = ['pna_status_code', 'pna_status_message', 'jajali_datetime', 'payment_request'] list_display = ['pna_status_code', 'pna_status_message', 'jajali_datetime', 'payment_request']
list_filter = ['pna_status'] list_filter = ['pna_status']
search_fields = ['pna_status_code', 'pna_status_message'] search_fields = ['pna_status_code', 'pna_status_message']
class SEPAdmin(admin.ModelAdmin):
list_display = ['sep_status_code', 'sep_status_message', 'jajali_datetime', 'payment_request']
list_filter = ['sep_status']
search_fields = ['sep_status_code', 'sep_status_message']
@admin.action(description="export exel") @admin.action(description="export exel")
def export_exel(modeladmin, request, queryset): def export_exel(modeladmin, request, queryset):
output = io.BytesIO() output = io.BytesIO()
@ -54,6 +62,7 @@ def export_exel(modeladmin, request, queryset):
# return the response # return the response
return response return response
@admin.action(description="mark as registered") @admin.action(description="mark as registered")
def mark_as_registered(model_admin, request, queryset): def mark_as_registered(model_admin, request, queryset):
for obj in queryset: for obj in queryset:
@ -62,12 +71,12 @@ def mark_as_registered(model_admin, request, queryset):
class PaymentRequestAdmin(admin.ModelAdmin): class PaymentRequestAdmin(admin.ModelAdmin):
list_display = ['state', 'check_success_again', 'uuid', 'splited_amount', 'user_id', 'jajali_datetime', 'is_registered', 'created_at', 'application', 'gateway'] list_display = ['state', 'check_success_again', 'uuid', 'splited_amount', 'user_id', 'jajali_datetime',
search_fields = ['application__uuid','application__client_id', 'amount', 'user__uuid'] 'is_registered', 'created_at', 'application', 'gateway']
search_fields = ['application__uuid', 'application__client_id', 'amount', 'user__uuid']
list_filter = ['application', 'state', 'gateway'] list_filter = ['application', 'state', 'gateway']
actions = [export_exel, mark_as_registered] actions = [export_exel, mark_as_registered]
def changelist_view(self, request, extra_context=None): def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {} extra_context = extra_context or {}
@ -80,7 +89,8 @@ class PaymentRequestAdmin(admin.ModelAdmin):
return super().changelist_view(request, extra_context=extra_context) return super().changelist_view(request, extra_context=extra_context)
def check_success_again(self, obj): def check_success_again(self, obj):
if obj.state in [StateChoices.IPG_SUCCESS, StateChoices.WALLET_SUBMIT_PENDING, StateChoices.WALLET_SUBMIT_SUCCESS]: if obj.state in [StateChoices.IPG_SUCCESS, StateChoices.WALLET_SUBMIT_PENDING,
StateChoices.WALLET_SUBMIT_SUCCESS]:
return format_html( return format_html(
'<a class="button" href="{}">بررسی دوباره موفقیت</a>', '<a class="button" href="{}">بررسی دوباره موفقیت</a>',
f'{obj.uuid}/check_success_again/' f'{obj.uuid}/check_success_again/'
@ -90,9 +100,11 @@ class PaymentRequestAdmin(admin.ModelAdmin):
def get_urls(self): def get_urls(self):
urls = super().get_urls() urls = super().get_urls()
custom_urls = [ custom_urls = [
path('<uuid:payment_request_uuid>/check_success_again/', self.admin_site.admin_view(self.check_success_again_view)) path('<uuid:payment_request_uuid>/check_success_again/',
self.admin_site.admin_view(self.check_success_again_view))
] ]
return custom_urls + urls return custom_urls + urls
def check_success_again_view(self, request, payment_request_uuid): def check_success_again_view(self, request, payment_request_uuid):
logger.info(f"[Admin panel] Check success again for payment_request_uuid={payment_request_uuid}") logger.info(f"[Admin panel] Check success again for payment_request_uuid={payment_request_uuid}")
payment_request: PaymentRequest = get_object_or_404(PaymentRequest, uuid=payment_request_uuid) payment_request: PaymentRequest = get_object_or_404(PaymentRequest, uuid=payment_request_uuid)
@ -110,8 +122,15 @@ class PaymentRequestAdmin(admin.ModelAdmin):
class FakeAdmin(admin.ModelAdmin): class FakeAdmin(admin.ModelAdmin):
list_display = [ 'created_at'] list_display = ['created_at']
class ApplicationGatewayPolicyAdmin(admin.ModelAdmin):
form = ApplicationGatewayPolicyForm
admin.site.register(PaymentRequest, PaymentRequestAdmin) admin.site.register(PaymentRequest, PaymentRequestAdmin)
admin.site.register(PNA, PNAAdmin) admin.site.register(PNA, PNAAdmin)
admin.site.register(SEP, SEPAdmin)
admin.site.register(Fake, FakeAdmin) admin.site.register(Fake, FakeAdmin)
admin.site.register(ApplicationGatewayPolicy, ApplicationGatewayPolicyAdmin)

View file

@ -1,6 +1,6 @@
from django import forms from django import forms
from .models import PaymentRequest from .models import PaymentRequest, GatewayChoise
class PaymentRequestForm(forms.ModelForm): class PaymentRequestForm(forms.ModelForm):
@ -8,3 +8,44 @@ class PaymentRequestForm(forms.ModelForm):
class Meta: class Meta:
model = PaymentRequest model = PaymentRequest
fields = [] fields = []
class DelimitedListField(forms.MultipleChoiceField):
"""
Django form field that allows for the use of list widgets with a text field
containing a delimited list.
"""
delimiter = ','
def __init__(self, delimiter=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.delimiter = delimiter or self.delimiter
def prepare_value(self, value):
# If the value is already a list or tuple, just use it as-is
if isinstance(value, (list, tuple)): return value
# Otherwise, prepare the value by splitting on the delimiter, trimming
# leading and trailing whitespace and excluding empty values
return [p.strip() for p in value.split(self.delimiter) if p.strip()]
def clean(self, value):
# Let the parent clean the value first, then join the result using the
# specified delimiter
return self.delimiter.join(super().clean(value))
class ApplicationGatewayPolicyForm(forms.ModelForm):
"""
Form for creating or updating a restricted application.
"""
# allowed_scope is a space-delimited list, but we want to present
# a selection of valid scopes with checkboxes
preferred_gateways = DelimitedListField(
label='preferred gateways',
# The choices and initial values are callables, because the scopes might
# not be available at import type, e.g. if coming from the database
choices=GatewayChoise.choices,
initial=GatewayChoise.choices,
delimiter=' ',
widget=forms.CheckboxSelectMultiple
)

View file

@ -0,0 +1,36 @@
# Generated by Django 5.1.4 on 2026-04-22 19:48
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pg', '0009_remove_sep_sep_order_id_remove_sep_sep_originator_and_more'),
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
]
operations = [
migrations.AlterField(
model_name='paymentrequest',
name='gateway',
field=models.CharField(blank=True, choices=[('fake', 'fake ipg'), ('pna', 'pna ipg'), ('sep', 'sep ipg')], max_length=255),
),
migrations.CreateModel(
name='ApplicationGatewayPolicy',
fields=[
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('preferred_gateways', models.CharField(blank=True, default='', max_length=100, null=True)),
('policy', models.CharField(choices=[('random', 'random'), ('selectable', 'selectable')], default='selectable', max_length=20)),
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL, unique=True)),
],
options={
'abstract': False,
},
),
]

View file

@ -22,18 +22,22 @@ from utils.pna import PNAClient
from utils.sep import SEPClient from utils.sep import SEPClient
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GatewayChoise(TextChoices): class GatewayChoise(TextChoices):
FAKE = "fake", _("fake") # FAKE = "fake", _("fake ipg")
PNA = 'pna', _('pna') PNA = 'pna', _('pna ipg')
SEP = 'sep', _('sep') SEP = 'sep', _('sep ipg')
class GatewayPolicy(BasePGModel): def get_gateway_class(gateway_label):
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, return {
related_name='+', null=True, blank=True, unique=True) # "fake": Fake,
preference_gateway = models.JSONField(default=list, blank=True) "pna": PNA,
"sep": SEP,
}[gateway_label]
# TODO: use generic relation # TODO: use generic relation
@ -44,7 +48,7 @@ class PaymentRequest(models.Model):
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) gateway = models.CharField(max_length=255, choices=GatewayChoise.choices, blank=True)
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)
@ -83,44 +87,41 @@ class PaymentRequest(models.Model):
self.commission = calculate_pna_commission(self.amount) self.commission = calculate_pna_commission(self.amount)
self.save(update_fields=['commission']) 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): def get_gateway_tools(self):
if self.gateway == GatewayChoise.PNA: gateway_class = get_gateway_class(self.gateway)
return PNA.objects.get(payment_request=self) return gateway_class.objects.get(payment_request=self)
if self.gateway == GatewayChoise.SEP: def set_gateway(self, gateway_label):
return SEP.objects.get(payment_request=self) preferred_gateways = self.get_preferred_gateways()
if gateway_label in preferred_gateways:
if self.gateway == GatewayChoise.FAKE: self.gateway = gateway_label
return Fake.objects.get(payment_request=self) gateway_class = get_gateway_class(gateway_label)
pg = gateway_class.objects.create(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
pg = PNA.objects.create(payment_request=self)
pg.payment_request = self pg.payment_request = self
elif 'sep' in data: self.save()
self.gateway = GatewayChoise.SEP
pg = SEP.objects.create(payment_request=self)
pg.payment_request = self
else: if not self.gateway:
raise Exception('Unknown gateway') raise Exception('No gateway')
self.save()
@property @property
def reference_number(self): def reference_number(self):
gateway_tools = self.get_gateway_tools() gateway_tools = self.get_gateway_tools()
if gateway_tools: if gateway_tools:
return f'{self.gateway.value}{gateway_tools.pk}' return f'{self.gateway}{gateway_tools.pk}'
else: else:
return '00000000' return '00000000'
@ -181,7 +182,8 @@ class PaymentRequest(models.Model):
raise Exception('Failed to get response uuid from wallet service') raise Exception('Failed to get response uuid from wallet service')
if verify_response.state != 5: if verify_response.state != 5:
logger.error(f"[Deposit Verify] Wallet verification failed | payment_id={self.pk}, response={verify_response}") logger.error(
f"[Deposit Verify] Wallet verification failed | payment_id={self.pk}, response={verify_response}")
# to_pay_failed = PaymentRequest.objects.filter( # to_pay_failed = PaymentRequest.objects.filter(
# pk=self.pk, # pk=self.pk,
# state=StateChoices.WALLET_SUBMIT_SUCCESS.value # state=StateChoices.WALLET_SUBMIT_SUCCESS.value
@ -225,6 +227,8 @@ class PaymentRequest(models.Model):
class PNA(BasePGModel): 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') payment_request = models.OneToOneField(PaymentRequest, on_delete=models.PROTECT, related_name='pna')
details = models.JSONField(null=True, blank=True, default=dict) details = models.JSONField(null=True, blank=True, default=dict)
@ -238,17 +242,6 @@ class PNA(BasePGModel):
client = PNAClient() 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): def make_request(self):
self.payment_request.change_state(StateChoices.CREATED, StateChoices.IPG_PENDING, 'can not change state') self.payment_request.change_state(StateChoices.CREATED, StateChoices.IPG_PENDING, 'can not change state')
amount = self.payment_request.amount + self.payment_request.commission amount = self.payment_request.amount + self.payment_request.commission
@ -261,6 +254,7 @@ class PNA(BasePGModel):
try: try:
response = self.client.request_normal_sale(amount, description, callback_url, order_id, phone_number) 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_code = response['status']
self.pna_status_message = response.get('title') self.pna_status_message = response.get('title')
@ -271,7 +265,8 @@ class PNA(BasePGModel):
self.payment_request.change_state(self.payment_request.state, StateChoices.FAILED, self.payment_request.change_state(self.payment_request.state, StateChoices.FAILED,
'error in pardakht novin gateway') 'error in pardakht novin gateway')
except Exception as e: except Exception as e:
self.payment_request.change_state(self.payment_request.state, StateChoices.FAILED, 'error in pardakht novin gateway') self.payment_request.change_state(self.payment_request.state, StateChoices.FAILED,
'error in pardakht novin gateway')
self.save() self.save()
@ -291,12 +286,13 @@ class PNA(BasePGModel):
pna_token = self.pna_token pna_token = self.pna_token
response = self.client.request_normal_sale_confirm(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=='} # 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': ['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']}>
self.details = response
if response['status'] == 0: if response['status'] == 0:
self.pna_status = StateChoices.SUCCESS.value self.pna_status = StateChoices.SUCCESS.value
self.payment_request.change_state(StateChoices.IPG_PENDING, StateChoices.IPG_SUCCESS, self.payment_request.change_state(StateChoices.IPG_PENDING, StateChoices.IPG_SUCCESS,
@ -322,6 +318,9 @@ class PNA(BasePGModel):
class SEP(BasePGModel): class SEP(BasePGModel):
label = GatewayChoise.SEP.value
image = f'image/{GatewayChoise.SEP.value}.png'
redirect_method = 'GET' redirect_method = 'GET'
payment_request = models.OneToOneField(PaymentRequest, on_delete=models.PROTECT, related_name='sep') payment_request = models.OneToOneField(PaymentRequest, on_delete=models.PROTECT, related_name='sep')
@ -348,7 +347,7 @@ class SEP(BasePGModel):
try: try:
response = self.client.request_token(amount, callback_url, order_id, phone_number) response = self.client.request_token(amount, callback_url, order_id, phone_number)
self.details['get_token_response'] = response self.details['make_request'] = response
self.sep_status_code = response['status'] self.sep_status_code = response['status']
self.sep_status_message = response.get('title') self.sep_status_message = response.get('title')
@ -375,7 +374,8 @@ class SEP(BasePGModel):
print(dict(request.POST)) print(dict(request.POST))
post_dict = dict(request.POST) post_dict = dict(request.POST)
self.details['redirect_post_data'] = post_dict 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']} # 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: try:
@ -421,6 +421,9 @@ class SEP(BasePGModel):
class Fake(BasePGModel): 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='+') payment_request = models.OneToOneField(PaymentRequest, on_delete=models.PROTECT, related_name='+')
def make_request(self): def make_request(self):
@ -438,3 +441,17 @@ class Fake(BasePGModel):
def reverse_request(self): def reverse_request(self):
raise NotImplementedError() 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()]

View file

@ -14,7 +14,7 @@ from django.shortcuts import render
from utils.commons import calculate_pna_commission from utils.commons import calculate_pna_commission
from .constans import StateChoices from .constans import StateChoices
from .forms import PaymentRequestForm from .forms import PaymentRequestForm
from .models import PaymentRequest from .models import PaymentRequest, get_gateway_class
from .serializers import PaymentRequestSerializer, CommissionSerializer from .serializers import PaymentRequestSerializer, CommissionSerializer
from ..core.models import BasePGModel from ..core.models import BasePGModel
from ..gooyal_oauth2.utils import get_application from ..gooyal_oauth2.utils import get_application
@ -74,16 +74,58 @@ class PaymentRequestView(UpdateView):
slug_field = 'uuid' slug_field = 'uuid'
slug_url_kwarg = 'uuid' slug_url_kwarg = 'uuid'
# TODO: remove it in future
# def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
# charge_request: PaymentRequest = self.get_object() charge_request: PaymentRequest = self.get_object()
# if charge_request.state == StateChoices.FAILED: # TODO: check more states
# return render(request, 'pg/payment_failure.html', { # TODO: prevent double
# 'object': charge_request if charge_request.state == StateChoices.FAILED:
# }) return render(request, 'pg/payment_failure.html', {
# redirect_url = tool.get_pg_page_url() 'object': charge_request
# return HttpResponseRedirect(redirect_url) })
#
# # gateway was setted in api call
# # TODO: double check
# if charge_request.gateway and not charge_request.commission:
# charge_request.save_commission()
# gateway_tool = charge_request.get_gateway_tools()
# gateway_tool.make_request()
# return HttpResponseRedirect(gateway_tool.redirect_url)
application_policy = charge_request.get_policy()
preferred_gateways = application_policy.get_preferred_gateways()
if preferred_gateways:
if application_policy.policy == application_policy.PolicyChoise.RANDOM:
# TODO: move to model
import random
gateway = random.choices(preferred_gateways)
charge_request.set_gateway(gateway)
charge_request.save_commission()
gateway_tool = charge_request.get_gateway_tools()
gateway_tool.make_request()
redirect_url = gateway_tool.get_pg_page_url()
if redirect_url:
return HttpResponseRedirect(redirect_url)
elif len(preferred_gateways) == 1:
gateway = preferred_gateways[0]
charge_request.set_gateway(gateway)
charge_request.save_commission()
gateway_tool = charge_request.get_gateway_tools()
gateway_tool.make_request()
redirect_url = gateway_tool.get_pg_page_url()
if redirect_url:
return HttpResponseRedirect(redirect_url)
else:
return super().get(request, *args, **kwargs)
return render(request, 'pg/payment_failure.html', {
'object': charge_request
})
# # TODO: remove it in future # # TODO: remove it in future
# def post(self, request, *args, **kwargs): # def post(self, request, *args, **kwargs):
# charge_request: PaymentRequest = self.get_object() # charge_request: PaymentRequest = self.get_object()
@ -104,30 +146,35 @@ class PaymentRequestView(UpdateView):
def form_valid(self, form): def form_valid(self, form):
self.object = form.save() self.object = form.save()
charge_request: PaymentRequest = self.get_object() charge_request: PaymentRequest = self.get_object()
application_policy = charge_request.get_policy()
preferred_gateways = application_policy.get_preferred_gateways()
charge_request.set_gateway(form.data) if preferred_gateways:
charge_request.save_commission() for gateway_label in preferred_gateways:
if gateway_label in form.data:
charge_request.set_gateway(gateway_label)
charge_request.save_commission()
tool:BasePGModel = charge_request.get_gateway_tools() tool:BasePGModel = charge_request.get_gateway_tools()
tool.make_request()
tool.make_request() redirect_url = tool.get_pg_page_url()
redirect_url = tool.get_pg_page_url() if redirect_url:
if tool.redirect_method == 'GET':
if redirect_url: return HttpResponseRedirect(redirect_url)
if tool.redirect_method == 'GET': elif tool.redirect_method == 'POST':
return HttpResponseRedirect(redirect_url) print(redirect_url)
elif tool.redirect_method == 'POST': return
print(redirect_url) else:
else: pass
pass
else:
messages.error(self.request, 'can not redirect to payment gateway') messages.error(self.request, 'can not redirect to payment gateway')
messages.info(self.request, 'در پردازش صفحه خطایی وجود دارد.') messages.info(self.request, 'در پردازش صفحه خطایی وجود دارد.')
# TODO: redirect to error page # TODO: redirect to error page
return HttpResponseRedirect(reverse('pg:payment-confirm-view', kwargs={'uuid': charge_request.uuid})) return HttpResponseRedirect(reverse('pg:payment-confirm-view', kwargs={'uuid': charge_request.uuid}))
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')

View file

View file

@ -0,0 +1,142 @@
import os
def get_settings(key):
from django.conf import settings
return getattr(settings, 'BASE_DIR')
def get_logging_setting():
BASE_DIR = get_settings('BASE_DIR')
LOG_DIR = BASE_DIR / 'log'
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
return {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{asctime} [{levelname}] {name} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'standard': {
'format': '{asctime} [{levelname}] {name}: {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
},
},
'handlers': {
# هندلر برای نوشتن در فایل
'file': {
'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_DIR, 'accounts.log'),
# 'maxBytes': 1024 * 1024 * 10, # 10 MB
'when': 'midnight',
'interval': 5,
'backupCount': 30,
'formatter': 'verbose',
'encoding': 'utf-8',
},
# هندلر برای خطاها در فایل جداگانه
'error_file': {
'level': 'ERROR',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_DIR, 'errors.log'),
# 'maxBytes': 1024 * 1024 * 10,
'when': 'midnight',
'interval': 5,
'backupCount': 30,
'formatter': 'verbose',
'encoding': 'utf-8',
},
# هندلر برای کنسول (فقط در حالت DEBUG)
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'simple',
},
# هندلر برای کنسول همیشه فعال (حتی در production)
'console_always': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'standard',
},
},
'loggers': {
# لاگر ریشه
'': { # empty string = root logger
'handlers': ['console', 'file', 'error_file'],
'level': 'INFO',
'propagate': True,
},
# لاگر اختصاصی برای Django
'django': {
'handlers': ['console', 'file'],
'level': 'INFO',
'propagate': False,
},
# لاگر اختصاصی برای درخواست‌ها
'django.request': {
'handlers': ['file', 'error_file', 'console'],
'level': 'ERROR',
'propagate': False,
},
# # لاگر اختصاصی برای برنامه خودتان
# 'root': {
# 'handlers': ['console', 'file', 'error_file'],
# 'level': 'DEBUG',
# 'propagate': False,
# },
},
}
# LOKI_BASE_PUBLIC_URL = config('LOKI_BASE_PUBLIC_URL', default=None, cast=str)
# {
# 'version': 1,
# 'disable_existing_loggers': False,
# 'formatters': {
# 'loki': {
# 'class': 'utils.logs.LokiFormatter', # required
# },
# },
#
# 'handlers': {
# 'loki': {
# 'level': 'DEBUG', # Log level. Required
# 'class': 'utils.logs.LokiHandler', # Required
# 'formatter': 'loki', # Loki formatter. Required
# 'timeout': 2, # Post request timeout, default is 0.5. Optional
# 'url': f'{LOKI_BASE_PUBLIC_URL}/loki/api/v1/push', # Loki url. Defaults to localhost. Optional.
# # 'auth': ("user", "password"), # Basic auth to authenticate with loki. Default is None (i.e. no auth). Optional
# 'tags': {"app": "accounts"}, # Tags / Labels to attach to the log. Optional, but strongly encoraged to use.
# 'mode': 'thread',
# # Push mode. Can be 'sync' or 'thread'. Sync is blocking, thread is non-blocking. Defaults to sync. Optional.
# },
# 'console': {
# 'level': 'DEBUG',
# 'class': 'logging.StreamHandler',
# # 'formatter': 'verbose',
# },
# },
# 'loggers': {
# '': {
# 'handlers': ['console', 'loki'],
# 'level': 'INFO',
# 'propagate': True,
# },
# },
# }

View file

@ -257,40 +257,8 @@ CACHES = {
LOKI_BASE_PUBLIC_URL = config('LOKI_BASE_PUBLIC_URL', default=None, cast=str) LOKI_BASE_PUBLIC_URL = config('LOKI_BASE_PUBLIC_URL', default=None, cast=str)
LOGGING = { from main.other_settings.logging import get_logging_setting
'version': 1, LOGGING = get_logging_setting()
'disable_existing_loggers': False,
'formatters': {
'loki': {
'class': 'utils.logs.LokiFormatter', # required
},
},
'handlers': {
'loki': {
'level': 'DEBUG', # Log level. Required
'class': 'utils.logs.LokiHandler', # Required
'formatter': 'loki', # Loki formatter. Required
'timeout': 2, # Post request timeout, default is 0.5. Optional
'url': f'{LOKI_BASE_PUBLIC_URL}/loki/api/v1/push', # Loki url. Defaults to localhost. Optional.
# 'auth': ("user", "password"), # Basic auth to authenticate with loki. Default is None (i.e. no auth). Optional
'tags': {"app": "ipg"}, # Tags / Labels to attach to the log. Optional, but strongly encoraged to use.
'mode': 'thread', # Push mode. Can be 'sync' or 'thread'. Sync is blocking, thread is non-blocking. Defaults to sync. Optional.
},
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
# 'formatter': 'verbose',
},
},
'loggers': {
'': {
'handlers': ['console', 'loki'],
'level': 'INFO',
'propagate': True,
},
},
}
PNA_CORPORATION_PIN = config('PNA_CORPORATION_PIN') PNA_CORPORATION_PIN = config('PNA_CORPORATION_PIN')
WALLET_BASE_PUBLIC_URL = config('WALLET_BASE_PUBLIC_URL') WALLET_BASE_PUBLIC_URL = config('WALLET_BASE_PUBLIC_URL')

View file

@ -22,17 +22,11 @@
<h6 class="h6">پرداخت با:</h6> <h6 class="h6">پرداخت با:</h6>
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col"> <div class="col">
{# <div class="">#} {% for gateway_tool in object.get_preferred_gateway_classes %}
<button type="submit" class="btn mx-auto center-block" name="pna"> <button type="submit" class="btn mx-auto center-block" name="{{ gateway_tool.label }}">
<img class="img-thumbnail" width="128" height="128" src="{% static "image/pna.png" %}"/> <img class="img-thumbnail" width="128" height="128" src="{% static gateway_tool.image %}"/>
</button> </button>
<button type="submit" class="btn mx-auto center-block" name="sep"> {% endfor %}
<img class="img-thumbnail" width="128" height="128" src="{% static "image/sep.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> </div>
</div> </div>

View file

@ -1,9 +1,13 @@
import logging import logging
from django.conf import settings
from django.utils import timezone
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework.views import exception_handler as drf_exception_handler from rest_framework.views import exception_handler as drf_exception_handler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def exception_handler(exc, context): def exception_handler(exc, context):
logger.exception(exc) logger.exception(exc)
# پاسخ پیش‌فرض DRF را دریافت می‌کنیم # پاسخ پیش‌فرض DRF را دریافت می‌کنیم
@ -13,8 +17,8 @@ def exception_handler(exc, context):
response_data = { response_data = {
'success': False, 'success': False,
'status_code': response.status_code, 'status_code': response.status_code,
'message': str(exc.default_detail), 'status_message': str(getattr(exc, 'default_detail', exc)),
'details': exc.detail, 'details': getattr(exc, 'detail', None),
} }
if isinstance(exc, APIException): if isinstance(exc, APIException):
@ -42,8 +46,12 @@ def exception_handler(exc, context):
# message = error # message = error
# #
# response_data['message'] = message # response_data['message'] = message
response_data['details'] = exc.detail if isinstance(exc.detail, dict) else {} if isinstance(exc.detail, dict):
response_data['details'] = exc.detail
else:
response_data['details'] = {"message": exc.detail}
response_data['details']['error'] = error response_data['details']['error'] = error
response_data['details']['timestamp'] = timezone.now().isoformat()
response.data = response_data response.data = response_data
@ -53,6 +61,7 @@ def exception_handler(exc, context):
# در فایل middleware.py # در فایل middleware.py
from django.http import JsonResponse from django.http import JsonResponse
class ErrorMiddleware: class ErrorMiddleware:
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
@ -67,13 +76,13 @@ class ErrorMiddleware:
{ {
"success": False, "success": False,
"status_code": 500, "status_code": 500,
'error_code': 500, "status_message": "Internal server error",
"message": "Internal server error", "details": str(exception) if settings.DEBUG else None,
"details": str(exception) # فقط در حالت توسعه! در تولید بهتر است لاگ شود.
}, },
status=500 status=500
) )
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework import status from rest_framework import status
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -84,8 +93,16 @@ class UnprocessableEntity(APIException):
default_detail = 'The request was well-formed but cannot be processed due to semantic errors.' default_detail = 'The request was well-formed but cannot be processed due to semantic errors.'
default_code = 'unprocessable_entity' default_code = 'unprocessable_entity'
class ServiceUnavailable(APIException): class ServiceUnavailable(APIException):
status_code = status.HTTP_503_SERVICE_UNAVAILABLE status_code = status.HTTP_503_SERVICE_UNAVAILABLE
default_ = _('SERVICE_UNAVAILABLE') default_ = _('SERVICE_UNAVAILABLE')
default_code = 'service_unavailable' default_code = 'service_unavailable'
default_detail = 'Service Unavailable' default_detail = 'Service Unavailable'
class Conflict(APIException):
status_code = status.HTTP_409_CONFLICT
default_ = _('CONFLICT')
default_code = 'conflict'
default_detail = 'Conflict'