feat(payments): per-entity payment providers + Stripe Connect (reseller SaaS Mode) - #18
Merged
Merged
Conversation
Foundation for reseller "SaaS Mode": let an agency collect on their OWN Stripe for their billing entity's customers, while org-level providers behave exactly as before. - Add nullable payment_providers.billing_entity_id (NULL = org-level = current behavior). Migration + structure.sql (hand-edited; regenerate in CI). - BaseProvider belongs_to :billing_entity, optional. - FindService: optional billing_entity_id — prefer entity-scoped provider, fall back to org-level; scope untouched when absent (webhooks/management unchanged). - Customers::PaymentProviderFinder passes customer.billing_entity_id. - StripeService#create_or_update accepts billing_entity_code (resolved via BillingEntities::ResolveService) to scope a NEW provider; exposed on GraphQL StripeInput. Provider codes stay org-unique (webhook-by-code routing unambiguous). No money-movement or webhook code changed. RSpec to run in CI (local Ruby 2.6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds on per-entity payment providers: an agency's own Stripe (a Connect
connected account, acct_…) can now collect for their billing entity's
customers. Fully backward-compatible — a provider with no connected account
behaves byte-identically to before.
- StripeProvider: settings accessor `connected_account_id`, `connected?`, and
`stripe_request_options` (returns {api_key:, stripe_account:} for a connected
account; {api_key:} otherwise). For a connected provider, secret_key holds the
PLATFORM key and calls run on the connected account via Stripe-Account.
- StripeService#create_or_update + GraphQL StripeInput accept
connected_account_id (set on creation, scoped to the billing entity).
- Threaded stripe_request_options through EVERY Stripe call path so connected
providers act on their account: payment intents, customer create/update,
checkout sessions, refunds, payment-method retrieve/list/check, funding
instructions, setup-intent webhook, and webhook register/refresh. Added a
shared helper on PaymentProviders::Stripe::BaseService.
- white_label/reset_service (agency-pays-us wholesale) intentionally stays
org-level.
NOT run locally (repo requires Ruby 4.0.2; local is 2.6). Needs RSpec in CI +
a Stripe test-mode end-to-end before merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rrader26
marked this pull request as ready for review
July 28, 2026 20:56
rrader26
pushed a commit
that referenced
this pull request
Jul 28, 2026
… lint - 20260728000000 (#18): use disable_ddl_transaction! + concurrent index + add_foreign_key validate:false, matching the repo convention (AddBillingEntityToWallets). The prior form failed `rails db:migrate` under strong_migrations (index inside a txn) — this blocked deploys. - spec: declare subject before let (RSpec/LeadingSubject). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rrader26
added a commit
that referenced
this pull request
Jul 28, 2026
… provider (#20) * feat(payments): REST endpoint to register a reseller's Stripe Connect provider POST /api/v1/payment_providers/stripe_connect (api_key auth) creates an entity-scoped Stripe provider from a reseller's connected account, so the agency's sub-account retail invoices collect on their OWN Stripe. - Reuses the organization's platform Stripe key (no secret over the wire); calls run on the connected account via Stripe-Account (per #18). - Idempotent by provider code. Requires a billing_entity_code + acct_. Consumed by growth-os resellerService.registerConnectedProviderInLago. Needs RSpec in CI (local Ruby 2.6 vs 4.0.2). Part of ThinkfleetAI/growth-os#1243. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(payments): request spec for stripe_connect provider endpoint Covers: creating an entity-scoped connected Stripe provider on the org's platform key, the resulting connected stripe_request_options (Stripe-Account header), not-found when the org has no platform Stripe provider, and failure on an unknown billing entity code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(payments): make billing_entity migration strong_migrations-safe + lint - 20260728000000 (#18): use disable_ddl_transaction! + concurrent index + add_foreign_key validate:false, matching the repo convention (AddBillingEntityToWallets). The prior form failed `rails db:migrate` under strong_migrations (index inside a txn) — this blocked deploys. - spec: declare subject before let (RSpec/LeadingSubject). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(payments): drop DB-level FK on payment_providers.billing_entity_id The schema-dump check failed because a hand-maintained FK can't match Rails' deterministic constraint name/order. billing_entity_id is an optional, app-managed reference — keep the column + concurrent index, drop the FK. Aligns the migration and structure.sql with the generated schema. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: rrader2890 <ryan@scheduleventures.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Per-entity payment providers + Stripe Connect (reseller "SaaS Mode")
Lets a reseller/agency collect on their own Stripe for their sub-accounts' customers, while org-level providers behave exactly as before. Backing feature: ThinkfleetAI/growth-os#1243.
Today payment providers are organization-scoped only; a customer resolves its provider by
organization_id+payment_provider_code, so an agency's billing entity is charged on the parent org's single Stripe. This PR adds (a) optional per-billing-entity provider scoping and (b) Stripe Connect so the provider can be a reseller's own connected account.Part 1 — per-entity provider scoping
payment_providers.billing_entity_id— new nullable FK. NULL = org-level = today's behavior.PaymentProviders::FindServicegains optionalbilling_entity_id: prefers an entity-scoped provider, falls back to org-level. Scope untouched when absent, so webhook routing (by org + code) is unchanged.Customers::PaymentProviderFinderpassescustomer.billing_entity_id.StripeService#create_or_update+ GraphQLStripeInputacceptbilling_entity_code.Part 2 — Stripe Connect (connected accounts)
StripeProvider:connected_account_idaccessor,connected?, andstripe_request_options— returns{api_key:, stripe_account:}for a connected account,{api_key:}otherwise. For a connected provider,secret_keyholds the platform key and calls run on the connected account via theStripe-Accountheader (Standard Connect account type).create_or_update+ GraphQL acceptconnected_account_id(set on creation, scoped to the billing entity).stripe_request_optionsthrough every Stripe call path so a connected provider acts on its own account: payment intents, customer create/update, checkout sessions, refunds, payment-method retrieve/list/check, funding instructions, the setup-intent webhook, and webhook register/refresh. A shared helper lives onPaymentProviders::Stripe::BaseService.white_label/reset_service(the agency-pays-us wholesale path) intentionally stays org-level.Why this is safe
Every change is gated on a NULL check: no
billing_entity_idand noconnected_account_id→ byte-identical to current behavior. No entity-scoped or connected providers exist yet, so existing org-level Stripe is completely unaffected; a bug could only affect the new reseller feature.connected_account_id, register webhook on the connected account, run a checkout + payment intent + refund + payment-method retrieval, confirm they land on the connected account and webhooks validate.db/structure.sqlwas hand-edited (Part 1 column/index/FK); regenerate viabin/rails db:migrate && db:schema:dumpto normalize the FK name.Follow-ups (separate)
flobyte:seed).acct_…; create the entity-scoped provider; route sub-account customers (growth-os#1246, merged, handles the entity routing).