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;
|
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 ───────────────────────────────────────────────────────────
|
// ── Trial ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
getTrialInfo: async (): Promise<TrialInfo> => {
|
getTrialInfo: async (): Promise<TrialInfo> => {
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export interface TariffListItem {
|
|||||||
show_in_gift: boolean;
|
show_in_gift: boolean;
|
||||||
is_daily: boolean;
|
is_daily: boolean;
|
||||||
daily_price_kopeks: number;
|
daily_price_kopeks: number;
|
||||||
|
/** UUID продукта Lava для рекуррентных подписок (цена/период заданы в кабинете Lava) */
|
||||||
|
lava_product_id?: string | null;
|
||||||
traffic_limit_gb: number;
|
traffic_limit_gb: number;
|
||||||
device_limit: number;
|
device_limit: number;
|
||||||
tier_level: number;
|
tier_level: number;
|
||||||
@@ -85,6 +87,8 @@ export interface TariffDetail {
|
|||||||
// Дневной тариф
|
// Дневной тариф
|
||||||
is_daily: boolean;
|
is_daily: boolean;
|
||||||
daily_price_kopeks: number;
|
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 = глобальная настройка
|
traffic_reset_mode: string | null; // 'DAY', 'WEEK', 'MONTH', 'MONTH_ROLLING', 'NO_RESET', null = глобальная настройка
|
||||||
// Внешний сквад Remnawave
|
// Внешний сквад Remnawave
|
||||||
@@ -124,6 +128,8 @@ export interface TariffCreateRequest {
|
|||||||
// Дневной тариф
|
// Дневной тариф
|
||||||
is_daily?: boolean;
|
is_daily?: boolean;
|
||||||
daily_price_kopeks?: number;
|
daily_price_kopeks?: number;
|
||||||
|
// Автопродление Lava: продукт из кабинета Lava
|
||||||
|
lava_product_id?: string | null;
|
||||||
// Режим сброса трафика
|
// Режим сброса трафика
|
||||||
traffic_reset_mode?: string | null;
|
traffic_reset_mode?: string | null;
|
||||||
// Внешний сквад Remnawave
|
// Внешний сквад Remnawave
|
||||||
@@ -168,6 +174,8 @@ export interface TariffUpdateRequest {
|
|||||||
// Дневной тариф
|
// Дневной тариф
|
||||||
is_daily?: boolean;
|
is_daily?: boolean;
|
||||||
daily_price_kopeks?: number;
|
daily_price_kopeks?: number;
|
||||||
|
// Автопродление Lava: продукт из кабинета Lava
|
||||||
|
lava_product_id?: string | null;
|
||||||
// Режим сброса трафика
|
// Режим сброса трафика
|
||||||
traffic_reset_mode?: string | null;
|
traffic_reset_mode?: string | null;
|
||||||
// Внешний сквад Remnawave
|
// Внешний сквад Remnawave
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export interface TariffPurchaseFormProps {
|
|||||||
balanceKopeks: number | undefined;
|
balanceKopeks: number | undefined;
|
||||||
/** СБП-оформление (Platega recurrent) доступно — показать вторую CTA. */
|
/** СБП-оформление (Platega recurrent) доступно — показать вторую CTA. */
|
||||||
sbpPurchaseEnabled?: boolean;
|
sbpPurchaseEnabled?: boolean;
|
||||||
|
/** Оформление привязкой Lava доступно — показать вторую CTA. */
|
||||||
|
lavaPurchaseEnabled?: boolean;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +46,7 @@ export function TariffPurchaseForm({
|
|||||||
subscriptionId,
|
subscriptionId,
|
||||||
balanceKopeks,
|
balanceKopeks,
|
||||||
sbpPurchaseEnabled = false,
|
sbpPurchaseEnabled = false,
|
||||||
|
lavaPurchaseEnabled = false,
|
||||||
onBack,
|
onBack,
|
||||||
}: TariffPurchaseFormProps) {
|
}: TariffPurchaseFormProps) {
|
||||||
const { t } = useTranslation();
|
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.
|
// Smooth scroll the form into view when first mounted.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
@@ -241,6 +285,7 @@ export function TariffPurchaseForm({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{sbpPurchaseButton}
|
{sbpPurchaseButton}
|
||||||
|
{lavaPurchaseButton}
|
||||||
|
|
||||||
{purchaseMutation.isError &&
|
{purchaseMutation.isError &&
|
||||||
!getInsufficientBalanceError(purchaseMutation.error) && (
|
!getInsufficientBalanceError(purchaseMutation.error) && (
|
||||||
@@ -666,6 +711,7 @@ export function TariffPurchaseForm({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{sbpPurchaseButton}
|
{sbpPurchaseButton}
|
||||||
|
{lavaPurchaseButton}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
@@ -724,7 +724,9 @@
|
|||||||
"halfYear": "every six months",
|
"halfYear": "every six months",
|
||||||
"year": "yearly"
|
"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",
|
"backToList": "Back to subscriptions",
|
||||||
"confirmDelete": "Delete subscription?",
|
"confirmDelete": "Delete subscription?",
|
||||||
@@ -2765,7 +2767,9 @@
|
|||||||
"periodsRequired": "Add at least one period",
|
"periodsRequired": "Add at least one period",
|
||||||
"dailyPriceRequired": "Enter a price per day greater than 0",
|
"dailyPriceRequired": "Enter a price per day greater than 0",
|
||||||
"trafficPackagesRequired": "Add at least one traffic package (or disable traffic topup)"
|
"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": {
|
"servers": {
|
||||||
"title": "Squad Management",
|
"title": "Squad Management",
|
||||||
|
|||||||
@@ -623,7 +623,9 @@
|
|||||||
"halfYear": "هر شش ماه",
|
"halfYear": "هر شش ماه",
|
||||||
"year": "سالانه"
|
"year": "سالانه"
|
||||||
},
|
},
|
||||||
"autopayHint": "اشتراک بهطور خودکار از طریق Lava تمدید میشود"
|
"autopayHint": "اشتراک بهطور خودکار از طریق Lava تمدید میشود",
|
||||||
|
"purchaseButton": "⚡ ثبتنام با تمدید خودکار Lava",
|
||||||
|
"purchaseHint": "صورتحساب اول از طریق لینک Lava پرداخت میشود؛ سپس تمدیدها بر اساس دوره محصول بهطور خودکار کسر میشوند."
|
||||||
},
|
},
|
||||||
"backToList": "بازگشت به فهرست اشتراکها",
|
"backToList": "بازگشت به فهرست اشتراکها",
|
||||||
"confirmDelete": "اشتراک حذف شود؟",
|
"confirmDelete": "اشتراک حذف شود؟",
|
||||||
@@ -2303,7 +2305,9 @@
|
|||||||
"periodsRequired": "حداقل یک دوره اضافه کنید",
|
"periodsRequired": "حداقل یک دوره اضافه کنید",
|
||||||
"dailyPriceRequired": "قیمت روزانه بزرگتر از 0 را وارد کنید",
|
"dailyPriceRequired": "قیمت روزانه بزرگتر از 0 را وارد کنید",
|
||||||
"trafficPackagesRequired": "حداقل یک بسته ترافیک اضافه کنید (یا خرید ترافیک را غیرفعال کنید)"
|
"trafficPackagesRequired": "حداقل یک بسته ترافیک اضافه کنید (یا خرید ترافیک را غیرفعال کنید)"
|
||||||
}
|
},
|
||||||
|
"lavaProductLabel": "محصول Lava (تمدید خودکار)",
|
||||||
|
"lavaProductDesc": "شناسه UUID محصول از پنل Lava: قیمت و دوره کسر در آنجا تنظیم میشود. خالی — تمدید خودکار برای این تعرفه در دسترس نیست."
|
||||||
},
|
},
|
||||||
"servers": {
|
"servers": {
|
||||||
"title": "مدیریت اسکوادها",
|
"title": "مدیریت اسکوادها",
|
||||||
|
|||||||
@@ -743,7 +743,9 @@
|
|||||||
"halfYear": "раз в полгода",
|
"halfYear": "раз в полгода",
|
||||||
"year": "ежегодно"
|
"year": "ежегодно"
|
||||||
},
|
},
|
||||||
"autopayHint": "Подписка будет автоматически продлеваться через Lava"
|
"autopayHint": "Подписка будет автоматически продлеваться через Lava",
|
||||||
|
"purchaseButton": "⚡ Оформить с автооплатой Lava",
|
||||||
|
"purchaseHint": "Первое списание оплачивается по ссылке Lava; дальше продления списываются автоматически по периоду продукта."
|
||||||
},
|
},
|
||||||
"backToList": "К списку подписок",
|
"backToList": "К списку подписок",
|
||||||
"confirmDelete": "Удалить подписку?",
|
"confirmDelete": "Удалить подписку?",
|
||||||
@@ -3154,7 +3156,9 @@
|
|||||||
"periodsRequired": "Добавьте хотя бы один период",
|
"periodsRequired": "Добавьте хотя бы один период",
|
||||||
"dailyPriceRequired": "Укажите цену за день больше 0",
|
"dailyPriceRequired": "Укажите цену за день больше 0",
|
||||||
"trafficPackagesRequired": "Добавьте хотя бы один пакет трафика (или отключите докупку)"
|
"trafficPackagesRequired": "Добавьте хотя бы один пакет трафика (или отключите докупку)"
|
||||||
}
|
},
|
||||||
|
"lavaProductLabel": "Продукт Lava (автопродление)",
|
||||||
|
"lavaProductDesc": "UUID продукта из кабинета Lava: цена и периодичность списаний задаются там. Пусто — автопродление для тарифа недоступно."
|
||||||
},
|
},
|
||||||
"servers": {
|
"servers": {
|
||||||
"title": "Управление сквадами",
|
"title": "Управление сквадами",
|
||||||
|
|||||||
@@ -623,7 +623,9 @@
|
|||||||
"halfYear": "每半年",
|
"halfYear": "每半年",
|
||||||
"year": "每年"
|
"year": "每年"
|
||||||
},
|
},
|
||||||
"autopayHint": "订阅将通过 Lava 自动续订"
|
"autopayHint": "订阅将通过 Lava 自动续订",
|
||||||
|
"purchaseButton": "⚡ 使用 Lava 自动续订下单",
|
||||||
|
"purchaseHint": "首期账单通过 Lava 链接支付;之后按产品周期自动扣款续订。"
|
||||||
},
|
},
|
||||||
"backToList": "返回订阅列表",
|
"backToList": "返回订阅列表",
|
||||||
"confirmDelete": "删除订阅?",
|
"confirmDelete": "删除订阅?",
|
||||||
@@ -2369,7 +2371,9 @@
|
|||||||
"periodsRequired": "请添加至少一个期限",
|
"periodsRequired": "请添加至少一个期限",
|
||||||
"dailyPriceRequired": "请输入大于0的每日价格",
|
"dailyPriceRequired": "请输入大于0的每日价格",
|
||||||
"trafficPackagesRequired": "请添加至少一个流量包(或关闭流量购买)"
|
"trafficPackagesRequired": "请添加至少一个流量包(或关闭流量购买)"
|
||||||
}
|
},
|
||||||
|
"lavaProductLabel": "Lava 产品(自动续订)",
|
||||||
|
"lavaProductDesc": "来自 Lava 后台的产品 UUID:价格与扣款周期在那里设置。留空则该套餐不支持自动续订。"
|
||||||
},
|
},
|
||||||
"servers": {
|
"servers": {
|
||||||
"title": "服务器管理",
|
"title": "服务器管理",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export default function AdminTariffCreate() {
|
|||||||
const [selectedExternalSquad, setSelectedExternalSquad] = useState<string | null>(null);
|
const [selectedExternalSquad, setSelectedExternalSquad] = useState<string | null>(null);
|
||||||
const [selectedPromoGroups, setSelectedPromoGroups] = useState<number[]>([]);
|
const [selectedPromoGroups, setSelectedPromoGroups] = useState<number[]>([]);
|
||||||
const [dailyPriceKopeks, setDailyPriceKopeks] = useState<number | ''>(0);
|
const [dailyPriceKopeks, setDailyPriceKopeks] = useState<number | ''>(0);
|
||||||
|
const [lavaProductId, setLavaProductId] = useState('');
|
||||||
|
|
||||||
// Traffic topup
|
// Traffic topup
|
||||||
const [trafficTopupEnabled, setTrafficTopupEnabled] = useState(false);
|
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) || [],
|
data.promo_groups?.filter((pg) => pg.is_selected).map((pg) => pg.id) || [],
|
||||||
);
|
);
|
||||||
setDailyPriceKopeks(data.daily_price_kopeks || 0);
|
setDailyPriceKopeks(data.daily_price_kopeks || 0);
|
||||||
|
setLavaProductId(data.lava_product_id || '');
|
||||||
setTrafficTopupEnabled(data.traffic_topup_enabled || false);
|
setTrafficTopupEnabled(data.traffic_topup_enabled || false);
|
||||||
setMaxTopupTrafficGb(data.max_topup_traffic_gb || 0);
|
setMaxTopupTrafficGb(data.max_topup_traffic_gb || 0);
|
||||||
setTrafficTopupPackages(data.traffic_topup_packages || {});
|
setTrafficTopupPackages(data.traffic_topup_packages || {});
|
||||||
@@ -176,6 +178,8 @@ export default function AdminTariffCreate() {
|
|||||||
max_topup_traffic_gb: toNumber(maxTopupTrafficGb),
|
max_topup_traffic_gb: toNumber(maxTopupTrafficGb),
|
||||||
is_daily: isDaily,
|
is_daily: isDaily,
|
||||||
daily_price_kopeks: isDaily ? toNumber(dailyPriceKopeks) : 0,
|
daily_price_kopeks: isDaily ? toNumber(dailyPriceKopeks) : 0,
|
||||||
|
// Пустая строка отвязывает тариф от продукта Lava
|
||||||
|
lava_product_id: lavaProductId.trim(),
|
||||||
traffic_reset_mode: trafficResetMode,
|
traffic_reset_mode: trafficResetMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -460,6 +464,25 @@ export default function AdminTariffCreate() {
|
|||||||
</div>
|
</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 */}
|
{/* Traffic Limit */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -292,6 +292,12 @@ export default function SubscriptionPurchase() {
|
|||||||
'platega_recurrent_enabled' in purchaseOptions &&
|
'platega_recurrent_enabled' in purchaseOptions &&
|
||||||
purchaseOptions.platega_recurrent_enabled === true
|
purchaseOptions.platega_recurrent_enabled === true
|
||||||
}
|
}
|
||||||
|
lavaPurchaseEnabled={
|
||||||
|
isTariffsMode &&
|
||||||
|
purchaseOptions !== undefined &&
|
||||||
|
'lava_recurrent_enabled' in purchaseOptions &&
|
||||||
|
purchaseOptions.lava_recurrent_enabled === true
|
||||||
|
}
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
setShowTariffPurchase(false);
|
setShowTariffPurchase(false);
|
||||||
setSelectedTariff(null);
|
setSelectedTariff(null);
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ export interface TariffsPurchaseOptions {
|
|||||||
// СБП-оформление (Platega recurrent): показывать кнопку «Оформить с
|
// СБП-оформление (Platega recurrent): показывать кнопку «Оформить с
|
||||||
// автооплатой СБП» рядом с покупкой с баланса
|
// автооплатой СБП» рядом с покупкой с баланса
|
||||||
platega_recurrent_enabled?: boolean;
|
platega_recurrent_enabled?: boolean;
|
||||||
|
lava_recurrent_enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClassicPurchaseOptions {
|
export interface ClassicPurchaseOptions {
|
||||||
|
|||||||
Reference in New Issue
Block a user