feat: add support for controlling user subscription is billed to - #16
Open
mrbasel wants to merge 7 commits into
Open
feat: add support for controlling user subscription is billed to#16mrbasel wants to merge 7 commits into
mrbasel wants to merge 7 commits into
Conversation
Who gets billed needs no configuration: HTTP endpoints bill the session
user; the server-only endpoints bill the reference.
- organization: { enabled: true } makes org references billable: the org
gets its own consumer, stored on a new organization.streampayConsumerId
column (schema gated behind the option); optional
organization.getBillingDetails supplies contact/tax fields while
name/email/external_id stay plugin-owned
- new server-only auth.api.upgradeSubscriptionForReference: trusted server
code starts a subscription checkout billed to any user or organization —
no session, caller is the gate, every validation still runs
- new server-only auth.api.checkoutForReference: one-time payment link
billed to any user or organization, with validated referenceId/
referenceType attribution in link metadata; HTTP checkout unchanged
- org consumers pinned to external_id ref:organization:<orgId>; user
external ids unchanged (bare user.id)
- typed errors: SUBSCRIPTION_ORG_BILLING_NOT_ENABLED, ORG_NOT_FOUND,
BILLING_CONTACT_REQUIRED
- requires better-auth >= 1.6.23 (createAuthEndpoint.serverOnly)
Co-authored-by: Basel <baselalnassr8@gmail.com>
…ints Co-authored-by: Basel <baselalnassr8@gmail.com>
- Pending subscription checkouts only resume for the same StreamPay consumer; a link created for one payer is never handed to another. - Organization upgrades grant no trial when isTrialEligible is configured (user-scoped policy fails closed for orgs). - checkoutForReference allows one payment by default, passes the resolved user and referenceType to onCheckoutCreated. - Organization consumer claims are compare-and-set; concurrent first checkouts settle on one consumer. - organization.modelName preserves custom org table names; startup fails fast when the organization plugin is missing or mismatched. - Drop accidental pnpm-workspace.yaml; CI minimum-peer job now installs better-auth 1.6.23. Co-authored-by: Basel <baselalnassr8@gmail.com>
- A StreamPay consumer linked to an organization can never be claimed by a user, and the other way around; the user recovery and duplicate- claim paths now check the organization table when org billing is on, before any external_id rewrite. - Webhook reconciliation verifies the payer: a provider subscription billed to a different consumer than the local row dead-letters the protected correlation and is never matched by the fallback. - A consumer created by the losing side of a concurrent organization claim is deleted best-effort. - organization.getBillingDetails may supply a billing email; name and external_id stay plugin-owned. - Export BillingOrganization, OrganizationBillingOptions, and OrganizationConsumerOverrides from the package root; internalize helpers only used inside billing-account.ts; drop the unused BillingAccountResolution.referenceId field. - Real-router integration test: both server-only endpoints exist on auth.api and no guessed HTTP route serves them. Co-authored-by: Basel <baselalnassr8@gmail.com>
- Known-subscription webhooks dead-letter when the provider consumer differs from the stored payer; a provider payload without a consumer never clears the stored payer (existing-ID, protected, and fallback paths all covered). - Signup consumer adoption (createConsumerOnSignUp + claimExistingConsumerBy) enforces the same ownership rules as lazy provisioning, and a consumer whose external_id carries the organization prefix is never claimable by a user at all. - The organization ownership check also runs when org billing is currently disabled, so retained organization links stay protected after a feature rollback. - The losing side of an organization claim re-checks ownership right before deleting its unused consumer, and a failed persistence also cleans up the consumer it created. - Organization billing always requires the Better Auth organization plugin; model names are normalized before comparison, so a custom name on either side without the other fails fast at startup. - BILLING_CONTACT_REQUIRED is raised only when StreamPay's field-level validation names email or phone_number; other provider validation errors pass through unchanged. - Peer floor corrected to better-auth ^1.6.17 (verified: serverOnly ships in 1.6.17, absent in 1.6.16) across package.json, CI, README, and the skill; the changeset is now major because dropping the 1.5 peer range is a breaking release. Co-authored-by: Basel <baselalnassr8@gmail.com>
…g rollback policy - Regenerate pnpm-lock.yaml so `pnpm install --frozen-lockfile` passes from a clean checkout (specifier was stale after the ^1.6.17 peer change); pin the toolchain with `packageManager: pnpm@10.13.1` and let CI read it. - INVOICE_COMPLETED now runs the same payer assertion as SUBSCRIPTION_* events: a provider consumer that differs from the stored payer dead-letters the event before any projection or renewal callback. - Fallback correlation refuses to create a replacement row when matching reference/plan candidates exist but are all billed to a different consumer — independent of subscription status or active-slot uniqueness, closing the terminal-status bypass. - Organization rollback policy is now real: the org consumer field is registered whenever the `organization` option is present, so `enabled: false` keeps retained org-owned consumers protected; ownership queries fail closed instead of converting errors into "unowned". - Remove synchronous best-effort consumer deletion: no ownership read can authorize a later provider delete race-free. Unlinked consumers are left in place and re-adopted via their deterministic external id. - auth.api endpoint types now derive from the configured `use` tuple; unconfigured methods fail at compile time instead of being undefined at runtime. - README no longer promises atomic cross-model ownership; changeset and docs describe the rollback mode and the remaining same-instant race. Co-authored-by: Basel <baselalnassr8@gmail.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.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.