Skip to content

Commit 516f096

Browse files
fix(sub v3): Show limited subscription details to non-billing users (#102492)
<img width="2293" height="1045" alt="Screenshot 2025-10-31 at 1 21 50 PM" src="https://github.com/user-attachments/assets/b5a2fa11-393f-4691-990a-40ad6f7e31af" /> - Any user on self-serve org can see reserved spend and PAYG spend columns - Any user on self-serve org can see a read-only version of the PAYG card - Adds back payment source check for PAYG edit
1 parent ba0d137 commit 516f096

File tree

11 files changed

+146
-78
lines changed

11 files changed

+146
-78
lines changed

static/gsApp/views/onDemandBudgets/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ class OnDemandBudgets extends Component<Props> {
203203
return this.renderNotEnabled();
204204
}
205205

206-
if (!hasPaymentSource && !subscription.onDemandInvoicedManual) {
206+
if (!hasPaymentSource) {
207207
return this.renderNeedsPaymentSource();
208208
}
209209

static/gsApp/views/onDemandBudgets/onDemandBudgets.spec.tsx

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -116,45 +116,6 @@ describe('OnDemandBudgets', () => {
116116
expect(await screen.findByText('Stripe')).toBeInTheDocument();
117117
});
118118

119-
it('allows VC partner accounts to set up on-demand budget without credit card', () => {
120-
const subscription = SubscriptionFixture({
121-
plan: 'am3_business',
122-
planTier: PlanTier.AM3,
123-
isFree: false,
124-
isTrial: false,
125-
supportsOnDemand: true,
126-
organization,
127-
partner: {
128-
externalId: 'x123x',
129-
name: 'VC Org',
130-
partnership: {
131-
id: 'VC',
132-
displayName: 'VC',
133-
supportNote: '',
134-
},
135-
isActive: true,
136-
},
137-
onDemandBudgets: {
138-
enabled: false,
139-
budgetMode: OnDemandBudgetMode.SHARED,
140-
sharedMaxBudget: 0,
141-
onDemandSpendUsed: 0,
142-
},
143-
});
144-
SubscriptionStore.set(organization.slug, subscription);
145-
146-
const isVCPartner = subscription.partner?.partnership?.id === 'VC';
147-
createWrapper({
148-
subscription,
149-
onDemandEnabled: true,
150-
hasPaymentSource: isVCPartner,
151-
});
152-
153-
// Should show Set Up Pay-as-you-go button instead of Add Credit Card
154-
expect(screen.getByText('Set Up Pay-as-you-go')).toBeInTheDocument();
155-
expect(screen.queryByText('Add Credit Card')).not.toBeInTheDocument();
156-
});
157-
158119
it('renders initial on-demand budget setup state', () => {
159120
const subscription = SubscriptionFixture({
160121
plan: 'am1_business',

static/gsApp/views/subscriptionPage/headerCards/headerCards.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@ function getCards(organization: Organization, subscription: Subscription) {
2929
const cards: React.ReactNode[] = [];
3030
const isTrialOrFreePlan =
3131
isTrialPlan(subscription.plan) || isDeveloperPlan(subscription.planDetails);
32+
33+
// the organization can use PAYG
3234
const canUsePayg = supportsPayg(subscription);
3335

36+
// the user can update the PAYG budget
37+
const canUpdatePayg = canUsePayg && hasBillingPerms;
38+
3439
if (subscription.canSelfServe && !isTrialOrFreePlan && hasBillingPerms) {
3540
cards.push(
3641
<NextBillCard
@@ -41,9 +46,7 @@ function getCards(organization: Organization, subscription: Subscription) {
4146
);
4247
}
4348

44-
const canUpdatePayg = canUsePayg && hasBillingPerms;
45-
46-
if (canUpdatePayg) {
49+
if (canUsePayg) {
4750
cards.push(
4851
<PaygCard key="payg" subscription={subscription} organization={organization} />
4952
);

static/gsApp/views/subscriptionPage/headerCards/paygCard.spec.tsx

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import {OrganizationFixture} from 'sentry-fixture/organization';
22

3-
import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';
3+
import {
4+
InvoicedSubscriptionFixture,
5+
SubscriptionFixture,
6+
} from 'getsentry-test/fixtures/subscription';
47
import {
58
render,
69
renderGlobalModal,
@@ -26,6 +29,28 @@ describe('PaygCard', () => {
2629
resetMockDate();
2730
});
2831

32+
it('renders set/edit button for users with billing perms', () => {
33+
const subscription = SubscriptionFixture({
34+
organization,
35+
plan: 'am3_team',
36+
});
37+
render(<PaygCard organization={organization} subscription={subscription} />);
38+
expect(screen.getByRole('button', {name: 'Set limit'})).toBeInTheDocument();
39+
});
40+
41+
it('does not render set/edit button for users without billing perms', () => {
42+
const diffOrg = OrganizationFixture({
43+
access: [],
44+
});
45+
const subscription = SubscriptionFixture({
46+
organization: diffOrg,
47+
plan: 'am3_team',
48+
});
49+
render(<PaygCard organization={diffOrg} subscription={subscription} />);
50+
expect(screen.queryByRole('button', {name: 'Set limit'})).not.toBeInTheDocument();
51+
expect(screen.queryByRole('button', {name: 'Edit limit'})).not.toBeInTheDocument();
52+
});
53+
2954
it('renders for plan with no budget modes', async () => {
3055
const subscription = SubscriptionFixture({
3156
organization,
@@ -148,4 +173,45 @@ describe('PaygCard', () => {
148173
// closes inline edit
149174
expect(screen.getByRole('heading', {name: 'Pay-as-you-go'})).toBeInTheDocument();
150175
});
176+
177+
it('enables edit button for present payment source', () => {
178+
const subscription = SubscriptionFixture({
179+
organization,
180+
plan: 'am3_team',
181+
});
182+
render(<PaygCard organization={organization} subscription={subscription} />);
183+
expect(screen.getByRole('button', {name: 'Set limit'})).toBeEnabled();
184+
});
185+
186+
it('disables edit button if no payment source', () => {
187+
const subscription = SubscriptionFixture({
188+
organization,
189+
plan: 'am3_team', // we should never have a paid plan without a payment source IRL, but for testing purposes
190+
paymentSource: null,
191+
});
192+
render(<PaygCard organization={organization} subscription={subscription} />);
193+
expect(screen.getByRole('button', {name: 'Set limit'})).toBeDisabled();
194+
});
195+
196+
it('enables edit button for self-serve partner accounts', () => {
197+
const subscription = SubscriptionFixture({
198+
organization,
199+
plan: 'am3_team',
200+
paymentSource: null,
201+
isSelfServePartner: true,
202+
});
203+
render(<PaygCard organization={organization} subscription={subscription} />);
204+
expect(screen.getByRole('button', {name: 'Set limit'})).toBeEnabled();
205+
});
206+
207+
it('enables edit button for manually invoiced PAYG', () => {
208+
const subscription = InvoicedSubscriptionFixture({
209+
organization,
210+
plan: 'am3_business_ent',
211+
paymentSource: null,
212+
onDemandInvoicedManual: true,
213+
});
214+
render(<PaygCard organization={organization} subscription={subscription} />);
215+
expect(screen.getByRole('button', {name: 'Set limit'})).toBeEnabled();
216+
});
151217
});

static/gsApp/views/subscriptionPage/headerCards/paygCard.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
type OnDemandBudgets,
2222
type Subscription,
2323
} from 'getsentry/types';
24-
import {displayBudgetName} from 'getsentry/utils/billing';
24+
import {displayBudgetName, hasBillingAccess} from 'getsentry/utils/billing';
2525
import {displayPrice} from 'getsentry/views/amCheckout/utils';
2626
import {openOnDemandBudgetEditModal} from 'getsentry/views/onDemandBudgets/editOnDemandButton';
2727
import {
@@ -40,6 +40,12 @@ function PaygCard({
4040
organization: Organization;
4141
subscription: Subscription;
4242
}) {
43+
const hasBillingPerms = hasBillingAccess(organization);
44+
const hasPaymentSource = !!(
45+
subscription.paymentSource ||
46+
subscription.isSelfServePartner ||
47+
subscription.onDemandInvoicedManual
48+
);
4349
const api = useApi();
4450
const theme = useTheme();
4551
const paygBudget = parseOnDemandBudgetsFromSubscription(subscription);
@@ -88,6 +94,9 @@ function PaygCard({
8894

8995
const handleEditPayg = useCallback(
9096
(shouldHighlight = false) => {
97+
if (!hasBillingPerms) {
98+
return;
99+
}
91100
if (hasBudgetModes) {
92101
openOnDemandBudgetEditModal({organization, subscription, theme});
93102
} else {
@@ -97,7 +106,7 @@ function PaygCard({
97106
setIsEditing(true);
98107
}
99108
},
100-
[hasBudgetModes, organization, subscription, theme]
109+
[hasBudgetModes, organization, subscription, theme, hasBillingPerms]
101110
);
102111

103112
useEffect(() => {
@@ -193,14 +202,22 @@ function PaygCard({
193202
<Heading as="h2" size="lg">
194203
{displayBudgetName(subscription.planDetails, {title: true})}
195204
</Heading>
196-
<Button
197-
size="xs"
198-
onClick={() => {
199-
handleEditPayg(false);
200-
}}
201-
>
202-
{totalBudget > 0 ? t('Edit limit') : t('Set limit')}
203-
</Button>
205+
{hasBillingPerms && (
206+
<Button
207+
size="xs"
208+
disabled={!hasPaymentSource}
209+
title={
210+
hasPaymentSource
211+
? undefined
212+
: t('You must add a payment method to edit the limit')
213+
}
214+
onClick={() => {
215+
handleEditPayg(false);
216+
}}
217+
>
218+
{totalBudget > 0 ? t('Edit limit') : t('Set limit')}
219+
</Button>
220+
)}
204221
</Flex>,
205222
<Container key="payg-budget">
206223
<Text size="xl" bold>

static/gsApp/views/subscriptionPage/onDemandSettings.spec.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe('edit on-demand budget', () => {
3232
});
3333
});
3434

35-
it('allows VC partner accounts to edit on-demand budget without payment source', () => {
35+
it('allows self-serve partner accounts to edit on-demand budget without payment source', () => {
3636
const subscription = SubscriptionFixture({
3737
plan: 'am3_business',
3838
planTier: PlanTier.AM3,
@@ -41,16 +41,7 @@ describe('edit on-demand budget', () => {
4141
supportsOnDemand: true,
4242
organization: onDemandOrg,
4343
paymentSource: null,
44-
partner: {
45-
externalId: 'x123x',
46-
name: 'VC Org',
47-
partnership: {
48-
id: 'VC',
49-
displayName: 'VC',
50-
supportNote: '',
51-
},
52-
isActive: true,
53-
},
44+
isSelfServePartner: true,
5445
onDemandBudgets: {
5546
enabled: true,
5647
budgetMode: OnDemandBudgetMode.SHARED,

static/gsApp/views/subscriptionPage/onDemandSettings.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,11 @@ export function OnDemandSettings({subscription, organization}: OnDemandSettingsP
6666
}
6767

6868
const onDemandEnabled = subscription.planDetails.allowOnDemand;
69-
// VC partner accounts don't require a payment source (i.e. credit card) since they make all payments via VC
70-
const isVCPartner = subscription.partner?.partnership?.id === 'VC';
71-
const hasPaymentSource = !!subscription.paymentSource || isVCPartner;
69+
const hasPaymentSource = !!(
70+
subscription.paymentSource ||
71+
subscription.isSelfServePartner ||
72+
subscription.onDemandInvoicedManual
73+
);
7274
const hasOndemandBudgets =
7375
hasOnDemandBudgetsFeature(organization, subscription) &&
7476
Boolean(subscription.onDemandBudgets);

static/gsApp/views/subscriptionPage/onDemandSummary.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ class OnDemandSummary extends Component<Props, State> {
239239
return this.renderNotEnabled();
240240
}
241241

242-
if (!hasPaymentSource && !subscription.onDemandInvoicedManual) {
242+
if (!hasPaymentSource) {
243243
return this.renderNeedsPaymentSource();
244244
}
245245

static/gsApp/views/subscriptionPage/subscriptionHeader.spec.tsx

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ describe('SubscriptionHeader', () => {
6161
hasPaygCard: boolean;
6262
organization: Organization;
6363
}) {
64+
const hasBillingPerms = organization.access?.includes('org:billing');
6465
await screen.findByRole('heading', {name: 'Subscription'});
6566

6667
if (hasNextBillCard) {
@@ -83,16 +84,22 @@ describe('SubscriptionHeader', () => {
8384

8485
if (hasPaygCard) {
8586
await screen.findByRole('heading', {name: 'Pay-as-you-go'});
86-
screen.getByRole('button', {name: 'Set limit'});
87+
88+
if (hasBillingPerms) {
89+
expect(screen.getByRole('button', {name: 'Set limit'})).toBeInTheDocument();
90+
} else {
91+
expect(screen.queryByRole('button', {name: 'Set limit'})).not.toBeInTheDocument();
92+
expect(
93+
screen.queryByRole('button', {name: 'Edit limit'})
94+
).not.toBeInTheDocument();
95+
}
8796
} else {
8897
expect(
8998
screen.queryByRole('heading', {name: 'Pay-as-you-go'})
9099
).not.toBeInTheDocument();
91100
expect(screen.queryByRole('button', {name: 'Set limit'})).not.toBeInTheDocument();
92101
}
93102

94-
const hasBillingPerms = organization.access?.includes('org:billing');
95-
96103
// all subscriptions have links card
97104
if (hasBillingPerms) {
98105
expect(
@@ -240,7 +247,7 @@ describe('SubscriptionHeader', () => {
240247
});
241248
});
242249

243-
it('renders new header cards for self-serve customers and user without billing perms', async () => {
250+
it('renders new header cards for self-serve free customers and user without billing perms', async () => {
244251
const organization = OrganizationFixture({
245252
features: ['subscriptions-v3'],
246253
});
@@ -260,6 +267,26 @@ describe('SubscriptionHeader', () => {
260267
});
261268
});
262269

270+
it('renders new header cards for self-serve paid customers and user without billing perms', async () => {
271+
const organization = OrganizationFixture({
272+
features: ['subscriptions-v3'],
273+
});
274+
const subscription = SubscriptionFixture({
275+
organization,
276+
plan: 'am3_team',
277+
});
278+
SubscriptionStore.set(organization.slug, subscription);
279+
render(
280+
<SubscriptionHeader organization={organization} subscription={subscription} />
281+
);
282+
await assertNewHeaderCards({
283+
organization,
284+
hasNextBillCard: false,
285+
hasBillingInfoCard: false,
286+
hasPaygCard: true,
287+
});
288+
});
289+
263290
it('renders new header cards for self-serve customers on subscription trial', async () => {
264291
const organization = OrganizationFixture({
265292
features: ['subscriptions-v3'],

static/gsApp/views/subscriptionPage/usageOverview.spec.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,12 @@ describe('UsageOverview', () => {
7676
expect(screen.getByRole('columnheader', {name: 'Product'})).toBeInTheDocument();
7777
expect(screen.getByRole('columnheader', {name: 'Total usage'})).toBeInTheDocument();
7878
expect(screen.getByRole('columnheader', {name: 'Reserved'})).toBeInTheDocument();
79-
expect(screen.queryByText('Reserved spend')).not.toBeInTheDocument();
80-
expect(screen.queryByText('Pay-as-you-go spend')).not.toBeInTheDocument();
79+
expect(
80+
screen.getByRole('columnheader', {name: 'Reserved spend'})
81+
).toBeInTheDocument();
82+
expect(
83+
screen.getByRole('columnheader', {name: 'Pay-as-you-go spend'})
84+
).toBeInTheDocument();
8185
expect(
8286
screen.queryByRole('button', {name: 'View usage history'})
8387
).not.toBeInTheDocument();

0 commit comments

Comments
 (0)