diff --git a/apps/core/admin.py b/apps/core/admin.py index c6fe108..ae11a7c 100644 --- a/apps/core/admin.py +++ b/apps/core/admin.py @@ -1,2 +1,4 @@ from django.contrib import admin +from .models import Charge +admin.site.register(Charge) diff --git a/apps/core/forms.py b/apps/core/forms.py new file mode 100644 index 0000000..6f329f5 --- /dev/null +++ b/apps/core/forms.py @@ -0,0 +1,11 @@ +from django import forms + +from apps.core.models import Charge + + +class ChargeForm(forms.ModelForm): + amount = forms.IntegerField(label='مبلغ', widget=forms.NumberInput(attrs={'class': 'form-control'}), required=True) + + class Meta: + model = Charge + fields = ('amount',) diff --git a/apps/core/migrations/0001_initial.py b/apps/core/migrations/0001_initial.py new file mode 100644 index 0000000..f90cf31 --- /dev/null +++ b/apps/core/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.7 on 2025-03-31 10:05 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Charge', + fields=[ + ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True, null=True)), + ('state', models.IntegerField(choices=[(1, 'created'), (2, 'delayed'), (3, 'pending'), (4, 'incomplete'), (5, 'success'), (6, 'failed'), (7, 'expected_failure')], default=1)), + ('amount', models.PositiveBigIntegerField(blank=True, null=True)), + ('application', models.ForeignKey(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)), + ], + ), + ] diff --git a/apps/core/models.py b/apps/core/models.py index 56987d4..b81963e 100644 --- a/apps/core/models.py +++ b/apps/core/models.py @@ -1,2 +1,37 @@ +from django.conf import settings from django.db import models +import uuid from django.utils.translation import gettext_lazy as _ +from oauth2_provider.settings import oauth2_settings +from rest_framework.exceptions import APIException + +from apps.wallet.constans import StateChoices + + +class Charge(models.Model): + uuid = models.UUIDField(primary_key=True, editable=True, default=uuid.uuid4, unique=True, + db_index=True) # reserve_code + created_at = models.DateTimeField(auto_now_add=True, null=True) + updated_at = models.DateTimeField(auto_now=True, null=True) + application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, + related_name='+', null=True) + 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) + + def change_state(self, from_state, to_state, error_message=None, **data): + if self.state == from_state: + updated = Charge.objects.filter(pk=self.pk, state=from_state).update( + state=to_state, + **data + ) + if updated: # double check in db level + self.state = to_state + else: + raise APIException(error_message) + else: + raise APIException(error_message) + + + + diff --git a/apps/core/serializers.py b/apps/core/serializers.py new file mode 100644 index 0000000..f5e9cc6 --- /dev/null +++ b/apps/core/serializers.py @@ -0,0 +1,21 @@ +from rest_framework import serializers + +from apps.core.models import Charge + + +class ChargeSerializer(serializers.ModelSerializer): + # uuid = serializers.UUIDField(required=True) + + class Meta: + model = Charge + fields = ( + 'uuid', + 'user', + "created_at", + "state", + 'amount', + 'updated_at', + ) + + read_only_fields = ['uuid', 'user','updated_at', 'state', 'created_at', 'amount'] + diff --git a/apps/core/urls.py b/apps/core/urls.py index 7f82b8a..5a868f1 100644 --- a/apps/core/urls.py +++ b/apps/core/urls.py @@ -1,7 +1,10 @@ from django.urls import path, include -from .views import HomeView +from .views import HomeView, ChargeView, ChargeRequestApiView + app_name = "core" urlpatterns = [ path('', HomeView.as_view(), name='home'), + path('charge//', ChargeView.as_view(), name='charge'), + path('api/charge_create', ChargeRequestApiView.as_view(), name='charge_create_api'), ] diff --git a/apps/core/views.py b/apps/core/views.py index 3d3cbe5..df53a3f 100644 --- a/apps/core/views.py +++ b/apps/core/views.py @@ -1,10 +1,74 @@ +from django.contrib import messages from django.contrib.auth.decorators import login_required +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 TemplateView +from django.views.generic import TemplateView, FormView, UpdateView +from oauth2_provider.contrib.rest_framework import TokenHasScope, IsAuthenticatedOrTokenHasScope +from rest_framework import generics +from rest_framework.views import APIView + +from apps.core.forms import ChargeForm +from apps.core.models import Charge +from apps.core.serializers import ChargeSerializer +from apps.wallet.models import Transaction + # Create your views here. @method_decorator(login_required, name='dispatch') class HomeView(TemplateView): template_name = 'core/home.html' + +class ChargeRequestApiView(generics.CreateAPIView): + permission_classes = [IsAuthenticatedOrTokenHasScope] + serializer_class = ChargeSerializer + required_scopes = ['wallet.wallet:get_balance'] + model = Charge + + def perform_create(self, serializer): + user = self.request.user + serializer.save(user = user) + + +class ChargeView(UpdateView): + template_name = 'core/charge.html' + form_class = ChargeForm + + def get_queryset(self): + return Charge.objects.all() + + def form_valid(self, form): + self.object = form.save() + amount = form.cleaned_data['amount'] + payer_pk = 'a95557d6-4f21-4717-99c2-354db85f940e' + charge = self.get_object() + 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': 'شارژ مجازی کیف پول'} + ) + + try: + transaction.submit() + transaction.verify() + messages.success(self.request, 'شارژ انجام شد') + + except: + messages.error(self.request, 'خطایی رخ داده است') + + return HttpResponseRedirect(self.get_success_url()) + + + + def get_success_url(self): + return reverse('core:charge', kwargs={'pk': self.object.pk}) + + # def form_valid(self, form): + + diff --git a/apps/wallet/models.py b/apps/wallet/models.py index a10c3d5..3dda456 100755 --- a/apps/wallet/models.py +++ b/apps/wallet/models.py @@ -74,7 +74,8 @@ class Transaction(models.Model): def withdraw_from_payer_balance(self): payer = self.get_payer() - if self.amount > payer.balance: + payer_is_allowed_negative_balance = self.payer_type==TypeChoices.APPLICATION.value and str(self.payer_id) in settings.ALLOWED_NEGATIVE_BALANCE_APPLICATIONS + if self.amount > payer.balance and not payer_is_allowed_negative_balance : raise APIException('not enough balance') payer.balance = F('balance') - self.amount payer.save(update_fields=['balance']) @@ -249,4 +250,7 @@ class Transaction(models.Model): def __str__(self): - return f"{self.get_payer()} -> {self.get_payee()} ({self.uuid}): {self.amount} - {self.get_state_display()}" + try: + return f"{self.get_payer()} -> {self.get_payee()} ({self.uuid}): {self.amount} - {self.get_state_display()}" + except: + return str(self.uuid) \ No newline at end of file diff --git a/templates/base_charge.html b/templates/base_charge.html new file mode 100644 index 0000000..0d2c4b4 --- /dev/null +++ b/templates/base_charge.html @@ -0,0 +1,108 @@ +{% load static %} +{#{% load view_breadcrumbs %}#} + + + + + + + + + {% block title %}welcome{% endblock %} + + + + + + + + + {# #} + + + + + + +
+
+
+
+
+
+ {% include "include/messages.html" %} +
+
+
+
+

