feat(billing): AWS Marketplace SaaS contract registration and entitlement verification
Merged
WA
4d ago+38314+38314+01234567890123456789012345678901234567890123456789-7-7-0123456789
Summary
- AWS Marketplace SaaS contract integration, so the SaaS listing can pass AWS's public-visibility audit. AWS requires successful Marketplace Entitlement Service calls from a real buyer flow.
- Fulfillment URL
POST /api/marketplace/aws/register:- AWS posts
x-amzn-marketplace-token. The route callsResolveCustomer, rejects other products, and identifies buyers byCustomerAWSAccountId+LicenseArn; AWS no longer returnsCustomerIdentifierfor new products. - It calls
GetEntitlementsfiltered byLICENSE_ARN, upserts a row keyed by license ARN (one account can hold several, per concurrent agreements), and always 303s to/marketplace/aws. - An active or still-confirming license gets a one-time claim: an httpOnly cookie, hash stored, valid 4h. A future-dated agreement with no ARN yet gets its own message. The token is never logged.
- AWS posts
- Onboarding
/marketplace/aws: the buyer signs in or signs up, confirms a contact email (optional company), and links the license to an org they own or administer, or registers it to their account if they have none.- Both actions re-verify the entitlement and recheck the claim under a row lock; a license belongs to at most one org.
- The card shows the masked AWS account and the signed-in email, with no preselected org.
- If AWS hasn't confirmed yet, the buyer sees that state and can check again; linking stays refused until a fresh read is active.
- Provisioning stays manual, on AWS's documented path: the buyer is told their account is being set up and to wait to be contacted, with the turnaround and a support contact, on screen and by email. Staff provision through the existing Enterprise flow with
pausePaymentCollection, because AWS bills these customers. No entitlement or Stripe code changes. - Durable notices: team and buyer notices go through the transactional outbox, in the same transaction as the state change. One
applyVerificationunder the row lock serves every writer, so any change to status, dimension, quantity or expiry notifies the team, and an older read never overwrites a newer one. - Subscription status: billing shows licenses at org and personal scope, and a buyer registered to their account also sees status on
/marketplace/aws. While that scope's license is pending or active, the billing plans chip and the upgrade page's plans are hidden. Those are UI gates: credit chips and teammate gates still link to the upgrade page, which then explains billing is through Marketplace, and the Chat plan gate is unchanged, because the buyer is told to wait for setup. - Reconcile cron
GET /api/cron/aws-marketplace-reconcile:- Drains license and agreement events from SQS and re-reads
GetEntitlementsrather than trusting the event. Poison messages and agreements we don't hold are discarded; transient failures stay for redelivery and the DLQ. - Replacements and renewals key on
previousAgreementId: the new license inherits org, linker and contact in either arrival order, and a superseded license never tells staff to cancel. Advisory events notify the team. - Re-verifies licenses not attempted in 24h. The run has a wall-clock budget inside the lock and visibility timeouts.
- Drains license and agreement events from SQS and re-reads
- Admin:
GET/POST /api/v1/admin/dashboard/aws-marketplace-licenseslists unlinked licenses and links one, with audit. - Credentials and limits: Marketplace and SQS calls sign as the ECS task role via container credentials, not the app's static user keys, with connection and request timeouts and SDK-default retries.
- Feature switch: the feature is off unless
AWS_MARKETPLACE_PRODUCT_CODEis set (registration, page, claim and admin routes 404, status lists are empty, cron skips), so self-hosted and non-prod envs are unaffected.AWS_MARKETPLACE_EVENTS_QUEUE_URLenables the drain. The AWS side is in the infra repo. - Migration
0454_aws_marketplace_license: a new table, enum and indexes only.
Type of Change
- New feature
Testing
bun run test:integration aws-marketplace/lifecycle.integration.ts: 64 pass against real Postgres and Redis, including a real two-tab race, stale reads, outbox retry, pending→active, renewals in both orders, a late duplicate renewal, admin authorization, and org and account read isolation.entitlements.test.tsandoutbox/handlers.test.ts: 34 pass.- Red-proofed by reverting each guard, in groups and in isolation where a group masked one.
tsc --noEmit -p apps/sim,bun run lint,check:migrations origin/staging,drizzle-kit generate(no drift), plus api-validation:strict, api-contract-routes, route-verbs, source-conventions, boundaries, principal-kind-parity, permission-group-enforcement, application-graph, react-query, client-boundary, schema-mock, comment-hygiene, test-patterns.- Real AWS, read-only from the seller account: a bad token raises
InvalidTokenException, and a cancelled agreement returns no entitlements. A real rule delivered the raw event JSON to an SSE-SQS queue. - Not yet exercised: a real token with an active entitlement, the pages in a browser, and task-role signing on ECS. Those need a Limited-listing test purchase after deploy.
Checklist
- Code follows project style guidelines
- Self-reviewed my changes
- Tests added/updated and passing (new tests pass the
test-auditauthoring gate) - No new warnings introduced
- I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)