Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions apps/sim/app/api/cron/aws-marketplace-meter/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createLogger } from '@sim/logger'
import { generateShortId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { verifyCronAuth } from '@/lib/auth/internal'
import { isAwsMarketplaceEnabled } from '@/lib/billing/aws-marketplace/config'
import { meterAwsMarketplaceUsage } from '@/lib/billing/aws-marketplace/metering'
import { acquireLock, releaseLock } from '@/lib/core/config/redis'
import { runDetached } from '@/lib/core/utils/background'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

const logger = createLogger('AwsMarketplaceMeterCron')

const LOCK_KEY = 'aws-marketplace-meter-lock'
/** Outlasts a run comfortably; the schedule runs every 15 minutes. */
const LOCK_TTL_SECONDS = 10 * 60

export const dynamic = 'force-dynamic'

/**
* Records newly closed hours of usage owed through AWS Marketplace and sends AWS what it has not
* accepted yet. Acknowledges the cron call at once and works in the background; a Redis lock keeps
* runs from overlapping, and the ledger makes a replayed run harmless. A no-op on deployments
* where no configured listing bills usage.
*/
export const GET = withRouteHandler(async (request: NextRequest) => {

@cubic-dev-ai cubic-dev-ai Bot Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This handler is not scheduled by the Helm chart, so Marketplace usage metering never runs in the standard deployment. Add a cronjobs.jobs entry for /api/cron/aws-marketplace-meter with the intended 15-minute schedule.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/app/api/cron/aws-marketplace-meter/route.ts, line 25:

<comment>This handler is not scheduled by the Helm chart, so Marketplace usage metering never runs in the standard deployment. Add a `cronjobs.jobs` entry for `/api/cron/aws-marketplace-meter` with the intended 15-minute schedule.</comment>

<file context>
@@ -0,0 +1,67 @@
+ * runs from overlapping, and the ledger makes a replayed run harmless. A no-op on deployments
+ * where no configured listing bills usage.
+ */
+export const GET = withRouteHandler(async (request: NextRequest) => {
+  const authError = verifyCronAuth(request, 'AWS Marketplace metering')
+  if (authError) {
</file context>
Fix with cubic

const authError = verifyCronAuth(request, 'AWS Marketplace metering')
if (authError) {
return authError
}

if (!isAwsMarketplaceEnabled()) {
return NextResponse.json({
success: true,
message: 'AWS Marketplace is not configured – skipped',
status: 'skip',
})
}

const lockValue = generateShortId()
const locked = await acquireLock(LOCK_KEY, lockValue, LOCK_TTL_SECONDS, {
reclaimOnFailure: true,
})
if (!locked) {
return NextResponse.json(
{
success: true,
message: 'AWS Marketplace metering already in progress – skipped',
status: 'skip',
},
{ status: 202 }
)
}

runDetached('aws-marketplace-meter', async () => {
try {
const summary = await meterAwsMarketplaceUsage()
if (summary) logger.info('AWS Marketplace metering completed', { ...summary })
} finally {
await releaseLock(LOCK_KEY, lockValue).catch(() => {})
}
})

return NextResponse.json(
{ success: true, message: 'AWS Marketplace metering started', status: 'started' },
{ status: 202 }
)
Comment on lines +63 to +66

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Responses lack a shared contract

The new route defines its JSON responses locally. The repository requires every API route's request and response shapes to live under apps/sim/lib/api/contracts/** through defineRouteContract. Having no request input does not remove the response requirement. Add a shared response contract before merging instead of relying only on the audit allowlist.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

})
22 changes: 22 additions & 0 deletions apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,28 @@ describe('linking a claimed license', () => {
})
})

it('starts the license usage at the first link and keeps it when the same organization links again', async () => {
const fresh = await createClaimedLicense()
entitlementsByLicense.set(fresh.licenseArn, [currentEntitlement()])
const before = Date.now()

await link(ownerId, { licenseId: fresh.id, claimToken: fresh.token })

const linkedAt = (await readLicense(fresh.licenseArn))?.organizationLinkedAt
expect(linkedAt?.getTime()).toBeGreaterThanOrEqual(before - 1000)

const firstLinkedAt = new Date(Date.now() - 5 * 60 * 60 * 1000)
const relinked = await createClaimedLicense({
organizationId,
organizationLinkedAt: firstLinkedAt,
})
entitlementsByLicense.set(relinked.licenseArn, [currentEntitlement()])

await link(ownerId, { licenseId: relinked.id, claimToken: relinked.token })

expect((await readLicense(relinked.licenseArn))?.organizationLinkedAt).toEqual(firstLinkedAt)
})

it('refuses to move a license already linked to another organization', async () => {
const otherOwnerId = await createUser()
const otherOrganizationId = await createOrganization([{ userId: otherOwnerId, role: 'owner' }])
Expand Down
Loading
Loading