From 7e4034ba24599249a2959310ba24cc11db151900 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:37:54 +0000 Subject: [PATCH 1/5] Initial plan From bd9063bd01980129ce8c3650f9effde99b8cbc1d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:47:21 +0000 Subject: [PATCH 2/5] feat: Add database schema and email templates for COD payment - Add COD-specific fields to Order model (codFee, phoneVerified, deliveryAttempts, codCollectionStatus, codCollectedAt) - Create migration for COD fields - Add COD confirmation email template with Bengali and English support - Add sendCODConfirmationEmail service function - Update email template imports Co-authored-by: rafiqul4 <124497017+rafiqul4@users.noreply.github.com> --- .../migration.sql | 6 + prisma/schema.prisma | 7 + src/lib/email-service.ts | 43 ++++ src/lib/email-templates.ts | 210 ++++++++++++++++++ 4 files changed, 266 insertions(+) create mode 100644 prisma/migrations/20251211120000_add_cod_fields/migration.sql diff --git a/prisma/migrations/20251211120000_add_cod_fields/migration.sql b/prisma/migrations/20251211120000_add_cod_fields/migration.sql new file mode 100644 index 00000000..25a4fe8f --- /dev/null +++ b/prisma/migrations/20251211120000_add_cod_fields/migration.sql @@ -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); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e189ca62..12ede03b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -774,6 +774,13 @@ model Order { ipAddress String? + // COD-specific fields + codFee Float? // Cash on Delivery fee + phoneVerified Boolean @default(false) // Phone verification status + deliveryAttempts Int @default(0) // Number of delivery attempts + codCollectionStatus String? // PENDING, COLLECTED, FAILED, PARTIAL + codCollectedAt DateTime? // When cash was collected + items OrderItem[] createdAt DateTime @default(now()) diff --git a/src/lib/email-service.ts b/src/lib/email-service.ts index e95d1cfe..d2694d82 100644 --- a/src/lib/email-service.ts +++ b/src/lib/email-service.ts @@ -20,6 +20,7 @@ import { roleRejectedEmail, roleModificationRequestedEmail, orderConfirmationEmail, + codConfirmationEmail, } from './email-templates'; const resend = new Resend(process.env.RESEND_API_KEY); @@ -421,3 +422,45 @@ export async function sendOrderConfirmationEmail( return { success: false, error: 'Failed to send email' }; } } + +/** + * Send COD order confirmation email + */ +export async function sendCODConfirmationEmail( + to: string, + orderData: { + customerName: string; + orderNumber: string; + orderTotal: string; + codFee: string; + orderItems: Array<{ name: string; quantity: number; price: string }>; + shippingAddress: { address: string; city: string; state?: string; postalCode: string; country: string }; + estimatedDelivery: string; + storeName: string; + locale?: 'en' | 'bn'; + } +): Promise { + try { + const locale = orderData.locale || 'en'; + const subject = locale === 'bn' + ? `অর্ডার কনফার্মেশন #${orderData.orderNumber}` + : `Order Confirmation #${orderData.orderNumber}`; + + const { data, error } = await resend.emails.send({ + from: FROM_EMAIL, + to, + subject, + html: codConfirmationEmail({ ...orderData, appUrl: APP_URL, locale }), + }); + + if (error) { + console.error('Failed to send COD confirmation email:', error); + return { success: false, error: error.message }; + } + + return { success: true, id: data?.id }; + } catch (error) { + console.error('Email service error:', error); + return { success: false, error: 'Failed to send email' }; + } +} diff --git a/src/lib/email-templates.ts b/src/lib/email-templates.ts index afb3234d..6dc8f624 100644 --- a/src/lib/email-templates.ts +++ b/src/lib/email-templates.ts @@ -787,3 +787,213 @@ export function orderConfirmationEmail({ `.trim(); } + +/** + * COD (Cash on Delivery) Order Confirmation Email Template + * Supports both Bengali and English languages + */ +export function codConfirmationEmail({ + customerName, + orderNumber, + orderTotal, + codFee, + orderItems, + shippingAddress, + estimatedDelivery, + storeName, + locale = 'en', + appUrl = 'https://stormcom.app', +}: { + customerName: string; + orderNumber: string; + orderTotal: string; + codFee: string; + orderItems: Array<{ name: string; quantity: number; price: string }>; + shippingAddress: { address: string; city: string; state?: string; postalCode: string; country: string }; + estimatedDelivery: string; + storeName: string; + locale?: 'en' | 'bn'; + appUrl?: string; +}): string { + // Escape user-provided content to prevent XSS attacks + const safeCustomerName = escapeHtml(customerName); + const safeOrderNumber = escapeHtml(orderNumber); + const safeStoreName = escapeHtml(storeName); + const safeAddress = { + address: escapeHtml(shippingAddress.address), + city: escapeHtml(shippingAddress.city), + state: shippingAddress.state ? escapeHtml(shippingAddress.state) : undefined, + postalCode: escapeHtml(shippingAddress.postalCode), + country: escapeHtml(shippingAddress.country), + }; + + const itemsHtml = orderItems + .map( + (item) => ` + + ${escapeHtml(item.name)} + ${item.quantity} + ${escapeHtml(item.price)} + + ` + ) + .join(''); + + // Bengali version + if (locale === 'bn') { + return ` + + + + + + অর্ডার কনফার্মেশন - ${safeStoreName} + + + +
+
+ +
+
+

আপনার অর্ডার সফলভাবে সম্পন্ন হয়েছে! 🎉

+

প্রিয় ${safeCustomerName},

+

আমরা আপনার অর্ডার পেয়েছি এবং এটি প্রসেস করা হচ্ছে। বিস্তারিত তথ্য নিচে দেওয়া হলো:

+ +
+

অর্ডার বিবরণ

+

অর্ডার নম্বর: ${safeOrderNumber}

+

পেমেন্ট পদ্ধতি: ক্যাশ অন ডেলিভারি (হোম ডেলিভারি পেমেন্ট)

+

মোট টাকা: ${orderTotal}

+

ডেলিভারি চার্জ: ${codFee}

+
+ +

গুরুত্বপূর্ণ নির্দেশনা

+
+
    +
  • আপনার পণ্য ডেলিভারি নেওয়ার সময় নগদ টাকা পরিশোধ করুন
  • +
  • পণ্য ভালোভাবে চেক করার পর টাকা দিন
  • +
  • ডেলিভারি ম্যান থেকে অবশ্যই রসিদ নিন
  • +
  • সঠিক টাকা রাখার চেষ্টা করুন
  • +
+
+ +

আনুমানিক ডেলিভারি: ${escapeHtml(estimatedDelivery)}

+ +

অর্ডার আইটেম

+ + + + + + + + + + ${itemsHtml} + +
পণ্যপরিমাণমূল্য
+ +

ডেলিভারি ঠিকানা

+
+

+ ${safeAddress.address}
+ ${safeAddress.city}${safeAddress.state ? `, ${safeAddress.state}` : ''} ${safeAddress.postalCode}
+ ${safeAddress.country} +

+
+ +

পণ্য পাঠানোর পর আমরা আপনাকে আরেকটি ইমেইল পাঠাবো। কোন সমস্যা হলে আমাদের সাপোর্ট টিমের সাথে যোগাযোগ করুন।

+ +

+ দোকান ভিজিট করুন +

+
+ +
+ + + `.trim(); + } + + // English version + return ` + + + + + + Order Confirmation - ${safeStoreName} + + + +
+
+ +
+
+

Your Order Has Been Confirmed! 🎉

+

Dear ${safeCustomerName},

+

We've received your order and it's being processed. Here are the details:

+ +
+

Order Details

+

Order Number: ${safeOrderNumber}

+

Payment Method: Cash on Delivery

+

Total Amount: ${orderTotal}

+

COD Fee: ${codFee}

+
+ +

Important Instructions

+
+
    +
  • Pay cash when you receive your products
  • +
  • Inspect products carefully before payment
  • +
  • Get a receipt from the delivery person
  • +
  • Please keep exact cash ready
  • +
+
+ +

Estimated Delivery: ${escapeHtml(estimatedDelivery)}

+ +

Order Items

+ + + + + + + + + + ${itemsHtml} + +
ProductQuantityPrice
+ +

Shipping Address

+
+

+ ${safeAddress.address}
+ ${safeAddress.city}${safeAddress.state ? `, ${safeAddress.state}` : ''} ${safeAddress.postalCode}
+ ${safeAddress.country} +

+
+ +

We'll send you another email when your order ships. If you have any questions, please contact our support team.

+ +

+ Visit Store +

+
+ +
+ + + `.trim(); +} From fa51568b712b5ead5118426c88573b6320dd6bf5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 12:06:23 +0000 Subject: [PATCH 3/5] feat: Implement COD backend service and vendor dashboard components - Create CODService with order creation, phone validation, fraud prevention - Add COD collection tracking functionality with inventory restoration - Implement COD order creation API endpoint (/api/orders/cod) - Implement COD status update API endpoint (/api/orders/[id]/cod-status) - Create CODCollectionActions component for vendor dashboard - Add Bangladesh phone validation regex - Calculate COD fees dynamically (BDT 50 for orders <500) - Support first-time customer limits and fraud prevention - Fix type errors and lint issues in new code Co-authored-by: rafiqul4 <124497017+rafiqul4@users.noreply.github.com> --- src/app/api/orders/[id]/cod-status/route.ts | 126 +++++ src/app/api/orders/cod/route.ts | 98 ++++ src/components/checkout/cod-payment.tsx | 259 +++++++++ .../checkout/payment-method-step.tsx | 147 +++-- .../orders/cod-collection-actions.tsx | 293 ++++++++++ src/lib/services/cod.service.ts | 528 ++++++++++++++++++ 6 files changed, 1399 insertions(+), 52 deletions(-) create mode 100644 src/app/api/orders/[id]/cod-status/route.ts create mode 100644 src/app/api/orders/cod/route.ts create mode 100644 src/components/checkout/cod-payment.tsx create mode 100644 src/components/orders/cod-collection-actions.tsx create mode 100644 src/lib/services/cod.service.ts diff --git a/src/app/api/orders/[id]/cod-status/route.ts b/src/app/api/orders/[id]/cod-status/route.ts new file mode 100644 index 00000000..469a836a --- /dev/null +++ b/src/app/api/orders/[id]/cod-status/route.ts @@ -0,0 +1,126 @@ +/** + * 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, + { params }: { params: { 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 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, + }, + }); + } catch (error) { + console.error('COD status update error:', error); + + // Handle validation errors + if (error instanceof z.ZodError) { + return NextResponse.json( + { + success: false, + error: 'Validation failed', + details: error.issues, + }, + { status: 400 } + ); + } + + // Handle business logic errors + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to update COD status', + }, + { status: 400 } + ); + } +} diff --git a/src/app/api/orders/cod/route.ts b/src/app/api/orders/cod/route.ts new file mode 100644 index 00000000..42fea496 --- /dev/null +++ b/src/app/api/orders/cod/route.ts @@ -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'), + shippingAddress: z.object({ + address: z.string().min(1, 'Address is required'), + city: z.string().min(1, 'City is required'), + state: z.string().optional(), + postalCode: z.string().min(1, 'Postal code is required'), + country: z.string().min(1, 'Country is required'), + }), + billingAddress: z.string().optional(), + items: z.array( + z.object({ + productId: z.string(), + variantId: z.string().optional(), + quantity: z.number().int().positive(), + }) + ).min(1, 'At least one item is required'), + subtotal: z.number().positive('Subtotal must be positive'), + taxAmount: z.number().nonnegative('Tax amount must be non-negative').default(0), + shippingAmount: z.number().nonnegative('Shipping amount must be non-negative').default(0), + discountAmount: z.number().nonnegative('Discount amount must be non-negative').default(0), + codFee: z.number().nonnegative('COD fee must be non-negative'), + notes: z.string().optional(), + locale: z.enum(['en', 'bn']).optional().default('en'), + idempotencyKey: z.string().optional(), +}); + +export async function POST(req: NextRequest) { + try { + // Get session (optional for guest checkout) + const session = await getServerSession(authOptions); + + // Parse and validate request body + const body = await req.json(); + const validatedData = createCODOrderSchema.parse(body); + + // Create COD order + const order = await CODService.createCODOrder({ + ...validatedData, + userId: session?.user?.id, + }); + + return NextResponse.json( + { + success: true, + order: { + id: order.id, + orderNumber: order.orderNumber, + status: order.status, + totalAmount: order.totalAmount, + codFee: order.codFee, + estimatedDelivery: order.estimatedDelivery, + }, + }, + { status: 201 } + ); + } catch (error) { + console.error('COD order creation error:', error); + + // Handle validation errors + if (error instanceof z.ZodError) { + return NextResponse.json( + { + success: false, + error: 'Validation failed', + details: error.issues, + }, + { status: 400 } + ); + } + + // Handle business logic errors + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to create COD order', + }, + { status: 400 } + ); + } +} diff --git a/src/components/checkout/cod-payment.tsx b/src/components/checkout/cod-payment.tsx new file mode 100644 index 00000000..8f051716 --- /dev/null +++ b/src/components/checkout/cod-payment.tsx @@ -0,0 +1,259 @@ +/** + * Cash on Delivery Payment Component + * Handles COD payment option for Bangladesh market + */ + +'use client'; + +import { useState } from 'react'; +import { Banknote, Phone, Calendar, AlertCircle } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Separator } from '@/components/ui/separator'; + +interface CodPaymentProps { + subtotal: number; + shipping: number; + tax: number; + onPlaceOrder: (phoneNumber: string) => void; + isSubmitting: boolean; +} + +// Bangladesh phone validation: +880 or 880 or 01XXXXXXXXX +const BD_PHONE_REGEX = /^(\+880|880|0)?1[3-9]\d{8}$/; + +// COD fee: BDT 50 for orders < 500, free for >= 500 +const COD_FEE_THRESHOLD = 500; +const COD_FEE_AMOUNT = 50; + +export function CodPayment({ + subtotal, + shipping, + tax, + onPlaceOrder, + isSubmitting, +}: CodPaymentProps) { + const [phoneNumber, setPhoneNumber] = useState(''); + const [termsAccepted, setTermsAccepted] = useState(false); + const [phoneError, setPhoneError] = useState(''); + + // Calculate COD fee + const codFee = subtotal < COD_FEE_THRESHOLD ? COD_FEE_AMOUNT : 0; + const total = subtotal + shipping + tax + codFee; + + // Calculate estimated delivery date (5-7 days from now) + const estimatedDeliveryStart = new Date(); + estimatedDeliveryStart.setDate(estimatedDeliveryStart.getDate() + 5); + const estimatedDeliveryEnd = new Date(); + estimatedDeliveryEnd.setDate(estimatedDeliveryEnd.getDate() + 7); + + const formatDate = (date: Date) => { + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + }); + }; + + const validatePhone = (value: string) => { + if (!value) { + setPhoneError('Phone number is required'); + return false; + } + + if (!BD_PHONE_REGEX.test(value)) { + setPhoneError('Please enter a valid Bangladesh phone number (e.g., 01XXXXXXXXX)'); + return false; + } + + setPhoneError(''); + return true; + }; + + const handlePhoneChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setPhoneNumber(value); + + // Clear error when user types + if (phoneError) { + setPhoneError(''); + } + }; + + const handlePhoneBlur = () => { + if (phoneNumber) { + validatePhone(phoneNumber); + } + }; + + const handlePlaceOrder = () => { + if (validatePhone(phoneNumber) && termsAccepted) { + onPlaceOrder(phoneNumber); + } + }; + + const isValid = BD_PHONE_REGEX.test(phoneNumber) && termsAccepted; + + return ( +
+ {/* COD Information */} + + + +
+

নগদে পণ্য গ্রহণের সময় পেমেন্ট করুন

+

Pay cash when you receive the products at your doorstep

+
+
+
+ + {/* COD Fee Notice */} + {codFee > 0 && ( + + + +

+ ৳{COD_FEE_AMOUNT} COD charge applies for orders under ৳{COD_FEE_THRESHOLD}. + Orders ৳{COD_FEE_THRESHOLD} and above have free COD. +

+
+
+ )} + + {/* Phone Number Input */} + + +
+
+ + + {phoneError && ( +

+ + {phoneError} +

+ )} +

+ Enter your Bangladesh mobile number (01XXXXXXXXX format) +

+
+
+
+
+ + {/* Estimated Delivery */} + + +
+ +
+

Estimated Delivery

+

+ {formatDate(estimatedDeliveryStart)} - {formatDate(estimatedDeliveryEnd)} (5-7 business days) +

+

+ আনুমানিক ডেলিভারি: ৫-৭ কার্যদিবস +

+
+
+
+
+ + {/* Order Breakdown */} + + +
+
+ Subtotal (সাবটোটাল) + ৳{subtotal.toFixed(2)} +
+
+ Shipping (শিপিং) + ৳{shipping.toFixed(2)} +
+
+ Tax (ট্যাক্স) + ৳{tax.toFixed(2)} +
+ {codFee > 0 && ( +
+ COD Fee (নগদ পেমেন্ট চার্জ) + ৳{codFee.toFixed(2)} +
+ )} + +
+ Total (মোট) + ৳{total.toFixed(2)} +
+
+
+
+ + {/* Terms and Conditions */} + + +
+ setTermsAccepted(checked === true)} + aria-label="Accept terms and conditions" + /> +
+ +

+ I agree to pay cash upon receiving the products +

+
+
+
+
+ + {/* Place Order Button */} + + + {/* Security Notice */} +

+ আপনার অর্ডার নিশ্চিত করার জন্য আপনি আমাদের শর্তাবলী এবং গোপনীয়তা নীতির সাথে সম্মত হচ্ছেন +
+ By placing your order, you agree to our Terms of Service and Privacy Policy +

+
+ ); +} diff --git a/src/components/checkout/payment-method-step.tsx b/src/components/checkout/payment-method-step.tsx index 2d1d1277..df7beafc 100644 --- a/src/components/checkout/payment-method-step.tsx +++ b/src/components/checkout/payment-method-step.tsx @@ -6,13 +6,14 @@ 'use client'; import { useState } from 'react'; -import { Loader2, CreditCard, Building2 } from 'lucide-react'; +import { Loader2, CreditCard, Building2, Banknote } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; import type { ShippingAddress } from '@/app/checkout/page'; +import { CodPayment } from './cod-payment'; interface PaymentMethodStepProps { shippingAddress: ShippingAddress; @@ -29,8 +30,9 @@ export function PaymentMethodStep({ }: PaymentMethodStepProps) { const [paymentMethod, setPaymentMethod] = useState('card'); const [isSubmitting, setIsSubmitting] = useState(false); + const [codPhoneNumber, setCodPhoneNumber] = useState(''); - const handlePlaceOrder = async () => { + const handlePlaceOrder = async (phoneNumber?: string) => { setIsSubmitting(true); try { @@ -84,6 +86,8 @@ export function PaymentMethodStep({ // body: JSON.stringify({ // paymentIntentId: paymentIntent.id, // shippingAddress, + // paymentMethod, + // ...(paymentMethod === 'cod' && phoneNumber ? { codPhoneNumber: phoneNumber } : {}), // }), // }); @@ -175,6 +179,26 @@ export function PaymentMethodStep({ + + + +
+ + +
+
+
@@ -215,62 +239,81 @@ export function PaymentMethodStep({ )} + {paymentMethod === 'cod' && ( + { + setCodPhoneNumber(phoneNumber); + handlePlaceOrder(phoneNumber); + }} + isSubmitting={isSubmitting} + /> + )} + {/* Order Total */} - - -
-
- Subtotal - $99.98 -
-
- Shipping - $10.00 -
-
- Tax - $8.80 -
- -
- Total - $118.78 + {paymentMethod !== 'cod' && ( + + +
+
+ Subtotal + $99.98 +
+
+ Shipping + $10.00 +
+
+ Tax + $8.80 +
+ +
+ Total + $118.78 +
-
- - + + + )} {/* Actions */} -
- - -
+ {paymentMethod !== 'cod' && ( +
+ + +
+ )} {/* Security Notice */} -

- By placing your order, you agree to our Terms of Service and Privacy Policy. - Your payment information is encrypted and secure. -

+ {paymentMethod !== 'cod' && ( +

+ By placing your order, you agree to our Terms of Service and Privacy Policy. + Your payment information is encrypted and secure. +

+ )}
); } diff --git a/src/components/orders/cod-collection-actions.tsx b/src/components/orders/cod-collection-actions.tsx new file mode 100644 index 00000000..ffa73633 --- /dev/null +++ b/src/components/orders/cod-collection-actions.tsx @@ -0,0 +1,293 @@ +/** + * COD Collection Actions Component + * + * Vendor dashboard component for managing COD order collection status + * Allows vendors to mark orders as collected or failed + */ + +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Alert, + AlertDescription, + AlertTitle, +} from '@/components/ui/alert'; +import { Badge } from '@/components/ui/badge'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; +import { CheckCircle2, XCircle, Banknote, AlertCircle } from 'lucide-react'; +import { toast } from 'sonner'; + +export type CODCollectionStatus = 'PENDING' | 'COLLECTED' | 'FAILED'; + +interface CODCollectionActionsProps { + orderId: string; + orderNumber: string; + amount: number; + codFee: number; + currentStatus: CODCollectionStatus; + deliveryAttempts?: number; + onStatusUpdate?: () => void; +} + +export function CODCollectionActions({ + orderId, + orderNumber, + amount, + codFee, + currentStatus, + deliveryAttempts = 0, + onStatusUpdate, +}: CODCollectionActionsProps) { + const [showDialog, setShowDialog] = useState(false); + const [action, setAction] = useState<'COLLECTED' | 'FAILED' | null>(null); + const [loading, setLoading] = useState(false); + const [notes, setNotes] = useState(''); + + const totalAmount = amount + codFee; + + const handleAction = async () => { + if (!action) return; + + setLoading(true); + + try { + const response = await fetch(`/api/orders/${orderId}/cod-status`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + status: action, + collectedAmount: action === 'COLLECTED' ? totalAmount : undefined, + failureReason: + action === 'FAILED' ? 'Customer refused to pay' : undefined, + notes: notes || undefined, + }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to update collection status'); + } + + const data = await response.json(); + void data; // Unused, but response is valid + + const title = action === 'COLLECTED' ? 'Cash Collected' : 'Delivery Failed'; + const description = + action === 'COLLECTED' + ? `Successfully recorded cash collection for order ${orderNumber}` + : `Order ${orderNumber} marked as failed. Inventory has been restored.`; + + toast.success(title, { description }); + + setShowDialog(false); + setNotes(''); + onStatusUpdate?.(); + } catch (error) { + console.error('COD status update error:', error); + toast.error('Error', { + description: error instanceof Error ? error.message : 'Failed to update collection status', + }); + } finally { + setLoading(false); + } + }; + + // Render status badge for completed orders + if (currentStatus === 'COLLECTED') { + return ( +
+ + + Cash Collected + +

+ Payment received: ৳{totalAmount.toFixed(2)} +

+
+ ); + } + + if (currentStatus === 'FAILED') { + return ( +
+ + + Delivery Failed + +

+ Inventory has been restored +

+
+ ); + } + + // Render action buttons for pending orders + return ( + <> +
+ {/* COD Order Alert */} + + + + Cash on Delivery Order + + + Collect ৳{totalAmount.toFixed(2)} from customer upon delivery + {codFee > 0 && ( + + (Subtotal: ৳{amount.toFixed(2)} + COD Fee: ৳{codFee.toFixed(2)}) + + )} + + + + {/* Delivery Attempts Warning */} + {deliveryAttempts > 0 && ( + + + + Delivery attempts: {deliveryAttempts}/3 + {deliveryAttempts >= 2 && ( + + Warning: Final attempt - order will be canceled if failed + + )} + + + )} + + {/* Action Buttons */} +
+ + +
+
+ + {/* Confirmation Dialog */} + + + + + {action === 'COLLECTED' + ? 'Confirm Cash Collection' + : 'Confirm Delivery Failure'} + + + {action === 'COLLECTED' ? ( + <> + Confirm that you have collected{' '} + + ৳{totalAmount.toFixed(2)} + {' '} + cash from the customer for order{' '} + {orderNumber}. +
+
+ This will mark the order as completed and the payment as + received. + + ) : ( + <> + Mark order{' '} + {orderNumber} as + failed delivery. +
+
+ + Inventory will be restored automatically + {' '} + and the order will be canceled. + + )} +
+
+ + {/* Notes Textarea */} +
+ +