Compare commits

..

No commits in common. "f5ce799a38a9270b835723e31df6e7e66602d9a4" and "da7d25d29eb64a88d8ee9a9e6c9e85cafe01c277" have entirely different histories.

12 changed files with 140 additions and 442 deletions

2
.gitignore vendored
View file

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

View file

@ -23,14 +23,6 @@ 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
@ -43,17 +35,6 @@ 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,24 +12,16 @@ 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 .forms import ApplicationGatewayPolicyForm from .models import PaymentRequest, PNA, Fake
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()
@ -62,7 +54,6 @@ 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:
@ -71,12 +62,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', list_display = ['state', 'check_success_again', 'uuid', 'splited_amount', 'user_id', 'jajali_datetime', 'is_registered', 'created_at', 'application', 'gateway']
'is_registered', 'created_at', 'application', 'gateway'] search_fields = ['application__uuid','application__client_id', 'amount', 'user__uuid']
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 {}
@ -89,8 +80,7 @@ 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, if obj.state in [StateChoices.IPG_SUCCESS, StateChoices.WALLET_SUBMIT_PENDING, StateChoices.WALLET_SUBMIT_SUCCESS]:
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/'
@ -100,11 +90,9 @@ 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/', path('<uuid:payment_request_uuid>/check_success_again/', self.admin_site.admin_view(self.check_success_again_view))
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)
@ -122,15 +110,8 @@ 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, GatewayChoise from .models import PaymentRequest
class PaymentRequestForm(forms.ModelForm): class PaymentRequestForm(forms.ModelForm):
@ -8,44 +8,3 @@ 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

@ -1,36 +0,0 @@
# 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,22 +22,18 @@ 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 ipg") FAKE = "fake", _("fake")
PNA = 'pna', _('pna ipg') PNA = 'pna', _('pna')
SEP = 'sep', _('sep ipg') SEP = 'sep', _('sep')
def get_gateway_class(gateway_label): class GatewayPolicy(BasePGModel):
return { application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
# "fake": Fake, related_name='+', null=True, blank=True, unique=True)
"pna": PNA, preference_gateway = models.JSONField(default=list, blank=True)
"sep": SEP,
}[gateway_label]
# TODO: use generic relation # TODO: use generic relation
@ -48,7 +44,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, 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)
@ -87,41 +83,44 @@ 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):
gateway_class = get_gateway_class(self.gateway) if self.gateway == GatewayChoise.PNA:
return gateway_class.objects.get(payment_request=self) return PNA.objects.get(payment_request=self)
def set_gateway(self, gateway_label): if self.gateway == GatewayChoise.SEP:
preferred_gateways = self.get_preferred_gateways() return SEP.objects.get(payment_request=self)
if gateway_label in preferred_gateways:
self.gateway = gateway_label if self.gateway == GatewayChoise.FAKE:
gateway_class = get_gateway_class(gateway_label) return Fake.objects.get(payment_request=self)
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
self.save() elif 'sep' in data:
self.gateway = GatewayChoise.SEP
pg = SEP.objects.create(payment_request=self)
pg.payment_request = self
if not self.gateway: else:
raise Exception('No gateway') raise Exception('Unknown 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}{gateway_tools.pk}' return f'{self.gateway.value}{gateway_tools.pk}'
else: else:
return '00000000' return '00000000'
@ -182,8 +181,7 @@ 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( logger.error(f"[Deposit Verify] Wallet verification failed | payment_id={self.pk}, response={verify_response}")
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
@ -227,8 +225,6 @@ 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)
@ -242,6 +238,17 @@ 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
@ -254,7 +261,6 @@ 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')
@ -265,8 +271,7 @@ 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, self.payment_request.change_state(self.payment_request.state, StateChoices.FAILED, 'error in pardakht novin gateway')
'error in pardakht novin gateway')
self.save() self.save()
@ -286,13 +291,12 @@ 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,
@ -318,9 +322,6 @@ 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')
@ -347,7 +348,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['make_request'] = response self.details['get_token_response'] = 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')
@ -374,8 +375,7 @@ class SEP(BasePGModel):
print(dict(request.POST)) print(dict(request.POST))
post_dict = dict(request.POST) post_dict = dict(request.POST)
self.details['post_dict'] = post_dict self.details['redirect_post_data'] = 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,9 +421,6 @@ 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):
@ -441,17 +438,3 @@ 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, get_gateway_class from .models import PaymentRequest
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,58 +74,16 @@ 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()
# TODO: check more states # if charge_request.state == StateChoices.FAILED:
# TODO: prevent double # return render(request, 'pg/payment_failure.html', {
if charge_request.state == StateChoices.FAILED: # 'object': charge_request
return render(request, 'pg/payment_failure.html', { # })
'object': charge_request # redirect_url = tool.get_pg_page_url()
}) # 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()
@ -146,16 +104,12 @@ 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()
if preferred_gateways: charge_request.set_gateway(form.data)
for gateway_label in preferred_gateways:
if gateway_label in form.data:
charge_request.set_gateway(gateway_label)
charge_request.save_commission() 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()
@ -165,12 +119,11 @@ class PaymentRequestView(UpdateView):
return HttpResponseRedirect(redirect_url) return HttpResponseRedirect(redirect_url)
elif tool.redirect_method == 'POST': elif tool.redirect_method == 'POST':
print(redirect_url) print(redirect_url)
return
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

View file

@ -1,142 +0,0 @@
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,8 +257,40 @@ 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)
from main.other_settings.logging import get_logging_setting LOGGING = {
LOGGING = get_logging_setting() '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": "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,11 +22,17 @@
<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">
{% for gateway_tool in object.get_preferred_gateway_classes %} {# <div class="">#}
<button type="submit" class="btn mx-auto center-block" name="{{ gateway_tool.label }}"> <button type="submit" class="btn mx-auto center-block" name="pna">
<img class="img-thumbnail" width="128" height="128" src="{% static gateway_tool.image %}"/> <img class="img-thumbnail" width="128" height="128" src="{% static "image/pna.png" %}"/>
</button> </button>
{% endfor %} <button type="submit" class="btn mx-auto center-block" name="sep">
<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,13 +1,9 @@
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 را دریافت می‌کنیم
@ -17,8 +13,8 @@ def exception_handler(exc, context):
response_data = { response_data = {
'success': False, 'success': False,
'status_code': response.status_code, 'status_code': response.status_code,
'status_message': str(getattr(exc, 'default_detail', exc)), 'message': str(exc.default_detail),
'details': getattr(exc, 'detail', None), 'details': exc.detail,
} }
if isinstance(exc, APIException): if isinstance(exc, APIException):
@ -46,12 +42,8 @@ def exception_handler(exc, context):
# message = error # message = error
# #
# response_data['message'] = message # response_data['message'] = message
if isinstance(exc.detail, dict): response_data['details'] = exc.detail if isinstance(exc.detail, dict) else {}
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
@ -61,7 +53,6 @@ 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
@ -76,13 +67,13 @@ class ErrorMiddleware:
{ {
"success": False, "success": False,
"status_code": 500, "status_code": 500,
"status_message": "Internal server error", 'error_code': 500,
"details": str(exception) if settings.DEBUG else None, "message": "Internal server error",
"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 _
@ -93,16 +84,8 @@ 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'