Skip to content

feat(payments): add client-facing invoice payments via Stripe#23

Open
edgarjc wants to merge 1 commit intomainfrom
feature/stripe-invoice-payments
Open

feat(payments): add client-facing invoice payments via Stripe#23
edgarjc wants to merge 1 commit intomainfrom
feature/stripe-invoice-payments

Conversation

@edgarjc
Copy link
Copy Markdown
Contributor

@edgarjc edgarjc commented Mar 27, 2026

Summary

  • Adds Stripe invoice payments with dual-mode support: direct API keys (self-hosted) and Stripe Connect OAuth (hosted product)
  • Clients see a "Pay Now" button on sent/overdue invoices in the portal, which redirects to Stripe Checkout
  • Webhooks auto-mark invoices as paid with notifications to admins
  • Dashboard shows payment source ("Paid via Stripe" vs "Marked as paid") and protects Stripe-paid invoices from deletion

How it works

Mode For How agencies connect Env vars needed
Direct keys Self-hosted Paste Stripe secret key in Settings None
Connect OAuth Hosted product Click "Connect with Stripe" STRIPE_CONNECT_CLIENT_ID

Both modes use the same checkout flow, webhook handling, and portal UI. Mode is auto-detected based on whether STRIPE_CONNECT_CLIENT_ID is set.

New files

  • apps/api/src/payments/ — NestJS module (service, controller, webhook controller, DTOs)
  • apps/web/.../payments-section.tsx — Settings UI for both modes
  • packages/email/src/templates/invoice-paid.tsx — Payment received email template
  • e2e/tests/invoice-payments.e2e.ts — 13 e2e tests
  • docs/testing-stripe-connect.md — Manual testing guide

Schema changes

  • SystemSettings: stripeSecretKey, stripeWebhookSecret, stripeConnectAccountId, stripeConnectEnabled, stripeConnectLivemode
  • Invoice: stripeCheckoutSessionId, stripePaymentIntentId, paidAt, paidAmount

Security

  • HMAC-signed OAuth state with timing-safe comparison
  • Org-scoped webhook URLs (/webhook/:orgId) prevent cross-org injection
  • URL origin validation prevents open redirects
  • AES-256-GCM encrypted key storage
  • Idempotent webhook processing (prevents double-charge notifications)
  • Stripe-paid invoices protected from deletion
  • Sensitive fields masked in all API responses

Test plan

  • TypeScript compiles clean (API + Web + Email)
  • Full production build passes
  • 440 unit tests pass
  • 13 new e2e tests for payments feature
  • Manual: paste test Stripe key in Settings, verify webhook auto-registers
  • Manual: client clicks Pay Now → Stripe Checkout → invoice marked as paid
  • Manual: Stripe Connect OAuth flow (requires STRIPE_CONNECT_CLIENT_ID)

Dual-mode Stripe integration for invoice payments:
- Direct keys mode (self-hosted): agency pastes Stripe secret key, app encrypts it and auto-registers webhook
- Connect OAuth mode (hosted): activated via STRIPE_CONNECT_CLIENT_ID env var, agencies click "Connect with Stripe"

Core features:
- Pay Now button on portal invoices (sent/overdue) redirects to Stripe Checkout
- Webhook auto-marks invoices as paid with idempotent processing
- Email + in-app + push notifications on payment received
- Dashboard shows "Paid via Stripe" vs "Marked as paid" distinction
- Stripe-paid invoices are protected from deletion
- Account deauthorization webhook auto-disables payments

Security:
- HMAC-signed OAuth state with timing-safe comparison
- Org-scoped webhook URLs prevent cross-org event injection
- URL origin validation prevents open redirects
- Encrypted key storage (AES-256-GCM)
- Sensitive fields masked in API responses

Schema changes:
- SystemSettings: stripeSecretKey, stripeWebhookSecret, stripeConnectAccountId, stripeConnectEnabled, stripeConnectLivemode
- Invoice: stripeCheckoutSessionId, stripePaymentIntentId, paidAt, paidAmount
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant