diff --git a/src/components/dashboard/CreditsPage.tsx b/src/components/dashboard/CreditsPage.tsx index f35e19a..f602a7b 100644 --- a/src/components/dashboard/CreditsPage.tsx +++ b/src/components/dashboard/CreditsPage.tsx @@ -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) { /> - +

- ✓ Coupon will be applied at checkout + ✓ Coupon applied — you save{" "} + {formatCurrency(checkout().couponDiscountPaise / 100)} +

+
+ +

+ {checkout().couponError}

@@ -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 + )}