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
150 lines
3.7 KiB
TypeScript
150 lines
3.7 KiB
TypeScript
/**
|
|
* Cash on Delivery (COD) payment logic
|
|
* Handles creation of COD transactions in Saleor
|
|
*/
|
|
|
|
import type { Money, TransactionInput } from '@/lib/saleor/payments/types';
|
|
import { generateCODReference } from '@/lib/config/paymentMethods';
|
|
|
|
import { gql } from "@apollo/client";
|
|
|
|
/**
|
|
* GraphQL mutation to create a transaction on an order
|
|
*/
|
|
export const CREATE_TRANSACTION_MUTATION = gql`
|
|
mutation TransactionCreate($id: ID!, $transaction: TransactionCreateInput!) {
|
|
transactionCreate(id: $id, transaction: $transaction) {
|
|
transaction {
|
|
id
|
|
name
|
|
pspReference
|
|
status
|
|
availableActions
|
|
amountAuthorized {
|
|
amount
|
|
currency
|
|
}
|
|
amountCharged {
|
|
amount
|
|
currency
|
|
}
|
|
}
|
|
errors {
|
|
field
|
|
message
|
|
code
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
/**
|
|
* Create a Cash on Delivery transaction configuration
|
|
* @param orderNumber - The order number for reference
|
|
* @param amount - The order total amount
|
|
* @returns TransactionInput for Saleor
|
|
*/
|
|
export function createCODTransactionInput(
|
|
orderNumber: string,
|
|
amount: Money
|
|
): TransactionInput {
|
|
return {
|
|
name: 'Cash on Delivery',
|
|
pspReference: generateCODReference(orderNumber),
|
|
availableActions: ['CHARGE'],
|
|
amountAuthorized: {
|
|
amount: 0,
|
|
currency: amount.currency,
|
|
},
|
|
amountCharged: {
|
|
amount: 0,
|
|
currency: amount.currency,
|
|
},
|
|
externalUrl: null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create COD transaction on an order
|
|
* This should be called after checkoutComplete creates the order
|
|
*
|
|
* @param orderId - Saleor order ID
|
|
* @param orderNumber - Human-readable order number
|
|
* @param amount - Order total amount
|
|
* @returns Promise with transaction result
|
|
*/
|
|
export async function createCODTransaction(
|
|
orderId: string,
|
|
orderNumber: string,
|
|
amount: Money
|
|
): Promise<{ success: boolean; transaction?: unknown; errors?: unknown[] }> {
|
|
try {
|
|
// Note: This function should be called from a Server Component or API route
|
|
// as it requires making a GraphQL mutation with authentication
|
|
|
|
const transactionInput = createCODTransactionInput(orderNumber, amount);
|
|
|
|
// The actual GraphQL call will be made in the checkout page
|
|
// This function just prepares the input
|
|
return {
|
|
success: true,
|
|
transaction: {
|
|
orderId,
|
|
...transactionInput,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error('Error creating COD transaction:', error);
|
|
return {
|
|
success: false,
|
|
errors: [{ message: 'Failed to create COD transaction' }],
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if an order has a COD transaction
|
|
* @param order - Order object from Saleor
|
|
* @returns boolean
|
|
*/
|
|
export function hasCODTransaction(order: { transactions?: Array<{ name?: string }> }): boolean {
|
|
if (!order.transactions || order.transactions.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
return order.transactions.some(
|
|
(t) => t.name === 'Cash on Delivery'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get COD transaction from order
|
|
* @param order - Order object from Saleor
|
|
* @returns COD transaction or undefined
|
|
*/
|
|
export function getCODTransaction(order: { transactions?: Array<{ name?: string }> }) {
|
|
if (!order.transactions) return undefined;
|
|
|
|
return order.transactions.find(
|
|
(t) => t.name === 'Cash on Delivery'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Format COD status for display
|
|
* @param transactionStatus - Transaction status from Saleor
|
|
* @returns Human-readable status
|
|
*/
|
|
export function formatCODStatus(transactionStatus: string): string {
|
|
switch (transactionStatus) {
|
|
case 'NOT_CHARGED':
|
|
return 'Pending Collection';
|
|
case 'CHARGED':
|
|
return 'Paid';
|
|
case 'CANCELLED':
|
|
return 'Cancelled';
|
|
default:
|
|
return transactionStatus;
|
|
}
|
|
}
|