Payments & Accounting Integrations
Overview of payment flows, multi-PSP abstraction, and accounting synchronization.
Payment Providers
The system supports four payment service providers (PSPs):
| Provider | Location | Features |
|---|---|---|
| Peach Payments | libs/payments/payment-psp-peach | Charge, refund, preauth, payouts, payment links, webhook HMAC verification |
| Stripe | libs/payments/payment-psp-stripe | PaymentIntent-based, network tokens, webhook validation |
| Rapyd | libs/payments/payment-psp-rapyd | Hosted checkout, portal links, settlement reconciliation |
| Braintree | libs/payments/payment-psp-braintree | Charges, refunds, payouts, webhook handling |
Architecture
Core Layer (payment-core)
Pure domain library with framework-agnostic abstractions:
interface PaymentAdapter {
charge(request: ChargeRequest): Promise<ChargeResponse>;
refund(request: RefundRequest): Promise<RefundResponse>;
preauth?(request: PreauthRequest): Promise<PreauthResponse>;
capture?(transactionId: string, amount: number): Promise<CaptureResponse>;
createPaymentLink?(request: PaymentLinkRequest): Promise<PaymentLink>;
payout?(request: PayoutRequest): Promise<PayoutResponse>;
}
Nest Integration (payment-nest)
PaymentSwitchModule — Dynamic PSP selection:
// Static provider
PaymentSwitchModule.register({ provider: 'peach' })
// Async/feature-flag resolution
PaymentSwitchModule.registerAsync({
useFactory: activePspFactory,
inject: [FeatureFlagsService],
})
PaymentOrchestrator — High-level orchestration:
- Order/intent/transaction persistence
- Idempotency checking (prevent duplicate charges)
- Over-refund protection
- Journal entry creation (double-entry accounting)
Payment Flows
Booking Payment Flow
Payment Completion
↓
Event: payment.recorded
↓
TeeTimeSyncService (listens)
↓
Resolve Provider (ProviderFactory)
↓
Create Invoice in Accounting System
↓
Create Payment Record
Order Context
interface PaymentOrderContext {
orderId: string;
tenantId?: string;
clubId?: string;
sourceType: 'BOOKING' | 'TOURNAMENT_ENTRY' | 'INVOICE' | 'SUBSCRIPTION';
sourceId?: string;
currency: CurrencyCode;
amountCents: number;
feeCents?: number;
persist?: boolean;
}
Order State Machine
PENDING → REQUIRES_ACTION → SUCCEEDED → [PARTIALLY_REFUNDED | REFUNDED]
↓
FAILED
↓
CANCELLED
Accounting Integration
Supported Accounting Providers
| Provider | Location | Auth |
|---|---|---|
| QuickBooks Online | libs/accounting-integrations/accounting-integrations-qbo | OAuth |
| Xero | libs/accounting-integrations/accounting-integrations-xero | OAuth |
| Sage | libs/accounting-integrations/accounting-integrations-sage | OAuth |
IAccountingProvider Interface
interface IAccountingProvider {
buildConsentUrl(): Promise<string>;
handleCallback(code: string): Promise<TokenResponse>;
refreshIfNeeded(): Promise<void>;
createInvoice(invoice: InvoiceInput): Promise<Invoice>;
createPayment(payment: PaymentInput): Promise<Payment>;
createBill(bill: BillInput): Promise<Bill>;
createExpense(expense: ExpenseInput): Promise<Expense>;
createJournalEntry(entry: JournalEntry): Promise<JournalEntryResponse>;
createCustomer(customer: CustomerInput): Promise<Customer>;
listLedgerAccounts(): Promise<LedgerAccount[]>;
listTaxRates(): Promise<TaxRate[]>;
}
Event-Driven Sync
Booking payments are synchronized via domain events:
// Event payload
{
type: 'teeTime.payment.recorded',
tenantId: string,
clubId: string,
bookingId: string,
amount: number,
currency: string
}
- Events emitted on booking confirm/cancel/reschedule
- Outbox pattern ensures at-least-once delivery
- Consumers should be idempotent
Configuration
Payment Provider
# Peach Payments
PEACH_BASE_URL=https://api.peachpayments.com
PEACH_API_KEY=<api-key>
PEACH_SECRET_KEY=<secret-key>
PEACH_ENTITY_ID=<entity-id>
PEACH_WEBHOOK_SECRET=<webhook-secret>
# Stripe
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Feature flag for PSP selection
ACTIVE_PSP=peach|stripe|rapyd|braintree
Accounting Provider
# QuickBooks Online
QBO_CLIENT_ID=<client-id>
QBO_CLIENT_SECRET=<client-secret>
QBO_REDIRECT_URI=https://app.example.com/qbo/callback
QBO_ENVIRONMENT=sandbox|production
# Xero
XERO_CLIENT_ID=<client-id>
XERO_CLIENT_SECRET=<client-secret>
XERO_REDIRECT_URI=https://app.example.com/xero/callback
SCL Billing Integration
Billing Service
Location: libs/scl/billing/src/lib/facade/billing.service.ts
interface BillingService {
preview(items: BillingItem[]): Promise<PricePreview>;
post(items: BillingItem[]): Promise<Invoice>;
}
Refund Service
Location: libs/scl/booking/src/lib/psp-refund-service.ts
interface PspRefundService {
refundBooking(bookingId: number): Promise<RefundResult>;
}
Persistence
Payment Repositories
| Repository | Purpose |
|---|---|
PAYMENT_ORDER_REPOSITORY | Order/intent/transaction management |
JOURNAL_REPOSITORY | Double-entry accounting entries |
PAYMENT_LINK_REPOSITORY | Payment link tracking |
PAYOUT_REPOSITORY | Payout tracking |
Journal Entry Example
{
debit: { accountId: 'receivables', amount: 10000 },
credit: { accountId: 'revenue', amount: 10000 },
reference: 'booking-123',
memo: 'Tee time booking payment'
}
Webhooks
Signature Verification
Each PSP has specific webhook verification:
// Peach - HMAC-SHA256
const isValid = verifyPeachWebhook(payload, signature, secret);
// Stripe
const event = stripe.webhooks.constructEvent(payload, signature, secret);
Idempotency
- Webhook handlers track processed event IDs
- Duplicate events are acknowledged but not reprocessed
- Failed handlers retry with exponential backoff
Metrics
| Metric | Description |
|---|---|
payment_requests_total | Counter by PSP and status |
payment_duration_seconds | Histogram of payment latency |
refund_requests_total | Counter of refund operations |
webhook_processed_total | Counter of webhook events |
Testing
UAT Environment
- Use staging credentials for each PSP
- Verify booking flows end-to-end (search → pricing → booking → payment)
- Test refund/cancellation flows
Accounting Validation
- Validate event payloads from outbox against ingestion schema
- Test OAuth flows with sandbox accounts
- Verify invoice/payment reconciliation
Quick Checklist
- Set payment provider credentials via env/secrets
- Configure webhook endpoints with PSP dashboard
- Ensure events worker is running (
teetime-events-worker) - Set up accounting OAuth connections per tenant
- Monitor outbox for undelivered events