Core Plugin Enhancements: - Add afterChange hook to payments collection to auto-update linked invoice status to 'paid' when payment succeeds - Add afterChange hook to invoices collection for bidirectional payment-invoice relationship management - Add invoice status sync when manually marked as paid - Update plugin config types to support collection extension options Demo Application Features: - Add professional invoice view page with print-friendly layout (/invoice/[id]) - Add custom message field to payment creation form - Add invoice API endpoint to fetch complete invoice data with customer info - Add payment API endpoint to retrieve payment with invoice relationship - Update payment success page with "View Invoice" button - Implement beforeChange hook to copy custom message from payment metadata to invoice - Remove customer collection dependency - use direct customerInfo fields instead Documentation: - Update README with automatic status synchronization section - Add collection extension examples to demo README - Document new features: bidirectional relationships, status sync, invoice view Technical Improvements: - Fix total calculation in invoice API (use 'amount' field instead of 'total') - Add proper TypeScript types with CollectionSlug casting - Implement Next.js 15 async params pattern in API routes - Add customer name/email/company fields to payment creation form 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Billing Plugin Demo Application
This is a demo application showcasing the @xtr-dev/payload-billing plugin for PayloadCMS 3.x.
Features
- 🧪 Test Payment Provider with customizable scenarios
- 💳 Payment Management with full CRUD operations
- 🧾 Invoice Generation with line items and tax calculation
- 🔄 Automatic Status Sync - payments and invoices stay in sync automatically
- 🔗 Bidirectional Relationships - payment/invoice links maintained by plugin hooks
- 🎨 Custom Payment UI with modern design
- 📄 Invoice View Page - professional printable invoice layout
- 🔧 Collection Extensions - demonstrates how to extend collections with custom fields and hooks
- 💬 Custom Message Field - shows hook-based data copying from payment to invoice
- 📊 No Customer Collection Required - uses direct customer info fields
Getting Started
Installation
# Install dependencies
pnpm install
Running the Demo
# Start the development server
pnpm dev
# The application will be available at http://localhost:3000
Default Credentials
- Email:
dev@payloadcms.com - Password:
test
Demo Routes
Interactive Demo Page
Visit http://localhost:3000 to access the interactive demo page where you can:
- Create test payments
- View the custom payment UI
- Test different payment scenarios
- Navigate to admin collections
Custom Payment UI
The custom test payment UI is available at:
http://localhost:3000/test-payment/{payment-id}
This page demonstrates:
- Modern, responsive payment interface
- Payment method selection
- Test scenario selection (success, failure, cancellation, etc.)
- Real-time payment status updates
- Test mode indicators and warnings
Invoice View Page
View and print invoices at:
http://localhost:3000/invoice/{invoice-id}
This page demonstrates:
- Professional printable invoice layout
- Customer billing information
- Line items table with quantities and amounts
- Tax calculations and totals
- Custom message field (populated from payment metadata)
- Print-friendly styling
Admin Routes
- Payments: http://localhost:3000/admin/collections/payments
- Invoices: http://localhost:3000/admin/collections/invoices
- Refunds: http://localhost:3000/admin/collections/refunds
- Customers: http://localhost:3000/admin/collections/customers
Sample Data
The application includes seed data with:
-
2 Customers
- John Doe (Acme Corporation)
- Jane Smith (Tech Innovations Inc.)
-
2 Invoices
- Paid invoice with web development services
- Open invoice with subscription and additional users
-
4 Payments
- Successful payment linked to invoice
- Pending payment linked to invoice
- Standalone successful payment
- Failed payment example
-
1 Refund
- Partial refund on a successful payment
To reset the sample data:
# Delete the database file
rm dev/payload.sqlite
# Restart the server (will re-seed automatically)
pnpm dev
Configuration
The plugin is configured in dev/payload.config.ts with:
Test Provider Setup
testProvider({
enabled: true,
testModeIndicators: {
showWarningBanners: true,
showTestBadges: true,
consoleWarnings: true
},
customUiRoute: '/test-payment',
})
Collection Extension Options
This demo showcases how to extend the plugin's collections with custom fields and hooks. The invoices collection is extended to include a customMessage field that is automatically populated from payment metadata:
collections: {
payments: 'payments',
invoices: {
slug: 'invoices',
extend: (config) => ({
...config,
fields: [
...(config.fields || []),
{
name: 'customMessage',
type: 'textarea',
admin: {
description: 'Custom message from the payment (auto-populated)',
},
},
],
hooks: {
...config.hooks,
beforeChange: [
...(config.hooks?.beforeChange || []),
async ({ data, req, operation }) => {
if (operation === 'create' && data.payment) {
const payment = await req.payload.findByID({
collection: 'payments',
id: typeof data.payment === 'object' ? data.payment.id : data.payment,
})
if (
payment?.metadata &&
typeof payment.metadata === 'object' &&
'customMessage' in payment.metadata &&
payment.metadata.customMessage
) {
data.customMessage = payment.metadata.customMessage as string
}
}
return data
},
],
},
}),
},
refunds: 'refunds',
}
Customer Relationship
customerRelationSlug: 'customers',
customerInfoExtractor: (customer) => ({
name: customer.name,
email: customer.email,
phone: customer.phone,
company: customer.company,
taxId: customer.taxId,
billingAddress: customer.address ? {
line1: customer.address.line1,
line2: customer.address.line2,
city: customer.address.city,
state: customer.address.state,
postalCode: customer.address.postalCode,
country: customer.address.country,
} : undefined,
})
Test Payment Scenarios
The test provider includes the following scenarios:
- Instant Success - Payment succeeds immediately
- Delayed Success - Payment succeeds after a delay (3s)
- Cancelled Payment - User cancels the payment (1s)
- Declined Payment - Payment is declined by the provider (2s)
- Expired Payment - Payment expires before completion (5s)
- Pending Payment - Payment remains in pending state (1.5s)
Payment Methods
The test provider supports these payment methods:
- 🏦 iDEAL
- 💳 Credit Card
- 🅿️ PayPal
- 🍎 Apple Pay
- 🏛️ Bank Transfer
API Examples
Creating a Payment (Local API)
import { getPayload } from 'payload'
import configPromise from '@payload-config'
const payload = await getPayload({ config: configPromise })
const payment = await payload.create({
collection: 'payments',
data: {
provider: 'test',
amount: 2500, // $25.00 in cents
currency: 'USD',
description: 'Demo payment',
status: 'pending',
}
})
// The payment will have a providerId that can be used in the custom UI
console.log(`Payment URL: /test-payment/${payment.providerId}`)
Creating an Invoice with Customer
const invoice = await payload.create({
collection: 'invoices',
data: {
customer: 'customer-id-here',
currency: 'USD',
items: [
{
description: 'Service',
quantity: 1,
unitAmount: 5000 // $50.00
}
],
taxAmount: 500, // $5.00
status: 'open'
}
})
REST API Example
# Create a payment
curl -X POST http://localhost:3000/api/payments \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"provider": "test",
"amount": 2500,
"currency": "USD",
"description": "Demo payment",
"status": "pending"
}'
Custom Routes
The demo includes custom API routes:
Create Payment
POST /api/demo/create-payment
Request body:
{
"amount": 2500,
"currency": "USD",
"description": "Demo payment",
"message": "Custom message to include in the invoice (optional)"
}
The message field will be stored in the payment's metadata and automatically copied to the invoice when it's created, thanks to the collection extension hook.
Response:
{
"success": true,
"payment": {
"id": "test_pay_1234567890_abc123",
"paymentId": "67890",
"amount": 2500,
"currency": "USD",
"description": "Demo payment"
}
}
Get Payment
GET /api/demo/payment/{payment-provider-id}
Fetches payment details including invoice relationship. Used by the payment success page to find the associated invoice.
Response:
{
"success": true,
"payment": {
"id": "67890",
"providerId": "test_pay_1234567890_abc123",
"amount": 2500,
"currency": "USD",
"status": "paid",
"description": "Demo payment",
"invoice": "invoice-id-here",
"metadata": {
"customMessage": "Your custom message"
}
}
}
Get Invoice
GET /api/demo/invoice/{invoice-id}
Fetches complete invoice data including customer details, line items, and custom message. Used by the invoice view page.
Response:
{
"success": true,
"invoice": {
"id": "invoice-id",
"invoiceNumber": "INV-2024-001",
"customer": {
"name": "John Doe",
"email": "john@example.com",
"company": "Acme Corp"
},
"currency": "USD",
"items": [
{
"description": "Service",
"quantity": 1,
"unitAmount": 2500
}
],
"subtotal": 2500,
"taxAmount": 250,
"total": 2750,
"status": "paid",
"customMessage": "Your custom message from payment"
}
}
Development
File Structure
dev/
├── app/
│ ├── page.tsx # Interactive demo page (root)
│ ├── test-payment/
│ │ └── [id]/
│ │ └── page.tsx # Custom payment UI
│ ├── invoice/
│ │ └── [id]/
│ │ └── page.tsx # Invoice view/print page
│ ├── payment-success/
│ │ └── page.tsx # Payment success page
│ ├── payment-failed/
│ │ └── page.tsx # Payment failed page
│ ├── api/
│ │ └── demo/
│ │ ├── create-payment/
│ │ │ └── route.ts # Payment creation endpoint
│ │ ├── invoice/
│ │ │ └── [id]/
│ │ │ └── route.ts # Invoice fetch endpoint
│ │ └── payment/
│ │ └── [id]/
│ │ └── route.ts # Payment fetch endpoint
│ └── (payload)/ # PayloadCMS admin routes
├── helpers/
│ └── credentials.ts # Default user credentials
├── payload.config.ts # PayloadCMS configuration
├── seed.ts # Sample data seeding
└── README.md # This file
Modifying the Demo
To customize the demo:
- Add more test scenarios: Edit the
testProviderconfig inpayload.config.ts - Customize the payment UI: Edit
app/test-payment/[id]/page.tsx - Add more sample data: Edit
seed.ts - Add custom collections: Add to
collectionsarray inpayload.config.ts
Testing Different Providers
To test with real payment providers:
// Install the provider
pnpm add stripe
// or
pnpm add @mollie/api-client
// Update payload.config.ts
import { stripeProvider, mollieProvider } from '../src/providers'
billingPlugin({
providers: [
stripeProvider({
secretKey: process.env.STRIPE_SECRET_KEY!,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
}),
mollieProvider({
apiKey: process.env.MOLLIE_API_KEY!,
webhookUrl: process.env.MOLLIE_WEBHOOK_URL,
}),
// Keep test provider for development
testProvider({ enabled: true }),
],
// ... rest of config
})
Troubleshooting
Database Issues
If you encounter database errors:
# Delete the database
rm dev/payload.sqlite
# Regenerate types
pnpm dev:generate-types
# Restart the server
pnpm dev
Port Already in Use
If port 3000 is already in use:
# Use a different port
PORT=3001 pnpm dev
TypeScript Errors
Regenerate Payload types:
pnpm dev:generate-types
Resources
License
MIT