@@ -3,6 +3,8 @@ import { subscription } from '@sim/db/schema'
33import { createLogger } from '@sim/logger'
44import { APIError } from 'better-auth/api'
55import { and , eq , isNotNull } from 'drizzle-orm'
6+ import { getAwsMarketplaceProductCode } from '@/lib/billing/aws-marketplace/config'
7+ import { holdsMarketplaceLicense } from '@/lib/billing/aws-marketplace/repository'
68import { isOrganizationOwnerOrAdmin } from '@/lib/billing/core/organization'
79import {
810 getOrganizationCoverageForMember ,
@@ -55,6 +57,22 @@ async function assertNoBoundIncompleteSubscription(referenceId: string): Promise
5557 } )
5658}
5759
60+ /**
61+ * Refuse a Stripe checkout for a reference AWS Marketplace bills. A listed product must be billed
62+ * entirely through AWS, and such a customer looks Free to Stripe, so a checkout would start a
63+ * second, real subscription. The client hides plans too, but only this check holds when its
64+ * license lookup fails.
65+ */
66+ async function assertNotBilledThroughMarketplace ( referenceId : string ) : Promise < void > {
67+ if ( ! getAwsMarketplaceProductCode ( ) ) return
68+ if ( ! ( await holdsMarketplaceLicense ( referenceId ) ) ) return
69+
70+ logger . warn ( 'Blocking checkout - reference is billed through AWS Marketplace' , { referenceId } )
71+ throw new APIError ( 'CONFLICT' , {
72+ message : 'This subscription is billed through AWS Marketplace. Contact support to change it.' ,
73+ } )
74+ }
75+
5876/**
5977 * Classify a `/subscription/upgrade` request as a personal checkout using
6078 * the same reference resolution as the Better Auth Stripe plugin
@@ -81,9 +99,10 @@ export function isPersonalCheckoutRequest(
8199/**
82100 * Guard for personal (user-referenced) checkouts on `/subscription/upgrade`.
83101 *
84- * Blocks both a bound, incomplete Stripe subscription and a member already
85- * covered by an entitled organization subscription. Throws {@link APIError}
86- * with a user-facing message; the checkout UI surfaces it as-is.
102+ * Blocks an account AWS Marketplace bills, a bound, incomplete Stripe
103+ * subscription, and a member already covered by an entitled organization
104+ * subscription. Throws {@link APIError} with a user-facing message; the
105+ * checkout UI surfaces it as-is.
87106 *
88107 * Called from the Better Auth `hooks.before` middleware, NOT from
89108 * `authorizeReference`: the Stripe plugin skips `authorizeReference`
@@ -94,6 +113,7 @@ export function isPersonalCheckoutRequest(
94113 * rejected rather than risking a duplicate subscription.
95114 */
96115export async function assertPersonalCheckoutAllowed ( userId : string ) : Promise < void > {
116+ await assertNotBilledThroughMarketplace ( userId )
97117 await assertNoBoundIncompleteSubscription ( userId )
98118
99119 const coverage = await getOrganizationCoverageForMember ( userId )
@@ -130,6 +150,7 @@ export async function assertPersonalCheckoutAllowed(userId: string): Promise<voi
130150 * For `upgrade-subscription` (checkout) this enforces, each thrown as an
131151 * {@link APIError} with a descriptive message so callers see the real
132152 * reason instead of a generic "Unauthorized":
153+ * - An organization AWS Marketplace bills cannot check out through Stripe.
133154 * - Organizations can only check out Team or Enterprise plans — a `pro_*`
134155 * plan can never become org-referenced.
135156 * - A bound, incomplete Stripe subscription must finish before another
@@ -148,6 +169,10 @@ export async function authorizeSubscriptionReference(
148169 return true
149170 }
150171
172+ if ( action === 'upgrade-subscription' ) {
173+ await assertNotBilledThroughMarketplace ( referenceId )
174+ }
175+
151176 if ( action === 'upgrade-subscription' && ! isOrgPlan ( plan ) ) {
152177 logger . warn ( 'Blocking checkout - organizations can only subscribe to Team or Enterprise' , {
153178 userId,
0 commit comments