fix(credits): wire coupon Apply button to real backend validation
All checks were successful
build-and-release / build (push) Successful in 1m41s
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:
parent
77febdaeb4
commit
55c832b620
1 changed files with 92 additions and 18 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue