feat: add automatic payment/invoice status sync and invoice view page

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>
This commit is contained in:
2025-11-08 16:20:01 +01:00
parent f096b5f17f
commit 27da194942
16 changed files with 1092 additions and 139 deletions

View File

@@ -7,20 +7,39 @@ export default function HomePage() {
const [paymentId, setPaymentId] = useState<string>('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string>('')
const [customerName, setCustomerName] = useState<string>('Demo Customer')
const [customerEmail, setCustomerEmail] = useState<string>('demo@example.com')
const [customerCompany, setCustomerCompany] = useState<string>('Demo Company')
const [message, setMessage] = useState<string>('')
const createDemoPayment = async () => {
setLoading(true)
setError('')
// Validate required fields
if (!customerName || !customerEmail) {
setError('Customer name and email are required')
setLoading(false)
return
}
try {
const requestBody = {
amount: 2500,
currency: 'USD',
description: 'Demo payment from custom UI',
customerName,
customerEmail,
customerCompany: customerCompany || undefined,
message: message || undefined,
}
console.log('Sending payment request:', requestBody)
const response = await fetch('/api/demo/create-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: 2500,
currency: 'USD',
description: 'Demo payment from custom UI',
}),
body: JSON.stringify(requestBody),
})
const data = await response.json()
@@ -64,17 +83,80 @@ export default function HomePage() {
</h3>
{!paymentId ? (
<div>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="customerName" className="block text-sm font-medium text-slate-700 mb-2">
Customer Name <span className="text-red-500">*</span>
</label>
<input
type="text"
id="customerName"
value={customerName}
onChange={(e) => setCustomerName(e.target.value)}
placeholder="John Doe"
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required
/>
</div>
<div>
<label htmlFor="customerEmail" className="block text-sm font-medium text-slate-700 mb-2">
Customer Email <span className="text-red-500">*</span>
</label>
<input
type="email"
id="customerEmail"
value={customerEmail}
onChange={(e) => setCustomerEmail(e.target.value)}
placeholder="john@example.com"
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required
/>
</div>
</div>
<div>
<label htmlFor="customerCompany" className="block text-sm font-medium text-slate-700 mb-2">
Company Name (Optional)
</label>
<input
type="text"
id="customerCompany"
value={customerCompany}
onChange={(e) => setCustomerCompany(e.target.value)}
placeholder="Acme Corporation"
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-slate-700 mb-2">
Custom Message (Optional)
</label>
<textarea
id="message"
rows={3}
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Enter a message to include in the invoice..."
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
/>
<p className="mt-1 text-xs text-slate-500">
This message will be added to the invoice using collection extension options
</p>
</div>
<button
onClick={createDemoPayment}
disabled={loading}
className="bg-gradient-to-r from-blue-600 to-blue-700 text-white px-6 py-3 rounded-lg font-semibold hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 text-white px-6 py-3 rounded-lg font-semibold hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
>
{loading ? 'Creating Payment...' : 'Create Demo Payment'}
</button>
{error && (
<div className="mt-4 p-4 bg-red-50 border border-red-200 text-red-800 rounded-lg">
<div className="p-4 bg-red-50 border border-red-200 text-red-800 rounded-lg">
{error}
</div>
)}
@@ -102,6 +184,10 @@ export default function HomePage() {
onClick={() => {
setPaymentId('')
setError('')
setCustomerName('Demo Customer')
setCustomerEmail('demo@example.com')
setCustomerCompany('Demo Company')
setMessage('')
}}
className="bg-slate-200 text-slate-700 px-6 py-3 rounded-lg font-semibold hover:bg-slate-300 transition-all cursor-pointer"
>