mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 10:27:49 +00:00
feat(lava): покупка привязкой, продукт тарифа в админке, CTA
Фронтовые хвосты автопродления Lava (бэкенд: de3518b7). - Поле «Продукт Lava» в форме создания/редактирования тарифа: раньше lava_product_id можно было задать только через API, из-за чего фичу нельзя было включить из интерфейса. Пустая строка отвязывает тариф от продукта. - CTA «Оформить с автооплатой Lava» в форме покупки тарифа — рядом с СБП-кнопкой, в обеих ветках рендера; флаг lava_recurrent_enabled из purchase-options наконец получил потребителя. - API-метод purchaseWithLavaRecurring + типы lava_product_id в списке, детали и запросах тарифа. - Локали ru/en/zh/fa: подписи покупки и поля продукта (плейсхолдеры сверены).
This commit is contained in:
@@ -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<TrialInfo> => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => lavaPurchaseMutation.mutate()}
|
||||
disabled={lavaPurchaseMutation.isPending || purchaseMutation.isPending}
|
||||
className="mt-2 w-full rounded-xl border border-accent-500/40 bg-accent-500/10 py-3 text-sm font-medium text-accent-400 transition-colors hover:bg-accent-500/20 disabled:opacity-50"
|
||||
>
|
||||
{lavaPurchaseMutation.isPending ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
{t('common.loading')}
|
||||
</span>
|
||||
) : (
|
||||
t('subscription.lavaRecurring.purchaseButton')
|
||||
)}
|
||||
</button>
|
||||
<div className="mt-1.5 text-center text-[11px] text-dark-500">
|
||||
{t('subscription.lavaRecurring.purchaseHint')}
|
||||
</div>
|
||||
{lavaPurchaseMutation.isError && (
|
||||
<div className="mt-2 text-center text-sm text-error-400">
|
||||
{getErrorMessage(lavaPurchaseMutation.error)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
// Smooth scroll the form into view when first mounted.
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
@@ -241,6 +285,7 @@ export function TariffPurchaseForm({
|
||||
</button>
|
||||
|
||||
{sbpPurchaseButton}
|
||||
{lavaPurchaseButton}
|
||||
|
||||
{purchaseMutation.isError &&
|
||||
!getInsufficientBalanceError(purchaseMutation.error) && (
|
||||
@@ -666,6 +711,7 @@ export function TariffPurchaseForm({
|
||||
</button>
|
||||
|
||||
{sbpPurchaseButton}
|
||||
{lavaPurchaseButton}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "مدیریت اسکوادها",
|
||||
|
||||
@@ -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": "Управление сквадами",
|
||||
|
||||
@@ -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": "服务器管理",
|
||||
|
||||
@@ -50,6 +50,7 @@ export default function AdminTariffCreate() {
|
||||
const [selectedExternalSquad, setSelectedExternalSquad] = useState<string | null>(null);
|
||||
const [selectedPromoGroups, setSelectedPromoGroups] = useState<number[]>([]);
|
||||
const [dailyPriceKopeks, setDailyPriceKopeks] = useState<number | ''>(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() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lava recurrent product */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="tariff-lava-product"
|
||||
className="mb-2 block text-sm font-medium text-dark-300"
|
||||
>
|
||||
{t('admin.tariffs.lavaProductLabel')}
|
||||
</label>
|
||||
<input
|
||||
id="tariff-lava-product"
|
||||
type="text"
|
||||
value={lavaProductId}
|
||||
onChange={(e) => setLavaProductId(e.target.value)}
|
||||
className="input w-full"
|
||||
placeholder="6be21df9-0bcd-44ac-9c2c-3be7bc94decc"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-dark-500">{t('admin.tariffs.lavaProductDesc')}</p>
|
||||
</div>
|
||||
|
||||
{/* Traffic Limit */}
|
||||
<div>
|
||||
<label
|
||||
|
||||
@@ -292,6 +292,12 @@ export default function SubscriptionPurchase() {
|
||||
'platega_recurrent_enabled' in purchaseOptions &&
|
||||
purchaseOptions.platega_recurrent_enabled === true
|
||||
}
|
||||
lavaPurchaseEnabled={
|
||||
isTariffsMode &&
|
||||
purchaseOptions !== undefined &&
|
||||
'lava_recurrent_enabled' in purchaseOptions &&
|
||||
purchaseOptions.lava_recurrent_enabled === true
|
||||
}
|
||||
onBack={() => {
|
||||
setShowTariffPurchase(false);
|
||||
setSelectedTariff(null);
|
||||
|
||||
@@ -373,6 +373,7 @@ export interface TariffsPurchaseOptions {
|
||||
// СБП-оформление (Platega recurrent): показывать кнопку «Оформить с
|
||||
// автооплатой СБП» рядом с покупкой с баланса
|
||||
platega_recurrent_enabled?: boolean;
|
||||
lava_recurrent_enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ClassicPurchaseOptions {
|
||||
|
||||
Reference in New Issue
Block a user