PaymentMethods.test.tsx 5.8 KB

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