diff --git a/src/api/subscription.ts b/src/api/subscription.ts index c80bad4..83b103c 100644 --- a/src/api/subscription.ts +++ b/src/api/subscription.ts @@ -398,6 +398,14 @@ export const subscriptionApi = { tariffId: number, periodDays: number, trafficGb?: number, + /** + * Subscription ID being renewed. Pass this when the user clicked + * "Renew" on an existing subscription so the backend can resolve + * the target row by ID instead of doing a (user_id, tariff_id) + * re-lookup that races with concurrent panel webhooks. Omit when + * this is a fresh purchase from the catalog. + */ + subscriptionId?: number, ): Promise<{ success: boolean; message: string; @@ -411,6 +419,7 @@ export const subscriptionApi = { tariff_id: tariffId, period_days: periodDays, traffic_gb: trafficGb, + subscription_id: subscriptionId, }); return response.data; }, diff --git a/src/components/dashboard/SubscriptionCardExpired.tsx b/src/components/dashboard/SubscriptionCardExpired.tsx index 4709ded..5d86ce7 100644 --- a/src/components/dashboard/SubscriptionCardExpired.tsx +++ b/src/components/dashboard/SubscriptionCardExpired.tsx @@ -59,8 +59,11 @@ export default function SubscriptionCardExpired({ // Resume daily subscription via toggle pause endpoint await subscriptionApi.togglePause(subscription.id); } else if (isDaily && subscription.tariff_id) { - // Expired daily tariff — purchase for 1 day - await subscriptionApi.purchaseTariff(subscription.tariff_id, 1); + // Expired daily tariff — purchase for 1 day. Pass subscription.id + // so the backend resolves the EXACT row instead of doing a + // (user_id, tariff_id) re-lookup that races with concurrent + // panel webhooks (would surface as "Тариф уже активен" + refund). + await subscriptionApi.purchaseTariff(subscription.tariff_id, 1, undefined, subscription.id); } else { await subscriptionApi.renewSubscription(30, subscription.id); } diff --git a/src/pages/SubscriptionPurchase.tsx b/src/pages/SubscriptionPurchase.tsx index 8221ece..300cd52 100644 --- a/src/pages/SubscriptionPurchase.tsx +++ b/src/pages/SubscriptionPurchase.tsx @@ -305,7 +305,17 @@ export default function SubscriptionPurchase() { : selectedTariffPeriod?.days || 30; const trafficGb = useCustomTraffic && selectedTariff.custom_traffic_enabled ? customTrafficGb : undefined; - return subscriptionApi.purchaseTariff(selectedTariff.id, days, trafficGb); + // Forward the subscription_id when the user landed here via the + // "Renew this subscription" flow (?subscriptionId=N). The backend + // uses it to resolve the exact target row by ID, avoiding the + // race with concurrent panel webhooks that would otherwise hit + // the partial UNIQUE on uq_subscriptions_user_tariff_active. + return subscriptionApi.purchaseTariff( + selectedTariff.id, + days, + trafficGb, + subscriptionId ?? undefined, + ); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['subscription'] });