diff --git a/apps/pg/__init__.py b/apps/pg/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/pg/admin.py b/apps/pg/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/pg/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/pg/apps.py b/apps/pg/apps.py new file mode 100644 index 0000000..3f0f917 --- /dev/null +++ b/apps/pg/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PgConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'pg' diff --git a/apps/pg/constans.py b/apps/pg/constans.py new file mode 100644 index 0000000..3531ce6 --- /dev/null +++ b/apps/pg/constans.py @@ -0,0 +1,17 @@ +from django.db.models import IntegerChoices +from django.utils.translation import gettext_lazy as _ + + +class StateChoices(IntegerChoices): + CREATED = 1 , _('created') + DELAYED = 2 , _('delayed') # delayed as user wish + PENDING = 3 , _('pending') # wait for external service response (or action) + INCOMPLETE = 4 , _('incomplete') # started and wait for internal progress to complete + SUCCESS = 5 , _('success') + FAILED = 6 , _('failed') + EXPECTED_FAILURE = 7 , _('expected_failure') # no exact data available but guessed to be failed + + +class TypeChoices(IntegerChoices): + USER = 1 , _('user') + APPLICATION = 2 , _('application') \ No newline at end of file diff --git a/apps/pg/migrations/__init__.py b/apps/pg/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/pg/models.py b/apps/pg/models.py new file mode 100644 index 0000000..978e2a4 --- /dev/null +++ b/apps/pg/models.py @@ -0,0 +1,77 @@ +from django.conf import settings +from django.db import models +from oauth2_provider.settings import oauth2_settings + +from apps.core.models import BaseModel +from apps.pg.constans import StateChoices +import uuid + +from utils.wallet_client import deposit_to_user_wallet_submit, withdraw_from_user_wallet_verify, \ + deposit_to_user_wallet_verify + + +class ChargeRequest(BaseModel): + 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) + callback_url = models.URLField(null=True, blank=True) + details = models.JSONField(null=True, blank=True, default=dict) + + # TODO: complete and use this + def change_state(self, from_state, to_state, error_message=None, **data): + if self.state == from_state: + updated = ChargeRequest.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 Exception(error_message) + else: + raise Exception(error_message) + + def charge_wallet_balance(self): + if self.state in [StateChoices.CREATED]: + to_pending = ChargeRequest.objects.filter( + pk=self.pk, + state=StateChoices.CREATED.value, + ).update( + state=StateChoices.PENDING.value) + if not to_pending: + raise Exception('Cannot charge balance because state') + + data = { + "uuid": str(self.pk), + "payer_id": str(self.user.uuid), + "payer_type": 1, + "amount": self.amount, + "details": { + 'description': "ad balance charge", + 'reference_id': str(self.pk), + 'application_details_url': '' + }, + } + submit_response = deposit_to_user_wallet_submit(data) + + if not submit_response.uuid: + raise Exception('Failed to submit payment in wallet') + + verify_response = deposit_to_user_wallet_verify(self.pk) + + if not verify_response.uuid or verify_response.state != 5: + raise Exception('Failed to verify payment in wallet') + + to_pay_success = ChargeRequest.objects.filter( + pk=self.pk, + state=StateChoices.PENDING.value + ).update( + publication_state=StateChoices.SUCCESS.value, + ) + + if not to_pay_success: + raise Exception('Failed to update state payment') + + return bool(to_pay_success) diff --git a/apps/pg/serializers.py b/apps/pg/serializers.py new file mode 100644 index 0000000..c215089 --- /dev/null +++ b/apps/pg/serializers.py @@ -0,0 +1,21 @@ +from rest_framework import serializers + +from .models import ChargeRequest + + +class ChargeRequestSerializer(serializers.ModelSerializer): + # uuid = serializers.UUIDField(required=True) + + class Meta: + model = ChargeRequest + fields = ( + 'uuid', + 'user', + "created_at", + "state", + 'amount', + 'updated_at', + ) + + read_only_fields = ['uuid', 'user','updated_at', 'state', 'created_at'] + diff --git a/apps/pg/views.py b/apps/pg/views.py new file mode 100644 index 0000000..1297705 --- /dev/null +++ b/apps/pg/views.py @@ -0,0 +1,68 @@ +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 UpdateView +from oauth2_provider.contrib.rest_framework import TokenHasScope, IsAuthenticatedOrTokenHasScope +from rest_framework import generics +from rest_framework.views import APIView + +from .forms import ChargeForm +from .models import ChargeRequest +from .serializers import ChargeRequestSerializer +from .constans import StateChoices +from ..gooyal_oauth2.utils import get_application + + +class ChargeRequestApiView(generics.CreateAPIView): + permission_classes = [IsAuthenticatedOrTokenHasScope] + serializer_class = ChargeRequestSerializer + required_scopes = ['wallet.wallet:get_balance'] # TODO: is it good scope? + model = ChargeRequest + + def perform_create(self, serializer): + user = self.request.user + application = get_application(self.request) + charge_request: ChargeRequest = serializer.save(user=user, application=application) + + # TODO: web view + # charge_request.charge_wallet_balance() + + +class ChargeView(UpdateView): + template_name = 'pg/charge-request.html' + form_class = ChargeForm + + def get_queryset(self): + return ChargeRequest.objects.all() + # return Charge.objects.filter(state__in=[StateChoices.CREATED.value, StateChoices.SUCCESS.value]) + + def form_valid(self, form): + self.object = form.save() + charge_request: ChargeRequest = self.get_object() + try: + charge_request.charge_wallet_balance() + # transaction = Transaction.objects.create( + # uuid=charge.uuid, + # payee_id=charge.user.pk, + # payee_type=1, + # payer_id=payer_pk, + # payer_type=2, + # amount=amount, + # details={'description': 'شارژ مجازی کیف پول'} + # ) + # transaction.submit() + # transaction.verify() + # charge.change_state(from_state=StateChoices.PENDING.value, to_state=StateChoices.SUCCESS.value) + messages.success(self.request, 'شارژ انجام شد') + + except: + messages.error(self.request, 'خطایی رخ داده است') + charge_request.change_state(from_state=StateChoices.PENDING.value, to_state=StateChoices.EXPECTED_FAILURE.value) + + return HttpResponseRedirect(self.get_success_url()) + + def get_success_url(self): + return reverse('core:charge', kwargs={'pk': self.object.pk}) diff --git a/templates/pg/charge-request.html b/templates/pg/charge-request.html new file mode 100644 index 0000000..c47c3e4 --- /dev/null +++ b/templates/pg/charge-request.html @@ -0,0 +1,32 @@ +{% extends "base_charge.html" %} +{% load static %} +{% load crispy_forms_tags %} +{% load jalali_tags %} + +{% block title %}افزایش موجودی{% endblock %} +{% block page_title %}افزایش موجودی{% endblock %} + + +{% block content %} + {% if object.state in '1' %} +
+ {% csrf_token %} +
+
+{#
#} +{#
#} + {{ form|crispy }} +{#
#} +{#
#} +
+
+ +
+
+
+ {% else %} + {{ object.amount }} in state {{ object.get_state_display }}
+ back to application + {% endif %} + +{% endblock %} \ No newline at end of file