Skip to content

Commit 3caea36

Browse files
committed
fix(aws-marketplace): discover a license from any entitlement and decide access from the plan's dimension
1 parent a78c117 commit 3caea36

5 files changed

Lines changed: 44 additions & 17 deletions

File tree

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe('verifyLicense', () => {
6666
const summary = await read(
6767
entitlement({ ExpirationDate: new Date('2026-05-31T23:59:59.000Z') })
6868
)
69-
expect(summary).toMatchObject({ active: false, entitlementCount: 1, quantity: 25 })
69+
expect(summary).toMatchObject({ active: false, accessReported: true, quantity: 25 })
7070
})
7171

7272
it('reads an entitlement with a future expiry, or none at all, as active', async () => {
@@ -106,7 +106,12 @@ describe('verifyLicense', () => {
106106

107107
it('reads a usage entitlement alone as no answer about access, not as access', async () => {
108108
const summary = await readAs(USAGE, entitlement({ Dimension: 'llm_credits' }))
109-
expect(summary).toMatchObject({ active: false, entitlementCount: 0, dimension: null })
109+
expect(summary).toMatchObject({
110+
active: false,
111+
licenseSeen: true,
112+
accessReported: false,
113+
dimension: null,
114+
})
110115
})
111116

112117
it('reads a lapsed platform as inactive even while usage is current', async () => {
@@ -115,7 +120,7 @@ describe('verifyLicense', () => {
115120
entitlement({ Dimension: 'llm_credits' }),
116121
entitlement({ Dimension: 'platform', ExpirationDate: new Date('2026-01-01T00:00:00.000Z') })
117122
)
118-
expect(summary).toMatchObject({ active: false, entitlementCount: 1, dimension: 'platform' })
123+
expect(summary).toMatchObject({ active: false, accessReported: true, dimension: 'platform' })
119124
})
120125

121126
it('reads a current platform as active and describes it, not the usage dimension', async () => {
@@ -139,7 +144,7 @@ describe('verifyLicense', () => {
139144

140145
const summary = await verifyLicense(CONTRACT, LICENSE)
141146

142-
expect(summary).toMatchObject({ entitlementCount: 2, dimension: 'one' })
147+
expect(summary).toMatchObject({ accessReported: true, dimension: 'one' })
143148
expect(mockSend.mock.calls.map(([command]) => command.input.NextToken)).toEqual([
144149
undefined,
145150
't1',

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ export interface EntitlementSummary {
1616
* no expiry or one in the future.
1717
*/
1818
active: boolean
19+
/** AWS returned an entitlement of any dimension: the license exists and names its account. */
20+
licenseSeen: boolean
1921
/**
20-
* Entitlements AWS returned for the dimension that grants access (any dimension, for a plan with
21-
* none), current or lapsed: whether AWS has answered about access at all. A usage dimension alone
22-
* is no answer. Zero is ambiguous, see the license's status.
22+
* AWS returned an entitlement, current or lapsed, for the dimension that grants access (any
23+
* dimension, for a plan with none). A usage dimension alone is not an answer about access, and
24+
* no answer is ambiguous: see the license's status.
2325
*/
24-
entitlementCount: number
26+
accessReported: boolean
2527
dimension: string | null
2628
quantity: number | null
2729
expiresAt: Date | null
@@ -58,7 +60,8 @@ function summarizeEntitlements(
5860
const chosen = current[0] ?? lapsed[0]
5961
return {
6062
active: current.length > 0,
61-
entitlementCount: entitlements.length,
63+
licenseSeen: allEntitlements.length > 0,
64+
accessReported: entitlements.length > 0,
6265
dimension: chosen?.Dimension ?? null,
6366
quantity: chosen?.Value?.IntegerValue ?? null,
6467
expiresAt: chosen?.ExpirationDate ?? null,

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1424,6 +1424,25 @@ describe('reconcileAwsMarketplaceLicenses', () => {
14241424
})
14251425

14261426
describe('license events across listings', () => {
1427+
it('records a usage listing license seen first by event as pending until AWS reports platform', async () => {
1428+
const licenseArn = uniqueArn()
1429+
entitlementsByLicense.set(licenseArn, [
1430+
currentEntitlement({ ProductCode: USAGE_PRODUCT, Dimension: 'llm_credits' }),
1431+
])
1432+
const deleted = deliverOnce([licenseUpdated(licenseArn, undefined, undefined, USAGE_PRODUCT)])
1433+
1434+
expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ applied: 1, discarded: 0 })
1435+
expect(deleted).toEqual(['r-0'])
1436+
expect(await readLicense(licenseArn)).toMatchObject({
1437+
productCode: USAGE_PRODUCT,
1438+
status: 'pending',
1439+
dimension: null,
1440+
})
1441+
expect(await emailsTo(TEAM)).toEqual([
1442+
expect.objectContaining({ text: expect.stringMatching(/Unconfirmed/) }),
1443+
])
1444+
})
1445+
14271446
it("applies a usage listing's event under its own plan: a lapsed platform ends access", async () => {
14281447
const { licenseArn } = await insertLicense({ productCode: USAGE_PRODUCT, status: 'active' })
14291448
entitlementsByLicense.set(licenseArn, [

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ async function readEntitlements(
3737
let verifiedAt = new Date()
3838
let summary = await verifyLicense(product, licenseArn)
3939
for (const delayMs of NO_ENTITLEMENT_RETRY_DELAYS_MS) {
40-
if (summary.entitlementCount > 0) break
40+
if (summary.accessReported) break
4141
await sleep(delayMs)
4242
verifiedAt = new Date()
4343
summary = await verifyLicense(product, licenseArn)

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,21 @@ export function isNewerRead(license: AwsMarketplaceLicenseRecord, verifiedAt: Da
5454
}
5555

5656
/**
57-
* No entitlements after AWS has said the license ended, or on a license that was already confirmed
58-
* once, is a lapse. Otherwise AWS may simply not have finished, so the license stays pending.
57+
* No answer about access after AWS has said the license ended, or on a license that was already
58+
* confirmed once, is a lapse. Otherwise AWS may simply not have finished, so the license stays pending.
5959
*/
6060
function statusFromRead(
6161
prior: AwsMarketplaceLicenseStatus | null,
6262
{ summary, ended }: LicenseVerification
6363
): AwsMarketplaceLicenseStatus {
6464
if (summary.active) return 'active'
65-
if (summary.entitlementCount > 0 || ended) return 'inactive'
65+
if (summary.accessReported || ended) return 'inactive'
6666
return prior === null || prior === 'pending' ? 'pending' : 'inactive'
6767
}
6868

6969
/**
70-
* The columns a read writes. With no entitlements the last-known dimension, quantity, and expiry
71-
* stay as they were, and only the status can change. An inactive license has nothing to claim.
70+
* The columns a read writes. With no answer about access the last-known dimension, quantity, and
71+
* expiry stay as they were, and only the status can change. An inactive license has nothing to claim.
7272
*/
7373
function verificationFields(
7474
prior: AwsMarketplaceLicenseStatus | null,
@@ -81,7 +81,7 @@ function verificationFields(
8181
lastVerifiedAt: verifiedAt,
8282
lastVerificationAttemptAt: verifiedAt,
8383
updatedAt: new Date(),
84-
...(summary.entitlementCount > 0 && {
84+
...(summary.accessReported && {
8585
dimension: summary.dimension,
8686
quantity: summary.quantity,
8787
expiresAt: summary.expiresAt,
@@ -277,7 +277,7 @@ export async function recordLicenseVerification(input: {
277277
const stamp = licenseEventReceivedAt ? { licenseEventReceivedAt } : {}
278278
return db.transaction(async (tx) => {
279279
const accountId = verification.summary.customerAwsAccountId ?? input.fallbackAccountId
280-
if (accountId && verification.summary.entitlementCount > 0) {
280+
if (accountId && verification.summary.licenseSeen) {
281281
const [created] = await tx
282282
.insert(awsMarketplaceLicense)
283283
.values({

0 commit comments

Comments
 (0)