Skip to content

Commit 2807b82

Browse files
committed
fix(billing): apply AWS Marketplace review fixes
Keep License Updated events for unknown licenses queued until AWS describes them, so a renewal cannot be lost. Tell the team when the event reaches a license already taken, and when a repeat take changes the contact details. Give the cancel instruction its required operationId, and only when an organization holds the license. Show account-registered licenses on organization billing, and stop offering plans on the upgrade page to anyone billed through AWS. Delete the expired and inactive claim states, which no reachable row can produce.
1 parent a0f42dd commit 2807b82

13 files changed

Lines changed: 237 additions & 66 deletions

File tree

‎apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export function AwsMarketplaceOnboarding({
134134
<InviteStatusCard
135135
type='login'
136136
title='Set up your AWS Marketplace subscription'
137-
description='Sign in to Sim to link this subscription to your organization.'
137+
description='Sign in or create an account to set up this subscription.'
138138
details={supportContact}
139139
actions={[
140140
...(registrationDisabled
@@ -199,18 +199,13 @@ export function AwsMarketplaceOnboarding({
199199
}
200200

201201
const claim = claimQuery.data
202-
if (claim.status === 'missing' || claim.status === 'expired' || claim.status === 'inactive') {
203-
const title = {
204-
missing: 'No subscription to set up',
205-
expired: 'Setup link expired',
206-
inactive: 'No active subscription found',
207-
}[claim.status]
202+
if (claim.status === 'missing') {
208203
return (
209204
<InviteLayout>
210205
<InviteStatusCard
211-
type='error'
212-
title={title}
213-
description={RESTART_FROM_AWS}
206+
type='warning'
207+
title='No setup in progress'
208+
description={`${RESTART_FROM_AWS} If you already finished setup, we will contact you ${AWS_MARKETPLACE_SETUP_TURNAROUND}.`}
214209
details={supportContact}
215210
actions={[returnHome]}
216211
/>

‎apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import { SettingsQueryErrorState } from '@/app/workspace/[workspaceId]/settings/
5353
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
5454
import { RESOURCE_ROW_ARROW_CLASSES } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
5555
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
56-
import { useAwsMarketplaceLicenses } from '@/hooks/queries/aws-marketplace'
56+
import { useAwsMarketplaceBilling } from '@/hooks/queries/aws-marketplace'
5757
import {
5858
useBillingUsageNotifications,
5959
useUpdateGeneralSetting,
@@ -135,10 +135,7 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps
135135
enabled: !isOrganizationScope,
136136
})
137137
const billingOrganizationId = isOrganizationScope ? (organizationId ?? null) : null
138-
const { data: marketplaceLicenses } = useAwsMarketplaceLicenses(billingOrganizationId)
139-
const billedThroughMarketplace = marketplaceLicenses?.some(
140-
(license) => license.status !== 'inactive'
141-
)
138+
const { billedThroughMarketplace } = useAwsMarketplaceBilling(billingOrganizationId)
142139

143140
const {
144141
data: organizationBillingData,
@@ -459,7 +456,9 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps
459456
: `Organization ${planName} plan`
460457
: `Personal ${planName} plan`
461458
const priceText = billedThroughMarketplace
462-
? 'Billed through AWS Marketplace'
459+
? subscription.isFree
460+
? 'AWS Marketplace setup in progress'
461+
: 'Billed through AWS Marketplace'
463462
: organizationSubscriptionState === 'free'
464463
? 'No active organization subscription'
465464
: organizationSubscriptionState === 'lapsed'
@@ -669,6 +668,12 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps
669668
)}
670669

671670
<AwsMarketplaceSubscriptionSection organizationId={billingOrganizationId} />
671+
{isOrganizationScope && (
672+
<AwsMarketplaceSubscriptionSection
673+
organizationId={null}
674+
label='AWS Marketplace subscription on your account'
675+
/>
676+
)}
672677

673678
{!subscription.isFree && invoices.length > 0 && (
674679
<SettingsSection label='Invoices'>

‎apps/sim/app/workspace/[workspaceId]/settings/components/billing/components/aws-marketplace-subscription-section/aws-marketplace-subscription-section.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,18 @@ const STATUS_BADGES = {
1515
interface AwsMarketplaceSubscriptionSectionProps {
1616
/** Null reads the licenses registered to the viewer's own account. */
1717
organizationId: string | null
18+
label?: string
1819
}
1920

2021
/** Read-only: AWS requires buyers to see their subscription status in the product. */
2122
export function AwsMarketplaceSubscriptionSection({
2223
organizationId,
24+
label = 'AWS Marketplace subscription',
2325
}: AwsMarketplaceSubscriptionSectionProps) {
2426
const { data: licenses, error, isFetching, refetch } = useAwsMarketplaceLicenses(organizationId)
2527
if (error && !licenses) {
2628
return (
27-
<SettingsSection label='AWS Marketplace subscription'>
29+
<SettingsSection label={label}>
2830
<SettingsQueryErrorState
2931
variant='inline'
3032
error={error}
@@ -38,7 +40,7 @@ export function AwsMarketplaceSubscriptionSection({
3840
if (!licenses?.length) return null
3941

4042
return (
41-
<SettingsSection label='AWS Marketplace subscription'>
43+
<SettingsSection label={label}>
4244
<div className='flex flex-col gap-6'>
4345
{licenses.map((license) => {
4446
const badge = STATUS_BADGES[license.status]

‎apps/sim/app/workspace/[workspaceId]/upgrade/hooks/use-upgrade-state.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { WorkspaceHostContext } from '@/lib/api/contracts/workspaces'
99
import { useSubscriptionUpgrade } from '@/lib/billing/client/upgrade'
1010
import { CREDIT_TIERS } from '@/lib/billing/constants'
1111
import { getPlanTierCredits, isEnterprise, isFree, isPro, isTeam } from '@/lib/billing/plan-helpers'
12+
import { useAwsMarketplaceBilling } from '@/hooks/queries/aws-marketplace'
1213
import { invalidateWorkspaceUsage } from '@/hooks/queries/utils/invalidate-usage'
1314
import { subscriptionKeys } from '@/hooks/queries/utils/subscription-keys'
1415
import { workspaceHostKeys } from '@/hooks/queries/workspace-host'
@@ -38,6 +39,7 @@ export interface UpgradeState {
3839
status: string
3940
}
4041
showUpgradePlans: boolean
42+
billedThroughMarketplace: boolean
4143
proTier: { credits: number; dollars: number; name: string }
4244
maxTier: { credits: number; dollars: number; name: string }
4345
isOnPro: boolean
@@ -65,6 +67,7 @@ export function useUpgradeState({
6567
const { handleUpgrade } = useSubscriptionUpgrade()
6668
const queryClient = useQueryClient()
6769
const { ownerBilling } = hostContext
70+
const marketplace = useAwsMarketplaceBilling(hostContext.hostOrganizationId ?? null)
6871
const [isAnnual, setIsAnnual] = useState(
6972
!ownerBilling.isPaid || ownerBilling.billingInterval === 'year'
7073
)
@@ -197,7 +200,9 @@ export function useUpgradeState({
197200
isAnnual,
198201
setIsAnnual,
199202
subscription,
200-
showUpgradePlans: !subscription.isEnterprise,
203+
showUpgradePlans:
204+
!subscription.isEnterprise && !marketplace.isPending && !marketplace.billedThroughMarketplace,
205+
billedThroughMarketplace: marketplace.billedThroughMarketplace,
201206
proTier: PRO_TIER,
202207
maxTier: MAX_TIER,
203208
isOnPro,

‎apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,12 @@ export function Upgrade({ workspaceId }: UpgradeProps) {
8585
return null
8686
}
8787

88-
if (!canManageBilling) {
89-
const description = hostContext.hostOrganizationId
90-
? `Plans for ${hostContext.workspace.name} are managed by its organization administrators. Contact an organization admin to change this workspace’s plan.`
91-
: `Only the owner of ${hostContext.workspace.name} can change this workspace’s plan.`
88+
if (!canManageBilling || state.billedThroughMarketplace) {
89+
const description = canManageBilling
90+
? 'Your subscription is billed through AWS Marketplace, so plans can’t be changed here.'
91+
: hostContext.hostOrganizationId
92+
? `Plans for ${hostContext.workspace.name} are managed by its organization administrators. Contact an organization admin to change this workspace’s plan.`
93+
: `Only the owner of ${hostContext.workspace.name} can change this workspace’s plan.`
9294

9395
return (
9496
<div className='flex h-full flex-col bg-[var(--bg)]'>

‎apps/sim/hooks/queries/aws-marketplace.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,22 @@ export function useSaveAwsMarketplaceClaimContact() {
6767
})
6868
}
6969

70+
/**
71+
* Whether AWS Marketplace bills this organization or the viewer's own account: a pending or active
72+
* license on either. Pending until both reads settle.
73+
*/
74+
export function useAwsMarketplaceBilling(organizationId: string | null) {
75+
const organizationLicenses = useAwsMarketplaceLicenses(organizationId)
76+
const accountLicenses = useAwsMarketplaceLicenses(null)
77+
return {
78+
isPending: organizationLicenses.isPending || accountLicenses.isPending,
79+
billedThroughMarketplace: [
80+
...(organizationLicenses.data ?? []),
81+
...(accountLicenses.data ?? []),
82+
].some((license) => license.status !== 'inactive'),
83+
}
84+
}
85+
7086
/** The Marketplace subscriptions an organization holds, or with null the viewer's own account. */
7187
export function useAwsMarketplaceLicenses(organizationId: string | null) {
7288
return useQuery({

‎apps/sim/lib/api/contracts/aws-marketplace.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,8 @@ const claimedLicenseSchema = awsMarketplaceLicenseSchema.extend({
4646
})
4747

4848
const awsMarketplaceClaimSchema = z.discriminatedUnion('status', [
49-
/** This browser holds no claim, or one already used. */
49+
/** No open claim: none in this browser, or it was used, expired, or cleared by a lapse. */
5050
z.object({ status: z.literal('missing') }),
51-
z.object({ status: z.literal('expired') }),
52-
z.object({ status: z.literal('inactive') }),
5351
/** AWS has returned no entitlement yet; the buyer can leave contact details meanwhile. */
5452
z.object({ status: z.literal('pending'), license: claimedLicenseSchema }),
5553
z.object({

‎apps/sim/lib/billing/aws-marketplace/application/use-cases.ts‎

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,13 @@ const SUBSCRIPTION_UNCONFIRMED =
4242
'AWS is still confirming your subscription. Try again in a few minutes.'
4343

4444
/**
45-
* Under the row lock: a newer verification than this request's wins, so a license a newer read
46-
* says is not active cannot be taken.
45+
* Under the row lock, after the claim is rechecked: a newer read that still says pending wins over
46+
* this request's. A newer read that says inactive already cleared the claim.
4747
*/
4848
function requireNotOverruled(locked: AwsMarketplaceLicenseRecord, verifiedAt: Date): void {
49-
if (isNewerRead(locked, verifiedAt) || locked.status === 'active') return
50-
throw new OrchestrationError(
51-
'conflict',
52-
locked.status === 'pending' ? SUBSCRIPTION_UNCONFIRMED : NO_ACTIVE_SUBSCRIPTION
53-
)
49+
if (locked.status === 'pending' && !isNewerRead(locked, verifiedAt)) {
50+
throw new OrchestrationError('conflict', SUBSCRIPTION_UNCONFIRMED)
51+
}
5452
}
5553

5654
const CLAIM_NOT_FOUND =
@@ -120,9 +118,7 @@ export const readAwsMarketplaceClaim: OperationUseCase<
120118
requireProductCode()
121119
if (!input.claimToken) return { status: 'missing' }
122120
const license = await findLicenseByClaimHash(hashClaimToken(input.claimToken))
123-
if (!license) return { status: 'missing' }
124-
if (!isClaimOpen(license, new Date())) return { status: 'expired' }
125-
if (license.status === 'inactive') return { status: 'inactive' }
121+
if (!license || !isClaimOpen(license, new Date())) return { status: 'missing' }
126122
if (license.status === 'pending') {
127123
return { status: 'pending', license: claimedLicenseView(license) }
128124
}

0 commit comments

Comments
 (0)