fix(promocode): correct activation UX, error mapping and validity dates

Map promocode errors by stable backend code (detail.code) instead of brittle English
substring matching, so previously-unmapped codes (active_discount_exists, daily_limit,
no_subscription_for_days, trial_*, etc.) show the right message instead of 'server error';
added the missing keys to all 4 locales.

valid_until now anchors to end-of-day in the admin's LOCAL timezone on both create AND
edit read-back, so codes no longer expire at the start of the day and editing no longer
drifts the date forward for negative-offset admins.

Allow 0-hour discount ('until first purchase'). Route user-facing discount deactivation
to the endpoint that rolls back promocode usage when the discount came from a promocode,
and parse the structured {code,message} error.
This commit is contained in:
c0mrade
2026-06-08 00:23:31 +03:00
parent 1a3236f650
commit 35428cc27d
7 changed files with 97 additions and 24 deletions

View File

@@ -118,9 +118,21 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
},
});
// Deactivate discount mutation
// Deactivate discount mutation.
// A discount granted by a PROMOCODE must be turned off via the dedicated route
// (POST /cabinet/promocode/deactivate-discount), which also rolls back the promocode
// usage (deletes the PromoCodeUse, decrements current_uses, strips the promo group) so
// the user can re-activate it later. The plain clearActiveDiscount route only zeroes
// the discount fields and is correct for admin/offer-sourced discounts.
const deactivateMutation = useMutation({
mutationFn: promoApi.clearActiveDiscount,
mutationFn: async () => {
const source = activeDiscount?.source ?? '';
if (source.startsWith('promocode:')) {
await promoApi.deactivateDiscount();
} else {
await promoApi.clearActiveDiscount();
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['active-discount'] });
queryClient.invalidateQueries({ queryKey: ['promo-offers'] });
@@ -131,8 +143,14 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
setTimeout(() => setSuccessMessage(null), 5000);
},
onError: (error: unknown) => {
const axiosErr = error as { response?: { data?: { detail?: string } } };
setErrorMessage(axiosErr.response?.data?.detail || t('promo.deactivate.error'));
// detail may be a plain string (clearActiveDiscount route) or a structured
// { code, message } object (deactivate-discount route). Handle both.
const axiosErr = error as {
response?: { data?: { detail?: string | { message?: string } } };
};
const detail = axiosErr.response?.data?.detail;
const message = typeof detail === 'string' ? detail : detail?.message;
setErrorMessage(message || t('promo.deactivate.error'));
setTimeout(() => setErrorMessage(null), 5000);
},