diff --git a/src/api/subscription.ts b/src/api/subscription.ts index bc2791e..cf0e26a 100644 --- a/src/api/subscription.ts +++ b/src/api/subscription.ts @@ -423,6 +423,21 @@ export const subscriptionApi = { return response.data; }, + /** + * Оформление подписки на тариф привязкой Lava: первое списание оплачивается + * по возвращённой ссылке и активирует подписку. + */ + purchaseWithLavaRecurring: async ( + tariffId: number, + ): Promise<{ status: string; redirect_url: string | null; subscription_id: number }> => { + const response = await apiClient.post( + '/cabinet/subscription/lava-recurrent/purchase', + {}, + { params: { tariff_id: tariffId } }, + ); + return response.data; + }, + // ── Trial ─────────────────────────────────────────────────────────── getTrialInfo: async (): Promise => { diff --git a/src/api/tariffs.ts b/src/api/tariffs.ts index ad0771d..d26a990 100644 --- a/src/api/tariffs.ts +++ b/src/api/tariffs.ts @@ -35,6 +35,8 @@ export interface TariffListItem { show_in_gift: boolean; is_daily: boolean; daily_price_kopeks: number; + /** UUID продукта Lava для рекуррентных подписок (цена/период заданы в кабинете Lava) */ + lava_product_id?: string | null; traffic_limit_gb: number; device_limit: number; tier_level: number; @@ -85,6 +87,8 @@ export interface TariffDetail { // Дневной тариф is_daily: boolean; daily_price_kopeks: number; + /** UUID продукта Lava для рекуррентных подписок (цена/период заданы в кабинете Lava) */ + lava_product_id?: string | null; // Режим сброса трафика traffic_reset_mode: string | null; // 'DAY', 'WEEK', 'MONTH', 'MONTH_ROLLING', 'NO_RESET', null = глобальная настройка // Внешний сквад Remnawave @@ -124,6 +128,8 @@ export interface TariffCreateRequest { // Дневной тариф is_daily?: boolean; daily_price_kopeks?: number; + // Автопродление Lava: продукт из кабинета Lava + lava_product_id?: string | null; // Режим сброса трафика traffic_reset_mode?: string | null; // Внешний сквад Remnawave @@ -168,6 +174,8 @@ export interface TariffUpdateRequest { // Дневной тариф is_daily?: boolean; daily_price_kopeks?: number; + // Автопродление Lava: продукт из кабинета Lava + lava_product_id?: string | null; // Режим сброса трафика traffic_reset_mode?: string | null; // Внешний сквад Remnawave diff --git a/src/components/subscription/purchase/TariffPurchaseForm.tsx b/src/components/subscription/purchase/TariffPurchaseForm.tsx index a234f11..5dad89c 100644 --- a/src/components/subscription/purchase/TariffPurchaseForm.tsx +++ b/src/components/subscription/purchase/TariffPurchaseForm.tsx @@ -36,6 +36,8 @@ export interface TariffPurchaseFormProps { balanceKopeks: number | undefined; /** СБП-оформление (Platega recurrent) доступно — показать вторую CTA. */ sbpPurchaseEnabled?: boolean; + /** Оформление привязкой Lava доступно — показать вторую CTA. */ + lavaPurchaseEnabled?: boolean; onBack: () => void; } @@ -44,6 +46,7 @@ export function TariffPurchaseForm({ subscriptionId, balanceKopeks, sbpPurchaseEnabled = false, + lavaPurchaseEnabled = false, onBack, }: TariffPurchaseFormProps) { const { t } = useTranslation(); @@ -143,6 +146,47 @@ export function TariffPurchaseForm({ ); + const lavaPurchaseMutation = useMutation({ + mutationFn: () => subscriptionApi.purchaseWithLavaRecurring(tariff.id), + onSuccess: (data) => { + if (data.redirect_url) { + openPaymentUrl(data.redirect_url, platform, openLink); + } + queryClient.invalidateQueries({ queryKey: ['subscription'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); + queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] }); + queryClient.invalidateQueries({ queryKey: ['lava-recurring', data.subscription_id] }); + navigate('/subscriptions', { replace: true }); + }, + }); + + const lavaPurchaseButton = lavaPurchaseEnabled && ( + <> + +
+ {t('subscription.lavaRecurring.purchaseHint')} +
+ {lavaPurchaseMutation.isError && ( +
+ {getErrorMessage(lavaPurchaseMutation.error)} +
+ )} + + ); + // Smooth scroll the form into view when first mounted. useEffect(() => { if (ref.current) { @@ -241,6 +285,7 @@ export function TariffPurchaseForm({ {sbpPurchaseButton} + {lavaPurchaseButton} {purchaseMutation.isError && !getInsufficientBalanceError(purchaseMutation.error) && ( @@ -666,6 +711,7 @@ export function TariffPurchaseForm({ {sbpPurchaseButton} + {lavaPurchaseButton} ); })()} diff --git a/src/locales/en.json b/src/locales/en.json index 76f3ca3..4f244b5 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -724,7 +724,9 @@ "halfYear": "every six months", "year": "yearly" }, - "autopayHint": "The subscription will renew automatically via Lava" + "autopayHint": "The subscription will renew automatically via Lava", + "purchaseButton": "⚡ Subscribe with Lava auto-renewal", + "purchaseHint": "The first invoice is paid via a Lava link; renewals are then charged automatically on the product’s schedule." }, "backToList": "Back to subscriptions", "confirmDelete": "Delete subscription?", @@ -2765,7 +2767,9 @@ "periodsRequired": "Add at least one period", "dailyPriceRequired": "Enter a price per day greater than 0", "trafficPackagesRequired": "Add at least one traffic package (or disable traffic topup)" - } + }, + "lavaProductLabel": "Lava product (auto-renewal)", + "lavaProductDesc": "Product UUID from the Lava dashboard: price and charge period are configured there. Empty — auto-renewal is unavailable for this tariff." }, "servers": { "title": "Squad Management", diff --git a/src/locales/fa.json b/src/locales/fa.json index aa8a8ea..9d9f8e4 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -623,7 +623,9 @@ "halfYear": "هر شش ماه", "year": "سالانه" }, - "autopayHint": "اشتراک به‌طور خودکار از طریق Lava تمدید می‌شود" + "autopayHint": "اشتراک به‌طور خودکار از طریق Lava تمدید می‌شود", + "purchaseButton": "⚡ ثبت‌نام با تمدید خودکار Lava", + "purchaseHint": "صورتحساب اول از طریق لینک Lava پرداخت می‌شود؛ سپس تمدیدها بر اساس دوره محصول به‌طور خودکار کسر می‌شوند." }, "backToList": "بازگشت به فهرست اشتراک‌ها", "confirmDelete": "اشتراک حذف شود؟", @@ -2303,7 +2305,9 @@ "periodsRequired": "حداقل یک دوره اضافه کنید", "dailyPriceRequired": "قیمت روزانه بزرگتر از 0 را وارد کنید", "trafficPackagesRequired": "حداقل یک بسته ترافیک اضافه کنید (یا خرید ترافیک را غیرفعال کنید)" - } + }, + "lavaProductLabel": "محصول Lava (تمدید خودکار)", + "lavaProductDesc": "شناسه UUID محصول از پنل Lava: قیمت و دوره کسر در آنجا تنظیم می‌شود. خالی — تمدید خودکار برای این تعرفه در دسترس نیست." }, "servers": { "title": "مدیریت اسکوادها", diff --git a/src/locales/ru.json b/src/locales/ru.json index 9696933..07d9a0b 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -743,7 +743,9 @@ "halfYear": "раз в полгода", "year": "ежегодно" }, - "autopayHint": "Подписка будет автоматически продлеваться через Lava" + "autopayHint": "Подписка будет автоматически продлеваться через Lava", + "purchaseButton": "⚡ Оформить с автооплатой Lava", + "purchaseHint": "Первое списание оплачивается по ссылке Lava; дальше продления списываются автоматически по периоду продукта." }, "backToList": "К списку подписок", "confirmDelete": "Удалить подписку?", @@ -3154,7 +3156,9 @@ "periodsRequired": "Добавьте хотя бы один период", "dailyPriceRequired": "Укажите цену за день больше 0", "trafficPackagesRequired": "Добавьте хотя бы один пакет трафика (или отключите докупку)" - } + }, + "lavaProductLabel": "Продукт Lava (автопродление)", + "lavaProductDesc": "UUID продукта из кабинета Lava: цена и периодичность списаний задаются там. Пусто — автопродление для тарифа недоступно." }, "servers": { "title": "Управление сквадами", diff --git a/src/locales/zh.json b/src/locales/zh.json index 6336f09..89b38e9 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -623,7 +623,9 @@ "halfYear": "每半年", "year": "每年" }, - "autopayHint": "订阅将通过 Lava 自动续订" + "autopayHint": "订阅将通过 Lava 自动续订", + "purchaseButton": "⚡ 使用 Lava 自动续订下单", + "purchaseHint": "首期账单通过 Lava 链接支付;之后按产品周期自动扣款续订。" }, "backToList": "返回订阅列表", "confirmDelete": "删除订阅?", @@ -2369,7 +2371,9 @@ "periodsRequired": "请添加至少一个期限", "dailyPriceRequired": "请输入大于0的每日价格", "trafficPackagesRequired": "请添加至少一个流量包(或关闭流量购买)" - } + }, + "lavaProductLabel": "Lava 产品(自动续订)", + "lavaProductDesc": "来自 Lava 后台的产品 UUID:价格与扣款周期在那里设置。留空则该套餐不支持自动续订。" }, "servers": { "title": "服务器管理", diff --git a/src/pages/AdminTariffCreate.tsx b/src/pages/AdminTariffCreate.tsx index f7203d2..5c1143d 100644 --- a/src/pages/AdminTariffCreate.tsx +++ b/src/pages/AdminTariffCreate.tsx @@ -50,6 +50,7 @@ export default function AdminTariffCreate() { const [selectedExternalSquad, setSelectedExternalSquad] = useState(null); const [selectedPromoGroups, setSelectedPromoGroups] = useState([]); const [dailyPriceKopeks, setDailyPriceKopeks] = useState(0); + const [lavaProductId, setLavaProductId] = useState(''); // Traffic topup const [trafficTopupEnabled, setTrafficTopupEnabled] = useState(false); @@ -122,6 +123,7 @@ export default function AdminTariffCreate() { data.promo_groups?.filter((pg) => pg.is_selected).map((pg) => pg.id) || [], ); setDailyPriceKopeks(data.daily_price_kopeks || 0); + setLavaProductId(data.lava_product_id || ''); setTrafficTopupEnabled(data.traffic_topup_enabled || false); setMaxTopupTrafficGb(data.max_topup_traffic_gb || 0); setTrafficTopupPackages(data.traffic_topup_packages || {}); @@ -176,6 +178,8 @@ export default function AdminTariffCreate() { max_topup_traffic_gb: toNumber(maxTopupTrafficGb), is_daily: isDaily, daily_price_kopeks: isDaily ? toNumber(dailyPriceKopeks) : 0, + // Пустая строка отвязывает тариф от продукта Lava + lava_product_id: lavaProductId.trim(), traffic_reset_mode: trafficResetMode, }; @@ -460,6 +464,25 @@ export default function AdminTariffCreate() { )} + {/* Lava recurrent product */} +
+ + setLavaProductId(e.target.value)} + className="input w-full" + placeholder="6be21df9-0bcd-44ac-9c2c-3be7bc94decc" + /> +

{t('admin.tariffs.lavaProductDesc')}

+
+ {/* Traffic Limit */}