mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
fix: promocode multi-tariff support in cabinet
- Balance page: handle select_subscription response — show subscription picker when promo code adds days and user has multiple tariffs - API: activatePromocode now accepts optional subscriptionId - AdminPromocodeCreate: show trial tariff info when creating trial_subscription promo code, warn if no trial tariff configured
This commit is contained in:
@@ -68,14 +68,21 @@ export const balanceApi = {
|
|||||||
// Activate promo code
|
// Activate promo code
|
||||||
activatePromocode: async (
|
activatePromocode: async (
|
||||||
code: string,
|
code: string,
|
||||||
|
subscriptionId?: number,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message: string;
|
message?: string;
|
||||||
balance_before: number;
|
balance_before?: number;
|
||||||
balance_after: number;
|
balance_after?: number;
|
||||||
bonus_description: string | null;
|
bonus_description?: string | null;
|
||||||
|
error?: string;
|
||||||
|
eligible_subscriptions?: Array<{ id: number; tariff_name: string; days_left: number }>;
|
||||||
|
code?: string;
|
||||||
}> => {
|
}> => {
|
||||||
const response = await apiClient.post('/cabinet/promocode/activate', { code });
|
const response = await apiClient.post('/cabinet/promocode/activate', {
|
||||||
|
code,
|
||||||
|
...(subscriptionId ? { subscription_id: subscriptionId } : {}),
|
||||||
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
PromoCodeUpdateRequest,
|
PromoCodeUpdateRequest,
|
||||||
PromoGroup,
|
PromoGroup,
|
||||||
} from '../api/promocodes';
|
} from '../api/promocodes';
|
||||||
|
import { tariffsApi } from '../api/tariffs';
|
||||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
@@ -69,6 +70,15 @@ export default function AdminPromocodeCreate() {
|
|||||||
|
|
||||||
const promoGroups: PromoGroup[] = promoGroupsData?.items || [];
|
const promoGroups: PromoGroup[] = promoGroupsData?.items || [];
|
||||||
|
|
||||||
|
// Fetch tariffs to show trial tariff info
|
||||||
|
const { data: tariffsData } = useQuery({
|
||||||
|
queryKey: ['admin-tariffs-for-promo'],
|
||||||
|
queryFn: () => tariffsApi.getTariffs(true),
|
||||||
|
enabled: type === 'trial_subscription',
|
||||||
|
});
|
||||||
|
|
||||||
|
const trialTariff = tariffsData?.tariffs?.find((t) => t.is_trial_available) || null;
|
||||||
|
|
||||||
// Fetch promocode for editing
|
// Fetch promocode for editing
|
||||||
const { isLoading: isLoadingPromocode } = useQuery({
|
const { isLoading: isLoadingPromocode } = useQuery({
|
||||||
queryKey: ['admin-promocode', id],
|
queryKey: ['admin-promocode', id],
|
||||||
@@ -301,6 +311,31 @@ export default function AdminPromocodeCreate() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{type === 'trial_subscription' && (
|
||||||
|
<div className="rounded-lg border border-dark-600 bg-dark-700/50 p-3">
|
||||||
|
{trialTariff ? (
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-dark-400">
|
||||||
|
{t('admin.promocodes.form.trialTariffInfo', 'Будет выдан тариф:')}
|
||||||
|
</span>{' '}
|
||||||
|
<span className="font-medium text-accent-400">{trialTariff.name}</span>
|
||||||
|
<span className="text-dark-500">
|
||||||
|
{' '}
|
||||||
|
({trialTariff.traffic_limit_gb} GB, {trialTariff.device_limit}{' '}
|
||||||
|
{t('admin.promocodes.form.devices', 'устр.')})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-warning-400">
|
||||||
|
{t(
|
||||||
|
'admin.promocodes.form.noTrialTariff',
|
||||||
|
'⚠️ Триальный тариф не настроен. Отметьте тариф как «доступен для триала» в настройках тарифов.',
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{type === 'promo_group' && (
|
{type === 'promo_group' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-sm font-medium text-dark-300">
|
<label className="mb-2 block text-sm font-medium text-dark-300">
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ export default function Balance() {
|
|||||||
message: string;
|
message: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [promoSelectSubs, setPromoSelectSubs] = useState<Array<{
|
||||||
|
id: number;
|
||||||
|
tariff_name: string;
|
||||||
|
days_left: number;
|
||||||
|
}> | null>(null);
|
||||||
|
const [promoSelectCode, setPromoSelectCode] = useState<string | null>(null);
|
||||||
const [transactionsPage, setTransactionsPage] = useState(1);
|
const [transactionsPage, setTransactionsPage] = useState(1);
|
||||||
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
|
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
|
||||||
|
|
||||||
@@ -135,27 +141,38 @@ export default function Balance() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePromocodeActivate = async () => {
|
const handlePromocodeActivate = async (subscriptionId?: number) => {
|
||||||
if (!promocode.trim()) return;
|
const code = subscriptionId ? promoSelectCode || '' : promocode.trim();
|
||||||
|
if (!code) return;
|
||||||
|
|
||||||
setPromocodeLoading(true);
|
setPromocodeLoading(true);
|
||||||
setPromocodeError(null);
|
setPromocodeError(null);
|
||||||
setPromocodeSuccess(null);
|
setPromocodeSuccess(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await balanceApi.activatePromocode(promocode.trim());
|
const result = await balanceApi.activatePromocode(code, subscriptionId);
|
||||||
|
|
||||||
|
if (result.error === 'select_subscription' && result.eligible_subscriptions) {
|
||||||
|
setPromoSelectSubs(result.eligible_subscriptions);
|
||||||
|
setPromoSelectCode(result.code || code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const bonusAmount = result.balance_after - result.balance_before;
|
const bonusAmount = (result.balance_after || 0) - (result.balance_before || 0);
|
||||||
setPromocodeSuccess({
|
setPromocodeSuccess({
|
||||||
message: result.bonus_description || t('balance.promocode.success'),
|
message: result.bonus_description || t('balance.promocode.success'),
|
||||||
amount: bonusAmount,
|
amount: bonusAmount,
|
||||||
});
|
});
|
||||||
setTransactionsPage(1);
|
setTransactionsPage(1);
|
||||||
setPromocode('');
|
setPromocode('');
|
||||||
|
setPromoSelectSubs(null);
|
||||||
|
setPromoSelectCode(null);
|
||||||
await refetchBalance();
|
await refetchBalance();
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
queryClient.invalidateQueries({ queryKey: ['transactions'] });
|
queryClient.invalidateQueries({ queryKey: ['transactions'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const axiosError = error as { response?: { data?: { detail?: string } } };
|
const axiosError = error as { response?: { data?: { detail?: string } } };
|
||||||
@@ -170,6 +187,8 @@ export default function Balance() {
|
|||||||
? 'already_used_by_user'
|
? 'already_used_by_user'
|
||||||
: 'server_error';
|
: 'server_error';
|
||||||
setPromocodeError(t(`balance.promocode.errors.${errorKey}`));
|
setPromocodeError(t(`balance.promocode.errors.${errorKey}`));
|
||||||
|
setPromoSelectSubs(null);
|
||||||
|
setPromoSelectCode(null);
|
||||||
} finally {
|
} finally {
|
||||||
setPromocodeLoading(false);
|
setPromocodeLoading(false);
|
||||||
}
|
}
|
||||||
@@ -214,7 +233,7 @@ export default function Balance() {
|
|||||||
disabled={promocodeLoading}
|
disabled={promocodeLoading}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handlePromocodeActivate}
|
onClick={() => handlePromocodeActivate()}
|
||||||
disabled={!promocode.trim()}
|
disabled={!promocode.trim()}
|
||||||
loading={promocodeLoading}
|
loading={promocodeLoading}
|
||||||
>
|
>
|
||||||
@@ -250,6 +269,39 @@ export default function Balance() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
{promoSelectSubs && promoSelectSubs.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="mt-3 space-y-2 rounded-linear border border-accent-500/30 bg-accent-500/10 p-3"
|
||||||
|
>
|
||||||
|
<div className="text-sm font-medium text-dark-200">
|
||||||
|
{t('balance.promocode.selectSubscription', 'К какой подписке применить промокод?')}
|
||||||
|
</div>
|
||||||
|
{promoSelectSubs.map((sub) => (
|
||||||
|
<button
|
||||||
|
key={sub.id}
|
||||||
|
onClick={() => handlePromocodeActivate(sub.id)}
|
||||||
|
disabled={promocodeLoading}
|
||||||
|
className="flex w-full items-center justify-between rounded-linear border border-dark-600 bg-dark-700 px-3 py-2 text-sm text-dark-200 transition-colors hover:border-accent-500/50 hover:bg-dark-600"
|
||||||
|
>
|
||||||
|
<span>{sub.tariff_name}</span>
|
||||||
|
<span className="text-dark-400">
|
||||||
|
{t('balance.promocode.daysLeft', '{{count}} дн.', { count: sub.days_left })}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setPromoSelectSubs(null);
|
||||||
|
setPromoSelectCode(null);
|
||||||
|
}}
|
||||||
|
className="text-xs text-dark-400 hover:text-dark-200"
|
||||||
|
>
|
||||||
|
{t('common.cancel', 'Отмена')}
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user