+ {% block page_title %} + {% endblock %} +

+
+
+
+
+
+
+ {% block page_action %} + {% endblock %} +
+
+
+
+ +
+ +
+
+
+
+
+ {% block content %} + {% endblock %} +
+
+
+
+
+
+
+ + +
+
+
+ + + + + + + diff --git a/templates/core/charge.html b/templates/core/charge.html new file mode 100644 index 0000000..787c2d2 --- /dev/null +++ b/templates/core/charge.html @@ -0,0 +1,27 @@ +{% extends "base_charge.html" %} +{% load static %} +{% load crispy_forms_tags %} +{% load jalali_tags %} + +{% block title %}افزایش موجودی{% endblock %} +{% block page_title %}افزایش موجودی{% endblock %} + + +{% block content %} +
+ {% csrf_token %} +
+
+{#
#} +{#
#} + {{ form|crispy }} +{#
#} +{#
#} +
+
+ +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/wallet/settings.py b/wallet/settings.py index 033f929..dd43e1c 100644 --- a/wallet/settings.py +++ b/wallet/settings.py @@ -258,4 +258,6 @@ CACHES = { "CLIENT_CLASS": "django_redis.client.DefaultClient", } } -} \ No newline at end of file +} + +ALLOWED_NEGATIVE_BALANCE_APPLICATIONS = config('ALLOWED_NEGATIVE_BALANCE_APPLICATIONS', cast=lambda v: [s.strip() for s in v.split(',')]) \ No newline at end of file