Skip to content

Commit 35e9a84

Browse files
committed
improvement(aws-marketplace): resolve each run's products once, keep env free of billing code, and drop derivable state
1 parent e99cece commit 35e9a84

9 files changed

Lines changed: 112 additions & 130 deletions

File tree

‎apps/sim/lib/billing/aws-marketplace/checkout-guard.integration.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ const PRODUCT = 'product-placeholder'
88
const ACCOUNT = '000000000000'
99
const TEAM_PLAN = 'team_6000'
1010

11-
const productCode = vi.hoisted(() => ({ current: 'product-placeholder' as string | null }))
11+
const marketplace = vi.hoisted(() => ({ enabled: true }))
1212

1313
vi.mock('@/lib/billing/aws-marketplace/config', () => ({
14-
isAwsMarketplaceEnabled: () => productCode.current !== null,
14+
isAwsMarketplaceEnabled: () => marketplace.enabled,
1515
}))
1616

1717
import {
@@ -84,7 +84,7 @@ async function organizationWithLicense(status: LicenseStatus | null) {
8484
}
8585

8686
afterEach(() => {
87-
productCode.current = PRODUCT
87+
marketplace.enabled = true
8888
})
8989

9090
afterAll(async () => {
@@ -147,7 +147,7 @@ describe('Stripe checkout for an AWS Marketplace-billed reference', () => {
147147
const { ownerId, organizationId } = await organizationWithLicense('active')
148148
const userId = await createUser()
149149
await insertLicense({ organizationId: null, linkedByUserId: userId, status: 'active' })
150-
productCode.current = null
150+
marketplace.enabled = false
151151

152152
await expect(
153153
authorizeSubscriptionReference(ownerId, organizationId, 'upgrade-subscription', TEAM_PLAN)

‎apps/sim/lib/billing/aws-marketplace/config.ts‎

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,18 @@ import {
44
} from '@/lib/billing/aws-marketplace/products'
55
import { env } from '@/lib/core/config/env'
66

7-
/** Parsed once per distinct setting; env validation already rejected a malformed one at boot. */
8-
let parsed: { raw: string | undefined; products: ReadonlyMap<string, AwsMarketplaceProduct> } = {
9-
raw: undefined,
10-
products: new Map(),
11-
}
7+
let products: ReadonlyMap<string, AwsMarketplaceProduct> | undefined
128

139
/**
1410
* The products this deployment sells through AWS Marketplace, by product code. Empty on
15-
* self-hosted installs and every environment but production, which turns the registration, claim,
16-
* and reconcile surfaces off.
11+
* self-hosted installs and every environment but production, which turns the Marketplace surfaces
12+
* off. A malformed setting throws a message naming the bad entry.
1713
*/
1814
export function getAwsMarketplaceProducts(): ReadonlyMap<string, AwsMarketplaceProduct> {
19-
const raw = env.AWS_MARKETPLACE_PRODUCTS
20-
if (parsed.raw !== raw) {
21-
parsed = { raw, products: raw ? parseAwsMarketplaceProducts(raw) : new Map() }
22-
}
23-
return parsed.products
15+
products ??= env.AWS_MARKETPLACE_PRODUCTS
16+
? parseAwsMarketplaceProducts(env.AWS_MARKETPLACE_PRODUCTS)
17+
: new Map()
18+
return products
2419
}
2520

2621
export function isAwsMarketplaceEnabled(): boolean {
@@ -33,8 +28,3 @@ export function findAwsMarketplaceProduct(
3328
): AwsMarketplaceProduct | null {
3429
return productCode ? (getAwsMarketplaceProducts().get(productCode) ?? null) : null
3530
}
36-
37-
/** Every configured product code, for queries that span all of them. */
38-
export function getAwsMarketplaceProductCodes(): string[] {
39-
return [...getAwsMarketplaceProducts().keys()]
40-
}

‎apps/sim/lib/billing/aws-marketplace/entitlements.test.ts‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,22 @@ vi.mock('@aws-sdk/client-marketplace-entitlement-service', () => ({
1414

1515
import { verifyLicense } from '@/lib/billing/aws-marketplace/entitlements'
1616
import {
17-
AWS_MARKETPLACE_PLANS,
1817
type AwsMarketplaceProduct,
18+
parseAwsMarketplaceProducts,
1919
} from '@/lib/billing/aws-marketplace/products'
2020

21-
const CONTRACT: AwsMarketplaceProduct = {
22-
productCode: 'product-placeholder',
23-
planId: 'contract',
24-
plan: AWS_MARKETPLACE_PLANS.contract,
25-
}
26-
const USAGE: AwsMarketplaceProduct = {
27-
productCode: 'usage-product-placeholder',
28-
planId: 'platform-with-usage',
29-
plan: AWS_MARKETPLACE_PLANS['platform-with-usage'],
21+
const PRODUCTS = parseAwsMarketplaceProducts(
22+
'product-placeholder=contract,usage-product-placeholder=platform-with-usage'
23+
)
24+
25+
function product(code: string): AwsMarketplaceProduct {
26+
const found = PRODUCTS.get(code)
27+
if (!found) throw new Error(`no test product ${code}`)
28+
return found
3029
}
30+
31+
const CONTRACT = product('product-placeholder')
32+
const USAGE = product('usage-product-placeholder')
3133
const LICENSE = 'license-placeholder'
3234
const NOW = new Date('2026-06-01T00:00:00.000Z')
3335

‎apps/sim/lib/billing/aws-marketplace/notifications.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ async function describeLicense(license: AwsMarketplaceLicenseRecord): Promise<st
6161
license.organizationId ? `${organizationName ?? 'Unknown'} (${license.organizationId})` : 'None'
6262
}
6363
License ID: ${license.id}
64-
Listing: ${license.productCode} (${findAwsMarketplaceProduct(license.productCode)?.planId ?? 'no longer sold'})
64+
Listing: ${license.productCode} (${findAwsMarketplaceProduct(license.productCode)?.plan.id ?? 'no longer sold'})
6565
License: ${license.licenseArn}
6666
AWS account: ${license.customerAwsAccountId}
6767
Agreement: ${license.agreementId ?? 'Not provided'}
Lines changed: 24 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,39 @@
11
/**
2-
* The AWS Marketplace products this deployment sells, and how each one is sold.
3-
*
4-
* A product's behavior lives in its plan, here in code; which product codes exist is deployment
5-
* configuration (`AWS_MARKETPLACE_PRODUCTS`), because codes differ per seller account and listing.
6-
* Every Marketplace path resolves a license's product from the code AWS returned or the code stored
7-
* on its row, so adding, repricing, or retiring a listing is a configuration change, never a branch.
8-
*
9-
* Free of imports from `@/lib/core/config/env`, so `env.ts` can validate the setting at boot.
2+
* How each AWS Marketplace listing is sold. Behavior lives in a plan, here in code; which product
3+
* codes exist is deployment configuration (`AWS_MARKETPLACE_PRODUCTS`), so adding, repricing, or
4+
* retiring a listing is a configuration change, never a branch.
105
*/
116

127
/** How one listing is sold, and so what Sim requires before granting access. */
138
export interface AwsMarketplacePlan {
9+
readonly id: string
1410
/**
1511
* The entitlement dimension that must be current for a license to count as active. Null accepts
16-
* any current entitlement, which is right for a listing with a single contract dimension.
12+
* any current entitlement, which suits a listing with a single contract dimension.
1713
*/
1814
readonly requiredDimension: string | null
1915
}
2016

21-
export const AWS_MARKETPLACE_PLANS = {
17+
const PLANS = [
2218
/** A contract-only listing: any current entitlement grants access. */
23-
contract: { requiredDimension: null },
19+
{ id: 'contract', requiredDimension: null },
2420
/** Platform access by contract, plus LLM usage on Sim's keys billed through AWS. */
25-
'platform-with-usage': { requiredDimension: 'platform' },
26-
} as const satisfies Record<string, AwsMarketplacePlan>
21+
{ id: 'platform-with-usage', requiredDimension: 'platform' },
22+
] as const satisfies readonly AwsMarketplacePlan[]
2723

28-
export type AwsMarketplacePlanId = keyof typeof AWS_MARKETPLACE_PLANS
24+
export const AWS_MARKETPLACE_PLANS: ReadonlyMap<string, AwsMarketplacePlan> = new Map(
25+
PLANS.map((plan) => [plan.id, plan])
26+
)
2927

3028
export interface AwsMarketplaceProduct {
3129
readonly productCode: string
32-
readonly planId: AwsMarketplacePlanId
3330
readonly plan: AwsMarketplacePlan
3431
}
3532

36-
function isPlanId(value: string): value is AwsMarketplacePlanId {
37-
return Object.hasOwn(AWS_MARKETPLACE_PLANS, value)
38-
}
39-
4033
/**
4134
* Parses `AWS_MARKETPLACE_PRODUCTS`: comma-separated `<product code>=<plan>` pairs, for example
42-
* `abc123=contract,def456=platform-with-usage`. Throws on an empty entry, an unknown plan, or a
43-
* repeated code, so a misconfigured deployment fails at boot instead of refusing buyers later.
35+
* `abc123=contract,def456=platform-with-usage`. Throws on a malformed entry, an unknown plan, or a
36+
* repeated code.
4437
*/
4538
export function parseAwsMarketplaceProducts(
4639
value: string
@@ -49,25 +42,20 @@ export function parseAwsMarketplaceProducts(
4942
for (const entry of value.split(',')) {
5043
const [productCode, planId, ...rest] = entry.split('=').map((part) => part.trim())
5144
if (!productCode || !planId || rest.length > 0) {
52-
throw new Error(`expected <product code>=<plan>, received "${entry.trim()}"`)
45+
throw new Error(
46+
`AWS_MARKETPLACE_PRODUCTS: expected <product code>=<plan>, got "${entry.trim()}"`
47+
)
5348
}
54-
if (!isPlanId(planId)) {
49+
const plan = AWS_MARKETPLACE_PLANS.get(planId)
50+
if (!plan) {
5551
throw new Error(
56-
`unknown plan "${planId}"; expected one of ${Object.keys(AWS_MARKETPLACE_PLANS).join(', ')}`
52+
`AWS_MARKETPLACE_PRODUCTS: unknown plan "${planId}"; expected one of ${[...AWS_MARKETPLACE_PLANS.keys()].join(', ')}`
5753
)
5854
}
59-
if (products.has(productCode)) throw new Error(`product code "${productCode}" is listed twice`)
60-
products.set(productCode, { productCode, planId, plan: AWS_MARKETPLACE_PLANS[planId] })
55+
if (products.has(productCode)) {
56+
throw new Error(`AWS_MARKETPLACE_PRODUCTS: product code "${productCode}" is listed twice`)
57+
}
58+
products.set(productCode, { productCode, plan })
6159
}
6260
return products
6361
}
64-
65-
/** `parseAwsMarketplaceProducts` as a predicate, for env validation. */
66-
export function isValidAwsMarketplaceProducts(value: string): boolean {
67-
try {
68-
parseAwsMarketplaceProducts(value)
69-
return true
70-
} catch {
71-
return false
72-
}
73-
}

‎apps/sim/lib/billing/aws-marketplace/reconcile.ts‎

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ import { getErrorMessage } from '@sim/utils/errors'
1010
import { toRecord } from '@sim/utils/object'
1111
import { truncate } from '@sim/utils/string'
1212
import { getEventsQueueClient } from '@/lib/billing/aws-marketplace/client'
13-
import {
14-
findAwsMarketplaceProduct,
15-
getAwsMarketplaceProductCodes,
16-
} from '@/lib/billing/aws-marketplace/config'
13+
import { getAwsMarketplaceProducts } from '@/lib/billing/aws-marketplace/config'
1714
import {
1815
isInvalidEntitlementRequest,
1916
verifyLicense,
@@ -84,8 +81,8 @@ interface AwsMarketplaceReconcileSummary {
8481
type BudgetCheck = (workMs: number) => boolean
8582

8683
interface DrainContext {
87-
/** Every configured product; an event for any other product is not ours to apply. */
88-
productCodes: readonly string[]
84+
/** This run's configured products, by code; an event for any other product is not ours. */
85+
products: ReadonlyMap<string, AwsMarketplaceProduct>
8986
queueUrl: string
9087
summary: AwsMarketplaceReconcileSummary
9188
fits: BudgetCheck
@@ -225,15 +222,13 @@ async function applyAgreementEnded(
225222
message: Message
226223
): Promise<MessageOutcome> {
227224
const messageId = message.MessageId
228-
const licenses = await listLicensesForAgreement(context.productCodes, agreementId)
225+
const licenses = await listLicensesForAgreement(context.products, agreementId)
229226
if (licenses.length === 0) {
230227
logger.info('Ignoring an AWS Marketplace agreement event for no stored license', { messageId })
231228
return 'discarded'
232229
}
233230
let incomplete = false
234-
for (const [index, { licenseArn, productCode }] of licenses.entries()) {
235-
const product = findAwsMarketplaceProduct(productCode)
236-
if (!product) continue
231+
for (const [index, { licenseArn, product }] of licenses.entries()) {
237232
if (!context.fits(LICENSE_READ_BUDGET_MS)) {
238233
context.summary.outOfBudget = true
239234
return 'retained'
@@ -264,10 +259,11 @@ async function applyAgreementEnded(
264259
}
265260

266261
async function applyLicenseEvent(
262+
context: DrainContext,
267263
event: Extract<MarketplaceEvent, { kind: 'license' }>,
268264
messageId: string | undefined
269265
): Promise<MessageOutcome> {
270-
const product = findAwsMarketplaceProduct(event.productCode)
266+
const product = event.productCode ? context.products.get(event.productCode) : undefined
271267
if (!product) {
272268
logger.warn('Discarding an AWS Marketplace event for another product', { messageId })
273269
return 'discarded'
@@ -309,24 +305,24 @@ async function applyEvent(
309305
event: MarketplaceEvent,
310306
message: Message
311307
): Promise<MessageOutcome> {
312-
const { productCodes } = context
308+
const { products } = context
313309
const messageId = message.MessageId
314-
if (event.kind === 'license') return applyLicenseEvent(event, messageId)
310+
if (event.kind === 'license') return applyLicenseEvent(context, event, messageId)
315311
if (event.kind === 'agreement-ended') {
316312
return applyAgreementEnded(context, event.agreementId, message)
317313
}
318314
const involved =
319315
event.kind === 'advisory'
320316
? await enqueueAgreementAdvisory({
321-
productCodes,
317+
products,
322318
agreementId: event.agreementId,
323319
state: event.state,
324320
detail: event.detail,
325321
messageId: messageId ?? event.agreementId,
326322
})
327323
: event.previousAgreementId
328324
? await applyAgreementRenewal({
329-
productCodes,
325+
products,
330326
previousAgreementId: event.previousAgreementId,
331327
agreementId: event.agreementId,
332328
})
@@ -418,15 +414,13 @@ async function drainEvents(context: DrainContext): Promise<void> {
418414
}
419415

420416
async function reverifyStaleLicenses(
421-
productCodes: readonly string[],
417+
products: ReadonlyMap<string, AwsMarketplaceProduct>,
422418
summary: AwsMarketplaceReconcileSummary,
423419
fits: BudgetCheck
424420
): Promise<void> {
425421
const cutoff = new Date(Date.now() - REVERIFY_AFTER_MS)
426-
const licenses = await listLicensesDueForReverify(productCodes, cutoff, REVERIFY_BATCH_SIZE)
427-
for (const { licenseArn, productCode } of licenses) {
428-
const product = findAwsMarketplaceProduct(productCode)
429-
if (!product) continue
422+
const licenses = await listLicensesDueForReverify(products, cutoff, REVERIFY_BATCH_SIZE)
423+
for (const { licenseArn, product } of licenses) {
430424
if (!fits(LICENSE_READ_BUDGET_MS)) {
431425
summary.outOfBudget = true
432426
return
@@ -450,8 +444,8 @@ async function reverifyStaleLicenses(
450444
* feature is off.
451445
*/
452446
export async function reconcileAwsMarketplaceLicenses(): Promise<AwsMarketplaceReconcileSummary | null> {
453-
const productCodes = getAwsMarketplaceProductCodes()
454-
if (productCodes.length === 0) return null
447+
const products = getAwsMarketplaceProducts()
448+
if (products.size === 0) return null
455449
const summary: AwsMarketplaceReconcileSummary = {
456450
received: 0,
457451
applied: 0,
@@ -465,7 +459,7 @@ export async function reconcileAwsMarketplaceLicenses(): Promise<AwsMarketplaceR
465459
const startedAt = Date.now()
466460
const fits: BudgetCheck = (workMs) => Date.now() - startedAt + workMs <= RUN_BUDGET_MS
467461
const queueUrl = env.AWS_MARKETPLACE_EVENTS_QUEUE_URL
468-
if (queueUrl) await drainEvents({ productCodes, queueUrl, summary, fits })
469-
await reverifyStaleLicenses(productCodes, summary, fits)
462+
if (queueUrl) await drainEvents({ products, queueUrl, summary, fits })
463+
await reverifyStaleLicenses(products, summary, fits)
470464
return summary
471465
}

‎apps/sim/lib/billing/aws-marketplace/registration.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,8 @@ async function readEntitlements(
4949
* Turns the registration token AWS posts to the fulfillment URL into a license row. Every listing
5050
* shares the one fulfillment URL, so the product comes from the code AWS resolves the token to,
5151
* and a code this deployment does not sell is refused. Every registration reads the license's
52-
* entitlements. A license AWS has not confirmed yet still gets a
53-
* claim, so the buyer can leave contact details; only a lapsed one does not. The token itself is
54-
* never logged.
52+
* entitlements. A license AWS has not confirmed yet still gets a claim, so the buyer can leave
53+
* contact details; only a lapsed one does not. The token itself is never logged.
5554
*/
5655
export async function registerAwsMarketplaceBuyer(
5756
registrationToken: string

0 commit comments

Comments
 (0)