PaymentMethods.test.tsx196 lines · main
1import { screen } from '@testing-library/react'
2import { beforeEach, describe, expect, it, vi } from 'vitest'
3
4import PaymentMethods from './PaymentMethods'
5import { MANAGED_BY } from '@/lib/constants/infrastructure'
6import { createMockOrganization, render } from '@/tests/helpers'
7
8const { mockSelectedOrganization, mockPaymentMethodsQuery, mockSubscription } = vi.hoisted(() => ({
9 mockSelectedOrganization: vi.fn(),
10 mockPaymentMethodsQuery: vi.fn(),
11 mockSubscription: vi.fn(),
12}))
13
14vi.mock('common', async (importOriginal) => {
15 const original = (await importOriginal()) as typeof import('common')
16 return {
17 ...original,
18 useParams: () => ({ slug: 'stripe-org' }),
19 }
20})
21
22vi.mock('@/hooks/misc/useSelectedOrganization', () => ({
23 useSelectedOrganizationQuery: () => ({ data: mockSelectedOrganization() }),
24}))
25
26vi.mock('@/data/subscriptions/org-subscription-query', () => ({
27 useOrgSubscriptionQuery: () => ({ data: mockSubscription() }),
28}))
29
30vi.mock('@/data/organizations/organization-payment-methods-query', () => ({
31 useOrganizationPaymentMethodsQuery: () => mockPaymentMethodsQuery(),
32}))
33
34vi.mock('@/hooks/misc/useCheckPermissions', () => ({
35 useAsyncCheckPermissions: () => ({ can: true, isSuccess: true }),
36}))
37
38vi.mock('@/components/ui/PartnerManagedResource', () => ({
39 default: () => <div data-testid="partner-managed-resource" />,
40}))
41
42vi.mock('@/components/interfaces/Billing/Payment/AddNewPaymentMethodModal', () => ({
43 default: () => null,
44}))
45
46vi.mock('./ChangePaymentMethodModal', () => ({
47 default: () => null,
48}))
49
50vi.mock('./DeletePaymentMethodModal', () => ({
51 default: () => null,
52}))
53
54describe('PaymentMethods', () => {
55 beforeEach(() => {
56 vi.clearAllMocks()
57 mockSelectedOrganization.mockReturnValue(
58 createMockOrganization({
59 slug: 'stripe-org',
60 billing_partner: null,
61 integration_source: 'stripe_projects',
62 managed_by: MANAGED_BY.STRIPE_PROJECTS,
63 })
64 )
65 mockSubscription.mockReturnValue({
66 payment_method_type: 'card',
67 plan: { id: 'pro', name: 'Pro' },
68 })
69 mockPaymentMethodsQuery.mockReturnValue({
70 data: { data: [] },
71 error: null,
72 isPending: false,
73 isError: false,
74 isSuccess: true,
75 })
76 })
77
78 it('does not treat Stripe-connected orgs as partner-billed', () => {
79 render(<PaymentMethods />)
80
81 expect(screen.queryByTestId('partner-managed-resource')).not.toBeInTheDocument()
82 expect(screen.getByText('No payment methods')).toBeInTheDocument()
83 })
84
85 it('uses Stripe Projects guidance instead of support for invoice-based Stripe orgs', () => {
86 mockSubscription.mockReturnValue({
87 payment_method_type: 'invoice',
88 plan: { id: 'pro', name: 'Pro' },
89 })
90
91 render(<PaymentMethods />)
92
93 expect(screen.getByText('Payment is currently by invoice')).toBeInTheDocument()
94 expect(screen.getByText(/Manage payment methods through Stripe Projects/)).toBeInTheDocument()
95 expect(
96 screen.getByRole('link', {
97 name: 'View Stripe Projects docs',
98 })
99 ).toBeInTheDocument()
100 expect(screen.queryByText('Contact support')).not.toBeInTheDocument()
101 })
102
103 it('still shows the partner-managed fallback for billing partners', () => {
104 mockSelectedOrganization.mockReturnValue(
105 createMockOrganization({
106 slug: 'aws-org',
107 billing_partner: 'aws_marketplace',
108 managed_by: MANAGED_BY.AWS_MARKETPLACE,
109 })
110 )
111
112 render(<PaymentMethods />)
113
114 expect(screen.getByTestId('partner-managed-resource')).toBeInTheDocument()
115 })
116
117 it('keeps the standard card row copy for non-Stripe payment methods', () => {
118 mockSelectedOrganization.mockReturnValue(
119 createMockOrganization({
120 slug: 'self-serve-org',
121 billing_partner: null,
122 integration_source: null,
123 managed_by: MANAGED_BY.BRIVEN,
124 })
125 )
126 mockPaymentMethodsQuery.mockReturnValue({
127 data: {
128 data: [
129 {
130 id: 'pm_123',
131 type: 'card',
132 is_default: true,
133 has_address: true,
134 created: 1_776_700_000,
135 card: {
136 brand: 'visa',
137 exp_month: 12,
138 exp_year: 2028,
139 last4: '4242',
140 },
141 shared_payment_token: null,
142 },
143 ],
144 },
145 error: null,
146 isPending: false,
147 isError: false,
148 isSuccess: true,
149 })
150
151 render(<PaymentMethods />)
152
153 expect(screen.getByText('Expires: 12/2028')).toBeInTheDocument()
154 expect(screen.queryByText(/Managed via Stripe Projects/i)).not.toBeInTheDocument()
155 })
156
157 it('shows Stripe Projects payment method details separately from the underlying card', () => {
158 mockPaymentMethodsQuery.mockReturnValue({
159 data: {
160 data: [
161 {
162 id: 'spt._1T0w6VJDPojXS6LNK3clVOLZ',
163 type: 'shared_payment_token',
164 is_default: true,
165 has_address: true,
166 created: 1_776_700_000,
167 card: {
168 brand: 'visa',
169 exp_month: 12,
170 exp_year: 2028,
171 last4: '4242',
172 },
173 shared_payment_token: {
174 last4: 'VOLZ',
175 expires_at: 1_860_883_200,
176 is_expired: false,
177 },
178 },
179 ],
180 },
181 error: null,
182 isPending: false,
183 isError: false,
184 isSuccess: true,
185 })
186
187 render(<PaymentMethods />)
188
189 expect(screen.getByText('**** **** **** 4242')).toBeInTheDocument()
190 expect(screen.getByText('Expires: 12/2028')).toBeInTheDocument()
191 expect(screen.getByText(/Managed via Stripe Projects/i)).toBeInTheDocument()
192 expect(screen.getByText('VOLZ')).toBeInTheDocument()
193 expect(screen.getByText(/Expires 12\/2028/i, { selector: 'span' })).toBeInTheDocument()
194 expect(screen.getByText('Active')).toBeInTheDocument()
195 })
196})