-
Notifications
You must be signed in to change notification settings - Fork 2
Checkout22 #342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Checkout22 #342
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,116 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // src/app/api/store/[slug]/coupons/validate/route.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Public storefront endpoint – validate a discount/coupon code at checkout | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { NextRequest, NextResponse } from 'next/server'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import prisma from '@/lib/prisma'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type RouteContext = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| params: Promise<{ slug: string }>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * POST /api/store/[slug]/coupons/validate | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Validates a coupon code and returns the calculated discount amount. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * No authentication required (public storefront endpoint). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Body: { code: string; orderAmount: number } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns: { valid: true; code; discountAmount; description; type; value } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * or { error: string } with a 4xx status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function POST(request: NextRequest, context: RouteContext) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { slug } = await context.params; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const body = await request.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const code: string = (body.code ?? '').toString().toUpperCase().trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const orderAmount: number = Number(body.orderAmount ?? 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!code) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ error: 'Coupon code is required' }, { status: 400 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (orderAmount < 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+32
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const orderAmount: number = Number(body.orderAmount ?? 0); | |
| if (!code) { | |
| return NextResponse.json({ error: 'Coupon code is required' }, { status: 400 }); | |
| } | |
| if (orderAmount < 0) { | |
| const rawOrderAmount = body.orderAmount; | |
| const orderAmount: number = | |
| typeof rawOrderAmount === 'number' ? rawOrderAmount : Number(rawOrderAmount); | |
| if (!code) { | |
| return NextResponse.json({ error: 'Coupon code is required' }, { status: 400 }); | |
| } | |
| if ( | |
| !Number.isFinite(orderAmount) || | |
| !Number.isInteger(orderAmount) || | |
| orderAmount < 0 | |
| ) { |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This endpoint re-implements discount code validation via a raw Prisma query. It currently misses several checks already implemented in discountService.validateCode (e.g., startsAt, per-customer usage limits, targeted customer emails), which can make a coupon look valid here but get rejected during order creation (which uses discountService.applyCode). Consider delegating to discountService to keep behavior consistent.
| OR: [ | |
| { expiresAt: null }, | |
| { expiresAt: { gt: new Date() } }, | |
| AND: [ | |
| // Coupon has started (or has no start date) | |
| { | |
| OR: [ | |
| { startsAt: null }, | |
| { startsAt: { lte: new Date() } }, | |
| ], | |
| }, | |
| // Coupon has not expired (or has no expiry date) | |
| { | |
| OR: [ | |
| { expiresAt: null }, | |
| { expiresAt: { gt: new Date() } }, | |
| ], | |
| }, |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The else branch treats all non-PERCENTAGE discount types as a fixed-amount discount against orderAmount. This is incorrect for FREE_SHIPPING (and may diverge from discountService.applyCode, which also supports FIXED, FIXED_AMOUNT, and FREE_SHIPPING). Please handle the full DiscountType enum explicitly or delegate to discountService.applyCode to avoid incorrect discount calculations.
| if (coupon.type === 'PERCENTAGE') { | |
| // value is 0-100 (e.g. 10 = 10%) | |
| discountAmount = Math.round((orderAmount * coupon.value) / 100); | |
| // Apply cap if set | |
| if (coupon.maxDiscountAmount !== null) { | |
| discountAmount = Math.min(discountAmount, coupon.maxDiscountAmount); | |
| } | |
| } else { | |
| // FIXED – value is minor units (paisa) | |
| // Cannot discount more than the order amount | |
| discountAmount = Math.min(coupon.value, orderAmount); | |
| switch (coupon.type) { | |
| case 'PERCENTAGE': { | |
| // value is 0-100 (e.g. 10 = 10%) | |
| discountAmount = Math.round((orderAmount * coupon.value) / 100); | |
| // Apply cap if set | |
| if (coupon.maxDiscountAmount !== null) { | |
| discountAmount = Math.min(discountAmount, coupon.maxDiscountAmount); | |
| } | |
| break; | |
| } | |
| case 'FIXED': | |
| case 'FIXED_AMOUNT': { | |
| // FIXED / FIXED_AMOUNT – value is minor units (paisa) | |
| // Cannot discount more than the order amount | |
| discountAmount = Math.min(coupon.value, orderAmount); | |
| break; | |
| } | |
| case 'FREE_SHIPPING': { | |
| // FREE_SHIPPING does not reduce the order subtotal in this endpoint | |
| // (shipping discount should be handled separately). | |
| discountAmount = 0; | |
| break; | |
| } | |
| default: { | |
| // Unknown or unsupported discount type – do not apply a subtotal discount | |
| discountAmount = 0; | |
| break; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a public endpoint and currently has no rate limiting. Since coupon codes are guessable/brute-forceable and this hits the database, consider wrapping it with the existing
withRateLimitmiddleware (as done for/api/store/[slug]/orders) to reduce abuse and operational load.