mirror of
https://github.com/xtr-dev/payload-billing.git
synced 2025-12-10 10:53:23 +00:00
feat: add comprehensive demo application with custom payment UI
- Custom test payment UI with modern Tailwind CSS design - Payment method selection (iDEAL, Credit Card, PayPal, Apple Pay, Bank Transfer) - Test scenario selection (6 scenarios: success, delayed, cancelled, declined, expired, pending) - Real-time payment status polling - Success and failure result pages with payment details - Interactive demo homepage at root path - Sample data seeding (customers, invoices) - Customers collection with auto-sync to invoices - Comprehensive documentation (README.md, DEMO_GUIDE.md) - Proper cursor styles for all interactive elements 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
223
dev/DEMO_GUIDE.md
Normal file
223
dev/DEMO_GUIDE.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# Demo Project Quick Start Guide
|
||||
|
||||
This guide will help you quickly get started with the billing plugin demo.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. **Install dependencies** (if not already done):
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. **Start the development server**:
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
3. **Access the demo**:
|
||||
- Open [http://localhost:3000](http://localhost:3000)
|
||||
- Login with `dev@payloadcms.com` / `test` if prompted
|
||||
|
||||
## 🎯 What's Included
|
||||
|
||||
### Custom Test Payment UI
|
||||
A beautiful, modern payment interface built with React and Tailwind CSS that demonstrates:
|
||||
- Payment method selection (iDEAL, Credit Card, PayPal, Apple Pay, Bank Transfer)
|
||||
- Test scenario selection (success, failure, cancellation, etc.)
|
||||
- Real-time payment status updates
|
||||
- Test mode indicators and warnings
|
||||
- Responsive design
|
||||
|
||||
**Location**: `/dev/app/test-payment/[id]/page.tsx`
|
||||
|
||||
### Interactive Demo Page
|
||||
A landing page that showcases the plugin features and allows you to:
|
||||
- Create test payments with one click
|
||||
- Navigate to custom payment UI
|
||||
- Access admin collections
|
||||
- Learn about the plugin features
|
||||
|
||||
**Location**: `/dev/app/page.tsx`
|
||||
|
||||
### Customer Management
|
||||
Full customer collection with:
|
||||
- Name, email, phone, company
|
||||
- Tax ID support
|
||||
- Complete address fields
|
||||
- Auto-sync with invoices via `customerInfoExtractor`
|
||||
|
||||
**Location**: Configured in `/dev/payload.config.ts`
|
||||
|
||||
### Sample Data
|
||||
Comprehensive seed data including:
|
||||
- 2 sample customers
|
||||
- 2 invoices (1 paid, 1 open)
|
||||
- 4 payments (various statuses)
|
||||
- 1 refund
|
||||
|
||||
**Location**: `/dev/seed.ts`
|
||||
|
||||
### Custom API Routes
|
||||
Demo API endpoint for creating payments:
|
||||
- `POST /api/demo/create-payment`
|
||||
|
||||
**Location**: `/dev/app/api/demo/create-payment/route.ts`
|
||||
|
||||
## 🧪 Testing the Flow
|
||||
|
||||
### Complete Payment Flow Test
|
||||
|
||||
1. **Go to the demo page**: [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
2. **Click "Create Demo Payment"** - This creates a test payment
|
||||
|
||||
3. **Click "Go to Payment Page"** - Opens the custom payment UI
|
||||
|
||||
4. **Select a payment method** - Choose any method (e.g., Credit Card)
|
||||
|
||||
5. **Select a test scenario** - Try different scenarios:
|
||||
- **Instant Success**: See immediate payment success
|
||||
- **Delayed Success**: See processing indicator, then success
|
||||
- **Declined Payment**: See failure handling
|
||||
- **Cancelled Payment**: See cancellation flow
|
||||
|
||||
6. **Click "Process Test Payment"** - Watch the payment process
|
||||
|
||||
7. **View in admin** - After success, you'll be redirected to the payments list
|
||||
|
||||
### Testing with Different Scenarios
|
||||
|
||||
Each scenario simulates a different payment outcome:
|
||||
|
||||
| Scenario | Delay | Outcome | Use Case |
|
||||
|----------|-------|---------|----------|
|
||||
| Instant Success | 0ms | Success | Testing happy path |
|
||||
| Delayed Success | 3s | Success | Testing async processing |
|
||||
| Cancelled Payment | 1s | Cancelled | Testing user cancellation |
|
||||
| Declined Payment | 2s | Failed | Testing payment failures |
|
||||
| Expired Payment | 5s | Cancelled | Testing timeout handling |
|
||||
| Pending Payment | 1.5s | Pending | Testing long-running payments |
|
||||
|
||||
## 📊 Viewing Data
|
||||
|
||||
### Admin Collections
|
||||
|
||||
After running the demo, explore the seeded data:
|
||||
|
||||
1. **Payments** ([http://localhost:3000/admin/collections/payments](http://localhost:3000/admin/collections/payments))
|
||||
- View all payment transactions
|
||||
- See payment statuses and provider data
|
||||
- Check linked invoices
|
||||
|
||||
2. **Invoices** ([http://localhost:3000/admin/collections/invoices](http://localhost:3000/admin/collections/invoices))
|
||||
- View generated invoices
|
||||
- See line items and totals
|
||||
- Check customer relationships
|
||||
|
||||
3. **Refunds** ([http://localhost:3000/admin/collections/refunds](http://localhost:3000/admin/collections/refunds))
|
||||
- View processed refunds
|
||||
- See refund amounts and reasons
|
||||
|
||||
4. **Customers** ([http://localhost:3000/admin/collections/customers](http://localhost:3000/admin/collections/customers))
|
||||
- View customer information
|
||||
- Edit customer details (invoices will auto-update!)
|
||||
|
||||
## 🔧 Configuration Highlights
|
||||
|
||||
### Plugin Configuration
|
||||
```typescript
|
||||
billingPlugin({
|
||||
providers: [
|
||||
testProvider({
|
||||
enabled: true,
|
||||
customUiRoute: '/test-payment', // Custom UI route
|
||||
testModeIndicators: {
|
||||
showWarningBanners: true,
|
||||
showTestBadges: true,
|
||||
consoleWarnings: true
|
||||
}
|
||||
})
|
||||
],
|
||||
collections: {
|
||||
payments: 'payments',
|
||||
invoices: 'invoices',
|
||||
refunds: 'refunds',
|
||||
},
|
||||
customerRelationSlug: 'customers',
|
||||
customerInfoExtractor: (customer) => ({
|
||||
// Auto-extract customer info for invoices
|
||||
name: customer.name,
|
||||
email: customer.email,
|
||||
// ... more fields
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
## 🎨 Customization Ideas
|
||||
|
||||
### 1. Modify the Payment UI
|
||||
Edit `/dev/app/test-payment/[id]/page.tsx` to:
|
||||
- Change colors and styling
|
||||
- Add your brand logo
|
||||
- Modify the layout
|
||||
- Add additional fields
|
||||
|
||||
### 2. Add More Test Scenarios
|
||||
Edit `testProvider` config to add custom scenarios:
|
||||
```typescript
|
||||
testProvider({
|
||||
scenarios: [
|
||||
{
|
||||
id: 'custom-scenario',
|
||||
name: 'Custom Scenario',
|
||||
description: 'Your custom test scenario',
|
||||
outcome: 'paid',
|
||||
delay: 2000
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Create Invoice Templates
|
||||
Add invoice generation endpoints that use specific templates
|
||||
|
||||
### 4. Add Webhooks
|
||||
Create webhook handlers to process real payment events
|
||||
|
||||
## 💡 Tips
|
||||
|
||||
- **Reset Data**: Delete `dev/payload.sqlite` and restart to re-seed
|
||||
- **Check Console**: Test provider logs all events to the console
|
||||
- **Test Mode Warnings**: Notice the warning banners and badges in test mode
|
||||
- **Auto-sync**: Edit a customer's info and see invoices update automatically
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Payment not processing?
|
||||
- Check browser console for errors
|
||||
- Check server console for logs
|
||||
- Verify the test provider is enabled in config
|
||||
|
||||
### Custom UI not loading?
|
||||
- Check that `customUiRoute` matches your page route
|
||||
- Verify the payment ID is valid (starts with `test_pay_`)
|
||||
|
||||
### Types not matching?
|
||||
Run `pnpm dev:generate-types` to regenerate Payload types
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
1. **Explore the Admin** - Login and browse the collections
|
||||
2. **Create Custom Invoices** - Try creating invoices with line items
|
||||
3. **Process Refunds** - Create refunds for successful payments
|
||||
4. **Add Real Providers** - Configure Stripe or Mollie (see README.md)
|
||||
5. **Build Your Integration** - Use this as a template for your app
|
||||
|
||||
## 🎓 Learning Resources
|
||||
|
||||
- Review `/dev/seed.ts` for data structure examples
|
||||
- Check `/dev/payload.config.ts` for plugin configuration
|
||||
- See `/dev/app/test-payment/[id]/page.tsx` for UI integration
|
||||
- Read the main [README.md](../README.md) for API documentation
|
||||
|
||||
Happy testing! 🚀
|
||||
350
dev/README.md
Normal file
350
dev/README.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# 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
|
||||
- 👥 **Customer Management** with relationship support
|
||||
- 🔄 **Refund Processing** and tracking
|
||||
- 🎨 **Custom Payment UI** with modern design
|
||||
- 📊 **Sample Data** for quick testing
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Running the Demo
|
||||
|
||||
```bash
|
||||
# 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](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
|
||||
|
||||
### Admin Routes
|
||||
|
||||
- **Payments**: [http://localhost:3000/admin/collections/payments](http://localhost:3000/admin/collections/payments)
|
||||
- **Invoices**: [http://localhost:3000/admin/collections/invoices](http://localhost:3000/admin/collections/invoices)
|
||||
- **Refunds**: [http://localhost:3000/admin/collections/refunds](http://localhost:3000/admin/collections/refunds)
|
||||
- **Customers**: [http://localhost:3000/admin/collections/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:
|
||||
```bash
|
||||
# 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
|
||||
```typescript
|
||||
testProvider({
|
||||
enabled: true,
|
||||
testModeIndicators: {
|
||||
showWarningBanners: true,
|
||||
showTestBadges: true,
|
||||
consoleWarnings: true
|
||||
},
|
||||
customUiRoute: '/test-payment',
|
||||
})
|
||||
```
|
||||
|
||||
### Customer Relationship
|
||||
```typescript
|
||||
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:
|
||||
|
||||
1. **Instant Success** - Payment succeeds immediately
|
||||
2. **Delayed Success** - Payment succeeds after a delay (3s)
|
||||
3. **Cancelled Payment** - User cancels the payment (1s)
|
||||
4. **Declined Payment** - Payment is declined by the provider (2s)
|
||||
5. **Expired Payment** - Payment expires before completion (5s)
|
||||
6. **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)
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
```json
|
||||
{
|
||||
"amount": 2500,
|
||||
"currency": "USD",
|
||||
"description": "Demo payment"
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"payment": {
|
||||
"id": "test_pay_1234567890_abc123",
|
||||
"paymentId": "67890",
|
||||
"amount": 2500,
|
||||
"currency": "USD",
|
||||
"description": "Demo payment"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
dev/
|
||||
├── app/
|
||||
│ ├── page.tsx # Interactive demo page (root)
|
||||
│ ├── test-payment/
|
||||
│ │ └── [id]/
|
||||
│ │ └── page.tsx # Custom payment UI
|
||||
│ ├── api/
|
||||
│ │ └── demo/
|
||||
│ │ └── create-payment/
|
||||
│ │ └── route.ts # Payment creation 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:
|
||||
|
||||
1. **Add more test scenarios**: Edit the `testProvider` config in `payload.config.ts`
|
||||
2. **Customize the payment UI**: Edit `app/test-payment/[id]/page.tsx`
|
||||
3. **Add more sample data**: Edit `seed.ts`
|
||||
4. **Add custom collections**: Add to `collections` array in `payload.config.ts`
|
||||
|
||||
### Testing Different Providers
|
||||
|
||||
To test with real payment providers:
|
||||
|
||||
```typescript
|
||||
// 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:
|
||||
```bash
|
||||
# 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:
|
||||
```bash
|
||||
# Use a different port
|
||||
PORT=3001 pnpm dev
|
||||
```
|
||||
|
||||
### TypeScript Errors
|
||||
|
||||
Regenerate Payload types:
|
||||
```bash
|
||||
pnpm dev:generate-types
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Plugin Documentation](../README.md)
|
||||
- [PayloadCMS Documentation](https://payloadcms.com/docs)
|
||||
- [GitHub Repository](https://github.com/xtr-dev/payload-billing)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
56
dev/app/api/demo/create-payment/route.ts
Normal file
56
dev/app/api/demo/create-payment/route.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const payload = await getPayload({
|
||||
config: configPromise,
|
||||
})
|
||||
|
||||
const body = await request.json()
|
||||
const { amount, currency, description } = body
|
||||
|
||||
if (!amount || !currency) {
|
||||
return Response.json(
|
||||
{ success: false, error: 'Amount and currency are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Create a payment using the test provider
|
||||
const payment = await payload.create({
|
||||
collection: 'payments',
|
||||
data: {
|
||||
provider: 'test',
|
||||
amount,
|
||||
currency,
|
||||
description: description || 'Demo payment',
|
||||
status: 'pending',
|
||||
metadata: {
|
||||
source: 'demo-ui',
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
payment: {
|
||||
id: payment.providerId, // Use the test provider ID for the UI
|
||||
paymentId: payment.id,
|
||||
amount: payment.amount,
|
||||
currency: payment.currency,
|
||||
description: payment.description,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to create payment:', error)
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create payment',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
1
dev/app/globals.css
Normal file
1
dev/app/globals.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
19
dev/app/layout.tsx
Normal file
19
dev/app/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Billing Plugin Demo - PayloadCMS',
|
||||
description: 'Demo application for @xtr-dev/payload-billing plugin',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
183
dev/app/page.tsx
Normal file
183
dev/app/page.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function HomePage() {
|
||||
const [paymentId, setPaymentId] = useState<string>('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string>('')
|
||||
|
||||
const createDemoPayment = async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
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',
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setPaymentId(data.payment.id)
|
||||
} else {
|
||||
setError(data.error || 'Failed to create payment')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-700 p-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="bg-white rounded-xl shadow-2xl overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-blue-600 to-purple-600 p-8 text-white">
|
||||
<h1 className="text-4xl font-bold mb-2">Billing Plugin Demo</h1>
|
||||
<p className="text-blue-100">
|
||||
Test the @xtr-dev/payload-billing plugin with the test provider
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-slate-800 mb-4">
|
||||
🎮 Interactive Demo
|
||||
</h2>
|
||||
<p className="text-slate-600 mb-6">
|
||||
This demo shows how to integrate the billing plugin into your application. Click
|
||||
the button below to create a test payment and see the custom payment UI in action.
|
||||
</p>
|
||||
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-lg p-6">
|
||||
<h3 className="font-semibold text-slate-800 mb-4">
|
||||
Create Test Payment
|
||||
</h3>
|
||||
|
||||
{!paymentId ? (
|
||||
<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"
|
||||
>
|
||||
{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">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-green-800 font-semibold mb-2">
|
||||
<span>✓</span>
|
||||
<span>Payment Created Successfully!</span>
|
||||
</div>
|
||||
<p className="text-sm text-green-700">
|
||||
Payment ID: <code className="bg-green-100 px-2 py-1 rounded">{paymentId}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Link
|
||||
href={`/test-payment/${paymentId}`}
|
||||
className="bg-gradient-to-r from-green-600 to-green-700 text-white px-6 py-3 rounded-lg font-semibold hover:shadow-lg transition-all inline-block cursor-pointer"
|
||||
>
|
||||
Go to Payment Page →
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => {
|
||||
setPaymentId('')
|
||||
setError('')
|
||||
}}
|
||||
className="bg-slate-200 text-slate-700 px-6 py-3 rounded-lg font-semibold hover:bg-slate-300 transition-all cursor-pointer"
|
||||
>
|
||||
Create Another
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-slate-800 mb-4">
|
||||
📚 Quick Links
|
||||
</h2>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Link
|
||||
href="/admin/collections/payments"
|
||||
className="p-4 border-2 border-slate-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-all cursor-pointer"
|
||||
>
|
||||
<div className="font-semibold text-slate-800 mb-1">💳 Payments</div>
|
||||
<div className="text-sm text-slate-600">View all payment transactions</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/admin/collections/invoices"
|
||||
className="p-4 border-2 border-slate-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-all cursor-pointer"
|
||||
>
|
||||
<div className="font-semibold text-slate-800 mb-1">🧾 Invoices</div>
|
||||
<div className="text-sm text-slate-600">Manage invoices and billing</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/admin/collections/refunds"
|
||||
className="p-4 border-2 border-slate-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-all cursor-pointer"
|
||||
>
|
||||
<div className="font-semibold text-slate-800 mb-1">🔄 Refunds</div>
|
||||
<div className="text-sm text-slate-600">Process and track refunds</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/admin/collections/customers"
|
||||
className="p-4 border-2 border-slate-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-all cursor-pointer"
|
||||
>
|
||||
<div className="font-semibold text-slate-800 mb-1">👥 Customers</div>
|
||||
<div className="text-sm text-slate-600">Manage customer information</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6">
|
||||
<h2 className="text-xl font-bold text-slate-800 mb-3">
|
||||
💡 About This Demo
|
||||
</h2>
|
||||
<div className="space-y-3 text-slate-700">
|
||||
<p>
|
||||
This demo application showcases the <code className="bg-blue-100 px-2 py-1 rounded">@xtr-dev/payload-billing</code> plugin
|
||||
for PayloadCMS 3.x with the following features:
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-2 ml-4">
|
||||
<li>Test payment provider with customizable scenarios</li>
|
||||
<li>Custom payment UI page with modern design</li>
|
||||
<li>Customer relationship management with auto-sync</li>
|
||||
<li>Invoice generation with line items and tax calculation</li>
|
||||
<li>Refund processing and tracking</li>
|
||||
<li>Sample data seeding for quick testing</li>
|
||||
</ul>
|
||||
<p className="pt-2">
|
||||
The test provider allows you to simulate different payment outcomes including
|
||||
success, failure, cancellation, and more - perfect for development and testing!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
185
dev/app/payment-failed/page.tsx
Normal file
185
dev/app/payment-failed/page.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { Suspense } from 'react'
|
||||
|
||||
function PaymentFailedContent() {
|
||||
const searchParams = useSearchParams()
|
||||
const paymentId = searchParams.get('paymentId')
|
||||
const reason = searchParams.get('reason') || 'unknown'
|
||||
const amount = searchParams.get('amount')
|
||||
const currency = searchParams.get('currency')
|
||||
|
||||
const getReasonText = (reason: string) => {
|
||||
switch (reason) {
|
||||
case 'failed':
|
||||
return 'Payment was declined'
|
||||
case 'cancelled':
|
||||
return 'Payment was cancelled'
|
||||
case 'expired':
|
||||
return 'Payment session expired'
|
||||
default:
|
||||
return 'Payment could not be completed'
|
||||
}
|
||||
}
|
||||
|
||||
const getReasonDescription = (reason: string) => {
|
||||
switch (reason) {
|
||||
case 'failed':
|
||||
return 'The payment provider declined the transaction. This is a simulated failure for testing purposes.'
|
||||
case 'cancelled':
|
||||
return 'The payment was cancelled before completion. You can try again with a different test scenario.'
|
||||
case 'expired':
|
||||
return 'The payment session timed out. Please create a new payment to try again.'
|
||||
default:
|
||||
return 'An unexpected error occurred during payment processing.'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-red-600 to-orange-700 flex items-center justify-center p-4">
|
||||
<div className="max-w-2xl w-full bg-white rounded-xl shadow-2xl overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-red-600 to-orange-600 p-8 text-white text-center">
|
||||
<div className="mb-4">
|
||||
<div className="w-20 h-20 bg-white rounded-full flex items-center justify-center mx-auto">
|
||||
<svg
|
||||
className="w-12 h-12 text-red-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-2">Payment {reason.charAt(0).toUpperCase() + reason.slice(1)}</h1>
|
||||
<p className="text-red-100 text-lg">
|
||||
{getReasonText(reason)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-6 mb-8">
|
||||
<h2 className="font-semibold text-red-900 mb-3 text-lg">
|
||||
What Happened?
|
||||
</h2>
|
||||
<p className="text-red-800 mb-4">
|
||||
{getReasonDescription(reason)}
|
||||
</p>
|
||||
|
||||
<div className="space-y-3 pt-4 border-t border-red-200">
|
||||
{paymentId && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Payment ID:</span>
|
||||
<code className="bg-red-100 text-red-800 px-3 py-1 rounded font-mono text-sm">
|
||||
{paymentId}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
{amount && currency && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Amount:</span>
|
||||
<span className="text-red-900 font-bold text-xl">
|
||||
{currency.toUpperCase()} {(parseInt(amount) / 100).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Status:</span>
|
||||
<span className="bg-red-500 text-white px-3 py-1 rounded-full text-sm font-semibold capitalize">
|
||||
{reason}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-slate-800 text-lg">Try Again</h3>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center justify-between p-4 border-2 border-red-300 bg-red-50 rounded-lg hover:border-red-500 hover:bg-red-100 transition-all group cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<div className="font-semibold text-red-800 group-hover:text-red-900">
|
||||
🔄 Try Another Payment
|
||||
</div>
|
||||
<div className="text-sm text-red-700">
|
||||
Create a new test payment with different scenario
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
className="w-5 h-5 text-red-500 group-hover:text-red-700"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/admin/collections/payments"
|
||||
className="flex items-center justify-between p-4 border-2 border-slate-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-all group cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<div className="font-semibold text-slate-800 group-hover:text-blue-700">
|
||||
💳 View Payment History
|
||||
</div>
|
||||
<div className="text-sm text-slate-600">
|
||||
Check all payments in admin
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
className="w-5 h-5 text-slate-400 group-hover:text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>💡 Testing Tip:</strong> This failure was simulated using the test provider.
|
||||
Try selecting a different test scenario like "Instant Success" or "Delayed Success"
|
||||
to see a successful payment flow.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PaymentFailedPage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen bg-gradient-to-br from-red-600 to-orange-700 flex items-center justify-center">
|
||||
<div className="text-white text-xl">Loading...</div>
|
||||
</div>
|
||||
}>
|
||||
<PaymentFailedContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
184
dev/app/payment-success/page.tsx
Normal file
184
dev/app/payment-success/page.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { Suspense } from 'react'
|
||||
|
||||
function PaymentSuccessContent() {
|
||||
const searchParams = useSearchParams()
|
||||
const paymentId = searchParams.get('paymentId')
|
||||
const amount = searchParams.get('amount')
|
||||
const currency = searchParams.get('currency')
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-600 to-emerald-700 flex items-center justify-center p-4">
|
||||
<div className="max-w-2xl w-full bg-white rounded-xl shadow-2xl overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-green-600 to-emerald-600 p-8 text-white text-center">
|
||||
<div className="mb-4">
|
||||
<div className="w-20 h-20 bg-white rounded-full flex items-center justify-center mx-auto">
|
||||
<svg
|
||||
className="w-12 h-12 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-2">Payment Successful!</h1>
|
||||
<p className="text-green-100 text-lg">
|
||||
Your test payment has been processed successfully
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-8">
|
||||
<h2 className="font-semibold text-green-900 mb-4 text-lg">
|
||||
Payment Details
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{paymentId && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Payment ID:</span>
|
||||
<code className="bg-green-100 text-green-800 px-3 py-1 rounded font-mono text-sm">
|
||||
{paymentId}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
{amount && currency && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Amount:</span>
|
||||
<span className="text-green-900 font-bold text-xl">
|
||||
{currency.toUpperCase()} {(parseInt(amount) / 100).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Status:</span>
|
||||
<span className="bg-green-500 text-white px-3 py-1 rounded-full text-sm font-semibold">
|
||||
Succeeded
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Provider:</span>
|
||||
<span className="text-slate-900 font-medium">Test Provider</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-slate-800 text-lg">What's Next?</h3>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center justify-between p-4 border-2 border-slate-200 rounded-lg hover:border-green-500 hover:bg-green-50 transition-all group cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<div className="font-semibold text-slate-800 group-hover:text-green-700">
|
||||
🏠 Back to Demo
|
||||
</div>
|
||||
<div className="text-sm text-slate-600">
|
||||
Try another test payment
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
className="w-5 h-5 text-slate-400 group-hover:text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/admin/collections/payments"
|
||||
className="flex items-center justify-between p-4 border-2 border-slate-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-all group cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<div className="font-semibold text-slate-800 group-hover:text-blue-700">
|
||||
💳 View All Payments
|
||||
</div>
|
||||
<div className="text-sm text-slate-600">
|
||||
Check payment history in admin
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
className="w-5 h-5 text-slate-400 group-hover:text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/admin/collections/invoices"
|
||||
className="flex items-center justify-between p-4 border-2 border-slate-200 rounded-lg hover:border-purple-500 hover:bg-purple-50 transition-all group cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<div className="font-semibold text-slate-800 group-hover:text-purple-700">
|
||||
🧾 View Invoices
|
||||
</div>
|
||||
<div className="text-sm text-slate-600">
|
||||
Check invoices in admin
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
className="w-5 h-5 text-slate-400 group-hover:text-purple-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>💡 Demo Tip:</strong> This was a simulated payment using the test provider.
|
||||
In production, you would integrate with real providers like Stripe or Mollie.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PaymentSuccessPage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-600 to-emerald-700 flex items-center justify-center">
|
||||
<div className="text-white text-xl">Loading...</div>
|
||||
</div>
|
||||
}>
|
||||
<PaymentSuccessContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
291
dev/app/test-payment/[id]/page.tsx
Normal file
291
dev/app/test-payment/[id]/page.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
'use client'
|
||||
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface PaymentMethod {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
interface Scenario {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
outcome: string
|
||||
delay?: number
|
||||
}
|
||||
|
||||
interface TestProviderConfig {
|
||||
enabled: boolean
|
||||
scenarios: Scenario[]
|
||||
methods: PaymentMethod[]
|
||||
testModeIndicators: {
|
||||
showWarningBanners: boolean
|
||||
showTestBadges: boolean
|
||||
consoleWarnings: boolean
|
||||
}
|
||||
defaultDelay: number
|
||||
customUiRoute: string
|
||||
}
|
||||
|
||||
interface PaymentSession {
|
||||
id: string
|
||||
amount: number
|
||||
currency: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export default function TestPaymentPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const paymentId = params.id as string
|
||||
|
||||
const [config, setConfig] = useState<TestProviderConfig | null>(null)
|
||||
const [session, setSession] = useState<PaymentSession | null>(null)
|
||||
const [selectedMethod, setSelectedMethod] = useState<string | null>(null)
|
||||
const [selectedScenario, setSelectedScenario] = useState<string | null>(null)
|
||||
const [processing, setProcessing] = useState(false)
|
||||
const [status, setStatus] = useState<{
|
||||
type: 'idle' | 'processing' | 'success' | 'error'
|
||||
message: string
|
||||
}>({ type: 'idle', message: '' })
|
||||
|
||||
useEffect(() => {
|
||||
// Load test provider config
|
||||
fetch('/api/payload-billing/test/config')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setConfig(data)
|
||||
if (data.testModeIndicators?.consoleWarnings) {
|
||||
console.warn('[Test Provider] 🧪 TEST MODE: This is a simulated payment interface')
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to load test provider config:', err)
|
||||
})
|
||||
|
||||
// Load payment session (mock data for demo)
|
||||
setSession({
|
||||
id: paymentId,
|
||||
amount: 2500,
|
||||
currency: 'USD',
|
||||
description: 'Demo payment for testing the billing plugin',
|
||||
})
|
||||
}, [paymentId])
|
||||
|
||||
const handleProcessPayment = async () => {
|
||||
if (!selectedMethod || !selectedScenario) return
|
||||
|
||||
setProcessing(true)
|
||||
setStatus({ type: 'processing', message: 'Initiating payment...' })
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/payload-billing/test/process', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
paymentId,
|
||||
scenarioId: selectedScenario,
|
||||
method: selectedMethod,
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
const scenario = config?.scenarios.find((s) => s.id === selectedScenario)
|
||||
setStatus({
|
||||
type: 'processing',
|
||||
message: `Processing payment with ${scenario?.name}...`,
|
||||
})
|
||||
|
||||
// Poll for status updates
|
||||
setTimeout(() => pollStatus(), result.delay || 1000)
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to process payment')
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus({
|
||||
type: 'error',
|
||||
message: error instanceof Error ? error.message : 'An error occurred',
|
||||
})
|
||||
setProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const pollStatus = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/payload-billing/test/status/${paymentId}`)
|
||||
const result = await response.json()
|
||||
|
||||
if (result.status === 'paid') {
|
||||
setStatus({ type: 'success', message: '✅ Payment successful!' })
|
||||
setTimeout(() => {
|
||||
const params = new URLSearchParams({
|
||||
paymentId: paymentId,
|
||||
amount: session.amount.toString(),
|
||||
currency: session.currency,
|
||||
})
|
||||
router.push(`/payment-success?${params.toString()}`)
|
||||
}, 2000)
|
||||
} else if (['failed', 'cancelled', 'expired'].includes(result.status)) {
|
||||
setStatus({ type: 'error', message: `❌ Payment ${result.status}` })
|
||||
setTimeout(() => {
|
||||
const params = new URLSearchParams({
|
||||
paymentId: paymentId,
|
||||
amount: session.amount.toString(),
|
||||
currency: session.currency,
|
||||
reason: result.status,
|
||||
})
|
||||
router.push(`/payment-failed?${params.toString()}`)
|
||||
}, 2000)
|
||||
} else if (result.status === 'pending') {
|
||||
setStatus({ type: 'processing', message: 'Payment is still pending...' })
|
||||
setTimeout(() => pollStatus(), 2000)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Test Provider] Failed to poll status:', error)
|
||||
setStatus({ type: 'error', message: 'Failed to check payment status' })
|
||||
setProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!config || !session) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-600 to-purple-700 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-xl shadow-2xl p-8">
|
||||
<div className="animate-pulse">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-600 to-purple-700 p-4 md:p-8">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-white rounded-xl shadow-2xl overflow-hidden">
|
||||
{config.testModeIndicators.showWarningBanners && (
|
||||
<div className="bg-gradient-to-r from-orange-500 to-red-500 text-white px-6 py-3 text-center font-semibold">
|
||||
🧪 TEST MODE - This is a simulated payment for development purposes
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-slate-50 px-8 py-6 border-b border-slate-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h1 className="text-2xl font-bold text-slate-800">Test Payment Checkout</h1>
|
||||
{config.testModeIndicators.showTestBadges && (
|
||||
<span className="bg-slate-600 text-white px-3 py-1 rounded text-xs font-bold uppercase">
|
||||
Test
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-green-600 mb-3">
|
||||
{session.currency.toUpperCase()} {(session.amount / 100).toFixed(2)}
|
||||
</div>
|
||||
{session.description && (
|
||||
<p className="text-slate-600 text-base">{session.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
{/* Payment Methods */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
|
||||
💳 Select Payment Method
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{config.methods.map((method) => (
|
||||
<button
|
||||
key={method.id}
|
||||
onClick={() => setSelectedMethod(method.id)}
|
||||
disabled={processing}
|
||||
className={`p-4 rounded-lg border-2 transition-all cursor-pointer ${
|
||||
selectedMethod === method.id
|
||||
? 'border-blue-500 bg-blue-500 text-white shadow-lg'
|
||||
: 'border-slate-200 bg-white text-slate-700 hover:border-blue-300 hover:bg-blue-50'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
<div className="text-2xl mb-2">{method.icon}</div>
|
||||
<div className="text-sm font-medium">{method.name}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Test Scenarios */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
|
||||
🎭 Select Test Scenario
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{config.scenarios.map((scenario) => (
|
||||
<button
|
||||
key={scenario.id}
|
||||
onClick={() => setSelectedScenario(scenario.id)}
|
||||
disabled={processing}
|
||||
className={`w-full p-4 rounded-lg border-2 text-left transition-all cursor-pointer ${
|
||||
selectedScenario === scenario.id
|
||||
? 'border-green-500 bg-green-500 text-white shadow-lg'
|
||||
: 'border-slate-200 bg-white text-slate-700 hover:border-green-300 hover:bg-green-50'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
<div className="font-semibold mb-1">{scenario.name}</div>
|
||||
<div className={`text-sm ${selectedScenario === scenario.id ? 'text-white/90' : 'text-slate-600'}`}>
|
||||
{scenario.description}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Process Button */}
|
||||
<button
|
||||
onClick={handleProcessPayment}
|
||||
disabled={!selectedMethod || !selectedScenario || processing}
|
||||
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 text-white font-semibold py-4 rounded-lg transition-all hover:shadow-lg hover:-translate-y-0.5 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none cursor-pointer"
|
||||
>
|
||||
{processing ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<span className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></span>
|
||||
Processing...
|
||||
</span>
|
||||
) : (
|
||||
'Process Test Payment'
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Status Message */}
|
||||
{status.type !== 'idle' && (
|
||||
<div
|
||||
className={`mt-6 p-4 rounded-lg text-center font-semibold ${
|
||||
status.type === 'processing'
|
||||
? 'bg-yellow-50 text-yellow-800 border border-yellow-200'
|
||||
: status.type === 'success'
|
||||
? 'bg-green-50 text-green-800 border border-green-200'
|
||||
: 'bg-red-50 text-red-800 border border-red-200'
|
||||
}`}
|
||||
>
|
||||
{status.type === 'processing' && (
|
||||
<span className="inline-block animate-spin rounded-full h-5 w-5 border-b-2 border-yellow-800 mr-2"></span>
|
||||
)}
|
||||
{status.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Card */}
|
||||
<div className="mt-6 bg-white/10 backdrop-blur-sm text-white rounded-lg p-6">
|
||||
<h3 className="font-semibold mb-2">💡 Demo Information</h3>
|
||||
<p className="text-sm text-white/90">
|
||||
This is a custom test payment UI for the @xtr-dev/payload-billing plugin. Select a
|
||||
payment method and scenario to simulate different payment outcomes. The payment will be
|
||||
processed according to the selected scenario.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -69,6 +69,7 @@ export interface Config {
|
||||
collections: {
|
||||
posts: Post;
|
||||
media: Media;
|
||||
customers: Customer;
|
||||
payments: Payment;
|
||||
invoices: Invoice;
|
||||
refunds: Refund;
|
||||
@@ -81,6 +82,7 @@ export interface Config {
|
||||
collectionsSelect: {
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
customers: CustomersSelect<false> | CustomersSelect<true>;
|
||||
payments: PaymentsSelect<false> | PaymentsSelect<true>;
|
||||
invoices: InvoicesSelect<false> | InvoicesSelect<true>;
|
||||
refunds: RefundsSelect<false> | RefundsSelect<true>;
|
||||
@@ -148,6 +150,28 @@ export interface Media {
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "customers".
|
||||
*/
|
||||
export interface Customer {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string | null;
|
||||
company?: string | null;
|
||||
taxId?: string | null;
|
||||
address?: {
|
||||
line1?: string | null;
|
||||
line2?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
postalCode?: string | null;
|
||||
country?: string | null;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payments".
|
||||
@@ -213,17 +237,21 @@ export interface Invoice {
|
||||
*/
|
||||
number: string;
|
||||
/**
|
||||
* Customer billing information
|
||||
* Link to customer record (optional)
|
||||
*/
|
||||
customerInfo: {
|
||||
customer?: (number | null) | Customer;
|
||||
/**
|
||||
* Customer billing information (auto-populated from customer relationship)
|
||||
*/
|
||||
customerInfo?: {
|
||||
/**
|
||||
* Customer name
|
||||
*/
|
||||
name: string;
|
||||
name?: string | null;
|
||||
/**
|
||||
* Customer email address
|
||||
*/
|
||||
email: string;
|
||||
email?: string | null;
|
||||
/**
|
||||
* Customer phone number
|
||||
*/
|
||||
@@ -238,18 +266,18 @@ export interface Invoice {
|
||||
taxId?: string | null;
|
||||
};
|
||||
/**
|
||||
* Billing address
|
||||
* Billing address (auto-populated from customer relationship)
|
||||
*/
|
||||
billingAddress: {
|
||||
billingAddress?: {
|
||||
/**
|
||||
* Address line 1
|
||||
*/
|
||||
line1: string;
|
||||
line1?: string | null;
|
||||
/**
|
||||
* Address line 2
|
||||
*/
|
||||
line2?: string | null;
|
||||
city: string;
|
||||
city?: string | null;
|
||||
/**
|
||||
* State or province
|
||||
*/
|
||||
@@ -257,11 +285,11 @@ export interface Invoice {
|
||||
/**
|
||||
* Postal or ZIP code
|
||||
*/
|
||||
postalCode: string;
|
||||
postalCode?: string | null;
|
||||
/**
|
||||
* Country code (e.g., US, GB)
|
||||
*/
|
||||
country: string;
|
||||
country?: string | null;
|
||||
};
|
||||
status: 'draft' | 'open' | 'paid' | 'void' | 'uncollectible';
|
||||
/**
|
||||
@@ -402,6 +430,10 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'media';
|
||||
value: number | Media;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'customers';
|
||||
value: number | Customer;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'payments';
|
||||
value: number | Payment;
|
||||
@@ -485,6 +517,29 @@ export interface MediaSelect<T extends boolean = true> {
|
||||
focalX?: T;
|
||||
focalY?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "customers_select".
|
||||
*/
|
||||
export interface CustomersSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
email?: T;
|
||||
phone?: T;
|
||||
company?: T;
|
||||
taxId?: T;
|
||||
address?:
|
||||
| T
|
||||
| {
|
||||
line1?: T;
|
||||
line2?: T;
|
||||
city?: T;
|
||||
state?: T;
|
||||
postalCode?: T;
|
||||
country?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payments_select".
|
||||
@@ -510,6 +565,7 @@ export interface PaymentsSelect<T extends boolean = true> {
|
||||
*/
|
||||
export interface InvoicesSelect<T extends boolean = true> {
|
||||
number?: T;
|
||||
customer?: T;
|
||||
customerInfo?:
|
||||
| T
|
||||
| {
|
||||
|
||||
@@ -36,6 +36,71 @@ const buildConfigWithSQLite = () => {
|
||||
staticDir: path.resolve(dirname, 'media'),
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'customers',
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'phone',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'company',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'taxId',
|
||||
type: 'text',
|
||||
label: 'Tax ID',
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'line1',
|
||||
type: 'text',
|
||||
label: 'Address Line 1',
|
||||
},
|
||||
{
|
||||
name: 'line2',
|
||||
type: 'text',
|
||||
label: 'Address Line 2',
|
||||
},
|
||||
{
|
||||
name: 'city',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'state',
|
||||
type: 'text',
|
||||
label: 'State/Province',
|
||||
},
|
||||
{
|
||||
name: 'postalCode',
|
||||
type: 'text',
|
||||
label: 'Postal Code',
|
||||
},
|
||||
{
|
||||
name: 'country',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
db: sqliteAdapter({
|
||||
client: {
|
||||
@@ -56,7 +121,8 @@ const buildConfigWithSQLite = () => {
|
||||
showWarningBanners: true,
|
||||
showTestBadges: true,
|
||||
consoleWarnings: true
|
||||
}
|
||||
},
|
||||
customUiRoute: '/test-payment',
|
||||
})
|
||||
],
|
||||
collections: {
|
||||
@@ -64,6 +130,22 @@ const buildConfigWithSQLite = () => {
|
||||
invoices: 'invoices',
|
||||
refunds: 'refunds',
|
||||
},
|
||||
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,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
secret: process.env.PAYLOAD_SECRET || 'test-secret_key',
|
||||
|
||||
8
dev/postcss.config.mjs
Normal file
8
dev/postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
188
dev/seed.ts
188
dev/seed.ts
@@ -21,9 +21,189 @@ export const seed = async (payload: Payload) => {
|
||||
}
|
||||
|
||||
// Seed billing sample data
|
||||
// await seedBillingData(payload)
|
||||
await seedBillingData(payload)
|
||||
}
|
||||
|
||||
// async function seedBillingData(payload: Payload): Promise<void> {
|
||||
// payload.logger.info('Seeding billing sample data...')
|
||||
// }
|
||||
async function seedBillingData(payload: Payload): Promise<void> {
|
||||
payload.logger.info('Seeding billing sample data...')
|
||||
|
||||
// Check if we already have data
|
||||
const existingPayments = await payload.count({
|
||||
collection: 'payments',
|
||||
})
|
||||
|
||||
if (existingPayments.totalDocs > 0) {
|
||||
payload.logger.info('Billing data already exists, skipping seed...')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if customers collection exists
|
||||
const hasCustomers = payload.collections['customers'] !== undefined
|
||||
|
||||
let customer1Id: string | number | undefined
|
||||
let customer2Id: string | number | undefined
|
||||
|
||||
if (hasCustomers) {
|
||||
// Seed customers
|
||||
payload.logger.info('Seeding customers...')
|
||||
const customer1 = await payload.create({
|
||||
collection: 'customers',
|
||||
data: {
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
phone: '+1 (555) 123-4567',
|
||||
company: 'Acme Corporation',
|
||||
taxId: 'US-123456789',
|
||||
address: {
|
||||
line1: '123 Main Street',
|
||||
line2: 'Suite 100',
|
||||
city: 'New York',
|
||||
state: 'NY',
|
||||
postalCode: '10001',
|
||||
country: 'US',
|
||||
},
|
||||
},
|
||||
})
|
||||
customer1Id = customer1.id
|
||||
|
||||
const customer2 = await payload.create({
|
||||
collection: 'customers',
|
||||
data: {
|
||||
name: 'Jane Smith',
|
||||
email: 'jane.smith@example.com',
|
||||
phone: '+1 (555) 987-6543',
|
||||
company: 'Tech Innovations Inc.',
|
||||
address: {
|
||||
line1: '456 Tech Avenue',
|
||||
city: 'San Francisco',
|
||||
state: 'CA',
|
||||
postalCode: '94102',
|
||||
country: 'US',
|
||||
},
|
||||
},
|
||||
})
|
||||
customer2Id = customer2.id
|
||||
}
|
||||
|
||||
// Seed invoices
|
||||
payload.logger.info('Seeding invoices...')
|
||||
|
||||
const invoiceData1 = hasCustomers
|
||||
? {
|
||||
customer: customer1Id,
|
||||
currency: 'USD',
|
||||
items: [
|
||||
{
|
||||
description: 'Web Development Services',
|
||||
quantity: 40,
|
||||
unitAmount: 12500, // $125/hour
|
||||
},
|
||||
{
|
||||
description: 'Hosting & Deployment',
|
||||
quantity: 1,
|
||||
unitAmount: 5000, // $50
|
||||
},
|
||||
],
|
||||
taxAmount: 52500, // $525 tax (10%)
|
||||
status: 'paid',
|
||||
notes: 'Thank you for your business!',
|
||||
}
|
||||
: {
|
||||
customerInfo: {
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
phone: '+1 (555) 123-4567',
|
||||
company: 'Acme Corporation',
|
||||
taxId: 'US-123456789',
|
||||
},
|
||||
billingAddress: {
|
||||
line1: '123 Main Street',
|
||||
line2: 'Suite 100',
|
||||
city: 'New York',
|
||||
state: 'NY',
|
||||
postalCode: '10001',
|
||||
country: 'US',
|
||||
},
|
||||
currency: 'USD',
|
||||
items: [
|
||||
{
|
||||
description: 'Web Development Services',
|
||||
quantity: 40,
|
||||
unitAmount: 12500,
|
||||
},
|
||||
{
|
||||
description: 'Hosting & Deployment',
|
||||
quantity: 1,
|
||||
unitAmount: 5000,
|
||||
},
|
||||
],
|
||||
taxAmount: 52500,
|
||||
status: 'paid',
|
||||
notes: 'Thank you for your business!',
|
||||
}
|
||||
|
||||
const invoice1 = await payload.create({
|
||||
collection: 'invoices',
|
||||
data: invoiceData1 as any,
|
||||
})
|
||||
|
||||
const invoiceData2 = hasCustomers
|
||||
? {
|
||||
customer: customer2Id,
|
||||
currency: 'USD',
|
||||
items: [
|
||||
{
|
||||
description: 'Monthly Subscription - Pro Plan',
|
||||
quantity: 1,
|
||||
unitAmount: 9900, // $99
|
||||
},
|
||||
{
|
||||
description: 'Additional Users (x5)',
|
||||
quantity: 5,
|
||||
unitAmount: 2000, // $20 each
|
||||
},
|
||||
],
|
||||
taxAmount: 1990,
|
||||
status: 'open',
|
||||
}
|
||||
: {
|
||||
customerInfo: {
|
||||
name: 'Jane Smith',
|
||||
email: 'jane.smith@example.com',
|
||||
phone: '+1 (555) 987-6543',
|
||||
company: 'Tech Innovations Inc.',
|
||||
},
|
||||
billingAddress: {
|
||||
line1: '456 Tech Avenue',
|
||||
city: 'San Francisco',
|
||||
state: 'CA',
|
||||
postalCode: '94102',
|
||||
country: 'US',
|
||||
},
|
||||
currency: 'USD',
|
||||
items: [
|
||||
{
|
||||
description: 'Monthly Subscription - Pro Plan',
|
||||
quantity: 1,
|
||||
unitAmount: 9900,
|
||||
},
|
||||
{
|
||||
description: 'Additional Users (x5)',
|
||||
quantity: 5,
|
||||
unitAmount: 2000,
|
||||
},
|
||||
],
|
||||
taxAmount: 1990,
|
||||
status: 'open',
|
||||
}
|
||||
|
||||
const invoice2 = await payload.create({
|
||||
collection: 'invoices',
|
||||
data: invoiceData2 as any,
|
||||
})
|
||||
|
||||
// Note: Skip payment seeding during initialization because the billing plugin
|
||||
// providers aren't fully initialized yet. Payments can be created via the demo UI.
|
||||
|
||||
payload.logger.info('✅ Billing sample data seeded successfully!')
|
||||
}
|
||||
|
||||
14
dev/tailwind.config.ts
Normal file
14
dev/tailwind.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
export default config
|
||||
Reference in New Issue
Block a user