@@ -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: 2026-04-30 14:11+0300\n" +"POT-Creation-Date: 2026-05-06 17:58+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1181,7 +1181,7 @@ msgstr "Последнее время платежа" msgid "Next payment at" msgstr "Следующее время платежа" -#: payments/models/payment_plan.py:63 payments/models/user_payment_method.py:17 +#: payments/models/payment_plan.py:63 payments/models/user_payment_method.py:25 msgid "Payment Method" msgstr "Платежный метод" @@ -1265,23 +1265,47 @@ msgstr "Активация Промокода" msgid "Promocode Activations" msgstr "Активации Промокодов" -#: payments/models/user_payment_method.py:8 -msgid "Payment method UID" -msgstr "UID платёжного метода" - #: payments/models/user_payment_method.py:9 -msgid "Card type" -msgstr "Тип карты" +msgid "Bank Card" +msgstr "Банковская карта" #: payments/models/user_payment_method.py:10 -msgid "Last four card digits" -msgstr "Последние 4 цифры карты" +msgid "Mir Pay" +msgstr "Mir Pay" #: payments/models/user_payment_method.py:11 +msgid "Sberbank" +msgstr "Сбербанк" + +#: payments/models/user_payment_method.py:12 +msgid "YooMoney" +msgstr "ЮMoney" + +#: payments/models/user_payment_method.py:13 +msgid "T-bank" +msgstr "Т-банк" + +#: payments/models/user_payment_method.py:14 +msgid "SBP" +msgstr "СБП" + +#: payments/models/user_payment_method.py:16 +msgid "Gateway" +msgstr "Шлюз" + +#: payments/models/user_payment_method.py:17 +msgid "Payment method UID" +msgstr "UID платёжного метода" + +#: payments/models/user_payment_method.py:18 tools/media/models.py:78 +msgid "Meta" +msgstr "Метаданные" + +#: payments/models/user_payment_method.py:19 msgid "Attempts" msgstr "Попытки" -#: payments/models/user_payment_method.py:18 +#: payments/models/user_payment_method.py:26 msgid "Payment Methods" msgstr "Платежные методы" @@ -1355,7 +1379,7 @@ msgstr "Публичный API" msgid "Media" msgstr "Медиа" -#: tools/chats/apis.py:201 tools/media/apis.py:205 +#: tools/chats/apis.py:201 tools/media/apis.py:207 #: tools/public_api/views/base.py:100 msgid "" "An unexpected generation error has occurred. Please try again later or use a " @@ -1377,12 +1401,12 @@ msgstr "Чат %(id)s" msgid "Chat" msgstr "Чат" -#: tools/media/apis.py:174 +#: tools/media/apis.py:175 msgid "" "Temporary issues with the service, we are already working on a solution." msgstr "Временные неполадки с сервисом, мы уже работаем над их решением." -#: tools/media/apis.py:252 tools/public_api/views/ml_service.py:63 +#: tools/media/apis.py:254 tools/public_api/views/ml_service.py:63 #: tools/public_api/views/ml_service.py:107 msgid "Voice not found." msgstr "Голос не найден." @@ -1411,10 +1435,6 @@ msgstr "Тип" msgid "File" msgstr "Файл" -#: tools/media/models.py:78 -msgid "Meta" -msgstr "Метаданные" - #: tools/media/models.py:84 msgid "Preset" msgstr "Пресет" @@ -1515,6 +1535,12 @@ msgstr "Название голоса успешно обновлено" msgid "Preset voices are shared and cannot be deleted. Use your own voice id." msgstr "Пресеты общие для всех — их нельзя удалить. Укажите id своего голоса." +#~ msgid "Card type" +#~ msgstr "Тип карты" + +#~ msgid "Last four card digits" +#~ msgstr "Последние 4 цифры карты" + #~ msgid "" #~ "Error occured when create generation. It may cause NSFW-content not " #~ "allowed, retry again" @@ -0,0 +1,18 @@ +# Generated by Django 5.0.11 on 2026-05-06 13:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ml_model', '0053_alter_modelcategory_options_modelcategory_order'), + ] + + operations = [ + migrations.AlterField( + model_name='modelinput', + name='type', + field=models.CharField(choices=[('text', 'Text'), ('image', 'Image'), ('pdf', 'PDF'), ('docx', 'DOCX'), ('doc', 'DOC'), ('txt', 'Text File (Notebook)'), ('zip', 'ZIP Archive'), ('audio', 'Audio'), ('video', 'Video')], default='text', max_length=32, verbose_name='Type'), + ), + ] @@ -0,0 +1,44 @@ +# Generated by Django 5.0.11 on 2026-05-06 13:35 + +from django.db import migrations, models + +def fill_metadata(apps, schema_editor): + PaymentMethod = apps.get_model('payments', 'PaymentMethod') + payment_methods = [] + + for payment_method in PaymentMethod.objects.all(): + payment_method.metadata = {'card_type': payment_method.card_type, 'last4': payment_method.last_four} + payment_methods.append(payment_method) + + PaymentMethod.objects.bulk_update(payment_methods, fields=['metadata']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('payments', '0028_paymentplanuserinfo_referral_balance'), + ] + + operations = [ + migrations.AddField( + model_name='paymentmethod', + name='gateway', + field=models.CharField(choices=[('bank_card', 'Bank Card'), ('mir_pay', 'Mir Pay'), ('sberbank', 'Sberbank'), ('yoo_money', 'YooMoney'), ('tinkoff_bank', 'T-bank'), ('sbp', 'SBP')], default='bank_card', max_length=20, verbose_name='Gateway'), + preserve_default=False, + ), + migrations.AddField( + model_name='paymentmethod', + name='metadata', + field=models.JSONField(default=dict, verbose_name='Meta'), + preserve_default=False, + ), + migrations.RunPython(fill_metadata, migrations.RunPython.noop), + migrations.RemoveField( + model_name='paymentmethod', + name='card_type', + ), + migrations.RemoveField( + model_name='paymentmethod', + name='last_four', + ), + ] @@ -5,13 +5,21 @@ from core.models import BaseModel class PaymentMethod(BaseModel): + class GatewayChoices(models.TextChoices): + BANK_CARD = ('bank_card', _('Bank Card')) + MIR_PAY = ('mir_pay', _('Mir Pay')) + SBERBANK = ('sberbank', _('Sberbank')) + YOOMONEY = ('yoo_money', _('YooMoney')) + T_BANK = ('tinkoff_bank', _('T-bank')) + SBP = ('sbp', _('SBP')) + + gateway = models.CharField(max_length=20, choices=GatewayChoices.choices, verbose_name=_('Gateway')) payment_method_id = models.UUIDField(unique=True, verbose_name=_('Payment method UID')) - card_type = models.CharField(max_length=15, verbose_name=_('Card type')) - last_four = models.CharField(max_length=4, verbose_name=_('Last four card digits')) + metadata = models.JSONField(verbose_name=_('Meta')) attempts = models.PositiveSmallIntegerField(default=0, verbose_name=_('Attempts')) def __str__(self) -> str: - return f'Card {self.card_type} ****{self.last_four}' + return f'{self.gateway} ({self.payment_method_id})' class Meta: verbose_name = _('Payment Method') @@ -62,8 +62,8 @@ def get_user_balance(request): async def handle_yookassa_webhook(request): data = orjson.loads(request.body) logger.info('YooKassa webhook received: payment_id=%s', data['object']['id']) - payment = await PaymentService.handle_payment(data['object']['id']) try: + payment = await PaymentService.handle_payment(data['object']['id']) payer = await CustomUserModel.objects.prefetch_related('payment_plan', 'payment_plan__plan', 'payment_plan__method').aget( uid=payment.description ) @@ -239,7 +239,7 @@ async def handle_gitlab_webhook(request): updated_count = await PaymentPlanUserInfo.objects.filter( plan__price__gt=0, plan__individual=False, - ).aupdate(next_payment_at=None if not is_active else (timezone.now() + relativedelta(months=1))) + ).aupdate(next_payment_at=None if not is_active else (timezone.now() + timedelta(days=30))) logger.info( 'Recurring feature flag synced: active=%s updated_subscriptions=%s', is_active, @@ -1,5 +1,4 @@ import logging -from uuid import UUID from authentication.models import CustomUserModel from payments.models.user_payment_method import PaymentMethod @@ -12,16 +11,35 @@ class PaymentMethodService: def __init__(self, user: CustomUserModel) -> None: self.user = user - def add_payment_method(self, method_id: UUID, card_type: str, last_four: str): + def add_payment_method(self, yookassa_payment_method): + gateway = yookassa_payment_method.type + if yookassa_payment_method.type in ('bank_card', 'sberbank', 'tinkoff_bank'): + metadata = { + 'card_type': yookassa_payment_method.card.card_type, + 'last4': yookassa_payment_method.card.last4, + } + if source := getattr(yookassa_payment_method.card, 'source', None): + gateway = source + elif yookassa_payment_method.type == 'yoo_money': + metadata = {'account_number': yookassa_payment_method.account_number} + elif yookassa_payment_method.type == 'sbp': + metadata = {'sbp_operation_id': yookassa_payment_method.sbp_operation_id} + else: + metadata = {} payment_method, created = PaymentMethod.objects.update_or_create( user_plan_info__user=self.user, - defaults={'payment_method_id': method_id, 'last_four': last_four, 'card_type': card_type, 'attempts': 0}, + defaults={ + 'gateway': gateway, + 'payment_method_id': yookassa_payment_method.id, + 'metadata': metadata, + 'attempts': 0, + }, ) logger.info( 'Payment method saved: email=%s method_uid=%s payment_method_id=%s created=%s', self.user.email, payment_method.uid, - method_id, + yookassa_payment_method.id, created, ) return payment_method @@ -1,10 +1,10 @@ import logging +from datetime import timedelta from decimal import Decimal from uuid import UUID, uuid4 from asgiref.sync import sync_to_async -from dateutil.relativedelta import relativedelta from django.conf import settings from django.db import transaction from django.utils import timezone @@ -14,7 +14,6 @@ from yookassa.domain.response import PaymentResponse as YookassaPaymentResponse from authentication.models import CustomUserModel from lib.unleash.client import web_client -from payments.exceptions.full_balance import FullBalanceException from payments.models.payment import Payment as PaymentModel from payments.models.payment_plan import PaymentPlan, PaymentPlanUserInfo from payments.services.payment_method_service import PaymentMethodService @@ -49,7 +48,6 @@ class PaymentService: ) payment_data = { 'amount': {'value': f'{plan.price}', 'currency': 'RUB'}, - 'payment_method_data': {'type': 'bank_card'}, 'receipt': receipt_data, 'confirmation': { 'type': 'redirect', @@ -118,12 +116,9 @@ class PaymentService: and web_client.get_flag_state('recurring_payments', self.user.email) and not plan.individual ): - card = payment.payment_method.card - payment_method = PaymentMethodService(self.user).add_payment_method( - payment.payment_method.id, card.card_type, card.last4 - ) + payment_method = PaymentMethodService(self.user).add_payment_method(payment.payment_method) PaymentPlanUserInfo.objects.filter(user=self.user).update( - method=payment_method, next_payment_at=timezone.now() + relativedelta(months=1) + method=payment_method, next_payment_at=timezone.now() + timedelta(days=30) ) logger.info( 'Recurring payment method saved: email=%s method_uid=%s next_payment_at_set=true', @@ -133,7 +128,7 @@ class PaymentService: else: if web_client.get_flag_state('recurring_payments', self.user.email) and not plan.individual: PaymentPlanUserInfo.objects.filter(user=self.user).update( - next_payment_at=timezone.now() + relativedelta(months=1) + next_payment_at=timezone.now() + timedelta(days=30) ) logger.info( 'Recurring schedule updated without saved method: email=%s next_payment_at_set=true', @@ -75,7 +75,7 @@ class PaymentPlanFeatureAdmin(OrderedModelAdmin): @admin.register(PaymentMethod) class PaymentMethodAdmin(admin.ModelAdmin): - list_display = ['payment_method_id', 'card_type', 'last_four', 'attempts'] + list_display = ['gateway', 'payment_method_id', 'attempts'] @admin.register(Invoice)