index.spec.tsx 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646
  1. import moment from 'moment-timezone';
  2. import {OrganizationFixture} from 'sentry-fixture/organization';
  3. import {RouteComponentPropsFixture} from 'sentry-fixture/routeComponentPropsFixture';
  4. import {BillingConfigFixture} from 'getsentry-test/fixtures/billingConfig';
  5. import {MetricHistoryFixture} from 'getsentry-test/fixtures/metricHistory';
  6. import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';
  7. import {
  8. act,
  9. render,
  10. screen,
  11. userEvent,
  12. waitFor,
  13. within,
  14. } from 'sentry-test/reactTestingLibrary';
  15. import SubscriptionStore from 'getsentry/stores/subscriptionStore';
  16. import type {Subscription as SubscriptionType} from 'getsentry/types';
  17. import {OnDemandBudgetMode, PlanTier} from 'getsentry/types';
  18. import AMCheckout from 'getsentry/views/amCheckout';
  19. import {getCheckoutAPIData} from 'getsentry/views/amCheckout/utils';
  20. import {hasOnDemandBudgetsFeature} from 'getsentry/views/onDemandBudgets/utils';
  21. describe('AM1 Checkout', function () {
  22. let mockResponse: any;
  23. const api = new MockApiClient();
  24. const organization = OrganizationFixture({features: []});
  25. const subscription = SubscriptionFixture({organization});
  26. const params = {};
  27. beforeEach(function () {
  28. SubscriptionStore.set(organization.slug, subscription);
  29. MockApiClient.clearMockResponses();
  30. MockApiClient.addMockResponse({
  31. url: `/subscriptions/${organization.slug}/`,
  32. method: 'GET',
  33. body: {},
  34. });
  35. mockResponse = MockApiClient.addMockResponse({
  36. url: `/customers/${organization.slug}/billing-config/`,
  37. method: 'GET',
  38. body: BillingConfigFixture(PlanTier.AM2),
  39. });
  40. MockApiClient.addMockResponse({
  41. url: `/organizations/${organization.slug}/promotions/trigger-check/`,
  42. method: 'POST',
  43. body: {},
  44. });
  45. MockApiClient.addMockResponse({
  46. url: `/customers/${organization.slug}/plan-migrations/?applied=0`,
  47. method: 'GET',
  48. body: {},
  49. });
  50. });
  51. it('renders', async function () {
  52. render(
  53. <AMCheckout
  54. {...RouteComponentPropsFixture()}
  55. checkoutTier={PlanTier.AM1}
  56. params={params}
  57. api={api}
  58. onToggleLegacy={jest.fn()}
  59. />,
  60. {organization}
  61. );
  62. expect(
  63. await screen.findByRole('heading', {name: 'Change Subscription'})
  64. ).toBeInTheDocument();
  65. expect(screen.getByTestId('checkout-steps')).toBeInTheDocument();
  66. expect(screen.getByRole('radio', {name: 'Business'})).toBeInTheDocument();
  67. await waitFor(() => {
  68. expect(mockResponse).toHaveBeenCalledWith(
  69. `/customers/${organization.slug}/billing-config/`,
  70. expect.objectContaining({
  71. method: 'GET',
  72. data: {tier: 'am1'},
  73. })
  74. );
  75. });
  76. });
  77. it('can skip to step and continue', async function () {
  78. render(
  79. <AMCheckout
  80. {...RouteComponentPropsFixture()}
  81. onToggleLegacy={jest.fn()}
  82. params={params}
  83. api={api}
  84. checkoutTier={PlanTier.AM1}
  85. />,
  86. {organization}
  87. );
  88. expect(
  89. await screen.findByRole('heading', {name: 'Change Subscription'})
  90. ).toBeInTheDocument();
  91. await userEvent.click(screen.getByText('Reserved Volumes'));
  92. await userEvent.click(screen.getByRole('button', {name: 'Continue'}));
  93. // Both steps are complete
  94. expect(
  95. within(screen.getByTestId('header-choose-your-plan')).getByTestId('icon-check-mark')
  96. ).toBeInTheDocument();
  97. expect(
  98. within(screen.getByTestId('header-reserved-volumes')).getByTestId('icon-check-mark')
  99. ).toBeInTheDocument();
  100. });
  101. it('renders cancel subscription button', async function () {
  102. const sub: SubscriptionType = {...subscription, canCancel: true};
  103. SubscriptionStore.set(organization.slug, sub);
  104. render(
  105. <AMCheckout
  106. {...RouteComponentPropsFixture()}
  107. params={params}
  108. api={api}
  109. onToggleLegacy={jest.fn()}
  110. checkoutTier={sub.planTier as PlanTier}
  111. />,
  112. {organization}
  113. );
  114. expect(
  115. await screen.findByRole('heading', {name: 'Change Subscription'})
  116. ).toBeInTheDocument();
  117. expect(screen.getByRole('button', {name: 'Cancel Subscription'})).toBeInTheDocument();
  118. });
  119. it('renders pending cancellation button', async function () {
  120. const sub: SubscriptionType = {
  121. ...subscription,
  122. canCancel: true,
  123. cancelAtPeriodEnd: true,
  124. };
  125. SubscriptionStore.set(organization.slug, sub);
  126. render(
  127. <AMCheckout
  128. {...RouteComponentPropsFixture()}
  129. params={params}
  130. api={api}
  131. onToggleLegacy={jest.fn()}
  132. checkoutTier={sub.planTier as PlanTier}
  133. />,
  134. {organization}
  135. );
  136. expect(
  137. await screen.findByRole('heading', {name: 'Change Subscription'})
  138. ).toBeInTheDocument();
  139. expect(await screen.findByText('Pending Cancellation')).toBeInTheDocument();
  140. });
  141. it('does not renders cancel subscription button if cannot cancel', async function () {
  142. render(
  143. <AMCheckout
  144. {...RouteComponentPropsFixture()}
  145. params={params}
  146. api={api}
  147. onToggleLegacy={jest.fn()}
  148. checkoutTier={subscription.planTier as PlanTier}
  149. />,
  150. {organization}
  151. );
  152. expect(
  153. await screen.findByRole('heading', {name: 'Change Subscription'})
  154. ).toBeInTheDocument();
  155. expect(
  156. screen.queryByRole('button', {name: 'Cancel Subscription'})
  157. ).not.toBeInTheDocument();
  158. });
  159. it('renders annual terms for annual plan', async function () {
  160. const sub: SubscriptionType = {
  161. ...subscription,
  162. plan: 'am1_team_auf',
  163. contractInterval: 'annual',
  164. billingInterval: 'annual',
  165. };
  166. SubscriptionStore.set(organization.slug, sub);
  167. const {container} = render(
  168. <AMCheckout
  169. {...RouteComponentPropsFixture()}
  170. params={params}
  171. api={api}
  172. onToggleLegacy={jest.fn()}
  173. checkoutTier={sub.planTier as PlanTier}
  174. />,
  175. {organization}
  176. );
  177. expect(
  178. await screen.findByRole('heading', {name: 'Change Subscription'})
  179. ).toBeInTheDocument();
  180. expect(container).toHaveTextContent(
  181. 'Annual subscriptions require a one-year non-cancellable commitment'
  182. );
  183. });
  184. it('does not render annual terms for monthly plan', async function () {
  185. const sub = {...subscription};
  186. SubscriptionStore.set(organization.slug, sub);
  187. const {container} = render(
  188. <AMCheckout
  189. {...RouteComponentPropsFixture()}
  190. params={params}
  191. api={api}
  192. onToggleLegacy={jest.fn()}
  193. checkoutTier={sub.planTier as PlanTier}
  194. />,
  195. {organization}
  196. );
  197. expect(
  198. await screen.findByRole('heading', {name: 'Change Subscription'})
  199. ).toBeInTheDocument();
  200. expect(container).not.toHaveTextContent(
  201. 'Annual subscriptions require a one-year non-cancellable commitment'
  202. );
  203. });
  204. it('renders default plan data', async function () {
  205. render(
  206. <AMCheckout
  207. {...RouteComponentPropsFixture()}
  208. params={params}
  209. api={api}
  210. onToggleLegacy={jest.fn()}
  211. checkoutTier={subscription.planTier as PlanTier}
  212. />,
  213. {organization}
  214. );
  215. expect(
  216. await screen.findByRole('heading', {name: 'Change Subscription'})
  217. ).toBeInTheDocument();
  218. expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
  219. await userEvent.click(screen.getByText('Reserved Volumes'));
  220. // TODO: Can better write this once we have
  221. // https://github.com/testing-library/jest-dom/issues/478
  222. expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
  223. 'aria-valuetext',
  224. '50000'
  225. );
  226. expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
  227. 'aria-valuetext',
  228. '100000'
  229. );
  230. expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
  231. 'aria-valuetext',
  232. '500'
  233. );
  234. expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
  235. 'aria-valuetext',
  236. '1'
  237. );
  238. await userEvent.click(screen.getByText('On-Demand Max Spend'));
  239. expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('');
  240. });
  241. it('prefills with am1 team subscription data', async function () {
  242. const sub: SubscriptionType = SubscriptionFixture({
  243. organization,
  244. plan: 'am1_business',
  245. planTier: 'am1',
  246. categories: {
  247. errors: MetricHistoryFixture({reserved: 200000}),
  248. transactions: MetricHistoryFixture({reserved: 250000}),
  249. replays: MetricHistoryFixture({reserved: 10_000}),
  250. attachments: MetricHistoryFixture({reserved: 25}),
  251. monitorSeats: MetricHistoryFixture({reserved: 1}),
  252. },
  253. onDemandMaxSpend: 10000,
  254. });
  255. SubscriptionStore.set(organization.slug, sub);
  256. render(
  257. <AMCheckout
  258. {...RouteComponentPropsFixture()}
  259. params={params}
  260. api={api}
  261. onToggleLegacy={jest.fn()}
  262. checkoutTier={PlanTier.AM2}
  263. />,
  264. {
  265. organization,
  266. }
  267. );
  268. expect(
  269. await screen.findByRole('heading', {name: 'Change Subscription'})
  270. ).toBeInTheDocument();
  271. expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
  272. await userEvent.click(screen.getByText('Reserved Volumes'));
  273. // TODO: Can better write this once we have
  274. // https://github.com/testing-library/jest-dom/issues/478
  275. expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
  276. 'aria-valuetext',
  277. '200000'
  278. );
  279. expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
  280. 'aria-valuetext',
  281. '250000'
  282. );
  283. expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
  284. 'aria-valuetext',
  285. '10000'
  286. );
  287. expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
  288. 'aria-valuetext',
  289. '25'
  290. );
  291. await userEvent.click(screen.getByText('On-Demand Max Spend'));
  292. expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('100');
  293. });
  294. it('prefills with am1 business subscription data', async function () {
  295. const sub = SubscriptionFixture({
  296. organization,
  297. plan: 'am1_business',
  298. planTier: 'am1',
  299. categories: {
  300. errors: MetricHistoryFixture({reserved: 50000}),
  301. transactions: MetricHistoryFixture({reserved: 250000}),
  302. replays: MetricHistoryFixture({reserved: 500}),
  303. attachments: MetricHistoryFixture({reserved: 50}),
  304. monitorSeats: MetricHistoryFixture({reserved: 1}),
  305. },
  306. onDemandMaxSpend: 10000,
  307. });
  308. SubscriptionStore.set(organization.slug, sub);
  309. render(
  310. <AMCheckout
  311. {...RouteComponentPropsFixture()}
  312. params={params}
  313. api={api}
  314. onToggleLegacy={jest.fn()}
  315. checkoutTier={PlanTier.AM2}
  316. />,
  317. {
  318. organization,
  319. }
  320. );
  321. expect(
  322. await screen.findByRole('heading', {name: 'Change Subscription'})
  323. ).toBeInTheDocument();
  324. expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
  325. await userEvent.click(screen.getByText('Reserved Volumes'));
  326. // TODO: Can better write this once we have
  327. // https://github.com/testing-library/jest-dom/issues/478
  328. expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
  329. 'aria-valuetext',
  330. '50000'
  331. );
  332. expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
  333. 'aria-valuetext',
  334. '250000'
  335. );
  336. expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
  337. 'aria-valuetext',
  338. '500'
  339. );
  340. expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
  341. 'aria-valuetext',
  342. '50'
  343. );
  344. await userEvent.click(screen.getByText('On-Demand Max Spend'));
  345. expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('100');
  346. });
  347. it('prefills with mm2 team subscription data', async function () {
  348. const sub = SubscriptionFixture({
  349. organization,
  350. plan: 'mm2_b_100k',
  351. planTier: 'mm2',
  352. categories: {errors: MetricHistoryFixture({reserved: 100000})},
  353. onDemandMaxSpend: 2000,
  354. });
  355. SubscriptionStore.set(organization.slug, sub);
  356. render(
  357. <AMCheckout
  358. {...RouteComponentPropsFixture()}
  359. params={params}
  360. api={api}
  361. onToggleLegacy={jest.fn()}
  362. checkoutTier={sub.planTier as PlanTier}
  363. />,
  364. {
  365. organization,
  366. }
  367. );
  368. expect(
  369. await screen.findByRole('heading', {name: 'Change Subscription'})
  370. ).toBeInTheDocument();
  371. expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
  372. await userEvent.click(screen.getByText('Reserved Volumes'));
  373. // TODO: Can better write this once we have
  374. // https://github.com/testing-library/jest-dom/issues/478
  375. expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
  376. 'aria-valuetext',
  377. '100000'
  378. );
  379. expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
  380. 'aria-valuetext',
  381. '100000'
  382. );
  383. expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
  384. 'aria-valuetext',
  385. '1'
  386. );
  387. await userEvent.click(screen.getByText('On-Demand Max Spend'));
  388. expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
  389. });
  390. it('prefills with mm2 biz subscription data', async function () {
  391. const sub = SubscriptionFixture({
  392. organization,
  393. plan: 'mm2_a_100k',
  394. planTier: 'mm2',
  395. categories: {errors: MetricHistoryFixture({reserved: 100_000})},
  396. onDemandMaxSpend: 2000,
  397. });
  398. SubscriptionStore.set(organization.slug, sub);
  399. render(
  400. <AMCheckout
  401. {...RouteComponentPropsFixture()}
  402. params={params}
  403. api={api}
  404. onToggleLegacy={jest.fn()}
  405. checkoutTier={PlanTier.AM2}
  406. />
  407. );
  408. expect(
  409. await screen.findByRole('heading', {name: 'Change Subscription'})
  410. ).toBeInTheDocument();
  411. expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
  412. await userEvent.click(screen.getByText('Reserved Volumes'));
  413. // TODO: Can better write this once we have
  414. // https://github.com/testing-library/jest-dom/issues/478
  415. expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
  416. 'aria-valuetext',
  417. '100000'
  418. );
  419. expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
  420. 'aria-valuetext',
  421. '100000'
  422. );
  423. expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
  424. 'aria-valuetext',
  425. '1'
  426. );
  427. await userEvent.click(screen.getByText('On-Demand Max Spend'));
  428. expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
  429. });
  430. it('prefills with s1 subscription data', async function () {
  431. const sub = SubscriptionFixture({
  432. organization,
  433. plan: 's1',
  434. planTier: 'mm1',
  435. categories: {errors: MetricHistoryFixture({reserved: 100000})},
  436. onDemandMaxSpend: 2000,
  437. });
  438. SubscriptionStore.set(organization.slug, sub);
  439. render(
  440. <AMCheckout
  441. {...RouteComponentPropsFixture()}
  442. params={params}
  443. api={api}
  444. onToggleLegacy={jest.fn()}
  445. checkoutTier={sub.planTier as PlanTier}
  446. />,
  447. {
  448. organization,
  449. }
  450. );
  451. expect(
  452. await screen.findByRole('heading', {name: 'Change Subscription'})
  453. ).toBeInTheDocument();
  454. expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
  455. await userEvent.click(screen.getByText('Reserved Volumes'));
  456. // TODO: Can better write this once we have
  457. // https://github.com/testing-library/jest-dom/issues/478
  458. expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
  459. 'aria-valuetext',
  460. '100000'
  461. );
  462. expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
  463. 'aria-valuetext',
  464. '100000'
  465. );
  466. expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
  467. 'aria-valuetext',
  468. '1'
  469. );
  470. await userEvent.click(screen.getByText('On-Demand Max Spend'));
  471. expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
  472. });
  473. it('prefills with l1 subscription data', async function () {
  474. const sub = SubscriptionFixture({
  475. organization,
  476. plan: 'l1',
  477. planTier: 'mm1',
  478. categories: {errors: MetricHistoryFixture({reserved: 100000})},
  479. onDemandMaxSpend: 2000,
  480. });
  481. SubscriptionStore.set(organization.slug, sub);
  482. render(
  483. <AMCheckout
  484. {...RouteComponentPropsFixture()}
  485. params={params}
  486. api={api}
  487. onToggleLegacy={jest.fn()}
  488. checkoutTier={sub.planTier as PlanTier}
  489. />,
  490. {
  491. organization,
  492. }
  493. );
  494. expect(
  495. await screen.findByRole('heading', {name: 'Change Subscription'})
  496. ).toBeInTheDocument();
  497. expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
  498. await userEvent.click(screen.getByText('Reserved Volumes'));
  499. // TODO: Can better write this once we have
  500. // https://github.com/testing-library/jest-dom/issues/478
  501. expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
  502. 'aria-valuetext',
  503. '100000'
  504. );
  505. expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
  506. 'aria-valuetext',
  507. '100000'
  508. );
  509. expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
  510. 'aria-valuetext',
  511. '1'
  512. );
  513. await userEvent.click(screen.getByText('On-Demand Max Spend'));
  514. expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
  515. });
  516. it('handles subscription with unlimited ondemand', async function () {
  517. const sub = {...subscription, onDemandMaxSpend: -1};
  518. SubscriptionStore.set(organization.slug, sub);
  519. render(
  520. <AMCheckout
  521. {...RouteComponentPropsFixture()}
  522. params={params}
  523. api={api}
  524. onToggleLegacy={jest.fn()}
  525. checkoutTier={PlanTier.AM2}
  526. />,
  527. {
  528. organization,
  529. }
  530. );
  531. expect(
  532. await screen.findByRole('heading', {name: 'Change Subscription'})
  533. ).toBeInTheDocument();
  534. await userEvent.click(screen.getByText('On-Demand Max Spend'));
  535. expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('');
  536. });
  537. });
  538. describe('AM2 Checkout', function () {
  539. let mockResponse: any;
  540. const api = new MockApiClient();
  541. const organization = OrganizationFixture();
  542. const subscription = SubscriptionFixture({organization});
  543. const params = {};
  544. beforeEach(function () {
  545. SubscriptionStore.set(organization.slug, subscription);
  546. MockApiClient.clearMockResponses();
  547. MockApiClient.addMockResponse({
  548. url: `/subscriptions/${organization.slug}/`,
  549. method: 'GET',
  550. body: {},
  551. });
  552. mockResponse = MockApiClient.addMockResponse({
  553. url: `/customers/${organization.slug}/billing-config/`,
  554. method: 'GET',
  555. body: BillingConfigFixture(PlanTier.AM2),
  556. });
  557. MockApiClient.addMockResponse({
  558. url: `/organizations/${organization.slug}/promotions/trigger-check/`,
  559. method: 'POST',
  560. });
  561. MockApiClient.addMockResponse({
  562. url: `/customers/${organization.slug}/plan-migrations/?applied=0`,
  563. method: 'GET',
  564. body: {},
  565. });
  566. });
  567. it('renders for am1 team plan', async function () {
  568. const sub = SubscriptionFixture({organization, plan: 'am1_team'});
  569. SubscriptionStore.set(organization.slug, sub);
  570. render(
  571. <AMCheckout
  572. {...RouteComponentPropsFixture()}
  573. params={params}
  574. api={api}
  575. onToggleLegacy={jest.fn()}
  576. checkoutTier={PlanTier.AM2}
  577. />,
  578. {organization}
  579. );
  580. expect(
  581. await screen.findByRole('heading', {name: 'Change Subscription'})
  582. ).toBeInTheDocument();
  583. expect(screen.getByText('Choose Your Plan')).toBeInTheDocument();
  584. expect(screen.getByRole('radio', {name: 'Business'})).toBeInTheDocument();
  585. expect(screen.getByText('Cross-project visibility')).toBeInTheDocument();
  586. expect(screen.getByRole('radio', {name: 'Team'})).toBeInTheDocument();
  587. expect(screen.getByText('Unlimited members')).toBeInTheDocument();
  588. expect(mockResponse).toHaveBeenCalledWith(
  589. `/customers/${organization.slug}/billing-config/`,
  590. expect.objectContaining({
  591. method: 'GET',
  592. data: {tier: PlanTier.AM2},
  593. })
  594. );
  595. });
  596. it('renders for am2 free plan', async function () {
  597. const sub = SubscriptionFixture({organization, plan: 'am2_f'});
  598. SubscriptionStore.set(organization.slug, sub);
  599. render(
  600. <AMCheckout
  601. {...RouteComponentPropsFixture()}
  602. params={params}
  603. api={api}
  604. onToggleLegacy={jest.fn()}
  605. checkoutTier={PlanTier.AM2}
  606. />,
  607. {organization}
  608. );
  609. expect(
  610. await screen.findByRole('heading', {name: 'Change Subscription'})
  611. ).toBeInTheDocument();
  612. expect(screen.getByText('Choose Your Plan')).toBeInTheDocument();
  613. expect(screen.getByRole('radio', {name: 'Business'})).toBeInTheDocument();
  614. expect(screen.getByText('Cross-project visibility')).toBeInTheDocument();
  615. expect(screen.getByRole('radio', {name: 'Team'})).toBeInTheDocument();
  616. expect(screen.getByText('Unlimited members')).toBeInTheDocument();
  617. expect(mockResponse).toHaveBeenCalledWith(
  618. `/customers/${organization.slug}/billing-config/`,
  619. expect.objectContaining({
  620. method: 'GET',
  621. data: {tier: PlanTier.AM2},
  622. })
  623. );
  624. });
  625. it('prefills subscription data based on price with same plan type', async function () {
  626. const sub = SubscriptionFixture({
  627. organization,
  628. plan: 'am1_business',
  629. planTier: 'am1',
  630. categories: {
  631. errors: MetricHistoryFixture({reserved: 50_000}),
  632. transactions: MetricHistoryFixture({reserved: 20_000_000}),
  633. replays: MetricHistoryFixture({reserved: 500}),
  634. attachments: MetricHistoryFixture({reserved: 1}),
  635. monitorSeats: MetricHistoryFixture({reserved: 1}),
  636. },
  637. onDemandMaxSpend: 2000,
  638. });
  639. SubscriptionStore.set(organization.slug, sub);
  640. render(
  641. <AMCheckout
  642. {...RouteComponentPropsFixture()}
  643. params={params}
  644. api={api}
  645. onToggleLegacy={jest.fn()}
  646. checkoutTier={PlanTier.AM2}
  647. />,
  648. {organization}
  649. );
  650. expect(
  651. await screen.findByRole('heading', {name: 'Change Subscription'})
  652. ).toBeInTheDocument();
  653. expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
  654. await userEvent.click(screen.getByText('Reserved Volumes'));
  655. // TODO: Can better write this once we have
  656. // https://github.com/testing-library/jest-dom/issues/478
  657. expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
  658. 'aria-valuetext',
  659. '50000'
  660. );
  661. expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
  662. 'aria-valuetext',
  663. '35000000'
  664. );
  665. expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
  666. 'aria-valuetext',
  667. '500'
  668. );
  669. expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
  670. 'aria-valuetext',
  671. '1'
  672. );
  673. await userEvent.click(screen.getByText('On-Demand Max Spend'));
  674. expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
  675. });
  676. it('prefills subscription data based on price with annual plan', async function () {
  677. const sub = SubscriptionFixture({
  678. organization,
  679. plan: 'am1_business_auf',
  680. planTier: 'am1',
  681. categories: {
  682. errors: MetricHistoryFixture({reserved: 100_000}),
  683. transactions: MetricHistoryFixture({reserved: 20_000_000}),
  684. replays: MetricHistoryFixture({reserved: 500}),
  685. attachments: MetricHistoryFixture({reserved: 1}),
  686. monitorSeats: MetricHistoryFixture({reserved: 1}),
  687. },
  688. onDemandMaxSpend: 2000,
  689. });
  690. SubscriptionStore.set(organization.slug, sub);
  691. render(
  692. <AMCheckout
  693. {...RouteComponentPropsFixture()}
  694. params={params}
  695. api={api}
  696. onToggleLegacy={jest.fn()}
  697. checkoutTier={PlanTier.AM2}
  698. />,
  699. {organization}
  700. );
  701. expect(
  702. await screen.findByRole('heading', {name: 'Change Subscription'})
  703. ).toBeInTheDocument();
  704. expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
  705. await userEvent.click(screen.getByText('Reserved Volumes'));
  706. // TODO: Can better write this once we have
  707. // https://github.com/testing-library/jest-dom/issues/478
  708. expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
  709. 'aria-valuetext',
  710. '100000'
  711. );
  712. expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
  713. 'aria-valuetext',
  714. '35000000'
  715. );
  716. expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
  717. 'aria-valuetext',
  718. '500'
  719. );
  720. expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
  721. 'aria-valuetext',
  722. '1'
  723. );
  724. await userEvent.click(screen.getByText('On-Demand Max Spend'));
  725. expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
  726. });
  727. it('prefills subscription data based on events with different plan type', async function () {
  728. const sub = SubscriptionFixture({
  729. organization,
  730. plan: 'am1_team',
  731. planTier: 'am1',
  732. categories: {
  733. errors: MetricHistoryFixture({reserved: 100_000}),
  734. transactions: MetricHistoryFixture({reserved: 20_000_000}),
  735. attachments: MetricHistoryFixture({reserved: 1}),
  736. replays: MetricHistoryFixture({reserved: 500}),
  737. monitorSeats: MetricHistoryFixture({reserved: 1}),
  738. },
  739. onDemandMaxSpend: 2000,
  740. });
  741. SubscriptionStore.set(organization.slug, sub);
  742. render(
  743. <AMCheckout
  744. {...RouteComponentPropsFixture()}
  745. params={params}
  746. api={api}
  747. onToggleLegacy={jest.fn()}
  748. checkoutTier={PlanTier.AM2}
  749. />,
  750. {organization}
  751. );
  752. expect(
  753. await screen.findByRole('heading', {name: 'Change Subscription'})
  754. ).toBeInTheDocument();
  755. expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
  756. await userEvent.click(screen.getByText('Reserved Volumes'));
  757. // TODO: Can better write this once we have
  758. // https://github.com/testing-library/jest-dom/issues/478
  759. expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
  760. 'aria-valuetext',
  761. '100000'
  762. );
  763. expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
  764. 'aria-valuetext',
  765. '20000000'
  766. );
  767. expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
  768. 'aria-valuetext',
  769. '500'
  770. );
  771. expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
  772. 'aria-valuetext',
  773. '1'
  774. );
  775. await userEvent.click(screen.getByText('On-Demand Max Spend'));
  776. expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
  777. });
  778. it('displays 40% india promotion', async function () {
  779. const promotionData = {
  780. completedPromotions: [
  781. {
  782. promotion: {
  783. name: 'Test Promotion',
  784. slug: 'test_promotion',
  785. timeLimit: null,
  786. startDate: null,
  787. endDate: null,
  788. showDiscountInfo: true,
  789. discountInfo: {
  790. amount: 4000,
  791. billingInterval: 'monthly',
  792. billingPeriods: 3,
  793. creditCategory: 'subscription',
  794. discountType: 'percentPoints',
  795. disclaimerText:
  796. "*Receive 40% off the monthly price of Sentry's Team or Business plan subscriptions for your first three months if you upgrade today",
  797. durationText: 'First three months',
  798. },
  799. },
  800. },
  801. ],
  802. };
  803. MockApiClient.addMockResponse({
  804. url: `/organizations/${organization.slug}/promotions/trigger-check/`,
  805. method: 'POST',
  806. body: promotionData,
  807. });
  808. render(
  809. <AMCheckout
  810. {...RouteComponentPropsFixture()}
  811. params={params}
  812. api={api}
  813. onToggleLegacy={jest.fn()}
  814. checkoutTier={PlanTier.AM2}
  815. />,
  816. {organization}
  817. );
  818. await screen.findByText('Choose Your Plan');
  819. expect(
  820. screen.getByText(
  821. "*Receive 40% off the monthly price of Sentry's Team or Business plan subscriptions for your first three months if you upgrade today"
  822. )
  823. ).toBeInTheDocument();
  824. expect(screen.getByText('First three months 40% off')).toBeInTheDocument();
  825. expect(screen.getAllByText('53.40')).toHaveLength(2);
  826. });
  827. it('skips step 1 for business plan in same tier', async function () {
  828. const am2BizSubscription = SubscriptionFixture({
  829. organization,
  830. plan: 'am2_business',
  831. planTier: 'am2',
  832. categories: {
  833. errors: MetricHistoryFixture({reserved: 100_000}),
  834. transactions: MetricHistoryFixture({reserved: 20_000_000}),
  835. attachments: MetricHistoryFixture({reserved: 1}),
  836. monitorSeats: MetricHistoryFixture({reserved: 1}),
  837. profileDuration: MetricHistoryFixture({reserved: 1}),
  838. replays: MetricHistoryFixture({reserved: 10_000}),
  839. },
  840. onDemandMaxSpend: 2000,
  841. });
  842. SubscriptionStore.set(organization.slug, am2BizSubscription);
  843. render(
  844. <AMCheckout
  845. {...RouteComponentPropsFixture()}
  846. params={params}
  847. api={api}
  848. onToggleLegacy={jest.fn()}
  849. checkoutTier={PlanTier.AM2}
  850. />,
  851. {organization}
  852. );
  853. await screen.findByText('Choose Your Plan');
  854. expect(screen.queryByTestId('body-choose-your-plan')).not.toBeInTheDocument();
  855. expect(screen.getByTestId('errors-volume-item')).toBeInTheDocument();
  856. });
  857. it('test business bundle standard checkout', async function () {
  858. const am2BizSubscription = SubscriptionFixture({
  859. organization,
  860. plan: 'am2_business_bundle',
  861. planTier: 'am2',
  862. categories: {
  863. errors: MetricHistoryFixture({reserved: 100_000}),
  864. transactions: MetricHistoryFixture({reserved: 20_000_000}),
  865. attachments: MetricHistoryFixture({reserved: 1}),
  866. monitorSeats: MetricHistoryFixture({reserved: 1}),
  867. profileDuration: MetricHistoryFixture({reserved: 1}),
  868. replays: MetricHistoryFixture({reserved: 10_000}),
  869. },
  870. onDemandMaxSpend: 2000,
  871. });
  872. SubscriptionStore.set(organization.slug, am2BizSubscription);
  873. render(
  874. <AMCheckout
  875. {...RouteComponentPropsFixture()}
  876. params={params}
  877. api={api}
  878. onToggleLegacy={jest.fn()}
  879. checkoutTier={PlanTier.AM2}
  880. />,
  881. {organization}
  882. );
  883. // wait for page load
  884. await screen.findByText('Choose Your Plan');
  885. // "Choose Your Plan" should be skipped and "Reserved Volumes" should be visible
  886. // This is existing behavior to skip "Choose Your Plan" step for existing business customers
  887. expect(screen.queryByTestId('body-choose-your-plan')).not.toBeInTheDocument();
  888. expect(screen.getByTestId('errors-volume-item')).toBeInTheDocument();
  889. // Click on "Choose Your Plan" and verify that Business is selected
  890. await userEvent.click(screen.getByText('Choose Your Plan'));
  891. expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
  892. });
  893. it('handles missing categories in subscription.categories', async function () {
  894. /**
  895. * In this test, we create a subscription where some categories are missing from
  896. * `subscription.categories`. We then verify that the component renders correctly
  897. * without throwing errors, and that the missing categories default to a reserved
  898. * value of 0.
  899. */
  900. const sub = SubscriptionFixture({
  901. organization,
  902. plan: 'am2_business',
  903. planTier: 'am2',
  904. categories: {
  905. // Intentionally omitting 'transactions' and 'replays' categories
  906. errors: MetricHistoryFixture({reserved: 100_000}),
  907. attachments: MetricHistoryFixture({reserved: 1}),
  908. monitorSeats: MetricHistoryFixture({reserved: 1}),
  909. },
  910. onDemandMaxSpend: 2000,
  911. });
  912. SubscriptionStore.set(organization.slug, sub);
  913. render(
  914. <AMCheckout
  915. {...RouteComponentPropsFixture()}
  916. params={params}
  917. api={api}
  918. onToggleLegacy={jest.fn()}
  919. checkoutTier={PlanTier.AM2}
  920. />,
  921. {organization}
  922. );
  923. expect(
  924. await screen.findByRole('heading', {name: 'Change Subscription'})
  925. ).toBeInTheDocument();
  926. // Verify that the component renders without errors
  927. expect(screen.getByTestId('errors-volume-item')).toBeInTheDocument();
  928. // Open 'Reserved Volumes' section
  929. await userEvent.click(screen.getByText('Reserved Volumes'));
  930. // Check that missing categories default to 0
  931. expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
  932. 'aria-valuetext',
  933. '100000'
  934. );
  935. // For missing 'Performance units', should default to 100,000 units
  936. expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute(
  937. 'aria-valuetext',
  938. '100000'
  939. );
  940. // For missing 'Replays', should default to 500
  941. expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
  942. 'aria-valuetext',
  943. '500'
  944. );
  945. // Check that 'Attachments' category is correctly set
  946. expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
  947. 'aria-valuetext',
  948. '1'
  949. );
  950. // Open 'On-Demand Max Spend' section
  951. await userEvent.click(screen.getByText('On-Demand Max Spend'));
  952. expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20');
  953. });
  954. });
  955. describe('AM3 Checkout', function () {
  956. const api = new MockApiClient();
  957. const organization = OrganizationFixture({
  958. features: ['ondemand-budgets', 'am3-billing'],
  959. });
  960. const params = {};
  961. beforeEach(function () {
  962. MockApiClient.clearMockResponses();
  963. MockApiClient.addMockResponse({
  964. url: `/subscriptions/${organization.slug}/`,
  965. method: 'GET',
  966. body: {},
  967. });
  968. MockApiClient.addMockResponse({
  969. url: `/organizations/${organization.slug}/promotions/trigger-check/`,
  970. method: 'POST',
  971. });
  972. MockApiClient.addMockResponse({
  973. url: `/customers/${organization.slug}/plan-migrations/?applied=0`,
  974. method: 'GET',
  975. body: {},
  976. });
  977. });
  978. it('renders for new customers (AM3 free plan)', async function () {
  979. const sub = SubscriptionFixture({
  980. organization,
  981. plan: 'am3_f',
  982. planTier: PlanTier.AM3,
  983. });
  984. act(() => SubscriptionStore.set(organization.slug, sub));
  985. const mockResponse = MockApiClient.addMockResponse({
  986. url: `/customers/${organization.slug}/billing-config/`,
  987. method: 'GET',
  988. body: BillingConfigFixture(PlanTier.AM3),
  989. });
  990. render(
  991. <AMCheckout
  992. {...RouteComponentPropsFixture()}
  993. params={params}
  994. api={api}
  995. onToggleLegacy={jest.fn()}
  996. checkoutTier={PlanTier.AM3}
  997. />,
  998. {organization}
  999. );
  1000. expect(await screen.findByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument();
  1001. expect(mockResponse).toHaveBeenCalledWith(
  1002. `/customers/${organization.slug}/billing-config/`,
  1003. expect.objectContaining({
  1004. method: 'GET',
  1005. data: {tier: PlanTier.AM3},
  1006. })
  1007. );
  1008. });
  1009. it('renders for customers migrating from partner billing', async function () {
  1010. organization.features.push('partner-billing-migration');
  1011. const contractPeriodEnd = moment();
  1012. const sub = SubscriptionFixture({
  1013. organization,
  1014. contractPeriodEnd: contractPeriodEnd.toString(),
  1015. plan: 'am2_sponsored_team_auf',
  1016. planTier: PlanTier.AM2,
  1017. isSponsored: true,
  1018. partner: {
  1019. isActive: true,
  1020. externalId: 'yuh',
  1021. partnership: {
  1022. id: 'FOO',
  1023. displayName: 'FOO',
  1024. supportNote: '',
  1025. },
  1026. name: '',
  1027. },
  1028. });
  1029. act(() => SubscriptionStore.set(organization.slug, sub));
  1030. const mockResponse = MockApiClient.addMockResponse({
  1031. url: `/customers/${organization.slug}/billing-config/`,
  1032. method: 'GET',
  1033. body: BillingConfigFixture(PlanTier.AM3),
  1034. });
  1035. render(
  1036. <AMCheckout
  1037. {...RouteComponentPropsFixture()}
  1038. params={params}
  1039. api={api}
  1040. onToggleLegacy={jest.fn()}
  1041. checkoutTier={PlanTier.AM3}
  1042. />,
  1043. {organization}
  1044. );
  1045. expect(await screen.findByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument();
  1046. expect(
  1047. screen.getByText(
  1048. 'Your promotional plan with FOO ends on ' + contractPeriodEnd.format('ll') + '.'
  1049. )
  1050. ).toBeInTheDocument();
  1051. // 500 replays from sponsored plan becomes 50 on am3
  1052. expect(screen.getByText('50')).toBeInTheDocument();
  1053. expect(mockResponse).toHaveBeenCalledWith(
  1054. `/customers/${organization.slug}/billing-config/`,
  1055. expect.objectContaining({
  1056. method: 'GET',
  1057. data: {tier: PlanTier.AM3},
  1058. })
  1059. );
  1060. });
  1061. it('renders for self-serve partners', async function () {
  1062. const contractPeriodEnd = moment();
  1063. const sub = SubscriptionFixture({
  1064. organization,
  1065. contractPeriodEnd: contractPeriodEnd.toString(),
  1066. plan: 'am3_f',
  1067. planTier: PlanTier.AM3,
  1068. isSelfServePartner: true,
  1069. partner: {
  1070. isActive: true,
  1071. externalId: 'foo',
  1072. partnership: {
  1073. id: 'XX',
  1074. displayName: 'BAR',
  1075. supportNote: '',
  1076. },
  1077. name: '',
  1078. },
  1079. });
  1080. act(() => SubscriptionStore.set(organization.slug, sub));
  1081. const mockResponse = MockApiClient.addMockResponse({
  1082. url: `/customers/${organization.slug}/billing-config/`,
  1083. method: 'GET',
  1084. body: BillingConfigFixture(PlanTier.AM3),
  1085. });
  1086. render(
  1087. <AMCheckout
  1088. {...RouteComponentPropsFixture()}
  1089. params={params}
  1090. api={api}
  1091. onToggleLegacy={jest.fn()}
  1092. checkoutTier={PlanTier.AM3}
  1093. />,
  1094. {organization}
  1095. );
  1096. expect(await screen.findByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument();
  1097. expect(await screen.findByText('Contract Term & Discounts')).toBeInTheDocument();
  1098. expect(screen.getByText('Review & Confirm')).toBeInTheDocument();
  1099. expect(screen.queryByText('Payment Method')).not.toBeInTheDocument();
  1100. expect(screen.queryByText('Billing Details')).not.toBeInTheDocument();
  1101. expect(
  1102. screen.queryByText(
  1103. 'Your promotional plan with BAR ends on ' + contractPeriodEnd.format('ll') + '.'
  1104. )
  1105. ).not.toBeInTheDocument();
  1106. expect(mockResponse).toHaveBeenCalledWith(
  1107. `/customers/${organization.slug}/billing-config/`,
  1108. expect.objectContaining({
  1109. method: 'GET',
  1110. data: {tier: PlanTier.AM3},
  1111. })
  1112. );
  1113. });
  1114. it('renders for VC partners', async function () {
  1115. organization.features.push('vc-marketplace-active-customer');
  1116. const contractPeriodEnd = moment();
  1117. const sub = SubscriptionFixture({
  1118. organization,
  1119. contractPeriodEnd: contractPeriodEnd.toString(),
  1120. plan: 'am3_f',
  1121. planTier: PlanTier.AM3,
  1122. isSelfServePartner: true,
  1123. partner: {
  1124. isActive: true,
  1125. externalId: 'foo',
  1126. partnership: {
  1127. id: 'XX',
  1128. displayName: 'XX',
  1129. supportNote: '',
  1130. },
  1131. name: '',
  1132. },
  1133. });
  1134. act(() => SubscriptionStore.set(organization.slug, sub));
  1135. const mockResponse = MockApiClient.addMockResponse({
  1136. url: `/customers/${organization.slug}/billing-config/`,
  1137. method: 'GET',
  1138. body: BillingConfigFixture(PlanTier.AM3),
  1139. });
  1140. render(
  1141. <AMCheckout
  1142. {...RouteComponentPropsFixture()}
  1143. params={params}
  1144. api={api}
  1145. onToggleLegacy={jest.fn()}
  1146. checkoutTier={PlanTier.AM3}
  1147. />,
  1148. {organization}
  1149. );
  1150. expect(await screen.findByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument();
  1151. expect(screen.getByText('Review & Confirm')).toBeInTheDocument();
  1152. expect(screen.queryByText('Payment Method')).not.toBeInTheDocument();
  1153. expect(screen.queryByText('Billing Details')).not.toBeInTheDocument();
  1154. expect(screen.queryByText('Contract Term & Discounts')).not.toBeInTheDocument();
  1155. expect(mockResponse).toHaveBeenCalledWith(
  1156. `/customers/${organization.slug}/billing-config/`,
  1157. expect.objectContaining({
  1158. method: 'GET',
  1159. data: {tier: PlanTier.AM3},
  1160. })
  1161. );
  1162. });
  1163. it('does not render for AM2 customers', async function () {
  1164. const sub = SubscriptionFixture({
  1165. organization,
  1166. plan: 'am2_f',
  1167. planTier: PlanTier.AM2,
  1168. });
  1169. act(() => SubscriptionStore.set(organization.slug, sub));
  1170. const mockResponse = MockApiClient.addMockResponse({
  1171. url: `/customers/${organization.slug}/billing-config/`,
  1172. method: 'GET',
  1173. body: BillingConfigFixture(PlanTier.AM2),
  1174. });
  1175. render(
  1176. <AMCheckout
  1177. {...RouteComponentPropsFixture()}
  1178. params={params}
  1179. api={api}
  1180. onToggleLegacy={jest.fn()}
  1181. checkoutTier={PlanTier.AM2}
  1182. />,
  1183. {organization}
  1184. );
  1185. expect(await screen.findByText('Choose Your Plan')).toBeInTheDocument();
  1186. expect(screen.queryByText('Set Your Pay-as-you-go Budget')).not.toBeInTheDocument();
  1187. expect(mockResponse).toHaveBeenCalledWith(
  1188. `/customers/${organization.slug}/billing-config/`,
  1189. expect.objectContaining({
  1190. method: 'GET',
  1191. data: {tier: PlanTier.AM2},
  1192. })
  1193. );
  1194. });
  1195. it('does not render for AM1 customers', async function () {
  1196. const sub = SubscriptionFixture({
  1197. organization,
  1198. plan: 'am1_f',
  1199. planTier: PlanTier.AM1,
  1200. });
  1201. act(() => SubscriptionStore.set(organization.slug, sub));
  1202. const mockResponse = MockApiClient.addMockResponse({
  1203. url: `/customers/${organization.slug}/billing-config/`,
  1204. method: 'GET',
  1205. body: BillingConfigFixture(PlanTier.AM1),
  1206. });
  1207. render(
  1208. <AMCheckout
  1209. {...RouteComponentPropsFixture()}
  1210. params={params}
  1211. api={api}
  1212. onToggleLegacy={jest.fn()}
  1213. checkoutTier={PlanTier.AM1}
  1214. />,
  1215. {organization}
  1216. );
  1217. expect(await screen.findByText('Choose Your Plan')).toBeInTheDocument();
  1218. expect(screen.queryByText('Set Your Pay-as-you-go Budget')).not.toBeInTheDocument();
  1219. expect(mockResponse).toHaveBeenCalledWith(
  1220. `/customers/${organization.slug}/billing-config/`,
  1221. expect.objectContaining({
  1222. method: 'GET',
  1223. data: {tier: PlanTier.AM1},
  1224. })
  1225. );
  1226. });
  1227. it('prefills with existing subscription data', async function () {
  1228. MockApiClient.addMockResponse({
  1229. url: `/customers/${organization.slug}/billing-config/`,
  1230. method: 'GET',
  1231. body: BillingConfigFixture(PlanTier.AM3),
  1232. });
  1233. const sub = SubscriptionFixture({
  1234. organization,
  1235. plan: 'am3_business',
  1236. planTier: PlanTier.AM3,
  1237. categories: {
  1238. errors: MetricHistoryFixture({reserved: 100_000}),
  1239. attachments: MetricHistoryFixture({reserved: 25}),
  1240. replays: MetricHistoryFixture({reserved: 50}),
  1241. monitorSeats: MetricHistoryFixture({reserved: 1}),
  1242. spans: MetricHistoryFixture({reserved: 20_000_000}),
  1243. profileDuration: MetricHistoryFixture({reserved: 1}),
  1244. },
  1245. onDemandBudgets: {
  1246. onDemandSpendUsed: 0,
  1247. sharedMaxBudget: 2000,
  1248. budgetMode: OnDemandBudgetMode.SHARED,
  1249. enabled: true,
  1250. },
  1251. onDemandMaxSpend: 2000,
  1252. supportsOnDemand: true,
  1253. });
  1254. SubscriptionStore.set(organization.slug, sub);
  1255. render(
  1256. <AMCheckout
  1257. {...RouteComponentPropsFixture()}
  1258. params={params}
  1259. api={api}
  1260. onToggleLegacy={jest.fn()}
  1261. checkoutTier={PlanTier.AM3}
  1262. />,
  1263. {organization}
  1264. );
  1265. expect(
  1266. await screen.findByRole('heading', {name: 'Change Subscription'})
  1267. ).toBeInTheDocument();
  1268. expect(screen.getByTestId('errors-volume-item')).toBeInTheDocument(); // skips over first step when subscription is already on Business plan
  1269. expect(screen.getByRole('textbox', {name: 'Pay-as-you-go budget'})).toHaveValue('20');
  1270. // TODO: Can better write this once we have
  1271. // https://github.com/testing-library/jest-dom/issues/478
  1272. expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
  1273. 'aria-valuetext',
  1274. '100000'
  1275. );
  1276. expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
  1277. 'aria-valuetext',
  1278. '50'
  1279. );
  1280. expect(screen.getByRole('slider', {name: 'Spans'})).toHaveAttribute(
  1281. 'aria-valuetext',
  1282. '20000000'
  1283. );
  1284. expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
  1285. 'aria-valuetext',
  1286. '25'
  1287. );
  1288. expect(
  1289. screen.queryByRole('slider', {name: 'Accepted Spans'})
  1290. ).not.toBeInTheDocument();
  1291. expect(screen.queryByRole('slider', {name: 'Stored Spans'})).not.toBeInTheDocument();
  1292. expect(screen.queryByRole('slider', {name: 'Cron Monitors'})).not.toBeInTheDocument();
  1293. });
  1294. it('allows setting PAYG for customers switching to AM3', async function () {
  1295. const sub = SubscriptionFixture({
  1296. organization,
  1297. // This plan does not have hasOnDemandModes
  1298. plan: 'mm2_b_100k',
  1299. planTier: PlanTier.AM2,
  1300. });
  1301. act(() => SubscriptionStore.set(organization.slug, sub));
  1302. const mockResponse = MockApiClient.addMockResponse({
  1303. url: `/customers/${organization.slug}/billing-config/`,
  1304. method: 'GET',
  1305. body: BillingConfigFixture(PlanTier.AM3),
  1306. });
  1307. render(
  1308. <AMCheckout
  1309. {...RouteComponentPropsFixture()}
  1310. params={params}
  1311. api={api}
  1312. onToggleLegacy={jest.fn()}
  1313. checkoutTier={PlanTier.AM3}
  1314. />,
  1315. {organization}
  1316. );
  1317. expect(hasOnDemandBudgetsFeature(organization, sub)).toBe(false);
  1318. expect(await screen.findByText('Choose Your Plan')).toBeInTheDocument();
  1319. expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
  1320. await userEvent.click(screen.getByRole('button', {name: 'Continue'}));
  1321. await userEvent.clear(screen.getByRole('textbox', {name: 'Pay-as-you-go budget'}));
  1322. await userEvent.type(
  1323. screen.getByRole('textbox', {name: 'Pay-as-you-go budget'}),
  1324. '20'
  1325. );
  1326. expect(await screen.findByTestId('additional-monthly-charge')).toHaveTextContent(
  1327. '+ up to $20/mo based on PAYG usage'
  1328. );
  1329. expect(mockResponse).toHaveBeenCalledWith(
  1330. `/customers/${organization.slug}/billing-config/`,
  1331. expect.objectContaining({
  1332. method: 'GET',
  1333. data: {tier: PlanTier.AM3},
  1334. })
  1335. );
  1336. });
  1337. it('handles missing categories in subscription.categories', async function () {
  1338. // Add billing config mock response
  1339. MockApiClient.addMockResponse({
  1340. url: `/customers/${organization.slug}/billing-config/`,
  1341. method: 'GET',
  1342. body: BillingConfigFixture(PlanTier.AM3),
  1343. });
  1344. /**
  1345. * In this test, we create a subscription where some categories are missing from
  1346. * `subscription.categories`. We then verify that the component renders correctly
  1347. * without throwing errors, and that the missing categories default to a reserved
  1348. * value of 0.
  1349. */
  1350. const sub = SubscriptionFixture({
  1351. organization,
  1352. plan: 'am3_business',
  1353. planTier: PlanTier.AM3,
  1354. categories: {
  1355. // Intentionally omitting 'errors' and 'attachments' categories
  1356. replays: MetricHistoryFixture({reserved: 50}),
  1357. monitorSeats: MetricHistoryFixture({reserved: 1}),
  1358. spans: MetricHistoryFixture({reserved: 1}),
  1359. profileDuration: MetricHistoryFixture({reserved: 1}),
  1360. },
  1361. onDemandBudgets: {
  1362. onDemandSpendUsed: 0,
  1363. sharedMaxBudget: 2000,
  1364. budgetMode: OnDemandBudgetMode.SHARED,
  1365. enabled: true,
  1366. },
  1367. supportsOnDemand: true,
  1368. });
  1369. SubscriptionStore.set(organization.slug, sub);
  1370. render(
  1371. <AMCheckout
  1372. {...RouteComponentPropsFixture()}
  1373. params={params}
  1374. api={api}
  1375. onToggleLegacy={jest.fn()}
  1376. checkoutTier={PlanTier.AM3}
  1377. />,
  1378. {organization}
  1379. );
  1380. expect(
  1381. await screen.findByRole('heading', {name: 'Change Subscription'})
  1382. ).toBeInTheDocument();
  1383. // Verify that the component renders without errors
  1384. expect(screen.getByTestId('replays-volume-item')).toBeInTheDocument();
  1385. // For AM3, we should see "Set Your Pay-as-you-go Budget" first
  1386. expect(screen.getByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument();
  1387. // Check that missing 'Errors' category defaults to 50,000 errors
  1388. expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute(
  1389. 'aria-valuetext',
  1390. '50000'
  1391. );
  1392. // For 'Replays', should be set to 50 as per the subscription
  1393. expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute(
  1394. 'aria-valuetext',
  1395. '50'
  1396. );
  1397. // Check that missing 'Attachments' category defaults to 1 GB
  1398. expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute(
  1399. 'aria-valuetext',
  1400. '1'
  1401. );
  1402. // Verify that the 'Pay-as-you-go budget' is correctly set
  1403. expect(screen.getByRole('textbox', {name: 'Pay-as-you-go budget'})).toHaveValue('20');
  1404. });
  1405. it('handles zero platform reserve', function () {
  1406. const formData = {
  1407. plan: 'am3_business',
  1408. reserved: {
  1409. errors: 10000,
  1410. transactions: 0,
  1411. attachments: 0,
  1412. replays: 0,
  1413. monitorSeats: 0,
  1414. profileDuration: 0,
  1415. spans: 0,
  1416. },
  1417. };
  1418. expect(getCheckoutAPIData({formData})).toEqual({
  1419. onDemandBudget: undefined,
  1420. onDemandMaxSpend: 0,
  1421. plan: 'am3_business',
  1422. referrer: 'billing',
  1423. reservedErrors: 10000,
  1424. reservedTransactions: 0,
  1425. reservedAttachments: 0,
  1426. reservedReplays: 0,
  1427. reservedMonitorSeats: 0,
  1428. reservedProfileDuration: 0,
  1429. reservedSpans: 0,
  1430. });
  1431. });
  1432. });