feat: implement Cash on Delivery (COD) payment method
This commit adds comprehensive COD support using Saleor's native Transaction system:
**Architecture:**
- Uses Saleor's native Transaction objects (not metadata)
- Modular payment method configuration
- Extensible design for future payment types (cards, bank transfer, etc.)
**New Components:**
- PaymentMethodSelector: Reusable payment method selection UI
- PaymentMethodCard: Individual payment method card
- CODInstructions: COD-specific instructions and guidance
- PaymentSection: Checkout integration wrapper
**Core Features:**
- COD selected by default for Serbia (default-channel)
- Transaction created automatically on order completion
- Transaction visible in Saleor Dashboard
- Multi-language support (EN, SR, DE, FR)
- No additional fees
- Instructions shown to customer (prepare cash, inspect order, no fee)
**Files Added:**
- docs/COD-IMPLEMENTATION-PLAN.md
- src/lib/config/paymentMethods.ts
- src/lib/saleor/payments/types.ts
- src/lib/saleor/payments/cod.ts
- src/components/payment/PaymentMethodSelector.tsx
- src/components/payment/PaymentMethodCard.tsx
- src/components/payment/CODInstructions.tsx
- src/components/payment/index.ts
- src/app/[locale]/checkout/components/PaymentSection.tsx
**Files Modified:**
- src/app/[locale]/checkout/page.tsx (added payment section, transaction creation)
- src/i18n/messages/{en,sr,de,fr}.json (payment translations)
**Technical Details:**
- Transaction status: NOT_CHARGED
- Available actions: [CHARGE]
- PSP Reference format: COD-{orderNumber}-{timestamp}
- Staff collects cash and fulfills order via Dashboard
Closes: Cash on Delivery payment implementation
This commit is contained in:
84
src/components/payment/PaymentMethodCard.tsx
Normal file
84
src/components/payment/PaymentMethodCard.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { PaymentMethod } from "@/lib/saleor/payments/types";
|
||||
import { Banknote, CreditCard, Building2, LucideIcon } from "lucide-react";
|
||||
|
||||
// Icon mapping for payment methods
|
||||
const iconMap: Record<string, LucideIcon> = {
|
||||
Banknote,
|
||||
CreditCard,
|
||||
Building2,
|
||||
};
|
||||
|
||||
interface PaymentMethodCardProps {
|
||||
method: PaymentMethod;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
disabled?: boolean;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export function PaymentMethodCard({
|
||||
method,
|
||||
isSelected,
|
||||
onSelect,
|
||||
disabled = false,
|
||||
locale,
|
||||
}: PaymentMethodCardProps) {
|
||||
const Icon = method.icon ? iconMap[method.icon] : Banknote;
|
||||
|
||||
return (
|
||||
<label
|
||||
className={cn(
|
||||
"relative flex cursor-pointer items-start gap-3 rounded-lg border p-4 transition-all",
|
||||
"hover:border-gray-400",
|
||||
isSelected && "border-black bg-gray-50",
|
||||
!isSelected && "border-gray-200 bg-white",
|
||||
(disabled || !method.available) && "cursor-not-allowed opacity-50"
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="payment-method"
|
||||
value={method.id}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
disabled={disabled || !method.available}
|
||||
className="sr-only"
|
||||
/>
|
||||
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gray-100">
|
||||
{Icon && <Icon className="h-5 w-5 text-gray-600" />}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-gray-900">{method.name}</span>
|
||||
{method.fee > 0 && (
|
||||
<span className="text-sm text-amber-600">
|
||||
+{new Intl.NumberFormat(locale === 'sr' ? 'sr-RS' : 'en-US', {
|
||||
style: 'currency',
|
||||
currency: 'RSD',
|
||||
}).format(method.fee)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="mt-1 text-sm text-gray-600">{method.description}</p>
|
||||
|
||||
{!method.available && (
|
||||
<span className="mt-2 inline-block text-xs text-gray-500">
|
||||
{locale === 'sr' ? 'Uskoro dostupno' : 'Coming soon'}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{isSelected && (
|
||||
<div className="absolute right-4 top-4">
|
||||
<div className="h-2 w-2 rounded-full bg-black" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user