-
Notifications
You must be signed in to change notification settings - Fork 0
Implement Cash on Delivery (COD) payment for Bangladesh market #119
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
base: main
Are you sure you want to change the base?
Changes from all commits
7e4034b
bd9063b
fa51568
fbfd0f4
4521e09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| -- AlterTable | ||
| ALTER TABLE "Order" ADD COLUMN "codFee" DOUBLE PRECISION, | ||
| ADD COLUMN "phoneVerified" BOOLEAN NOT NULL DEFAULT false, | ||
| ADD COLUMN "deliveryAttempts" INTEGER NOT NULL DEFAULT 0, | ||
| ADD COLUMN "codCollectionStatus" TEXT, | ||
| ADD COLUMN "codCollectedAt" TIMESTAMP(3); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,127 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * COD Collection Status Update API | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * PUT /api/orders/[id]/cod-status | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Updates the collection status of a COD order (COLLECTED or FAILED) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { NextRequest, NextResponse } from 'next/server'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getServerSession } from 'next-auth'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { authOptions } from '@/lib/auth'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { CODService } from '@/lib/services/cod.service'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { prisma } from '@/lib/prisma'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { z } from 'zod'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Request validation schema | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const updateCODStatusSchema = z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: z.enum(['COLLECTED', 'FAILED']), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| collectedAmount: z.number().positive().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| failureReason: z.string().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| notes: z.string().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function PUT( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| req: NextRequest, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| context: { params: Promise<{ id: string }> } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get session - must be authenticated | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const session = await getServerSession(authOptions); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!session?.user?.id) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { success: false, error: 'Unauthorized' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 401 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const params = await context.params; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const orderId = params.id; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Parse and validate request body | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const body = await req.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validatedData = updateCODStatusSchema.parse(body); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get order and verify access | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const order = await prisma.order.findUnique({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| where: { id: orderId }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| include: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| store: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| include: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| organization: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| include: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| memberships: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| where: { userId: session.user.id }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| staff: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| where: { userId: session.user.id }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!order) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { success: false, error: 'Order not found' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 404 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if user has access to this store | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hasOrgAccess = order.store.organization.memberships.length > 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hasStaffAccess = order.store.staff.length > 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!hasOrgAccess && !hasStaffAccess) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { success: false, error: 'Forbidden - No access to this store' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 403 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Update COD collection status | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const updatedOrder = await CODService.updateCollectionStatus({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| orderId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: validatedData.status, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| collectedAmount: validatedData.collectedAmount, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| failureReason: validatedData.failureReason, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| notes: validatedData.notes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| order: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: updatedOrder!.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| orderNumber: updatedOrder!.orderNumber, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: updatedOrder!.status, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| paymentStatus: updatedOrder!.paymentStatus, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| codCollectionStatus: updatedOrder!.codCollectionStatus, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| codCollectedAt: updatedOrder!.codCollectedAt, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+92
to
+100
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ | |
| success: true, | |
| order: { | |
| id: updatedOrder!.id, | |
| orderNumber: updatedOrder!.orderNumber, | |
| status: updatedOrder!.status, | |
| paymentStatus: updatedOrder!.paymentStatus, | |
| codCollectionStatus: updatedOrder!.codCollectionStatus, | |
| codCollectedAt: updatedOrder!.codCollectedAt, | |
| if (!updatedOrder) { | |
| return NextResponse.json( | |
| { success: false, error: 'Failed to update COD status' }, | |
| { status: 400 } | |
| ); | |
| } | |
| return NextResponse.json({ | |
| success: true, | |
| order: { | |
| id: updatedOrder.id, | |
| orderNumber: updatedOrder.orderNumber, | |
| status: updatedOrder.status, | |
| paymentStatus: updatedOrder.paymentStatus, | |
| codCollectionStatus: updatedOrder.codCollectionStatus, | |
| codCollectedAt: updatedOrder.codCollectedAt, |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,98 @@ | ||||||
| /** | ||||||
| * COD Order Creation API | ||||||
| * POST /api/orders/cod | ||||||
| * | ||||||
| * Creates a Cash on Delivery order with phone verification and fraud checks | ||||||
| */ | ||||||
|
|
||||||
| import { NextRequest, NextResponse } from 'next/server'; | ||||||
| import { getServerSession } from 'next-auth'; | ||||||
| import { authOptions } from '@/lib/auth'; | ||||||
| import { CODService } from '@/lib/services/cod.service'; | ||||||
| import { z } from 'zod'; | ||||||
|
|
||||||
| // Request validation schema | ||||||
| const createCODOrderSchema = z.object({ | ||||||
| storeId: z.string().min(1, 'Store ID is required'), | ||||||
| customerEmail: z.string().email('Invalid email address'), | ||||||
| customerName: z.string().min(1, 'Customer name is required'), | ||||||
| customerPhone: z.string().min(10, 'Phone number is required'), | ||||||
|
||||||
| customerPhone: z.string().min(10, 'Phone number is required'), | |
| phoneNumber: z.string().min(10, 'Phone number is required'), |
Copilot
AI
Dec 11, 2025
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.
Client-provided COD fee could bypass fee calculation logic. The API accepts codFee as an input parameter rather than calculating it server-side using CODService.calculateCODFee(). This allows clients to submit any COD fee amount, potentially bypassing the business rule of BDT 50 for orders under 500.
The COD fee should be calculated on the server based on the actual order subtotal, not accepted as user input.
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
codCollectionStatusfield is defined asString?in the Prisma schema, which means it can be any string value. This can lead to data inconsistency issues as there are no database-level constraints enforcing valid values like 'PENDING', 'COLLECTED', 'FAILED', or 'PARTIAL'.Consider using a Prisma enum for
codCollectionStatusto ensure type safety and data integrity at both the application and database levels.