mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
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:
@@ -16,6 +16,16 @@ import { tariffsApi } from '../api/tariffs';
|
||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||
import { BackIcon, RefreshIcon } from '@/components/icons';
|
||||
|
||||
// valid_until is created as end-of-day in the admin's LOCAL tz, then stored/returned
|
||||
// as a UTC instant. Reading the picker back must convert UTC -> local date, otherwise
|
||||
// negative-offset admins see tomorrow's date and re-saving drifts the expiry forward a
|
||||
// day each time. (Mirror of DateField's local toISO; must NOT slice the raw UTC string.)
|
||||
const utcInstantToLocalDateInput = (iso: string): string => {
|
||||
const d = new Date(iso);
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||
};
|
||||
|
||||
export default function AdminPromocodeCreate() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -76,7 +86,7 @@ export default function AdminPromocodeCreate() {
|
||||
setMaxUses(data.max_uses || 1);
|
||||
setIsActive(data.is_active ?? true);
|
||||
setFirstPurchaseOnly(data.first_purchase_only || false);
|
||||
setValidUntil(data.valid_until ? data.valid_until.split('T')[0] : '');
|
||||
setValidUntil(data.valid_until ? utcInstantToLocalDateInput(data.valid_until) : '');
|
||||
setPromoGroupId(data.promo_group_id || null);
|
||||
setTariffId(data.tariff_id || null);
|
||||
return data;
|
||||
@@ -120,7 +130,12 @@ export default function AdminPromocodeCreate() {
|
||||
max_uses: maxUsesValue,
|
||||
is_active: isActive,
|
||||
first_purchase_only: firstPurchaseOnly,
|
||||
valid_until: validUntil ? new Date(validUntil).toISOString() : null,
|
||||
// The picker yields a date-only 'YYYY-MM-DD'. A promo "valid until D" must
|
||||
// stay valid through the WHOLE of day D, so anchor to end-of-day in the
|
||||
// admin's local timezone. `new Date('YYYY-MM-DD')` parses as UTC midnight
|
||||
// (the START of the day) — for a GMT+3 admin that made a code picked for
|
||||
// "today" already expired by 3am, surfacing as a bogus "expired" error.
|
||||
valid_until: validUntil ? new Date(`${validUntil}T23:59:59`).toISOString() : null,
|
||||
promo_group_id: type === 'promo_group' ? promoGroupId : null,
|
||||
...(type === 'trial_subscription' && tariffId ? { tariff_id: tariffId } : {}),
|
||||
};
|
||||
@@ -145,7 +160,8 @@ export default function AdminPromocodeCreate() {
|
||||
if ((type === 'subscription_days' || type === 'trial_subscription') && daysValue <= 0)
|
||||
return false;
|
||||
if (type === 'promo_group' && !promoGroupId) return false;
|
||||
if (type === 'discount' && (balanceValue <= 0 || balanceValue > 100 || daysValue <= 0))
|
||||
// For discount, validity hours of 0 = "until first purchase" (perpetual) — allowed.
|
||||
if (type === 'discount' && (balanceValue <= 0 || balanceValue > 100 || daysValue < 0))
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
@@ -410,7 +426,7 @@ export default function AdminPromocodeCreate() {
|
||||
}
|
||||
}}
|
||||
className="input w-32"
|
||||
min={1}
|
||||
min={0}
|
||||
placeholder="0"
|
||||
/>
|
||||
<span className="text-dark-400">{t('admin.promocodes.form.hours')}</span>
|
||||
|
||||
Reference in New Issue
Block a user