fake charge
This commit is contained in:
parent
a8006579bc
commit
1c437c49c6
11 changed files with 313 additions and 5 deletions
|
|
@ -1,2 +1,4 @@
|
|||
from django.contrib import admin
|
||||
from .models import Charge
|
||||
|
||||
admin.site.register(Charge)
|
||||
|
|
|
|||
11
apps/core/forms.py
Normal file
11
apps/core/forms.py
Normal file
|
|
@ -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',)
|
||||
31
apps/core/migrations/0001_initial.py
Normal file
31
apps/core/migrations/0001_initial.py
Normal file
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
21
apps/core/serializers.py
Normal file
21
apps/core/serializers.py
Normal file
|
|
@ -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']
|
||||
|
||||
|
|
@ -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/<pk>/', ChargeView.as_view(), name='charge'),
|
||||
path('api/charge_create', ChargeRequestApiView.as_view(), name='charge_create_api'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
108
templates/base_charge.html
Normal file
108
templates/base_charge.html
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
{% load static %}
|
||||
{#{% load view_breadcrumbs %}#}
|
||||
|
||||
|
||||
<!doctype html>
|
||||
<html lang="fa-ir" dir="rtl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="">
|
||||
<title>{% block title %}welcome{% endblock %}</title>
|
||||
|
||||
<link href="{% static 'bootstrap-5.3.0-dist/css/bootstrap.rtl.min.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'bootstrap-icons-1.11.3/font/bootstrap-icons.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'fonts/vazirmatn-v33.003/misc/UI-Farsi-Digits/Vazirmatn-UI-FD-font-face.css' %}"
|
||||
rel="stylesheet">
|
||||
<link href="{% static 'fonts/Pelak/fonts.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'main.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'jquery-3.6.0.min.js' %}"></script>
|
||||
|
||||
{# <link rel="stylesheet"#}
|
||||
{# href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css"/>#}
|
||||
<!-- Or for RTL support -->
|
||||
<style>
|
||||
@media (min-width: 576px) {
|
||||
.h-sm-100 {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row vh-100 overflow-auto">
|
||||
<div class="col d-flex flex-column h-sm-100">
|
||||
<div class="m-0 p-0 w-100 " style="background-color: red; height: 3px;"></div>
|
||||
<main class="row overflow-auto pb-3">
|
||||
<div class="col pt-2">
|
||||
{% include "include/messages.html" %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="grid">
|
||||
<div class="d-flex justify-content-start">
|
||||
<h4 class="m-3">
|
||||
{% block page_title %}
|
||||
{% endblock %}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="grid">
|
||||
<div class="d-flex justify-content-end mt-2">
|
||||
{% block page_action %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<div class="col-md-12 col-lg-12">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="row bg-light py-3 mt-auto small">
|
||||
<div class="d-flex">
|
||||
<div class="flex-grow-1">
|
||||
کلیه حقوق متعلق به
|
||||
<a class="text-decoration-none" href="/">addwin</a>
|
||||
است.
|
||||
</div>
|
||||
{# <div class="">#}
|
||||
{# توسعه#}
|
||||
{# <a href="https://gooyal.com" class="text-decoration-none" target="_blank"#}
|
||||
{# rel="noopener noreferrer">#}
|
||||
{# گویال#}
|
||||
{# <img src="{% static 'image/gooyal_logo_gray.png' %}" width="28" height="21">#}
|
||||
{# </a>#}
|
||||
{# </div>#}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
jalaliDatepicker.startWatch();
|
||||
</script>
|
||||
|
||||
<script src="{% static 'bootstrap-5.3.0-dist/js/bootstrap.bundle.min.js' %}"></script>
|
||||
<script src="{% static 'jquery.formset.js' %}"></script>
|
||||
</html>
|
||||
27
templates/core/charge.html
Normal file
27
templates/core/charge.html
Normal file
|
|
@ -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 %}
|
||||
<form action="{{ request.get_full_path }}" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12 col-lg-12" id="formset">
|
||||
{# <div class="card">#}
|
||||
{# <div class="card-body">#}
|
||||
{{ form|crispy }}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<input class="w-100 btn btn-primary btn-lg" type="submit" value="ثبت" name="save"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -258,4 +258,6 @@ CACHES = {
|
|||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALLOWED_NEGATIVE_BALANCE_APPLICATIONS = config('ALLOWED_NEGATIVE_BALANCE_APPLICATIONS', cast=lambda v: [s.strip() for s in v.split(',')])
|
||||
Loading…
Add table
Reference in a new issue