-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Priority: P1
Phase: 1.5
Parent Epic: #28 (Bangladesh Payment Methods)
Estimate: 2 days
Type: Story
Overview
Implement Cash on Delivery (COD) payment option for Bangladesh market where 60-70% of e-commerce transactions are settled via cash at the time of delivery. This feature enables customers to order without upfront payment, with collection tracking and reconciliation workflows for vendors.
Context
COD is the dominant payment method in Bangladesh e-commerce:
- Adoption Rate: 60-70% of all online orders (2024 statistics)
- Customer Trust: Preferred due to credit card penetration <5%
- Risk Factor: Higher fraud rate (5-8% failed deliveries vs 1-2% prepaid)
- Cash Flow Impact: 3-7 days payment settlement vs instant digital
- Courier Integration: Required for collection tracking (Pathao, Steadfast, RedX)
- Rural Penetration: Essential for areas without banking infrastructure
Acceptance Criteria
-
Payment Method Selection
- ✅ COD option visible on checkout page (radio button)
- ✅ Display COD fee if applicable (e.g., BDT 50 for orders <500)
- ✅ Show estimated delivery time (2-5 days metro, 5-7 days outside)
- ✅ Terms & conditions checkbox ("আমি প্রোডাক্ট রিসিভ করার সময় টাকা প্রদান করব")
-
Order Creation Workflow
- ✅ Order created with status=PENDING, paymentMethod=COD
- ✅ PaymentAttempt record with provider=COD, status=PENDING
- ✅ No payment redirect (skip Stripe/bKash flow)
- ✅ Inventory reserved immediately (prevent overselling)
-
Email Notifications
- ✅ Order confirmation email to customer (Bengali + English)
- ✅ Include payment instructions: "আপনার পণ্য ডেলিভারি নেওয়ার সময় টাকা পরিশোধ করুন"
- ✅ Order amount breakdown (subtotal + shipping + COD fee)
- ✅ Estimated delivery date
- ✅ Customer support contact (phone + WhatsApp)
-
Vendor Dashboard
- ✅ COD orders flagged with badge ("Cash on Delivery")
- ✅ Collection status field (Pending, Collected, Failed, Partial)
- ✅ Mark as collected button (updates order status to COMPLETED)
- ✅ Failed delivery workflow (restore inventory, mark as CANCELLED)
-
Collection Tracking
- ✅ Track delivery partner collection status
- ✅ Reconciliation report (daily/weekly/monthly)
- ✅ Pending collections dashboard widget
- ✅ SMS reminder to customer 1 day before delivery
-
Failed Delivery Handling
- ✅ 3 delivery attempts before marking failed
- ✅ Customer confirmation call before each attempt
- ✅ Automatic inventory restoration on final failure
- ✅ Admin notification for high failure rate (>10% in 7 days)
-
Security & Fraud Prevention
- ✅ Phone verification required for COD orders
- ✅ OTP sent to customer mobile (6-digit code)
- ✅ Block repeat offenders (2+ failed deliveries in 30 days)
- ✅ Order value limit (e.g., max BDT 10,000 for first-time COD)
-
Reporting & Analytics
- ✅ COD conversion rate (vs prepaid)
- ✅ Failed delivery rate by area (identify risky zones)
- ✅ Average collection time
- ✅ Revenue by payment method comparison
-
Multi-Language Support
- ✅ Bengali labels: "ক্যাশ অন ডেলিভারি", "হোম ডেলিভারি পেমেন্ট"
- ✅ English fallback for non-Bengali browsers
- ✅ SMS templates in Bengali (delivery confirmation, OTP)
-
Courier Integration
- ✅ Automatically create COD parcel in Pathao/Steadfast
- ✅ Track delivery status via webhook
- ✅ Update payment status when courier confirms collection
- ✅ Handle partial collections (customer refuses some items)
Technical Implementation
1. COD Order Creation
// src/lib/services/order.service.ts (add COD support)
import { prisma } from '@/lib/prisma';
import { sendCODConfirmationEmail } from '@/lib/email/cod-confirmation';
interface CreateCODOrderParams {
storeId: string;
userId: string | null;
customerEmail: string;
customerPhone: string;
shippingAddress: Address;
items: CartItem[];
subtotalAmount: number;
shippingAmount: number;
codFee: number;
locale: 'en' | 'bn';
}
export async function createCODOrder(params: CreateCODOrderParams) {
const totalAmount = params.subtotalAmount + params.shippingAmount + params.codFee;
// Phone verification
const isPhoneVerified = await verifyPhoneNumber(params.customerPhone);
if (!isPhoneVerified) {
throw new Error('Phone verification required for COD orders');
}
// Check COD eligibility
const customerOrders = await prisma.order.count({
where: {
customerEmail: params.customerEmail,
status: 'CANCELLED',
paymentMethod: 'COD',
},
});
if (customerOrders >= 2) {
throw new Error('COD not available due to previous failed deliveries');
}
// First-time customer limit
const isFirstTime = await prisma.order.count({
where: { customerEmail: params.customerEmail },
}) === 0;
if (isFirstTime && totalAmount > 10000 * 100) { // 10,000 BDT in paisa
throw new Error('COD limit exceeded for first-time customers (max BDT 10,000)');
}
// Create order
const order = await prisma.$transaction(async (tx) => {
// Create order
const newOrder = await tx.order.create({
data: {
storeId: params.storeId,
userId: params.userId,
customerEmail: params.customerEmail,
customerPhone: params.customerPhone,
shippingAddress: params.shippingAddress,
subtotalAmount: params.subtotalAmount,
shippingAmount: params.shippingAmount,
totalAmount: totalAmount,
paymentMethod: 'COD',
status: 'PENDING',
items: {
create: params.items.map((item) => ({
productId: item.productId,
variantId: item.variantId,
quantity: item.quantity,
unitPrice: item.unitPrice,
totalPrice: item.unitPrice * item.quantity,
productName: item.productName,
variantName: item.variantName,
})),
},
},
include: { items: true },
});
// Create COD payment attempt
await tx.paymentAttempt.create({
data: {
orderId: newOrder.id,
provider: 'COD',
amount: totalAmount,
status: 'PENDING',
metadata: {
codFee: params.codFee,
phoneVerified: true,
deliveryAttempts: 0,
},
},
});
// Reserve inventory
for (const item of params.items) {
await tx.product.update({
where: { id: item.productId },
data: {
inventory: { decrement: item.quantity },
},
});
// Create inventory log
await tx.inventoryLog.create({
data: {
productId: item.productId,
storeId: params.storeId,
reason: 'ORDER_CREATED',
quantityChange: -item.quantity,
userId: params.userId,
metadata: { orderId: newOrder.id },
},
});
}
return newOrder;
});
// Send confirmation email
await sendCODConfirmationEmail({
to: params.customerEmail,
orderId: order.id,
totalAmount,
codFee: params.codFee,
estimatedDelivery: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000), // 5 days
locale: params.locale,
});
return order;
}
// Phone verification via OTP
async function verifyPhoneNumber(phone: string): Promise<boolean> {
// TODO: Integrate with SMS gateway (SSL Wireless)
// For now, assume verified if valid Bangladesh number
const bdPhoneRegex = /^(\+880|880|0)?1[3-9]\d{8}$/;
return bdPhoneRegex.test(phone);
}2. COD Email Template
// src/lib/email/cod-confirmation.ts
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
interface CODConfirmationParams {
to: string;
orderId: string;
totalAmount: number; // in paisa
codFee: number; // in paisa
estimatedDelivery: Date;
locale: 'en' | 'bn';
}
export async function sendCODConfirmationEmail(params: CODConfirmationParams) {
const totalAmountBDT = (params.totalAmount / 100).toFixed(2);
const codFeeBDT = (params.codFee / 100).toFixed(2);
const subject = params.locale === 'bn'
? `অর্ডার কনফার্মেশন #${params.orderId.slice(0, 8)}`
: `Order Confirmation #${params.orderId.slice(0, 8)}`;
const htmlBody = params.locale === 'bn' ? `
<h2>আপনার অর্ডার সফলভাবে সম্পন্ন হয়েছে!</h2>
<p>অর্ডার নম্বর: <strong>#${params.orderId.slice(0, 8)}</strong></p>
<h3>পেমেন্ট বিবরণ</h3>
<ul>
<li><strong>পেমেন্ট পদ্ধতি:</strong> ক্যাশ অন ডেলিভারি (হোম ডেলিভারি পেমেন্ট)</li>
<li><strong>মোট টাকা:</strong> ৳${totalAmountBDT}</li>
<li><strong>ডেলিভারি চার্জ:</strong> ৳${codFeeBDT}</li>
</ul>
<h3>গুরুত্বপূর্ণ নির্দেশনা</h3>
<p>✅ আপনার পণ্য ডেলিভারি নেওয়ার সময় নগদ টাকা পরিশোধ করুন</p>
<p>✅ পণ্য চেক করার পর টাকা দিন</p>
<p>✅ ডেলিভারি ম্যান থেকে রসিদ নিন</p>
<p><strong>আনুমানিক ডেলিভারি:</strong> ${params.estimatedDelivery.toLocaleDateString('bn-BD')}</p>
<p>কোন সমস্যা হলে যোগাযোগ করুন: 01XXX-XXXXXX (হোয়াটসঅ্যাপ সাপোর্ট)</p>
` : `
<h2>Your Order Has Been Confirmed!</h2>
<p>Order Number: <strong>#${params.orderId.slice(0, 8)}</strong></p>
<h3>Payment Details</h3>
<ul>
<li><strong>Payment Method:</strong> Cash on Delivery</li>
<li><strong>Total Amount:</strong> Tk ${totalAmountBDT}</li>
<li><strong>COD Fee:</strong> Tk ${codFeeBDT}</li>
</ul>
<h3>Important Instructions</h3>
<p>✅ Pay cash when you receive your products</p>
<p>✅ Inspect products before payment</p>
<p>✅ Get receipt from delivery person</p>
<p><strong>Estimated Delivery:</strong> ${params.estimatedDelivery.toLocaleDateString('en-US')}</p>
<p>For support, contact: 01XXX-XXXXXX (WhatsApp available)</p>
`;
await resend.emails.send({
from: process.env.EMAIL_FROM!,
to: params.to,
subject,
html: htmlBody,
});
}3. COD Payment Button Component
// src/components/cod-payment-button.tsx
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Phone, AlertCircle } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
interface CODPaymentButtonProps {
orderId: string;
amount: number;
codFee: number;
disabled?: boolean;
locale?: 'en' | 'bn';
}
export function CODPaymentButton({
orderId,
amount,
codFee,
disabled,
locale = 'en'
}: CODPaymentButtonProps) {
const [agreed, setAgreed] = useState(false);
const [loading, setLoading] = useState(false);
const { toast } = useToast();
const totalAmount = amount + codFee;
const labels = {
en: {
title: 'Cash on Delivery',
terms: 'I agree to pay cash upon receiving the products',
button: `Place Order - Tk ${totalAmount.toFixed(2)}`,
codFee: `COD Fee: Tk ${codFee.toFixed(2)}`,
info: 'You will pay when you receive your order. Please keep exact cash ready.',
},
bn: {
title: 'ক্যাশ অন ডেলিভারি',
terms: 'আমি প্রোডাক্ট রিসিভ করার সময় টাকা প্রদান করতে সম্মত',
button: `অর্ডার করুন - ৳${totalAmount.toFixed(2)}`,
codFee: `ডেলিভারি চার্জ: ৳${codFee.toFixed(2)}`,
info: 'আপনার অর্ডার পাওয়ার সময় টাকা দিতে হবে। দয়া করে সঠিক টাকা রাখুন।',
},
};
const t = labels[locale];
const handleConfirmOrder = async () => {
if (!agreed) {
toast({
title: locale === 'bn' ? 'শর্ত স্বীকার করুন' : 'Accept Terms',
description: locale === 'bn'
? 'অর্ডার নিশ্চিত করতে শর্ত স্বীকার করুন'
: 'Please accept the terms to confirm your order',
variant: 'destructive',
});
return;
}
setLoading(true);
try {
const response = await fetch('/api/orders/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
paymentMethod: 'COD',
orderId,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to create order');
}
const { order } = await response.json();
// Redirect to success page
window.location.href = `/order/${order.id}/success`;
} catch (error: any) {
console.error('COD order error:', error);
toast({
title: locale === 'bn' ? 'অর্ডার ব্যর্থ' : 'Order Failed',
description: error.message,
variant: 'destructive',
});
setLoading(false);
}
};
return (
<div className="space-y-4">
<Alert>
<Phone className="h-4 w-4" />
<AlertDescription>{t.info}</AlertDescription>
</Alert>
<div className="rounded-lg border p-4 space-y-2">
<div className="flex justify-between">
<span>{locale === 'bn' ? 'পণ্যের মূল্য' : 'Subtotal'}</span>
<span>৳{amount.toFixed(2)}</span>
</div>
<div className="flex justify-between text-sm text-muted-foreground">
<span>{t.codFee}</span>
<span>৳{codFee.toFixed(2)}</span>
</div>
<div className="flex justify-between font-bold pt-2 border-t">
<span>{locale === 'bn' ? 'মোট' : 'Total'}</span>
<span>৳{totalAmount.toFixed(2)}</span>
</div>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="cod-terms"
checked={agreed}
onCheckedChange={(checked) => setAgreed(checked === true)}
/>
<label
htmlFor="cod-terms"
className="text-sm cursor-pointer"
>
{t.terms}
</label>
</div>
<Button
onClick={handleConfirmOrder}
disabled={disabled || loading || !agreed}
className="w-full"
>
{loading ? (
locale === 'bn' ? 'প্রসেস করা হচ্ছে...' : 'Processing...'
) : (
t.button
)}
</Button>
</div>
);
}4. Vendor Dashboard - Collection Tracking
// src/app/dashboard/orders/[id]/page.tsx (add COD collection UI)
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { CheckCircle2, XCircle } from 'lucide-react';
interface CODCollectionActionsProps {
orderId: string;
amount: number;
currentStatus: 'PENDING' | 'COLLECTED' | 'FAILED';
onStatusUpdate: () => void;
}
export function CODCollectionActions({
orderId,
amount,
currentStatus,
onStatusUpdate
}: CODCollectionActionsProps) {
const [showDialog, setShowDialog] = useState(false);
const [action, setAction] = useState<'collected' | 'failed' | null>(null);
const [loading, setLoading] = useState(false);
const handleAction = async () => {
setLoading(true);
try {
const response = await fetch(`/api/orders/${orderId}/cod-status`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: action }),
});
if (!response.ok) {
throw new Error('Failed to update collection status');
}
setShowDialog(false);
onStatusUpdate();
} catch (error) {
console.error('COD status update error:', error);
} finally {
setLoading(false);
}
};
if (currentStatus === 'COLLECTED') {
return (
<Badge variant="success" className="gap-1">
<CheckCircle2 className="h-3 w-3" />
Cash Collected
</Badge>
);
}
if (currentStatus === 'FAILED') {
return (
<Badge variant="destructive" className="gap-1">
<XCircle className="h-3 w-3" />
Delivery Failed
</Badge>
);
}
return (
<>
<div className="flex gap-2">
<Button
variant="default"
onClick={() => {
setAction('collected');
setShowDialog(true);
}}
>
<CheckCircle2 className="mr-2 h-4 w-4" />
Mark as Collected
</Button>
<Button
variant="destructive"
onClick={() => {
setAction('failed');
setShowDialog(true);
}}
>
<XCircle className="mr-2 h-4 w-4" />
Delivery Failed
</Button>
</div>
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{action === 'collected' ? 'Confirm Cash Collection' : 'Confirm Delivery Failure'}
</DialogTitle>
<DialogDescription>
{action === 'collected'
? `Confirm that you have collected Tk ${(amount / 100).toFixed(2)} cash from the customer. This action will mark the order as completed.`
: `Mark this delivery as failed. Inventory will be restored and the order will be cancelled.`
}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setShowDialog(false)}>
Cancel
</Button>
<Button onClick={handleAction} disabled={loading}>
{loading ? 'Processing...' : 'Confirm'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}Dependencies
Blocks:
- None (can be deployed independently)
Blocked By:
- [Phase 1] Order Processing API #24 (Order Processing API) - Requires Order model and order creation workflow
- [Phase 1.5] Bengali Localization Infrastructure #31 (Bengali Localization) - COD emails should support Bengali language
Enhances:
- [Phase 1.5] Pathao Courier Integration #32 (Pathao Courier) - COD orders automatically create courier parcels
References
- Gap Analysis:
docs/research/codebase_feature_gap_analysis.md(PaymentAttempt model with COD provider) - Implementation Plan:
docs/research/implementation_plan.md(Order lifecycle, inventory restoration) - Marketing Automation V2:
docs/research/MARKETING_AUTOMATION_V2.md(COD adoption rate 60-70%, fraud prevention strategies) - Feature Roadmap:
docs/research/feature_roadmap_user_stories.md(As a customer, I can pay cash on delivery)
Testing Checklist
Order Creation Testing
- Create COD order with valid phone number
- Verify inventory reserved immediately
- Check PaymentAttempt record created with provider=COD
- Confirm email sent with Bengali + English instructions
- Test COD fee calculation (<500 = ৳50, >=500 = free)
Phone Verification Testing
- Valid Bangladesh number accepted (+8801XXXXXXXXX)
- Invalid format rejected (show error message)
- OTP sent via SMS (6-digit code)
- OTP verification within 5 minutes
- Resend OTP option (max 3 attempts)
Fraud Prevention Testing
- First-time customer limited to BDT 10,000
- Block customer with 2+ failed deliveries
- High-value orders (>BDT 20,000) require admin approval
- Suspicious address detection (incomplete/fake addresses)
Collection Tracking Testing
- Vendor marks order as collected
- Order status updates to COMPLETED
- PaymentAttempt status updates to SUCCEEDED
- Customer receives receipt email
Failed Delivery Testing
- Vendor marks delivery as failed
- Inventory restored automatically
- Order status updates to CANCELLED
- PaymentAttempt status updates to FAILED
- Customer notified via email/SMS
Reporting Testing
- COD vs prepaid conversion rate
- Failed delivery rate by area (Dhaka vs outside)
- Average delivery time
- Pending collections dashboard widget
Current Status
⏳ Not Started - Awaiting Phase 1 completion
Success Metrics
| Metric | Target | Measurement |
|---|---|---|
| COD Adoption Rate | >60% of orders | Track payment method distribution |
| Successful Delivery Rate | >92% | (Collected orders / Total COD orders) × 100 |
| Failed Delivery Rate | <8% | (Failed deliveries / Total COD orders) × 100 |
| Average Collection Time | <5 days | Time from order creation to collection |
| Fraud Rate | <2% | (Fraudulent orders / Total COD orders) × 100 |
Implementation Notes: COD is non-negotiable for Bangladesh market success. Despite higher operational costs (failed deliveries, cash handling), it unlocks 60-70% of customer base. Phone verification and fraud detection are critical to minimize losses. Consider partnering with courier services (Pathao, Steadfast) for cash collection handling to reduce operational burden.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status