feat(billing): payment provider integration for invoice Pay Now (PMS-711) #489
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/PMS-711-stripe-pay-now"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Implements PMS-711: an invoice sent from Mokosh carries a working Pay Now action that takes the recipient to the sender's own Stripe account, and the resulting payment (and any refund) syncs back onto the invoice via a signed webhook. Stripe is the first adapter behind a provider interface; PayPal follows as an adapter, not a rewrite.
Design decisions
payment_gateway_configsblob (PMS-342: encrypted at rest, never returned to a client, never logged). Checkout sessions are created ON the tenant's account, so funds settle to them and the platform is never a money transmitter.recompute_invoice_payment_statenow derivesamount_paid = SUM(payments) - SUM(payment_refunds), so a full refund walks the invoice back tosentand a partial topartially_paidwith no bespoke transition code.POST /api/v1/stripe/webhooks/{tenant_id}. The signing secret is per-tenant, so the receiver must know which secret to verify against before trusting the body; the path segment selects the tenant and is trusted only after the signature verifies. Modeled on the existing bunyip webhook: mounted outside the JWT chain, verify-before-parse over raw bytes,migrator_poolfor the pre-auth credential read with a SAFETY note.payments(tenant_id, gateway_transaction_id)andUNIQUE(tenant_id, provider_reference)onpayment_refunds, both withON CONFLICT DO NOTHING, so Stripe's retries and cumulative refund lists converge.Surfaces
POST /invoices/{id}/paymints a hosted checkout session (company-scoped exactly likeget_invoice) and returns its URL for the SPA to redirect to.Out of scope (deliberate, documented)
Stripe Connect onboarding (the tenant's own key is used instead), PayPal (a follow-up adapter), and zero-decimal currencies (amounts convert at 100 minor units per major unit). Manual payments/refunds keep working through the existing
/paymentspath unchanged.Acceptance criteria
payment_gateway_secret_is_write_onlystill passes)./pay+ outbound email button).Testing
tests/pms711_stripe_pay_now.rs: paid + idempotent redelivery, partial refund, abandoned (unpaid) no-op, bad-signature 401 that changes nothing. All through the real route on the NOBYPASSRLS app role.billing,recurring_invoicing,portal,rls_coverage(new table carries fail-closed RLS) all green.just checkclean (fmt, clippy -D warnings, migration/mail/runner/pool-safety gates);just pre-commitclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01QLd9c7niucrqx68v4AxVss
Mounts POST /api/v1/stripe/webhooks/{tenant_id} OUTSIDE the JWT chain (Stripe authenticates itself via the per-tenant signing secret; the path tenant selects which secret to verify against and is trusted only after verification), adds the portal POST /invoices/{id}/pay that mints a checkout session scoped to the contact's own company, and switches the agent-facing BillingService to with_delivery so the first invoice send transition emails the billing contact a Pay Now link when a gateway is active. Verify-before-parse, migrator_pool for the pre-auth credential read with a SAFETY note, and the email is best-effort post-commit so a mail failure never undoes the send. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLd9c7niucrqx68v4AxVss