Customer Package Payments¶
Complete guide to package payment processing, payment link generation, and payment status tracking in the Reserva platform.
Overview¶
The customer package payments system provides comprehensive payment management for package purchases with support for:
- Manual Payment Recording - Record offline payments (cash, POS, bank transfer)
- Payment Link Generation - Create Paper.id payment links for online payments
- Payment Status Tracking - Monitor payment progress and history
- Automatic Credit Activation - Credits activated upon payment confirmation
- Multi-Channel Delivery - Send payment links via email, WhatsApp, or SMS
- Webhook Integration - Automatic payment confirmation via Paper.id webhooks
Key Concepts:
- Manual Payment = Cash, POS terminal, or bank transfer at venue (immediate activation)
- Payment Link = Online payment via Paper.id (webhook-triggered activation)
- Credit Activation = Package credits become available after payment confirmation
- Platform Fees = Package purchases are fee-exempt across all tiers
Portal: Shared (Staff and Customer)
Access Level: - Staff: TENANT_ADMIN, OUTLET_MANAGER, RECEPTIONIST, SUPER_ADMIN - Customer: Own packages only
Available Endpoints¶
| Endpoint | Method | Purpose | Access |
|---|---|---|---|
/customer-packages/{id}/record-payment |
POST | Record manual offline payment | Staff |
/customer-packages/{id}/create-payment-link |
POST | Create Paper.id payment link | Staff |
/customer-packages/{id}/payment-status |
GET | Check payment status | Staff/Customer |
Record Manual Payment¶
Record offline payment for package purchase with automatic credit activation.
Endpoint¶
Authentication: Required (Staff JWT - TENANT_ADMIN, OUTLET_MANAGER, RECEPTIONIST, SUPER_ADMIN)
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_package_id |
string | Yes | Customer package ID |
Request Body¶
{
"amount": 500000,
"payment_method": "cash",
"notes": "Paid in cash for package purchase",
"receipt_number": "PKG-RCPT-2025-001"
}
Parameters¶
| Field | Type | Required | Description |
|---|---|---|---|
amount |
decimal | Yes | Payment amount (must match package price exactly) |
payment_method |
string | Yes | Payment method: cash, pos_terminal, bank_transfer |
notes |
string | No | Payment notes (max 500 chars) |
receipt_number |
string | No | Receipt or reference number (max 100 chars) |
Supported Payment Methods¶
| Method | Description | Use Case |
|---|---|---|
cash |
Cash payment | Customer pays cash at location |
pos_terminal |
Credit/debit card via POS | Card swipe at venue terminal |
bank_transfer |
Direct bank transfer | Customer shows transfer confirmation |
Response¶
{
"status": "success",
"message": "Payment recorded successfully - package credits activated",
"payment": {
"id": "507f1f77bcf86cd799439011",
"amount": 500000.0,
"method": "cash",
"status": "completed",
"recorded_by": "Jane Smith",
"recorded_at": "2025-01-20T10:30:00",
"receipt_number": "PKG-RCPT-2025-001",
"reference_id": "PAY-PKG-20250120103000"
},
"package": {
"id": "507f1f77bcf86cd799439012",
"status": "active",
"payment_status": "paid",
"expires_at": "2025-04-20T10:30:00",
"total_credits": 10
}
}
Response Fields¶
| Field | Type | Description |
|---|---|---|
payment.id |
string | Payment record ID |
payment.amount |
decimal | Payment amount |
payment.method |
string | Payment method used |
payment.status |
string | Payment status (completed) |
payment.recorded_by |
string | Staff member who recorded payment |
payment.recorded_at |
datetime | When payment was recorded |
payment.receipt_number |
string | Receipt or reference number |
payment.reference_id |
string | Internal reference ID |
package.id |
string | Customer package ID |
package.status |
string | Package status (active) |
package.payment_status |
string | Payment status (paid) |
package.expires_at |
datetime | Package expiry date |
package.total_credits |
integer | Total credits allocated |
Business Rules¶
- Amount must match exactly - Payment amount must equal package price
- Package must be pending - Package must be in
pending_paymentstatus - Offline methods only - Only
cash,pos_terminal,bank_transferaccepted - Immediate activation - Credits activated immediately upon recording
- Audit trail - Staff member who recorded payment is tracked
- Tenant isolation - Package must belong to staff's tenant
Process Flow¶
graph TD
A[Staff records payment] --> B{Validate package}
B -->|Not found| C[404 Not Found]
B -->|Found| D{Check payment status}
D -->|Already paid| E[409 Conflict]
D -->|Pending| F{Validate method}
F -->|Online method| G[400 - Use payment link]
F -->|Offline method| H{Validate amount}
H -->|Mismatch| I[400 - Amount mismatch]
H -->|Match| J[Create payment record]
J --> K[Update package - PAID]
K --> L[Activate credits]
L --> M[Return success]
Error Responses¶
Package Not Found:
Package Already Paid:
Invalid Payment Method:
{
"detail": "Manual payments only support: ['cash', 'pos_terminal', 'bank_transfer']. Use create-payment-link endpoint for online payments."
}
Amount Mismatch:
Create Payment Link¶
Generate Paper.id payment link for package purchase with automatic credit activation on payment.
Endpoint¶
Authentication: Required (Staff JWT)
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_package_id |
string | Yes | Customer package ID |
Request Body¶
{
"send_email": true,
"send_whatsapp": true,
"send_sms": false,
"notes": "Payment link for package purchase",
"due_date": "2025-01-25"
}
Parameters¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
send_email |
boolean | No | true | Send payment link via email |
send_whatsapp |
boolean | No | false | Send payment link via WhatsApp |
send_sms |
boolean | No | false | Send payment link via SMS |
notes |
string | No | null | Invoice notes (max 500 chars) |
due_date |
date | No | 3 days | Payment due date |
Customer Contact Requirements¶
| Customer Has | SMS | ||
|---|---|---|---|
| Email only | Yes | No | No |
| Phone only | No | Yes | Yes |
| Both | Yes | Yes | Yes |
| Neither | Error | Error | Error |
Note: At least one delivery method must be enabled. If customer has no email, send_email is automatically disabled.
Response¶
{
"status": "payment_link_created",
"message": "Payment link sent to customer via email, whatsapp",
"invoice": {
"id": "507f1f77bcf86cd799439011",
"invoice_number": "INV-PKG-20250120-103000",
"amount": 500000.0,
"subtotal": 500000.0,
"platform_fee": 0.0,
"platform_fee_percentage": 0.0,
"currency": "IDR",
"due_date": "2025-01-25",
"paper_invoice_id": "paper_inv_abc123",
"invoice_url": "https://paper.id/invoice/abc123"
},
"payment": {
"id": "507f1f77bcf86cd799439012",
"reference_id": "PKG-507f1f77bcf86cd799439011-20250120103000",
"status": "pending",
"amount": 500000.0
},
"customer": {
"name": "John Doe",
"email": "john.doe@example.com",
"phone": "+628123456789"
},
"delivery_methods": ["email", "whatsapp"],
"webhook_url": "/webhooks/paper-invoice/tenant/507f1f77bcf86cd799439010"
}
Response Fields¶
| Field | Type | Description |
|---|---|---|
status |
string | Creation status |
message |
string | Success message with delivery methods |
invoice.id |
string | Internal invoice ID |
invoice.invoice_number |
string | Invoice number for reference |
invoice.amount |
decimal | Total invoice amount |
invoice.subtotal |
decimal | Subtotal before fees |
invoice.platform_fee |
decimal | Platform fee (always 0 for packages) |
invoice.platform_fee_percentage |
float | Fee percentage (always 0) |
invoice.currency |
string | Currency code (IDR) |
invoice.due_date |
date | Payment due date |
invoice.paper_invoice_id |
string | Paper.id invoice ID |
invoice.invoice_url |
string | Paper.id payment URL |
payment.id |
string | Payment record ID |
payment.reference_id |
string | Internal reference ID |
payment.status |
string | Payment status (pending) |
payment.amount |
decimal | Payment amount |
customer.name |
string | Customer name |
customer.email |
string | Customer email (if available) |
customer.phone |
string | Customer phone (if available) |
delivery_methods |
array | Methods used to send payment link |
webhook_url |
string | Webhook URL for payment confirmation |
Platform Fees¶
| Plan | Platform Fee |
|---|---|
| FREE | 0% |
| PRO | 0% |
| ENTERPRISE | 0% |
Note: Package purchases are fee-exempt across all subscription tiers. This is different from appointment payments which may have platform fees.
Business Rules¶
- Package must be pending - Package must be in
pending_paymentstatus - Customer assigned - Package must have a customer assigned
- Contact required - Customer must have email OR phone number
- Delivery method required - At least one delivery method must be enabled
- Tenant Paper.id configured - Tenant must have Paper.id integration enabled
- Default due date - 3 days from creation if not specified
- No platform fees - Package purchases don't incur platform fees
Payment Flow¶
graph TD
A[Staff creates payment link] --> B{Validate package}
B -->|Not found| C[404 Not Found]
B -->|Found| D{Check payment status}
D -->|Already paid| E[409 Conflict]
D -->|Pending| F{Check customer contact}
F -->|No contact| G[400 - Missing contact]
F -->|Has contact| H{Check Paper.id config}
H -->|Not configured| I[400 - Gateway not configured]
H -->|Configured| J[Create internal invoice]
J --> K[Generate Paper.id invoice]
K --> L[Create payment record - PENDING]
L --> M[Send via selected channels]
M --> N[Return payment link details]
N --> O[Customer receives link]
O --> P[Customer completes payment]
P --> Q[Paper.id webhook received]
Q --> R[Credits automatically activated]
R --> S[Package status: ACTIVE]
Webhook Payment Confirmation¶
When customer completes payment on Paper.id, a webhook is sent to:
The webhook handler:
- Validates invoice belongs to tenant
- Updates payment record to COMPLETED
- Updates customer package payment_status to PAID
- Activates package credits
- Sets package status to ACTIVE
See Webhook Integration for detailed webhook handling.
Error Responses¶
Package Not Found:
Package Already Paid:
Missing Customer Contact:
No Delivery Method:
Gateway Not Configured:
{
"detail": "Payment gateway not configured for this tenant. Please configure Paper.id credentials in tenant settings."
}
Paper.id Error:
Get Payment Status¶
Retrieve comprehensive payment status and history for a package purchase.
Endpoint¶
Authentication: Required (Staff or Customer JWT)
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_package_id |
string | Yes | Customer package ID |
Response¶
{
"customer_package_id": "507f1f77bcf86cd799439011",
"package_name": "Premium Hair Package - 10 Sessions",
"package_price": 500000.0,
"payment_status": "paid",
"package_status": "active",
"total_paid": 500000.0,
"remaining_balance": 0.0,
"is_paid": true,
"credits_activated": true,
"payments": [
{
"id": "507f1f77bcf86cd799439012",
"amount": 500000.0,
"method": "cash",
"provider": "manual",
"status": "completed",
"reference_id": "PAY-PKG-20250120103000",
"created_at": "2025-01-20T10:30:00",
"paid_at": "2025-01-20T10:30:00"
}
]
}
Response Fields¶
| Field | Type | Description |
|---|---|---|
customer_package_id |
string | Customer package ID |
package_name |
string | Package display name |
package_price |
decimal | Total package price |
payment_status |
string | Current payment status |
package_status |
string | Current package status |
total_paid |
decimal | Sum of completed payments |
remaining_balance |
decimal | Outstanding amount (should be 0 if paid) |
is_paid |
boolean | Whether package is fully paid |
credits_activated |
boolean | Whether credits are active and usable |
payments |
array | Payment history records |
payments[].id |
string | Payment record ID |
payments[].amount |
decimal | Payment amount |
payments[].method |
string | Payment method |
payments[].provider |
string | Payment provider (manual, paper_id) |
payments[].status |
string | Payment status |
payments[].reference_id |
string | Reference ID for tracking |
payments[].created_at |
datetime | When payment was created |
payments[].paid_at |
datetime | When payment was completed |
Payment Statuses¶
| Status | Description |
|---|---|
pending |
Payment awaiting completion |
paid |
Payment completed, credits activated |
failed |
Payment failed |
refunded |
Payment refunded |
Package Statuses¶
| Status | Description |
|---|---|
pending_payment |
Awaiting payment |
active |
Paid and credits available |
partially_used |
Some credits redeemed |
depleted |
All credits used |
expired |
Credits expired |
Business Rules¶
- Tenant isolation - Package must belong to user's tenant
- Customer access - Customers can only view their own packages
- Staff access - Staff can view any package in their tenant
- Payment history - Includes all payment attempts (successful and failed)
- Total calculation - Total paid calculated from COMPLETED payments only
Error Responses¶
Package Not Found:
Access Denied:
Payment Lifecycle¶
Overview¶
Package payments follow a defined lifecycle from creation to activation:
graph TD
A[Package Purchase Created] --> B{Payment Method?}
B -->|Manual On-Spot| C[Payment: COMPLETED]
C --> D[Package: ACTIVE]
D --> E[Credits: ALLOCATED]
B -->|Digital Payment| F[Payment: PENDING]
F --> G[Invoice Created]
G --> H[Payment Link Sent]
H --> I{Customer Pays?}
I -->|Yes| J[Webhook Received]
J --> K[Payment: COMPLETED]
K --> D
I -->|No/Expired| L[Payment: PENDING/FAILED]
B -->|Bank Transfer| M[Payment: PENDING]
M --> N[Customer Transfers]
N --> O{Staff Confirms?}
O -->|Yes| P[Record Manual Payment]
P --> C
O -->|No| Q[Payment: PENDING]
Payment States¶
| State | Package Status | Payment Status | Credits |
|---|---|---|---|
| Just Created (Manual) | active | paid | Allocated |
| Just Created (Digital) | pending_payment | pending | Not allocated |
| Payment Completed | active | paid | Allocated |
| Payment Failed | pending_payment | failed | Not allocated |
| Credits Used | active/depleted | paid | Partially/Fully used |
| Expired | expired | paid | Expired |
Integration with Package System¶
Related Endpoints¶
| System | Endpoint | Purpose |
|---|---|---|
| Customer Self-Service | POST /customer/packages/purchase |
Customer initiates purchase |
| Staff Manual Purchase | POST /staff/customer-packages |
Staff creates purchase for customer |
| Payment Processing | POST /customer-packages/{id}/record-payment |
Record offline payment |
| Payment Processing | POST /customer-packages/{id}/create-payment-link |
Create online payment link |
| Payment Processing | GET /customer-packages/{id}/payment-status |
Check payment status |
| Credit Redemption | POST /staff/customer-packages/credits/redeem |
Use credits for service |
Payment Flow Examples¶
Scenario 1: Walk-In Cash Payment¶
# 1. Staff creates package purchase for walk-in customer
curl -X POST https://api.example.com/api/v1/staff/customer-packages \
-H "Authorization: Bearer STAFF_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "507f1f77bcf86cd799439011",
"package_id": "507f1f77bcf86cd799439012",
"outlet_id": "507f1f77bcf86cd799439013",
"payment_method": "manual_onspot",
"amount_paid": 500000
}'
# Response: Credits already activated (manual_onspot auto-confirms)
Scenario 2: Customer Online Purchase¶
# 1. Customer purchases package online
curl -X POST https://api.example.com/api/v1/customer/packages/purchase \
-H "Authorization: Bearer CUSTOMER_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"package_id": "507f1f77bcf86cd799439012",
"outlet_id": "507f1f77bcf86cd799439013",
"payment_method": "paper_digital",
"send_email": true
}'
# Response includes payment_url - customer completes payment
# Webhook automatically activates credits
# 2. Check payment status
curl -X GET "https://api.example.com/api/v1/customer-packages/507f1f77bcf86cd799439020/payment-status" \
-H "Authorization: Bearer CUSTOMER_JWT_TOKEN"
Scenario 3: Bank Transfer with Staff Confirmation¶
# 1. Customer purchases with bank_transfer
curl -X POST https://api.example.com/api/v1/customer/packages/purchase \
-H "Authorization: Bearer CUSTOMER_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"package_id": "507f1f77bcf86cd799439012",
"outlet_id": "507f1f77bcf86cd799439013",
"payment_method": "bank_transfer"
}'
# 2. Customer makes bank transfer
# 3. Staff records manual payment
curl -X POST https://api.example.com/api/v1/customer-packages/507f1f77bcf86cd799439020/record-payment \
-H "Authorization: Bearer STAFF_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"amount": 500000,
"payment_method": "bank_transfer",
"notes": "Bank transfer confirmed - REF: TRF123456",
"receipt_number": "TRF123456"
}'
Scenario 4: Staff Creates Payment Link¶
# 1. Staff creates package purchase (pending)
curl -X POST https://api.example.com/api/v1/staff/customer-packages \
-H "Authorization: Bearer STAFF_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "507f1f77bcf86cd799439011",
"package_id": "507f1f77bcf86cd799439012",
"outlet_id": "507f1f77bcf86cd799439013",
"payment_method": "paper_digital",
"amount_paid": 500000
}'
# 2. Staff creates payment link
curl -X POST https://api.example.com/api/v1/customer-packages/507f1f77bcf86cd799439020/create-payment-link \
-H "Authorization: Bearer STAFF_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"send_email": true,
"send_whatsapp": true,
"notes": "Package payment for John Doe",
"due_date": "2025-01-25"
}'
# Customer receives payment link and completes payment
# Webhook activates credits automatically
Subscription Plan Considerations¶
Package Feature Availability¶
Package features depend on subscription plan:
| Plan | Packages Feature | Max Packages | Max Items/Package |
|---|---|---|---|
| FREE | Yes | 1 | 3 |
| PRO | Yes | 10 | 10 |
| ENTERPRISE | Yes | 100 | 20 |
Note: Plan limits apply to package creation, not to payment processing. Staff can process payments for any existing package regardless of plan tier.
Platform Fee Exemption¶
Package purchases are fee-exempt across all tiers:
| Payment Type | FREE Tier | PRO Tier | ENTERPRISE Tier |
|---|---|---|---|
| Appointment Payments | Variable | Variable | Variable |
| Package Payments | 0% | 0% | 0% |
| Subscription Payments | Variable | Variable | Variable |
See Package Management - Subscription Plan Limits for package creation limits.
Webhook Integration¶
Package Payment Webhook¶
Package payment webhooks are routed to the tenant-specific endpoint:
Invoice Metadata Structure¶
When creating a payment link, the invoice includes:
{
"invoice_type": "PACKAGE",
"metadata": {
"reference_id": "PKG-507f1f77bcf86cd799439011-20250120103000",
"customer_package_id": "507f1f77bcf86cd799439011",
"platform_fee": 0.0,
"platform_fee_percentage": 0.0
}
}
Webhook Handler¶
When payment is confirmed, the webhook handler:
- Validates invoice type is
PACKAGE - Finds invoice by Paper.id invoice ID
- Updates payment record to
COMPLETED - Updates customer package
payment_statustoPAID - Calls
activate_package_after_payment() - Creates credits for each package item
- Sets package status to
ACTIVE
See Webhook Integration for complete webhook documentation.
Best Practices¶
Recording Manual Payments¶
DO:
- Verify customer identity before recording payment
- Match payment amount exactly to package price
- Add descriptive notes for audit trail
- Include receipt/reference numbers when available
- Confirm package is in
pending_paymentstatus first
DON'T:
- Record payment without verifying customer
- Use online payment methods (use create-payment-link instead)
- Forget to add notes for unusual transactions
- Assume payment is recorded if error occurs
Creating Payment Links¶
DO:
- Verify customer has valid contact information
- Set appropriate due dates (not too short, not too long)
- Enable multiple delivery channels when possible
- Add helpful notes to the invoice
- Confirm tenant has Paper.id configured
DON'T:
- Create payment links for already paid packages
- Set due dates too far in the future (>7 days)
- Skip delivery method selection
- Ignore Paper.id configuration errors
Checking Payment Status¶
DO:
- Check status before processing related operations
- Use payment status to guide customer service
- Monitor for pending payments nearing due dates
- Review payment history for troubleshooting
DON'T:
- Assume payment is complete without checking
- Ignore failed payment records
- Provide inaccurate payment information to customers
Error Handling¶
Common Errors¶
| Error Code | Cause | Solution |
|---|---|---|
| 400 Bad Request | Invalid input or validation failure | Check request parameters |
| 401 Unauthorized | Missing/invalid token | Verify JWT token |
| 403 Forbidden | Insufficient permissions or wrong tenant | Check user role and tenant |
| 404 Not Found | Package not found | Verify package ID |
| 409 Conflict | Package already paid | Cannot process duplicate payment |
| 422 Validation Error | Invalid data format | Check field types and formats |
| 500 Internal Server Error | Server or gateway error | Contact support |
Gateway Errors¶
Paper.id Connection Error:
Solution: Check Paper.id credentials and network connectivity.
Paper.id API Error:
Solution: Verify customer has valid phone number format (E.164).
Related Documentation¶
Package System¶
- Customer Package Management - Customer self-service package operations
- Staff Customer Package Management - Staff-side package operations
- Package Management - Package creation and configuration
Appointment Integration¶
- Appointment Management - Complete appointment booking guide
- Appointment Credit Redemption - Using package credits for appointments
- Once package is paid, credits are activated
- Credits can be used at appointment booking time
- FIFO ordering ensures oldest credits used first
Supporting Documentation¶
- Webhook Integration - Payment webhook handling
- Invoice Management - Invoice details and downloads
- Payment History - Payment records and history
- Subscription Management - Plan limits and features
API Reference Summary¶
| Endpoint | Method | Purpose | Access |
|---|---|---|---|
/customer-packages/{id}/record-payment |
POST | Record offline payment | Staff |
/customer-packages/{id}/create-payment-link |
POST | Create payment link | Staff |
/customer-packages/{id}/payment-status |
GET | Check payment status | Staff/Customer |
Next Steps:
- Check package payment status:
GET /customer-packages/{id}/payment-status - For offline payments:
POST /customer-packages/{id}/record-payment - For online payments:
POST /customer-packages/{id}/create-payment-link - Monitor webhook deliveries for payment confirmation
- Verify credits activated:
GET /staff/customer-packages/{customer_id}/credits
For complete API testing, see Swagger UI or ReDoc.
Frontend UI Suggestions¶
This section provides UI/UX recommendations for frontend developers implementing package payment features.
Use Case 1: Customer Payment Status View¶
Customer-facing view showing package payment status and history.
Wireframe:
┌─────────────────────────────────────────────────────────────────────────┐
│ My Packages Customer Portal │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─ Premium Hair Package ────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ ✅ PAID │ │ │
│ │ │ Payment confirmed on Jan 20, 2025 │ │ │
│ │ │ │ │ │
│ │ └────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Package Price: Rp 500,000 │ │
│ │ Total Paid: Rp 500,000 │ │
│ │ Balance: Rp 0 │ │
│ │ │ │
│ │ ┌─ Package Status ───────────────────────────────────────────┐ │ │
│ │ │ Status: Active │ │ │
│ │ │ Credits: 10 available │ │ │
│ │ │ Expires: Apr 20, 2025 (90 days) │ │ │
│ │ └────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─ Payment History ──────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ Jan 20, 2025 Cash Payment Rp 500,000 ✅ Completed │ │ │
│ │ │ Receipt: PKG-RCPT-2025-001 │ │ │
│ │ │ │ │ │
│ │ └────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ [View Credits] [Book Service] │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Spa Relaxation Bundle ───────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ ⏳ PENDING PAYMENT │ │ │
│ │ │ Waiting for payment confirmation │ │ │
│ │ │ │ │ │
│ │ └────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Package Price: Rp 450,000 │ │
│ │ Total Paid: Rp 0 │ │
│ │ Balance Due: Rp 450,000 │ │
│ │ │ │
│ │ ⚠️ Credits will be activated after payment is confirmed │ │
│ │ │ │
│ │ [Complete Payment →] │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Key UI Elements:
- Status Badge: Color-coded (green=paid, yellow=pending, red=failed)
- Balance Summary: Clear display of amounts paid and due
- Credits Info: Show activation status
- Payment History: Chronological list of payment records
- Action Button: Context-aware (Pay Now vs View Credits)
Use Case 2: Staff Record Manual Payment¶
Staff interface for recording offline cash/POS payments.
Wireframe:
┌─────────────────────────────────────────────────────────────────────────┐
│ Record Payment - Premium Hair Package │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─ Package Details ─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Customer: John Smith │ │
│ │ Package: Premium Hair Package - 10 Sessions │ │
│ │ Package Price: Rp 500,000 │ │
│ │ Status: Pending Payment │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ Payment Method * │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ ● Cash │ │
│ │ Customer paid cash at location │ │
│ │ │ │
│ │ ○ POS Terminal │ │
│ │ Credit/debit card via POS machine │ │
│ │ │ │
│ │ ○ Bank Transfer │ │
│ │ Customer showed bank transfer confirmation │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ Payment Amount * │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Rp 500,000 [= Price] │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ ℹ️ Amount must match package price exactly │
│ │
│ Receipt/Reference Number │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ PKG-RCPT-2025-001 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ Notes │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Walk-in customer, paid cash at front desk │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ After Recording ─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ✓ Package status will change to ACTIVE │ │
│ │ ✓ 10 credits will be activated immediately │ │
│ │ ✓ Customer can start using credits right away │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ [Cancel] [Record Payment] │
└─────────────────────────────────────────────────────────────────────────┘
Success Confirmation:
┌─────────────────────────────────────────────────────────────────────────┐
│ ✅ Payment Recorded Successfully │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Package: Premium Hair Package - 10 Sessions │
│ Customer: John Smith │
│ Amount: Rp 500,000 │
│ Method: Cash │
│ Reference: PAY-PKG-20250120103000 │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ Package Status: ACTIVE ✓ │
│ Credits: 10 credits activated ✓ │
│ Expires: Apr 20, 2025 │
│ │
│ Recorded by: Sarah Thompson │
│ Time: Jan 20, 2025 10:30 AM │
│ │
│ [Print Receipt] [View Customer] [Done] │
└─────────────────────────────────────────────────────────────────────────┘
Key UI Elements:
- Package Summary: Show context before processing
- Method Radio: Clear description of each payment type
- Amount Auto-fill: Button to fill package price
- Validation Feedback: Inline error if amount mismatch
- Post-Payment Info: Explain what happens after recording
Use Case 3: Staff Create Payment Link¶
Staff interface for generating and sending payment links.
Wireframe:
┌─────────────────────────────────────────────────────────────────────────┐
│ Create Payment Link - Spa Relaxation Bundle │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─ Package Details ─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Customer: Jane Doe │ │
│ │ Package: Spa Relaxation Bundle │ │
│ │ Package Price: Rp 450,000 │ │
│ │ Platform Fee: Rp 0 (Fee-exempt) │ │
│ │ Total Amount: Rp 450,000 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Customer Contact ────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ✉️ Email: jane.doe@example.com ✓ Available │ │
│ │ 📱 Phone: +628123456789 ✓ Available │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ Send Payment Link Via * │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ ☑ Email │ │
│ │ Send invoice to jane.doe@example.com │ │
│ │ │ │
│ │ ☑ WhatsApp │ │
│ │ Send via WhatsApp to +628123456789 │ │
│ │ │ │
│ │ ☐ SMS │ │
│ │ Send via SMS (additional charges may apply) │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ Payment Due Date │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Jan 25, 2025 [📅] │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ Default: 3 days from today │
│ │
│ Invoice Notes (optional) │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Thank you for your purchase! Credits will be activated upon │ │
│ │ payment completion. │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ [Cancel] [Create & Send Link] │
└─────────────────────────────────────────────────────────────────────────┘
Success Confirmation:
┌─────────────────────────────────────────────────────────────────────────┐
│ ✅ Payment Link Created & Sent │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Invoice Number: INV-PKG-20250120-103000 │
│ Amount: Rp 450,000 │
│ Due Date: Jan 25, 2025 │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ Sent To: │
│ ✓ Email: jane.doe@example.com │
│ ✓ WhatsApp: +628123456789 │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ Payment Link: │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ https://pay.paper.id/invoice/abc123 [Copy] │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ℹ️ Credits will be activated automatically when payment is completed │
│ │
│ [Resend Link] [View Package] [Done] │
└─────────────────────────────────────────────────────────────────────────┘
Key UI Elements:
- Fee Transparency: Show platform fee is 0
- Contact Availability: Indicate which channels are available
- Multi-Channel Selection: Allow multiple delivery methods
- Copyable Link: Easy link sharing
- Auto-Activation Note: Reassure about credit activation
Use Case 4: Pending Payments Dashboard¶
Staff view for managing all pending package payments.
Wireframe:
┌─────────────────────────────────────────────────────────────────────────┐
│ Pending Package Payments Staff Portal │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Filter: [All Methods ▼] [Last 7 Days ▼] [All Outlets ▼] Search: [___] │
│ │
│ ┌─ Summary ─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ 5 │ │ Rp 2.5M │ │ 2 │ │ │
│ │ │ Pending │ │ Total │ │ Due Today │ │ │
│ │ │ Payments │ │ Amount │ │ │ │ │
│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ ⚠️ DUE TODAY │ │
│ │ │ │
│ │ Jane Doe Spa Relaxation Bundle │ │
│ │ Rp 450,000 Paper.id Payment Link │ │
│ │ Due: Jan 20, 2025 (Today) │ │
│ │ │ │
│ │ 📧 Email sent ✓ 📱 WhatsApp sent ✓ │ │
│ │ │ │
│ │ [Copy Link] [Resend] [Record Payment] [View Details] │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ ⏳ PENDING │ │
│ │ │ │
│ │ John Smith Premium Hair Package │ │
│ │ Rp 500,000 Bank Transfer │ │
│ │ Created: Jan 18, 2025 Due: Jan 21, 2025 │ │
│ │ │ │
│ │ Notes: Waiting for customer to complete bank transfer │ │
│ │ │ │
│ │ [Record Payment] [Create Payment Link] [View Details] │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 🔴 OVERDUE │ │
│ │ │ │
│ │ Mike Johnson Wellness Package │ │
│ │ Rp 800,000 Paper.id Payment Link │ │
│ │ Due: Jan 15, 2025 (5 days overdue) │ │
│ │ │ │
│ │ [Resend Link] [Call Customer] [Cancel Package] │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ Showing 3 of 5 pending payments [<] [1] [2] [>] │
└─────────────────────────────────────────────────────────────────────────┘
Key UI Elements:
- Summary Cards: Quick overview of pending amounts
- Priority Indicators: Due today and overdue badges
- Delivery Status: Show which channels were used
- Quick Actions: Context-appropriate buttons
- Filters: Filter by method, date, outlet
Use Case 5: Customer Payment Flow¶
Customer-facing payment completion interface.
Wireframe - Payment Link Landing:
┌─────────────────────────────────────────────────────────────────────────┐
│ Complete Your Payment [Brand Logo] │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Spa Relaxation Bundle │ │
│ │ │ │
│ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │
│ │ │ │
│ │ Package Includes: │ │
│ │ • Full Body Massage × 3 │ │
│ │ • Facial Treatment × 3 │ │
│ │ │ │
│ │ Valid for: 90 days after purchase │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Payment Summary ─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Package Price Rp 450,000 │ │
│ │ Platform Fee Rp 0 │ │
│ │ ─────────────────────────────────────────────────────────────── │ │
│ │ Total Rp 450,000 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ Invoice: INV-PKG-20250120-103000 │
│ Due: Jan 25, 2025 │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Complete Payment → │ │
│ └─────────────────────────────────┘ │
│ │
│ ℹ️ You will be redirected to Paper.id secure payment page │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Post-Payment Success:
┌─────────────────────────────────────────────────────────────────────────┐
│ Payment Successful! [Brand Logo] │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ │
│ │
│ Thank you for your purchase! │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Your credits have been activated! │ │
│ │ │ │
│ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │
│ │ │ │
│ │ Package: Spa Relaxation Bundle │ │
│ │ Credits: 6 credits now available │ │
│ │ Valid Until: Apr 20, 2025 │ │
│ │ │ │
│ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │
│ │ │ │
│ │ Amount Paid: Rp 450,000 │ │
│ │ Reference: PAY-PKG-20250120-ABC123 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ [View My Credits] [Book Appointment] [Download Receipt] │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Key UI Elements:
- Package Preview: Show what customer is paying for
- Fee Transparency: Clear breakdown showing no hidden fees
- Secure Payment Indicator: Trust signals
- Post-Payment CTA: Guide to next steps
UI Component Library Suggestions¶
React/JSX Component Examples:
// PaymentStatusBadge.jsx - Payment status indicator
const PaymentStatusBadge = ({ status, dueDate }) => {
const isOverdue = dueDate && new Date(dueDate) < new Date() && status === 'pending';
const isDueToday = dueDate && isToday(new Date(dueDate)) && status === 'pending';
const variants = {
paid: { color: 'green', icon: CheckIcon, label: 'Paid' },
pending: {
color: isOverdue ? 'red' : isDueToday ? 'orange' : 'yellow',
icon: isOverdue ? AlertIcon : ClockIcon,
label: isOverdue ? 'Overdue' : isDueToday ? 'Due Today' : 'Pending'
},
failed: { color: 'red', icon: XIcon, label: 'Failed' },
refunded: { color: 'gray', icon: RefundIcon, label: 'Refunded' }
};
const variant = variants[status] || variants.pending;
return (
<div className={`status-badge status-${variant.color}`}>
<variant.icon />
<span>{variant.label}</span>
</div>
);
};
// RecordPaymentForm.jsx - Staff manual payment form
const RecordPaymentForm = ({ customerPackage, onSuccess }) => {
const [method, setMethod] = useState('cash');
const [amount, setAmount] = useState(customerPackage.package_price);
const [receiptNumber, setReceiptNumber] = useState('');
const [notes, setNotes] = useState('');
const { mutate: recordPayment, isLoading } = useRecordPayment();
const handleSubmit = () => {
recordPayment({
customer_package_id: customerPackage.id,
amount: parseFloat(amount),
payment_method: method,
receipt_number: receiptNumber,
notes: notes
}, {
onSuccess: (data) => {
toast.success('Payment recorded - credits activated!');
onSuccess?.(data);
}
});
};
const isAmountValid = parseFloat(amount) === customerPackage.package_price;
return (
<form onSubmit={handleSubmit}>
<PackageSummary package={customerPackage} />
<RadioGroup
label="Payment Method"
value={method}
onChange={setMethod}
options={[
{ value: 'cash', label: 'Cash', description: 'Customer paid cash' },
{ value: 'pos_terminal', label: 'POS Terminal', description: 'Card via POS' },
{ value: 'bank_transfer', label: 'Bank Transfer', description: 'Transfer confirmed' }
]}
/>
<CurrencyInput
label="Payment Amount"
value={amount}
onChange={setAmount}
error={!isAmountValid && 'Amount must match package price exactly'}
helperText={`Package price: ${formatCurrency(customerPackage.package_price)}`}
endAdornment={
<Button
variant="text"
size="small"
onClick={() => setAmount(customerPackage.package_price)}
>
= Price
</Button>
}
/>
<TextField
label="Receipt/Reference Number"
value={receiptNumber}
onChange={(e) => setReceiptNumber(e.target.value)}
placeholder="PKG-RCPT-2025-001"
/>
<TextArea
label="Notes"
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Payment notes for audit trail"
/>
<InfoBox variant="success">
<strong>After recording:</strong>
<ul>
<li>Package status will change to ACTIVE</li>
<li>{customerPackage.total_credits} credits will be activated</li>
<li>Customer can start using credits immediately</li>
</ul>
</InfoBox>
<Button
type="submit"
loading={isLoading}
disabled={!isAmountValid}
>
Record Payment
</Button>
</form>
);
};
// CreatePaymentLinkForm.jsx - Generate payment link
const CreatePaymentLinkForm = ({ customerPackage, customer, onSuccess }) => {
const [sendEmail, setSendEmail] = useState(!!customer.email);
const [sendWhatsApp, setSendWhatsApp] = useState(!!customer.phone);
const [sendSms, setSendSms] = useState(false);
const [dueDate, setDueDate] = useState(addDays(new Date(), 3));
const [notes, setNotes] = useState('');
const { mutate: createPaymentLink, isLoading } = useCreatePaymentLink();
const hasDeliveryMethod = sendEmail || sendWhatsApp || sendSms;
const handleSubmit = () => {
createPaymentLink({
customer_package_id: customerPackage.id,
send_email: sendEmail,
send_whatsapp: sendWhatsApp,
send_sms: sendSms,
due_date: format(dueDate, 'yyyy-MM-dd'),
notes: notes
}, {
onSuccess: (data) => {
toast.success('Payment link sent successfully!');
onSuccess?.(data);
}
});
};
return (
<form onSubmit={handleSubmit}>
<PackageSummary package={customerPackage} showFees />
<CustomerContactCard customer={customer} />
<CheckboxGroup label="Send Payment Link Via">
<Checkbox
checked={sendEmail}
onChange={(e) => setSendEmail(e.target.checked)}
disabled={!customer.email}
label={`Email${customer.email ? `: ${customer.email}` : ' (not available)'}`}
/>
<Checkbox
checked={sendWhatsApp}
onChange={(e) => setSendWhatsApp(e.target.checked)}
disabled={!customer.phone}
label={`WhatsApp${customer.phone ? `: ${customer.phone}` : ' (not available)'}`}
/>
<Checkbox
checked={sendSms}
onChange={(e) => setSendSms(e.target.checked)}
disabled={!customer.phone}
label="SMS (additional charges may apply)"
/>
</CheckboxGroup>
{!hasDeliveryMethod && (
<Alert variant="error">
At least one delivery method must be selected
</Alert>
)}
<DatePicker
label="Payment Due Date"
value={dueDate}
onChange={setDueDate}
minDate={new Date()}
helperText="Default: 3 days from today"
/>
<TextArea
label="Invoice Notes (optional)"
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Notes to include on the invoice"
/>
<Button
type="submit"
loading={isLoading}
disabled={!hasDeliveryMethod}
>
Create & Send Link
</Button>
</form>
);
};
// PaymentHistoryList.jsx - Payment records display
const PaymentHistoryList = ({ payments }) => (
<div className="payment-history">
{payments.length === 0 ? (
<EmptyState message="No payment records yet" />
) : (
payments.map(payment => (
<div key={payment.id} className="payment-record">
<div className="payment-date">
{formatDate(payment.paid_at || payment.created_at)}
</div>
<div className="payment-method">
<PaymentMethodIcon method={payment.method} />
{PAYMENT_METHOD_LABELS[payment.method]}
</div>
<div className="payment-amount">
{formatCurrency(payment.amount)}
</div>
<PaymentStatusBadge status={payment.status} />
{payment.receipt_number && (
<div className="payment-reference">
Ref: {payment.receipt_number}
</div>
)}
</div>
))
)}
</div>
);
State Management Recommendations¶
TypeScript Interface for Payment State:
// types/packagePayment.ts
interface PackagePaymentState {
// Current package
currentPackage: CustomerPackage | null;
paymentStatus: PaymentStatusResponse | null;
// Record payment form
recordPaymentForm: {
method: PaymentMethod;
amount: number;
receiptNumber: string;
notes: string;
isSubmitting: boolean;
};
// Create payment link form
paymentLinkForm: {
sendEmail: boolean;
sendWhatsApp: boolean;
sendSms: boolean;
dueDate: Date;
notes: string;
isSubmitting: boolean;
};
// Pending payments dashboard
pendingPayments: PendingPayment[];
pendingFilters: {
method: PaymentMethod | 'all';
dateRange: DateRange;
outletId: string | null;
};
// UI state
isLoading: boolean;
error: string | null;
}
interface PaymentStatusResponse {
customer_package_id: string;
package_name: string;
package_price: number;
payment_status: 'pending' | 'paid' | 'failed' | 'refunded';
package_status: string;
total_paid: number;
remaining_balance: number;
is_paid: boolean;
credits_activated: boolean;
payments: PaymentRecord[];
}
interface PaymentRecord {
id: string;
amount: number;
method: PaymentMethod;
provider: 'manual' | 'paper_id';
status: 'pending' | 'completed' | 'failed';
reference_id: string;
created_at: string;
paid_at: string | null;
}
type PaymentMethod = 'cash' | 'pos_terminal' | 'bank_transfer' | 'paper_digital';
// Payment method labels
const PAYMENT_METHOD_LABELS: Record<PaymentMethod, string> = {
cash: 'Cash',
pos_terminal: 'POS Terminal',
bank_transfer: 'Bank Transfer',
paper_digital: 'Digital Payment'
};
API Integration Patterns¶
React Query Hooks:
// hooks/usePackagePayments.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Get payment status
export const usePaymentStatus = (customerPackageId: string) => {
return useQuery({
queryKey: ['package-payment', 'status', customerPackageId],
queryFn: () => api.get(
`/api/v1/customer-packages/${customerPackageId}/payment-status`
),
enabled: !!customerPackageId,
refetchInterval: (data) => {
// Poll more frequently for pending payments
return data?.payment_status === 'pending' ? 30000 : false;
}
});
};
// Record manual payment
export const useRecordPayment = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: {
customer_package_id: string;
amount: number;
payment_method: string;
notes?: string;
receipt_number?: string;
}) => api.post(
`/api/v1/customer-packages/${data.customer_package_id}/record-payment`,
data
),
onSuccess: (_, variables) => {
// Invalidate payment status
queryClient.invalidateQueries({
queryKey: ['package-payment', 'status', variables.customer_package_id]
});
// Invalidate customer credits (they're now activated)
queryClient.invalidateQueries({
queryKey: ['customer-credits']
});
// Invalidate pending payments list
queryClient.invalidateQueries({
queryKey: ['pending-payments']
});
},
onError: (error) => {
if (error.response?.status === 409) {
toast.error('Package is already paid');
} else if (error.response?.status === 400) {
toast.error(error.response.data.detail);
}
}
});
};
// Create payment link
export const useCreatePaymentLink = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: {
customer_package_id: string;
send_email?: boolean;
send_whatsapp?: boolean;
send_sms?: boolean;
due_date?: string;
notes?: string;
}) => api.post(
`/api/v1/customer-packages/${data.customer_package_id}/create-payment-link`,
data
),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ['package-payment', 'status', variables.customer_package_id]
});
}
});
};
// Get pending payments (staff dashboard)
export const usePendingPayments = (filters: PaymentFilters) => {
return useQuery({
queryKey: ['pending-payments', filters],
queryFn: () => api.get('/api/v1/staff/pending-payments', { params: filters }),
refetchInterval: 60000 // Refresh every minute
});
};
Error Handling UI Patterns¶
Common Error Scenarios:
// Error handling for payment operations
const handlePaymentError = (error) => {
const status = error.response?.status;
const detail = error.response?.data?.detail || '';
switch (status) {
case 400:
if (detail.includes('amount')) {
showModal({
type: 'error',
title: 'Amount Mismatch',
message: detail,
actions: [{ label: 'Fix Amount', onClick: () => focusAmountInput() }]
});
} else if (detail.includes('payment method')) {
toast.error('Invalid payment method. Use payment link for online payments.');
} else if (detail.includes('contact')) {
showModal({
type: 'error',
title: 'Missing Contact Information',
message: 'Customer must have email or phone to receive payment link.',
actions: [
{ label: 'Update Customer', onClick: () => editCustomer(), primary: true }
]
});
}
break;
case 409:
showModal({
type: 'info',
title: 'Already Paid',
message: 'This package has already been paid.',
actions: [
{ label: 'View Credits', onClick: () => viewCredits(), primary: true }
]
});
break;
case 500:
if (detail.includes('Paper.id')) {
toast.error('Payment gateway error. Please try again or use manual payment.');
} else {
toast.error('Server error. Please try again.');
}
break;
default:
toast.error('An error occurred. Please try again.');
}
};
// Amount validation component
const PaymentAmountInput = ({ value, onChange, packagePrice }) => {
const isValid = parseFloat(value) === packagePrice;
return (
<div className="amount-input-wrapper">
<CurrencyInput
value={value}
onChange={onChange}
error={value && !isValid}
/>
{value && !isValid && (
<div className="error-message">
Payment amount ({formatCurrency(value)}) must match package price
({formatCurrency(packagePrice)})
</div>
)}
<Button
variant="text"
size="small"
onClick={() => onChange(packagePrice.toString())}
>
Set to package price
</Button>
</div>
);
};
Accessibility Considerations¶
- Form Validation: Announce errors with
aria-liveregions - Status Changes: Use
aria-live="polite"for payment status updates - Radio Groups: Proper
role="radiogroup"with descriptions - Loading States: Announce "Processing payment..." during submission
- Currency Values: Use
aria-labelwith full amount spoken - Due Date Indicators: Color should not be sole indicator (use icons/text)
Mobile Responsive Guidelines¶
/* Payment form responsive */
.payment-form {
@media (max-width: 768px) {
.payment-summary {
padding: 1rem;
}
.payment-methods {
.method-option {
padding: 1rem;
margin-bottom: 0.5rem;
}
}
.amount-input-wrapper {
flex-direction: column;
.set-price-button {
width: 100%;
margin-top: 0.5rem;
}
}
}
}
/* Pending payments dashboard responsive */
.pending-payments {
@media (max-width: 768px) {
.summary-cards {
grid-template-columns: 1fr;
}
.payment-card {
.payment-actions {
flex-direction: column;
gap: 0.5rem;
button {
width: 100%;
}
}
}
.filters {
flex-direction: column;
select {
width: 100%;
}
}
}
}
/* Customer payment page responsive */
.customer-payment-page {
@media (max-width: 768px) {
.package-preview {
padding: 1rem;
}
.payment-summary {
.summary-row {
flex-direction: column;
text-align: center;
}
}
.complete-payment-button {
width: 100%;
padding: 1rem;
font-size: 1.1rem;
}
}
}