Skip to main content

Payments & Accounting Integrations

Overview of payment flows, multi-PSP abstraction, and accounting synchronization.

Payment Providers

The system supports four payment service providers (PSPs):

ProviderLocationFeatures
Peach Paymentslibs/payments/payment-psp-peachCharge, refund, preauth, payouts, payment links, webhook HMAC verification
Stripelibs/payments/payment-psp-stripePaymentIntent-based, network tokens, webhook validation
Rapydlibs/payments/payment-psp-rapydHosted checkout, portal links, settlement reconciliation
Braintreelibs/payments/payment-psp-braintreeCharges, 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

ProviderLocationAuth
QuickBooks Onlinelibs/accounting-integrations/accounting-integrations-qboOAuth
Xerolibs/accounting-integrations/accounting-integrations-xeroOAuth
Sagelibs/accounting-integrations/accounting-integrations-sageOAuth

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

RepositoryPurpose
PAYMENT_ORDER_REPOSITORYOrder/intent/transaction management
JOURNAL_REPOSITORYDouble-entry accounting entries
PAYMENT_LINK_REPOSITORYPayment link tracking
PAYOUT_REPOSITORYPayout 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

MetricDescription
payment_requests_totalCounter by PSP and status
payment_duration_secondsHistogram of payment latency
refund_requests_totalCounter of refund operations
webhook_processed_totalCounter 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