@@ -5,7 +5,7 @@ from ml_model.models import NeuronModel def _gen_only(chain: str, *fields: str): - return [(f"{chain}__{f}") for f in fields] + return [(f'{chain}__{f}') for f in fields] PATH_PREFETCH_MAP = { @@ -21,19 +21,42 @@ PATH_PREFETCH_MAP = { 'business_account__parent_company__user__payment_plan__plan', queryset=NeuronModel.objects.only('slug'), ), - 'social_auth' + 'social_auth', ), 'only': ( - 'uid', 'first_name', 'last_name', 'created_at', 'email', 'active', - 'is_superuser', 'is_staff', 'is_confirmed', 'is_subscribed_to_emails', 'profile_picture_name', - *_gen_only('business_account', 'uid', 'parent_company__uid', 'show_balance', - 'account_privileges', 'parent_company__user__uid'), + 'uid', + 'first_name', + 'last_name', + 'created_at', + 'email', + 'active', + 'is_superuser', + 'is_staff', + 'is_confirmed', + 'is_subscribed_to_emails', + 'profile_picture_name', + *_gen_only( + 'business_account', + 'uid', + 'parent_company__uid', + 'show_balance', + 'account_privileges', + 'parent_company__user__uid', + ), *_gen_only('payment_plan', 'uid', 'last_payment_at', 'next_payment_at'), - *_gen_only('payment_plan__plan', 'uid', 'title', 'price', 'tokens_per_plan', 'duration', 'points'), - *_gen_only('business_account__parent_company__user__payment_plan', 'uid', 'last_payment_at', - 'next_payment_at', 'plan__uid', 'plan__title', 'plan__price', 'plan__tokens_per_plan', - 'plan__duration', 'plan__points') - ) + *_gen_only('payment_plan__plan', 'uid', 'title', 'price', 'tokens_per_plan', 'duration'), + *_gen_only( + 'business_account__parent_company__user__payment_plan', + 'uid', + 'last_payment_at', + 'next_payment_at', + 'plan__uid', + 'plan__title', + 'plan__price', + 'plan__tokens_per_plan', + 'plan__duration', + ), + ), }, '/api/v1/payments/user-balance': { 'select': ( @@ -44,11 +67,31 @@ PATH_PREFETCH_MAP = { ), 'prefetch': (), 'only': ( - 'uid', 'is_staff', 'is_superuser', + 'uid', + 'is_staff', + 'is_superuser', *_gen_only('business_account', 'account_privileges', 'acceptance_status', 'token_limit'), *_gen_only('business_account__group', 'uid', 'token_limit'), *_gen_only('payment_plan', 'uid', 'current_token_balance'), - *_gen_only('business_account__parent_company__user__payment_plan', 'uid', 'current_token_balance') - ) - } + *_gen_only( + 'business_account__parent_company__user__payment_plan', 'uid', 'current_token_balance' + ), + ), + }, + '/api/v1/payments/plans': { + 'select': ( + 'host_account', + 'payment_plan', + 'business_account__parent_company__user__payment_plan__plan', + 'payment_plan__plan' + ), + 'prefetch': ( + Prefetch('payment_plan__plan__accessed_models', queryset=NeuronModel.objects.only('slug')), + Prefetch( + 'business_account__parent_company__user__payment_plan__plan', + queryset=NeuronModel.objects.only('slug'), + ), + 'payment_plan__plan__features', + ), + }, } @@ -10,7 +10,6 @@ from drf_spectacular.contrib.rest_framework_simplejwt import ( SimpleJWTScheme as BaseSimpleJWTScheme, ) from ninja.security import HttpBearer -from oauth2_provider.models import AccessToken from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed @@ -2,13 +2,13 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. -# +# #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-12-16 19:50+0300\n" +"POT-Creation-Date: 2025-12-19 18:36+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -162,8 +162,8 @@ msgstr "Бизнес Группы" #: authentication/models/business_host.py:22 #: authentication/models/email_token.py:13 authentication/models/user.py:222 #: authentication/models/user.py:223 authentication/models/user_telegram.py:22 -#: authentication/models/user_vk.py:12 payments/admin.py:35 -#: payments/admin.py:89 payments/models/invoice.py:15 +#: authentication/models/user_vk.py:12 payments/admin.py:36 +#: payments/admin.py:97 payments/models/invoice.py:15 #: payments/models/payment.py:26 payments/models/payment_plan.py:61 msgid "User" msgstr "Пользователь" @@ -211,7 +211,7 @@ msgid "PSRN" msgstr "ОГРН" #: authentication/models/business_host.py:69 ml_model/models.py:180 -#: tools/public_api/models.py:30 +#: payments/models/payment_plan_feature.py:12 tools/public_api/models.py:30 msgid "Name" msgstr "Наименование" @@ -455,21 +455,21 @@ msgstr "Вайтлист для отмены политик" msgid "Whitelists to cancel policies" msgstr "Вайтлисты для отмены политик" -#: authentication/security.py:35 +#: authentication/security.py:34 #, fuzzy #| msgid "Hidden" msgid "Forbidden" msgstr "Скрытый" -#: authentication/security.py:48 +#: authentication/security.py:47 msgid "Access token is expired" msgstr "Срок действия токена доступа истек" -#: authentication/security.py:61 +#: authentication/security.py:60 msgid "User not found" msgstr "Пользователь не найден" -#: authentication/security.py:99 authentication/security.py:130 +#: authentication/security.py:98 authentication/security.py:129 msgid "Access token expired or does not exist" msgstr "Токен доступа просрочен или не существует" @@ -505,7 +505,7 @@ msgstr "Пароли не совпадают" msgid "You cannot change the password of an unconfirmed e-mail user." msgstr "Вы не можете изменить пароль неподтвержденного по e-mail пользователя." -#: authentication/services/business_host_service.py:155 +#: authentication/services/business_host_service.py:154 msgid "No user_email is provided" msgstr "" @@ -661,6 +661,7 @@ msgid "Slug" msgstr "Ярлык" #: ml_model/models.py:28 ml_model/models.py:80 +#: payments/models/payment_plan_feature.py:18 msgid "Category" msgstr "Категория" @@ -709,7 +710,7 @@ msgstr "Теги" msgid "Neuron Model" msgstr "Нейронная Модель" -#: ml_model/models.py:156 ml_model/models.py:402 payments/admin.py:95 +#: ml_model/models.py:156 ml_model/models.py:402 payments/admin.py:103 msgid "Model" msgstr "Модель" @@ -955,13 +956,13 @@ msgstr "Нет изображения для улучшения" msgid "Model data cannot be retrieved" msgstr "Невозможно получить данные модели" -#: payments/admin.py:33 payments/admin.py:67 payments/admin.py:87 +#: payments/admin.py:34 payments/admin.py:75 payments/admin.py:95 msgid "You can search by user email, exacted company name" msgstr "" "Вы можете осуществлять поиск по e-mail пользователя, точному названию " "компании" -#: payments/admin.py:38 payments/admin.py:92 +#: payments/admin.py:39 payments/admin.py:100 msgid "Missing" msgstr "Отсутствующий" @@ -982,6 +983,18 @@ msgstr "" msgid "The payer does not exist" msgstr "Плательщик не существует" +#: payments/models/choices.py:6 +msgid "Text Page" +msgstr "Страница текста" + +#: payments/models/choices.py:7 +msgid "File (pcs)" +msgstr "Файл (шт.)" + +#: payments/models/choices.py:8 +msgid "Time (mins)" +msgstr "Время (мин.)" + #: payments/models/invoice.py:23 msgid "Generative Model" msgstr "Генеративная модель" @@ -1035,6 +1048,7 @@ msgid "Is visible" msgstr "Видимый" #: payments/models/payment_plan.py:52 payments/models/payment_plan.py:67 +#: payments/models/payment_plan_feature.py:11 msgid "Payment Plan" msgstr "Платежный План" @@ -1062,6 +1076,22 @@ msgstr "Рекуррентная задача на платеж" msgid "User Balance" msgstr "Баланс пользователя" +#: payments/models/payment_plan_feature.py:13 +msgid "Quantity" +msgstr "Количество" + +#: payments/models/payment_plan_feature.py:24 +msgid "Measurement unit" +msgstr "Единица измерения" + +#: payments/models/payment_plan_feature.py:31 +msgid "Payment plan feature" +msgstr "Преимущество платежного плана" + +#: payments/models/payment_plan_feature.py:32 +msgid "Payment plan features" +msgstr "Преимущества платежного плана" + #: payments/models/promocode.py:48 msgid "Action Function" msgstr "Активирующаяся функция" @@ -0,0 +1,33 @@ +# Generated by Django 5.0.11 on 2025-12-19 15:34 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ml_model', '0052_alter_modelinput_unique_together'), + ('payments', '0017_alter_paymentplan_points'), + ] + + operations = [ + migrations.CreateModel( + name='PaymentPlanFeature', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveIntegerField(db_index=True, editable=False, verbose_name='order')), + ('name', models.CharField(max_length=30, verbose_name='Name')), + ('quantity', models.PositiveSmallIntegerField(default=1, verbose_name='Quantity')), + ('measurement_unit', models.CharField(choices=[('text_page', 'Страница текста'), ('file', 'Файл (шт.)'), ('time', 'Время (мин.)')], default='text_page', max_length=15, verbose_name='Measurement unit')), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment_plan_feature', to='ml_model.modelcategory', verbose_name='Category')), + ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='features', to='payments.paymentplan', verbose_name='Payment Plan')), + ], + options={ + 'verbose_name': 'Payment plan feature', + 'verbose_name_plural': 'Payment plan features', + 'ordering': ('order',), + 'abstract': False, + }, + ), + ] @@ -0,0 +1,48 @@ +# Generated by Django 5.0.11 on 2025-12-19 15:35 +import re + +from django.db import migrations + + +def move_points_to_features(apps, schema_editor): + PaymentPlan = apps.get_model('payments', 'PaymentPlan') + PaymentPlanFeature = apps.get_model('payments', 'PaymentPlanFeature') + ModelCategory = apps.get_model('ml_model', 'ModelCategory') + + measurement_units = {'images': 'file', 'chat-bots': 'text_page'} + categories = { + 'images': ModelCategory.objects.get(slug='images'), + 'chat-bots': ModelCategory.objects.get(slug='chat-bots') + } + for plan in PaymentPlan.objects.all(): + order_counter = 1 + payment_plan_features_data = [] + for point in plan.points: + cleaned = re.sub(r'\b(в|во|или)\b', '', point) + cleaned = re.sub(r'(?<=\d)\s+(?=\d)', '', cleaned) + cleaned = re.sub(r'\bтекста\b', '', cleaned) + cleaned = re.sub(r'\bкартинок\b', 'images', cleaned) + cleaned = re.sub(r'\bстраниц\b', 'chat-bots', cleaned) + cleaned = re.sub(r'\s+', ' ', cleaned).strip() + data = cleaned.split() + payment_plan_feature = PaymentPlanFeature( + plan=plan, + name=' '.join(data[2:]), + quantity=int(data[0]), + category=categories[data[1]], + measurement_unit=measurement_units[data[1]], + order=order_counter + ) + payment_plan_features_data.append(payment_plan_feature) + order_counter += 1 + PaymentPlanFeature.objects.bulk_create(payment_plan_features_data) + + +class Migration(migrations.Migration): + dependencies = [ + ('payments', '0018_paymentplanfeature'), + ] + + operations = [ + migrations.RunPython(move_points_to_features, migrations.RunPython.noop) + ] @@ -3,6 +3,7 @@ from payments.models.payment_plan import PaymentPlan, PaymentPlanUserInfo from payments.models.user_payment_method import UserPaymentMethod from payments.models.invoice import Invoice from payments.models.promocode import PromoCode, PromoCodeActivation +from payments.models.payment_plan_feature import PaymentPlanFeature __all__ = ( 'Payment', @@ -12,4 +13,5 @@ __all__ = ( 'Invoice', 'PromoCode', 'PromoCodeActivation', + 'PaymentPlanFeature' ) @@ -32,13 +32,6 @@ class PaymentPlan(BaseModel): default=MONTH, ) is_visible = models.BooleanField(default=True, verbose_name=_('Is visible')) - points = ArrayField( - default=list, - blank=True, - base_field=models.CharField(), - verbose_name='Поинты', - help_text='Перечислять через запятую', - ) accessed_models = models.ManyToManyField( NeuronModel, verbose_name='Доступные модели', @@ -0,0 +1,38 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from ordered_model.models import OrderedModel + +from ml_model.models import ModelCategory +from payments.models import PaymentPlan + + +class PaymentPlanFeature(OrderedModel): + class MeasurementUnitChoices(models.TextChoices): + TEXT_PAGE = ('text_page', _('Text Page')) + FILE = ('file', _('File (pcs)')) + TIME = ('time', _('Time (mins)')) + + plan = models.ForeignKey( + PaymentPlan, on_delete=models.CASCADE, related_name='features', verbose_name=_('Payment Plan') + ) + name = models.CharField(max_length=30, verbose_name=_('Name')) + quantity = models.PositiveSmallIntegerField(default=1, verbose_name=_('Quantity')) + category = models.ForeignKey( + ModelCategory, + on_delete=models.CASCADE, + related_name='payment_plan_feature', + verbose_name=_('Category'), + ) + measurement_unit = models.CharField( + max_length=15, + choices=MeasurementUnitChoices.choices, + default=MeasurementUnitChoices.TEXT_PAGE, + verbose_name=_('Measurement unit'), + ) + + def __str__(self) -> str: + return self.name + + class Meta(OrderedModel.Meta): + verbose_name = _('Payment plan feature') + verbose_name_plural = _('Payment plan features') @@ -1,6 +1,8 @@ import calendar + from datetime import timedelta, date from decimal import Decimal +from collections import defaultdict from dateutil.relativedelta import relativedelta from django.db.models import Sum, F, Value, Func, CharField @@ -9,11 +11,14 @@ from django.utils.translation import gettext as _ from ninja import Router, Query from ninja.errors import HttpError -from authentication.security import SyncAuthBearer -from payments.models import Invoice, Payment +from authentication.security import SyncAuthBearer, AsyncAuthBearer +from payments.models import Invoice, Payment, PaymentPlan from payments.schema import UserBalance -from payments.schemas import ExpensesParamsSchema, ExpensesSchema +from payments.schemas import ExpensesParamsSchema, ExpensesSchema, PaymentPlanSchema, NewSubscriptionSchema, \ + PaymentLinkSchema from payments.selectors.payment_plan_selector import PaymentPlanSelector +from payments.services.payment_plan_service import PaymentPlanService +from payments.services.payment_service import PaymentService from payments.typing import IntervalStrategyEnum, SourceStrategyEnum router = Router(auth=SyncAuthBearer(), tags=['payments']) @@ -75,3 +80,63 @@ def list_expenses(request, data: ExpensesParamsSchema = Query(...)): return qs except Exception as exc: raise HttpError(400, f'{exc}') + + +@router.get('plans', tags=['payments/plans'], auth=AsyncAuthBearer(), response=list[PaymentPlanSchema]) +async def list_payment_plans(request): + try: + if request.auth.account_type not in {'regular', 'business_host'}: + raise HttpError(401, 'Unauthorized') + is_corporate = request.auth.account_type == 'business_host' + + plans = PaymentPlan.objects.filter( + price__gt=0, is_corporate=is_corporate, is_visible=True + ).prefetch_related('features__category', 'accessed_models') + + result = [] + async for plan in plans: + grouped = defaultdict(list) + for feature in plan.features.all(): + grouped[feature.category.title].append( + { + 'name': feature.name, + 'quantity': feature.quantity, + 'measurement_unit': feature.measurement_unit, + } + ) + result.append( + PaymentPlanSchema( + uid=plan.uid, + title=plan.title, + price=plan.price, + tokens_per_plan=plan.tokens_per_plan, + duration=plan.duration, + grouped_features=[{'name': cat, 'features': feats} for cat, feats in grouped.items()], + accessed_models=[model.slug for model in plan.accessed_models.all()], + ) + ) + return result + except Exception as exc: + raise HttpError(400, f'{exc}') + + +@router.post('plans', tags=['payments/plans'], auth=AsyncAuthBearer(), response=PaymentLinkSchema) +async def create_payment_link(request, body: NewSubscriptionSchema): + try: + if request.auth.account_type not in {'regular', 'business_host'}: + raise HttpError(401, 'Unauthorized') + payment_plan = await PaymentPlan.objects.aget(uid=body.uid) + payment_url = await PaymentService(request.auth).create_payment_link(payment_plan) + return PaymentLinkSchema(payment_url=payment_url) + except Exception as exc: + raise HttpError(400, f'{exc}') + + +@router.delete('plans', tags=['payments/plans'], auth=AsyncAuthBearer(), response={200: None, 400: str}) +async def cancel_plan_subscription(request): + try: + if request.auth.account_type not in {'regular', 'business_host'}: + raise HttpError(401, 'Unauthorized') + await PaymentPlanService(request.auth).cancel_payment_plan() + except Exception as exc: + raise HttpError(400, f'{exc}') @@ -1,19 +1,11 @@ import logging from decimal import Decimal -from typing import Literal -from uuid import UUID - -from django.db.models import Q -from django.utils.translation import gettext_lazy as _ from authentication.models.choices import InvitationStatus from authentication.models.user import CustomUserModel from authentication.selectors.user_selector import UserSelector from payments.models.payment_plan import PaymentPlan -from payments.serializers import ( - PaymentPlanSerializer, - UserPaymentPlanSerializer, -) +from payments.serializers import UserPaymentPlanSerializer logger = logging.getLogger(__name__) @@ -22,42 +14,6 @@ class PaymentPlanSelector: def __init__(self, user: CustomUserModel): self.user = user - def get_payment_plans( - self, - plan_duration: Literal['month', 'year'] | None = None, - serialize: bool = True, - ): - account_type = UserSelector(self.user).check_account_type() - if account_type == 'business_account': - raise Exception(_('Business accounts are not allowed to make purchases')) - - is_corporate = False if account_type == 'regular' else True - - plans = PaymentPlan.objects.filter( - ~Q(price=Decimal('0')) & Q(is_corporate=is_corporate) & Q(is_visible=True) - ).order_by('price') - - if plan_duration: - plans = plans.filter(duration=plan_duration) - - if serialize: - return PaymentPlanSerializer(plans, many=True) - - return plans - - def get_payment_plan_by_id(self, plan_id: UUID) -> PaymentPlan: - logger.info('START %s' % self.get_payment_plan_by_id.__name__) - plan = PaymentPlan.objects.get(uid=plan_id) - logger.info('PLAN: %s' % plan) - return plan - - def get_plan_detail_by_id(self, **kwargs) -> PaymentPlanSerializer: - plan = self.get_payment_plan_by_id(UUID(kwargs.get('id'))) - if plan is None: - raise Exception(_('No plan by this uid')) - - return PaymentPlanSerializer(plan) - def get_current_balance(self) -> Decimal: if ( self.user.account_type in ('business_account', 'business_admin', 'business_security') @@ -1,6 +1,7 @@ import logging from decimal import Decimal +from asgiref.sync import sync_to_async from rest_framework.request import Request from authentication.models import CustomUserModel @@ -9,7 +10,7 @@ from payments.selectors.payment_plan_selector import PaymentPlanSelector from payments.selectors.recurrent_payment_selector import ( RecurrentPaymentSelector, ) -from payments.serializers import PaymentLinkSerializer, SuccessPaymentResult +from payments.serializers import SuccessPaymentResult from payments.services.model_billing_service import ModelBillingService from payments.services.payment_service import PaymentService from payments.services.recurrent_payment_service import RecurrentPaymentService @@ -37,14 +38,6 @@ class PaymentPlanService: if payment.status == payment.SUCCEEDED: cls(payment.user).subscribe_user_to_plan(payment.plan) - def create_payment_plan_invoice(self, payment_plan_uid: str): - """""" - payment_plan = PaymentPlanSelector(self.user).get_payment_plan_by_id(payment_plan_uid) - resulting_link = self.payment_service.create_payment_link(plan=payment_plan) - result = PaymentLinkSerializer(data={'payment_url': resulting_link}) - result.is_valid(raise_exception=True) - return result - def handle_recurrent(self, plan: PaymentPlanUserInfo): current = RecurrentPaymentSelector.from_user_id(self.user.uid) if current is None: @@ -67,13 +60,12 @@ class PaymentPlanService: plan_info.current_token_balance += plan.tokens_per_plan plan_info.save() - def cancel_payment_plan(self): + async def cancel_payment_plan(self): plan_info: PaymentPlanUserInfo = self.user.payment_plan - task = plan_info.plan_schedule - if task is not None: - RecurrentPaymentService(task).delete() - plan_info.plan = PaymentPlanSelector(self.user).get_free_plan(corporate=self.user.is_corporate()) - plan_info.save() + plan_info.plan = await sync_to_async(PaymentPlanSelector(self.user).get_free_plan)( + corporate=self.user.is_corporate() + ) + await plan_info.asave() def update_per_token_plan_details(self, payment_amount: Decimal, model=None): ModelBillingService(self.user).charge(payment_amount) @@ -26,7 +26,7 @@ class PaymentService: def __init__(self, user: CustomUserModel): self.user = user - def create_payment_link(self, plan: PaymentPlan) -> str: + async def create_payment_link(self, plan: PaymentPlan) -> str: receipt_data = { 'customer': {'email': self.user.email}, 'items': [ @@ -5,6 +5,7 @@ from django.db import models from django.db.models import Count, Sum from django.db.models.functions import Coalesce from django.utils.translation import gettext_lazy as _ +from ordered_model.admin import OrderedTabularInline from authentication.admin import CustomUserModelAdmin from payments.models import ( @@ -15,6 +16,7 @@ from payments.models import ( PromoCode, PromoCodeActivation, UserPaymentMethod, + PaymentPlanFeature, ) from payments.models.referral_account import ReferralAccount, ReferralInvite @@ -39,6 +41,12 @@ class PaymentAdmin(admin.ModelAdmin): return str(obj.user) +class PaymentPlanFeatureInline(OrderedTabularInline): + model = PaymentPlanFeature + extra = 1 + ordering = ('order',) + + @admin.register(PaymentPlan) class PaymentPlanAdmin(admin.ModelAdmin): list_display = [ @@ -51,6 +59,7 @@ class PaymentPlanAdmin(admin.ModelAdmin): 'is_visible', ] search_fields = ['price', 'tokens_per_plan'] + inlines = [PaymentPlanFeatureInline] filter_horizontal = ('accessed_models',) @@ -3,25 +3,39 @@ from typing import List, Optional from uuid import UUID from ninja import Schema, ModelSchema, Query -from pydantic import field_validator, condecimal, field_serializer +from pydantic import condecimal, field_serializer from payments.typing import IntervalStrategyEnum, SourceStrategyEnum from payments.models import PromoCode +class PaymentPlanFeatureSchema(Schema): + name: str + quantity: int + measurement_unit: str + + +class GroupedPlanFeatureSchema(Schema): + name: str + features: List[PaymentPlanFeatureSchema] + + class PaymentPlanSchema(Schema): uid: UUID title: str price: condecimal(max_digits=10, decimal_places=2) tokens_per_plan: condecimal(max_digits=10, decimal_places=2) duration: str - points: List - accessed_models: List[str] + grouped_features: list[GroupedPlanFeatureSchema] = [] + accessed_models: list[str] = [] + + +class NewSubscriptionSchema(Schema): + uid: UUID + - @field_validator('accessed_models', mode='before') - @classmethod - def get_slugs(cls, value: str) -> List[str]: - return [obj.slug for obj in value] +class PaymentLinkSchema(Schema): + payment_url: str class UserPlanDetailSchema(Schema): @@ -56,4 +70,4 @@ class ExpensesParamsSchema(Schema): class ExpensesSchema(Schema): source: str - amount: condecimal(max_digits=10, decimal_places=2) + amount: condecimal(max_digits=10, decimal_places=2) \ No newline at end of file @@ -12,7 +12,6 @@ class PaymentPlanSerializer(serializers.Serializer): price = serializers.DecimalField(max_digits=10, decimal_places=2) tokens_per_plan = serializers.DecimalField(max_digits=50, decimal_places=2) duration = serializers.CharField(read_only=True) - points = serializers.ListField(read_only=True) accessed_models = serializers.SlugRelatedField( slug_field='slug', queryset=NeuronModel.objects.all(), many=True ) @@ -53,15 +52,6 @@ class UserPlanDetailSerializer(serializers.Serializer): current_token_balance = serializers.IntegerField() -class NewSubsriptionSerializer(serializers.Serializer): - uid = serializers.UUIDField() - is_test = serializers.IntegerField() - - -class PaymentLinkSerializer(serializers.Serializer): - payment_url = serializers.URLField() - - class SuccessPaymentResult(serializers.Serializer): id = serializers.UUIDField() @@ -10,7 +10,6 @@ urlpatterns = [ name='referral-account', ), path('invoices', views.InvoicesAPIView.as_view(), name='invoices'), - path('plans', views.PaymentPlanAPIView.as_view(), name='payment-plans'), path( 'methods', views.PaymentMethodsAPIView.as_view(), @@ -17,10 +17,8 @@ from rest_framework.views import APIView from authentication.permissions import IsAnonymous, IsTelegramAirBot from authentication.selectors.user_selector import UserSelector -from payments.exceptions.PlanIsFree import PlanIsFree from payments.exceptions.payer_not_found import PayerNotFound from payments.models import Invoice -from payments.models.payment import PaymentPlan from payments.permissions import IsAllowedToPay from payments.selectors.payment_method_selector import PaymentMethodSelector from payments.selectors.payment_plan_selector import PaymentPlanSelector @@ -28,9 +26,6 @@ from payments.selectors.payment_selector import PaymentSelector from payments.serializers import ( DeletePaymentMethodSerializer, InvoiceSerializer, - NewSubsriptionSerializer, - PaymentLinkSerializer, - PaymentPlanSerializer, ReferralAccountSerializer, ) from payments.services.payment_method_service import PaymentMethodService @@ -54,56 +49,6 @@ class PaymentAPIView(APIView): return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) -class PaymentPlanAPIView(APIView): - permission_classes = (IsAuthenticated, IsAllowedToPay) - - @extend_schema( - parameters=[OpenApiParameter('duration', str, enum=[PaymentPlan.MONTH, PaymentPlan.YEAR])], - responses={200: PaymentPlanSerializer}, - ) - def get(self, request: Request, *args, **kwargs): - """List available payment plans""" - try: - plan_duration = request.query_params.get('duration', None) - result = PaymentPlanSelector(self.request.user).get_payment_plans(plan_duration=plan_duration) - return Response(result.data, status=status.HTTP_200_OK) - except Exception as err: - logger.exception(err) - return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) - - @extend_schema( - request=NewSubsriptionSerializer, - responses={200: PaymentLinkSerializer}, - ) - def post(self, request, *args, **kwargs): - """Create new payment link for chosen Payment Plan""" - try: - payment_plan = NewSubsriptionSerializer(data=request.data) - payment_plan.is_valid() - result = PaymentPlanService(self.request.user).create_payment_plan_invoice( - payment_plan_uid=payment_plan.validated_data['uid'] - ) - return Response(result.validated_data, status=status.HTTP_200_OK) - except Exception as err: - logger.exception(err) - return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, *args, **kwargs): - """Cancel subscription (Payment Plan)""" - try: - PaymentPlanService(self.request.user).cancel_payment_plan() - return Response(status=status.HTTP_200_OK) - except PlanIsFree as err: - logger.exception(err) - return Response( - {'detail': 'Free plan cannot be cancelled'}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as err: - logger.exception(err) - return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) - - class PaymentMethodsAPIView(APIView): permission_classes = (IsAuthenticated, IsAllowedToPay) @@ -137,28 +82,6 @@ class PaymentMethodsAPIView(APIView): return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) -class UserPlanAPIView(APIView): - permission_classes = (IsAuthenticated,) - - def get(self, request, *args, **kwargs): - """Get user balance""" - try: - result = PaymentPlanSelector(self.request.user).get_current_balance() - return Response({'current_token_balance': result}, status=status.HTTP_200_OK) - except Exception as err: - logger.exception(err) - return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, *args, **kwargs): - """Cancel user paid Payment Plan (drop to free subscription)""" - try: - PaymentPlanService(request.user).cancel_payment_plan() - return Response({'ok': True}, status=status.HTTP_200_OK) - except Exception as err: - logger.exception(err) - return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) - - class PaymentConfirmationAPIView(APIView): permission_classes = (IsAnonymous,)