Cash on Delivery (COD) payment adapter for Payload CMS Ecommerce Plugin
npm install @wtree/payload-ecommerce-cod



Cash on Delivery (COD) payment adapter for Payload CMS Ecommerce Plugin. This adapter follows the same structure as the official Stripe adapter and integrates seamlessly with Payload's ecommerce plugin.
- ✅ Full Payload CMS Ecommerce Plugin compatibility
- ✅ Configurable order limits (minimum/maximum)
- ✅ Regional availability controls
- ✅ Currency restrictions
- ✅ Optional service charges (percentage or fixed)
- ✅ Delivery status tracking
- ✅ Payment collection tracking
- ✅ Admin UI integration
- ✅ TypeScript support
- ✅ 80%+ test coverage
``bash`
npm install @wtree/payload-ecommerce-cod
Add the COD adapter to your Payload config:
`typescript
import { ecommercePlugin } from '@payloadcms/plugin-ecommerce'
import { codAdapter } from '@wtree/payload-ecommerce-cod'
export default buildConfig({
// ... other config
plugins: [
ecommercePlugin({
payments: {
paymentMethods: [
codAdapter({
label: 'Cash on Delivery',
minimumOrder: 100, // $1.00 in cents
maximumOrder: 50000, // $500.00 in cents
allowedRegions: ['US', 'CA', 'IN'],
supportedCurrencies: ['USD', 'INR'],
serviceChargePercentage: 2, // 2% service charge
fixedServiceCharge: 50, // $0.50 in cents
}),
],
},
}),
],
})
`
Add the COD adapter client to your React app:
`typescript
import { EcommerceProvider } from '@payloadcms/plugin-ecommerce/client'
import { codAdapterClient } from '@wtree/payload-ecommerce-cod'
function App() {
return (
codAdapterClient({
label: 'Cash on Delivery',
}),
]}
>
{/ Your app /}
)
}
`
| Option | Type | Description |
|--------|------|-------------|
| label | string | Display label for the payment method (default: "Cash on Delivery") |minimumOrder
| | number | Minimum order amount in smallest currency unit (e.g., cents) |maximumOrder
| | number | Maximum order amount in smallest currency unit |allowedRegions
| | string[] | Array of ISO 3166-1 alpha-2 country codes where COD is available |supportedCurrencies
| | string[] | Array of ISO 4217 currency codes supported for COD |serviceChargePercentage
| | number | Percentage service charge to add to orders |fixedServiceCharge
| | number | Fixed service charge in smallest currency unit |groupOverrides
| | object | Override default transaction fields |
| Option | Type | Description |
|--------|------|-------------|
| label | string | Display label for the payment method (default: "Cash on Delivery") |
The COD adapter adds the following fields to transactions:
- orderID: Unique COD order identifier
- validationStatus: Order validation status (pending, validated, rejected)
- deliveryStatus: Delivery tracking (preparing, dispatched, out_for_delivery, delivered, returned)
- paymentCollected: Boolean flag for payment collection
- collectionDate: Date when payment was collected
The adapter uses Payload's built-in ecommerce endpoints:
- POST /api/payments/cod/initiate - Initiate a COD orderPOST /api/payments/cod/confirm-order
- - Confirm and create order
`typescript
import { useEcommerce } from '@payloadcms/plugin-ecommerce/client'
function Checkout() {
const { initiatePayment, confirmOrder } = useEcommerce()
const handleCheckout = async () => {
try {
// Step 1: Initiate COD payment
const initResult = await initiatePayment('cod', {
additionalData: {
// Additional data if needed
},
})
console.log('COD Order ID:', initResult.orderID)
// Step 2: Confirm order to complete purchase
const confirmResult = await confirmOrder('cod', {
additionalData: {
orderID: initResult.orderID,
},
})
console.log('Order confirmed:', confirmResult.orderID)
} catch (error) {
console.error('Checkout failed:', error)
}
}
return (
)
}
`
For more control over the checkout flow, use the ecommerce hooks:
`typescript
import { useCart, useAddresses, usePayments } from '@payloadcms/plugin-ecommerce/client/react'
function AdvancedCheckout() {
const cart = useCart()
const { shippingAddress, billingAddress } = useAddresses()
const { initiatePayment, confirmOrder } = usePayments()
const handleCheckout = async () => {
try {
// Validate cart
if (!cart || !cart.items || cart.items.length === 0) {
throw new Error('Cart is empty')
}
// Validate addresses
if (!shippingAddress) {
throw new Error('Shipping address is required')
}
// Step 1: Initiate COD payment
const initResult = await initiatePayment('cod', {
cart,
shippingAddress,
billingAddress: billingAddress || shippingAddress,
})
console.log('Order initiated:', {
orderID: initResult.orderID,
serviceCharge: initResult.serviceCharge,
})
// Step 2: Confirm order
const confirmResult = await confirmOrder('cod', {
orderID: initResult.orderID,
})
console.log('Order confirmed:', {
orderID: confirmResult.orderID,
transactionID: confirmResult.transactionID,
})
// Order complete, redirect to success page
window.location.href = /order/${confirmResult.orderID}
} catch (error) {
console.error('Checkout failed:', error.message)
}
}
return (
Total: ${(cart?.subtotal / 100).toFixed(2)}
$3
`typescript
import { useState } from 'react'
import { useCart, usePayments } from '@payloadcms/plugin-ecommerce/client/react'function CheckoutWithValidation() {
const cart = useCart()
const { initiatePayment, confirmOrder } = usePayments()
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const handleCheckout = async (formData: FormData) => {
setLoading(true)
setError(null)
try {
// Step 1: Initiate payment
const initResult = await initiatePayment('cod', {
additionalData: {
notes: formData.get('notes'),
preferredDeliveryDate: formData.get('deliveryDate'),
},
})
// Step 2: Confirm order
const confirmResult = await confirmOrder('cod', {
orderID: initResult.orderID,
})
// Success - clear cart and redirect
console.log('Order placed successfully:', confirmResult.orderID)
return confirmResult.orderID
} catch (err) {
const message = err instanceof Error ? err.message : 'Checkout failed'
setError(message)
console.error('Checkout error:', err)
} finally {
setLoading(false)
}
}
return (
)
}
`Admin Interface
The adapter integrates with Payload's admin UI, showing COD-specific fields in the transactions collection:
- View and update delivery status
- Track payment collection
- Manage order validation
Validation
The adapter performs automatic validation:
- ✅ Currency support check
- ✅ Order amount limits (min/max)
- ✅ Regional availability
- ✅ Service charge calculation
- ✅ Cart and customer data validation
Development
`bash
Install dependencies
npm installRun tests
npm testRun tests in watch mode
npm run test:watchRun tests with coverage
npm run test:coverageBuild the package
npm run buildWatch mode for development
npm run devLint code
npm run lintFormat code
npm run format
`Publishing
$3
This package uses GitHub Actions for automated publishing:
1. Create a release using the workflow:
- Go to Actions → Release workflow
- Click "Run workflow"
- Select version type (patch/minor/major) or specify version
- The workflow will automatically:
- Run tests
- Build the package
- Bump version
- Create git tag
- Trigger npm publish
2. Or manually tag a version:
`bash
npm version patch # or minor, major
git push origin main --follow-tags
`$3
`bash
Prepare release
./scripts/prepare-release.sh patch # or minor, majorReview changes
git logPush to trigger automated publish
git push origin main --follow-tags
`$3
To enable automated publishing, add the following secrets to your GitHub repository:
1. NPM_TOKEN: Your npm access token
- Generate at: https://www.npmjs.com/settings/YOUR_USERNAME/tokens
- Add to: Repository Settings → Secrets → Actions
2. CODECOV_TOKEN (optional): For coverage reporting
- Generate at: https://codecov.io/gh/technewwings/payload-ecommerce-cod
- Add to: Repository Settings → Secrets → Actions
Comparison with Stripe Adapter
This adapter follows the exact same structure as Payload's official Stripe adapter:
- ✅ Implements
PaymentAdapter interface
- ✅ Provides initiatePayment and confirmOrder methods
- ✅ Uses GroupField for admin UI integration
- ✅ Supports transaction tracking
- ✅ Compatible with Payload's ecommerce hooksImplementation Verification
$3
The adapter correctly implements the
PaymentAdapter interface with:1. initiatePayment: Creates a transaction and validates:
- Currency support (if
supportedCurrencies is configured)
- Order amount limits (minimum and maximum)
- Regional availability (if allowedRegions is configured)
- Calculates service charges (percentage and/or fixed)
- Generates unique COD order ID2. confirmOrder: Completes the payment flow:
- Verifies existing transaction by COD order ID
- Creates order with cart items and addresses
- Updates cart as purchased
- Updates transaction status to 'succeeded'
- Returns order and transaction IDs
3. Transaction Fields:
-
cod.orderID: Unique COD identifier
- cod.validationStatus: Order validation state
- cod.deliveryStatus: Delivery tracking
- cod.paymentCollected: Payment confirmation
- cod.collectionDate: Payment date$3
The adapter provides a client-compatible implementation with:
1. codAdapterClient: Exposes payment method capabilities
-
name: 'cod' (payment method identifier)
- label: Display name (customizable)
- initiatePayment: Boolean flag indicating support
- confirmOrder: Boolean flag indicating support2. Hook Compatibility: Works with Payload ecommerce hooks
-
usePayments(): Initiates and confirms payments
- useCart(): Access cart data and amounts
- useAddresses(): Access shipping/billing addresses
- useEcommerce(): Generic ecommerce context$3
All exports are properly typed:
`typescript
export type CODAdapterArgs = { / configuration options / }
export type CODAdapterClientArgs = PaymentAdapterClientArgs
export type InitiatePaymentReturnType = {
message: string
orderID: string
serviceCharge?: number
}
export type ConfirmOrderReturnType = {
message: string
orderID: string
transactionID: string
}
``See CONTRIBUTING.md for development guidelines.
MIT - See LICENSE for details.
For issues and questions:
- 🐛 Report a bug
- 💬 Start a discussion
- 📖 Read the docs
See CHANGELOG.md for version history.