fix(credits): wire coupon Apply button to real backend validation
All checks were successful
build-and-release / build (push) Successful in 1m41s

The previous coupon UI's Apply button just flipped a local boolean on
any non-empty text - no call to the backend, so an invalid/expired code
still showed a green "will be applied at checkout" confirmation. The
actual discount only surfaced (or failed generically) at order creation.

Now Apply calls the new POST /api/ai-credits/coupons/validate endpoint,
shows the real discount amount on success or the real error message on
failure, and the Pay button reflects the validated discounted price.
Editing the code after a successful apply clears the validated state so
a stale discount can't be sent. create_order still re-validates
server-side regardless (unchanged, already correct).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar Sivakumar 2026-08-14 15:26:56 +05:30
parent 77febdaeb4
commit 55c832b620

View file

@ -74,6 +74,10 @@ type CheckoutState = {
error: string;
couponCode: string;
couponApplied: boolean;
couponLoading: boolean;
couponError: string;
couponDiscountPaise: number;
couponFinalPriceInr: number;
};
async function apiFetch(path: string, opts?: RequestInit) {
@ -120,6 +124,10 @@ export default function CreditsPage(props: Props) {
error: "",
couponCode: "",
couponApplied: false,
couponLoading: false,
couponError: "",
couponDiscountPaise: 0,
couponFinalPriceInr: 0,
});
const isProfessional = () => PROFESSIONAL_ROLE_SET.has(props.roleKey);
const prefix = () => ROLE_PREFIXES[props.roleKey];
@ -302,22 +310,65 @@ export default function CreditsPage(props: Props) {
onMount(loadAllData);
const openCheckout = (pkg: Package) => {
setCheckout({ package: pkg, aiCreditPackage: null, orderId: null, step: "form", error: "", couponCode: "", couponApplied: false });
setCheckout({ package: pkg, aiCreditPackage: null, orderId: null, step: "form", error: "", couponCode: "", couponApplied: false, couponLoading: false, couponError: "", couponDiscountPaise: 0, couponFinalPriceInr: 0 });
};
const openAiCreditCheckout = (pkg: AiCreditPackage) => {
setCheckout({ package: null, aiCreditPackage: pkg, orderId: null, step: "form", error: "", couponCode: "", couponApplied: false });
setCheckout({ package: null, aiCreditPackage: pkg, orderId: null, step: "form", error: "", couponCode: "", couponApplied: false, couponLoading: false, couponError: "", couponDiscountPaise: 0, couponFinalPriceInr: 0 });
};
const closeCheckout = () => {
setCheckout({ package: null, aiCreditPackage: null, orderId: null, step: "form", error: "", couponCode: "", couponApplied: false });
setCheckout({ package: null, aiCreditPackage: null, orderId: null, step: "form", error: "", couponCode: "", couponApplied: false, couponLoading: false, couponError: "", couponDiscountPaise: 0, couponFinalPriceInr: 0 });
};
const applyCoupon = async (aiPkg: AiCreditPackage) => {
const code = checkout().couponCode.trim();
if (!code) return;
setCheckout((c) => ({ ...c, couponLoading: true, couponError: "", couponApplied: false }));
try {
const res = await apiFetch("/api/ai-credits/coupons/validate", {
method: "POST",
body: JSON.stringify({ package_id: aiPkg.id, coupon_code: code }),
});
const data = await res.json().catch(() => ({}));
if (res.ok && data.valid) {
setCheckout((c) => ({
...c,
couponApplied: true,
couponError: "",
couponDiscountPaise: data.discount_paise ?? 0,
couponFinalPriceInr: data.final_price_inr ?? aiPkg.price_inr,
}));
} else {
setCheckout((c) => ({
...c,
couponApplied: false,
couponError: data.message || "Invalid coupon code",
couponDiscountPaise: 0,
couponFinalPriceInr: 0,
}));
}
} catch {
setCheckout((c) => ({
...c,
couponApplied: false,
couponError: "Network error validating coupon",
couponDiscountPaise: 0,
couponFinalPriceInr: 0,
}));
} finally {
setCheckout((c) => ({ ...c, couponLoading: false }));
}
};
const processAiCreditPayment = async (aiPkg: AiCreditPackage) => {
setCheckout((c) => ({ ...c, step: "processing", error: "" }));
try {
const coupon = checkout().couponCode.trim();
// Only forward the coupon once it's actually been validated by
// applyCoupon - typed-but-unapplied text must not silently discount
// the order. create_order re-validates it server-side regardless.
const coupon = checkout().couponApplied ? checkout().couponCode.trim() : "";
const orderRes = await apiFetch("/api/ai-credits/order", {
method: "POST",
body: JSON.stringify({
@ -701,12 +752,26 @@ export default function CreditsPage(props: Props) {
type="text"
placeholder="Enter coupon code"
value={checkout().couponCode}
onInput={(e) => setCheckout((c) => ({ ...c, couponCode: e.currentTarget.value.toUpperCase(), couponApplied: false }))}
onInput={(e) =>
setCheckout((c) => ({
...c,
couponCode: e.currentTarget.value.toUpperCase(),
couponApplied: false,
couponError: "",
couponDiscountPaise: 0,
couponFinalPriceInr: 0,
}))
}
disabled={checkout().couponLoading}
style={{
flex: "1",
padding: "9px 12px",
"border-radius": "8px",
border: checkout().couponApplied ? "1.5px solid #16A34A" : "1.5px solid #D1D5DB",
border: checkout().couponApplied
? "1.5px solid #16A34A"
: checkout().couponError
? "1.5px solid #DC2626"
: "1.5px solid #D1D5DB",
"font-size": "13px",
outline: "none",
"letter-spacing": "0.05em",
@ -714,12 +779,8 @@ export default function CreditsPage(props: Props) {
/>
<button
type="button"
onClick={() => {
const code = checkout().couponCode.trim();
if (code) {
setCheckout((c) => ({ ...c, couponApplied: true }));
}
}}
onClick={() => checkout().aiCreditPackage && applyCoupon(checkout().aiCreditPackage!)}
disabled={checkout().couponLoading || !checkout().couponCode.trim()}
style={{
padding: "9px 14px",
"border-radius": "8px",
@ -727,17 +788,24 @@ export default function CreditsPage(props: Props) {
background: "#F9FAFB",
"font-size": "13px",
"font-weight": "600",
cursor: "pointer",
cursor: checkout().couponLoading ? "wait" : "pointer",
color: "#374151",
"white-space": "nowrap",
opacity: checkout().couponLoading || !checkout().couponCode.trim() ? 0.6 : 1,
}}
>
Apply
{checkout().couponLoading ? "Checking…" : "Apply"}
</button>
</div>
<Show when={checkout().couponApplied && checkout().couponCode.trim()}>
<Show when={checkout().couponApplied}>
<p style={{ margin: "0", "font-size": "12px", color: "#16A34A" }}>
Coupon will be applied at checkout
Coupon applied you save{" "}
{formatCurrency(checkout().couponDiscountPaise / 100)}
</p>
</Show>
<Show when={checkout().couponError}>
<p style={{ margin: "0", "font-size": "12px", color: "#DC2626" }}>
{checkout().couponError}
</p>
</Show>
</div>
@ -769,8 +837,14 @@ export default function CreditsPage(props: Props) {
"font-weight": "700",
}}
>
Pay {formatCurrency(checkout().package ? checkout().package!.price : checkout().aiCreditPackage!.price_inr)}
{checkout().couponApplied && checkout().couponCode.trim() ? " (coupon applied)" : ""}
Pay{" "}
{formatCurrency(
checkout().package
? checkout().package!.price
: checkout().couponApplied
? checkout().couponFinalPriceInr
: checkout().aiCreditPackage!.price_inr
)}
</button>
<p