utils.spec.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. import {OrganizationFixture} from 'sentry-fixture/organization';
  2. import {
  3. InvoicedSubscriptionFixture,
  4. SubscriptionFixture,
  5. } from 'getsentry-test/fixtures/subscription';
  6. import {OnDemandBudgetMode, type OnDemandBudgets} from 'getsentry/types';
  7. import {
  8. exceedsInvoicedBudgetLimit,
  9. getTotalBudget,
  10. parseOnDemandBudgetsFromSubscription,
  11. } from 'getsentry/views/onDemandBudgets/utils';
  12. describe('parseOnDemandBudgetsFromSubscription', function () {
  13. it('returns per-category budget for non-AM plans - with on-demand budget', function () {
  14. const organization = OrganizationFixture();
  15. const subscription = SubscriptionFixture({
  16. organization,
  17. plan: 'mm2_f',
  18. onDemandMaxSpend: 123,
  19. });
  20. const ondemandBudgets = parseOnDemandBudgetsFromSubscription(subscription);
  21. expect(ondemandBudgets).toEqual({
  22. budgetMode: OnDemandBudgetMode.SHARED,
  23. sharedMaxBudget: 123,
  24. });
  25. });
  26. it('returns shared on-demand budget for non-AM plans - without on-demand budget', function () {
  27. const organization = OrganizationFixture();
  28. let subscription = SubscriptionFixture({
  29. organization,
  30. plan: 'mm2_f',
  31. onDemandMaxSpend: 0,
  32. });
  33. let ondemandBudgets = parseOnDemandBudgetsFromSubscription(subscription);
  34. expect(ondemandBudgets).toEqual({
  35. budgetMode: OnDemandBudgetMode.SHARED,
  36. sharedMaxBudget: 0,
  37. });
  38. // omitted onDemandMaxSpend
  39. subscription = SubscriptionFixture({organization, plan: 'mm2_f'});
  40. ondemandBudgets = parseOnDemandBudgetsFromSubscription(subscription);
  41. expect(ondemandBudgets).toEqual({
  42. budgetMode: OnDemandBudgetMode.SHARED,
  43. sharedMaxBudget: 0,
  44. });
  45. });
  46. it('returns shared on-demand budget for AM plans', function () {
  47. const organization = OrganizationFixture();
  48. const subscription = SubscriptionFixture({
  49. organization,
  50. plan: 'am1_business',
  51. planTier: 'am1',
  52. reservedEvents: 200000,
  53. reservedTransactions: 250000,
  54. reservedAttachments: 25,
  55. onDemandMaxSpend: 123,
  56. onDemandBudgets: {
  57. enabled: true,
  58. budgetMode: OnDemandBudgetMode.SHARED,
  59. sharedMaxBudget: 123,
  60. onDemandSpendUsed: 0,
  61. },
  62. });
  63. const ondemandBudgets = parseOnDemandBudgetsFromSubscription(subscription);
  64. expect(ondemandBudgets).toEqual({
  65. budgetMode: OnDemandBudgetMode.SHARED,
  66. sharedMaxBudget: 123,
  67. });
  68. });
  69. it('returns shared on-demand budget for AM plans - without on-demand budget', function () {
  70. const organization = OrganizationFixture();
  71. let subscription = SubscriptionFixture({
  72. organization,
  73. plan: 'am1_business',
  74. planTier: 'am1',
  75. reservedEvents: 200000,
  76. reservedTransactions: 250000,
  77. reservedAttachments: 25,
  78. onDemandMaxSpend: 0,
  79. onDemandBudgets: {
  80. enabled: false,
  81. budgetMode: OnDemandBudgetMode.SHARED,
  82. sharedMaxBudget: 0,
  83. onDemandSpendUsed: 0,
  84. },
  85. });
  86. let ondemandBudgets = parseOnDemandBudgetsFromSubscription(subscription);
  87. expect(ondemandBudgets).toEqual({
  88. budgetMode: OnDemandBudgetMode.SHARED,
  89. sharedMaxBudget: 0,
  90. });
  91. // missing onDemandBudgets
  92. subscription = SubscriptionFixture({
  93. organization,
  94. plan: 'am1_business',
  95. planTier: 'am1',
  96. reservedEvents: 200000,
  97. reservedTransactions: 250000,
  98. reservedAttachments: 25,
  99. onDemandMaxSpend: 0,
  100. });
  101. ondemandBudgets = parseOnDemandBudgetsFromSubscription(subscription);
  102. expect(ondemandBudgets).toEqual({
  103. budgetMode: OnDemandBudgetMode.SHARED,
  104. sharedMaxBudget: 0,
  105. });
  106. // missing onDemandBudgets and onDemandMaxSpend
  107. subscription = SubscriptionFixture({
  108. organization,
  109. plan: 'am1_business',
  110. planTier: 'am1',
  111. reservedEvents: 200000,
  112. reservedTransactions: 250000,
  113. reservedAttachments: 25,
  114. });
  115. ondemandBudgets = parseOnDemandBudgetsFromSubscription(subscription);
  116. expect(ondemandBudgets).toEqual({
  117. budgetMode: OnDemandBudgetMode.SHARED,
  118. sharedMaxBudget: 0,
  119. });
  120. });
  121. it('returns per-category on-demand budget for AM plans', function () {
  122. const organization = OrganizationFixture();
  123. const subscription = SubscriptionFixture({
  124. organization,
  125. plan: 'am1_business',
  126. planTier: 'am1',
  127. reservedEvents: 200000,
  128. reservedTransactions: 250000,
  129. reservedAttachments: 25,
  130. onDemandMaxSpend: 100 + 200 + 300,
  131. onDemandBudgets: {
  132. enabled: true,
  133. budgetMode: OnDemandBudgetMode.PER_CATEGORY,
  134. errorsBudget: 100,
  135. transactionsBudget: 200,
  136. attachmentsBudget: 300,
  137. monitorSeatsBudget: 400,
  138. replaysBudget: 0,
  139. budgets: {
  140. errors: 100,
  141. transactions: 200,
  142. attachments: 300,
  143. replays: 0,
  144. monitorSeats: 400,
  145. uptime: 500,
  146. },
  147. attachmentSpendUsed: 0,
  148. errorSpendUsed: 0,
  149. transactionSpendUsed: 0,
  150. usedSpends: {
  151. errors: 0,
  152. transactions: 0,
  153. attachments: 0,
  154. replays: 0,
  155. monitorSeats: 0,
  156. uptime: 0,
  157. },
  158. },
  159. });
  160. subscription.categories.errors!.reserved = 200000;
  161. subscription.categories.transactions!.reserved = 250000;
  162. subscription.categories.attachments!.reserved = 25;
  163. subscription.categories.monitorSeats!.reserved = 1;
  164. const ondemandBudgets = parseOnDemandBudgetsFromSubscription(subscription);
  165. expect(ondemandBudgets).toEqual({
  166. budgetMode: OnDemandBudgetMode.PER_CATEGORY,
  167. errorsBudget: 100,
  168. transactionsBudget: 200,
  169. attachmentsBudget: 300,
  170. monitorSeatsBudget: 400,
  171. uptimeBudget: 500,
  172. replaysBudget: 0,
  173. budgets: {
  174. errors: 100,
  175. transactions: 200,
  176. attachments: 300,
  177. replays: 0,
  178. monitorSeats: 400,
  179. uptime: 500,
  180. },
  181. });
  182. });
  183. it('reconstructs shared on-demand budget if onDemandBudgets is missing', function () {
  184. const organization = OrganizationFixture();
  185. const subscription = SubscriptionFixture({
  186. organization,
  187. plan: 'am1_business',
  188. planTier: 'am1',
  189. reservedEvents: 200000,
  190. reservedTransactions: 250000,
  191. reservedAttachments: 25,
  192. onDemandMaxSpend: 123,
  193. });
  194. subscription.categories.errors!.reserved = 200000;
  195. subscription.categories.transactions!.reserved = 250000;
  196. subscription.categories.attachments!.reserved = 25;
  197. const ondemandBudgets = parseOnDemandBudgetsFromSubscription(subscription);
  198. expect(ondemandBudgets).toEqual({
  199. budgetMode: OnDemandBudgetMode.SHARED,
  200. sharedMaxBudget: 123,
  201. });
  202. });
  203. });
  204. describe('getTotalBudget', function () {
  205. it('returns total on-demand budget for non-AM plans - with on-demand budget', function () {
  206. const organization = OrganizationFixture();
  207. const subscription = SubscriptionFixture({
  208. organization,
  209. plan: 'mm2_f',
  210. onDemandMaxSpend: 123,
  211. });
  212. const actualTotalBudget = getTotalBudget(
  213. parseOnDemandBudgetsFromSubscription(subscription)
  214. );
  215. expect(actualTotalBudget).toBe(123);
  216. });
  217. it('returns total on-demand budget for non-AM plans - without on-demand budget', function () {
  218. const organization = OrganizationFixture();
  219. let subscription = SubscriptionFixture({
  220. organization,
  221. plan: 'mm2_f',
  222. onDemandMaxSpend: 0,
  223. });
  224. let actualTotalBudget = getTotalBudget(
  225. parseOnDemandBudgetsFromSubscription(subscription)
  226. );
  227. expect(actualTotalBudget).toBe(0);
  228. // omitted onDemandMaxSpend
  229. subscription = SubscriptionFixture({organization, plan: 'mm2_f'});
  230. actualTotalBudget = getTotalBudget(
  231. parseOnDemandBudgetsFromSubscription(subscription)
  232. );
  233. expect(actualTotalBudget).toBe(0);
  234. });
  235. it('returns total budget of shared on-demand budget for AM plans', function () {
  236. const organization = OrganizationFixture();
  237. const subscription = SubscriptionFixture({
  238. organization,
  239. plan: 'am1_business',
  240. planTier: 'am1',
  241. reservedEvents: 200000,
  242. reservedTransactions: 250000,
  243. reservedAttachments: 25,
  244. onDemandMaxSpend: 100 + 200 + 300,
  245. onDemandBudgets: {
  246. enabled: true,
  247. budgetMode: OnDemandBudgetMode.SHARED,
  248. sharedMaxBudget: 123,
  249. onDemandSpendUsed: 0,
  250. },
  251. });
  252. const actualTotalBudget = getTotalBudget(
  253. parseOnDemandBudgetsFromSubscription(subscription)
  254. );
  255. expect(actualTotalBudget).toBe(123);
  256. });
  257. it('returns total budget of shared on-demand budget for AM plans - without on-demand budget', function () {
  258. const organization = OrganizationFixture();
  259. let subscription = SubscriptionFixture({
  260. organization,
  261. plan: 'am1_business',
  262. planTier: 'am1',
  263. reservedEvents: 200000,
  264. reservedTransactions: 250000,
  265. reservedAttachments: 25,
  266. onDemandMaxSpend: 0,
  267. onDemandBudgets: {
  268. enabled: false,
  269. budgetMode: OnDemandBudgetMode.SHARED,
  270. sharedMaxBudget: 0,
  271. onDemandSpendUsed: 0,
  272. },
  273. });
  274. let actualTotalBudget = getTotalBudget(
  275. parseOnDemandBudgetsFromSubscription(subscription)
  276. );
  277. expect(actualTotalBudget).toBe(0);
  278. // missing onDemandBudgets
  279. subscription = SubscriptionFixture({
  280. organization,
  281. plan: 'am1_business',
  282. planTier: 'am1',
  283. reservedEvents: 200000,
  284. reservedTransactions: 250000,
  285. reservedAttachments: 25,
  286. onDemandMaxSpend: 0,
  287. });
  288. actualTotalBudget = getTotalBudget(
  289. parseOnDemandBudgetsFromSubscription(subscription)
  290. );
  291. expect(actualTotalBudget).toBe(0);
  292. // missing onDemandBudgets and onDemandMaxSpend
  293. subscription = SubscriptionFixture({
  294. organization,
  295. plan: 'am1_business',
  296. planTier: 'am1',
  297. reservedEvents: 200000,
  298. reservedTransactions: 250000,
  299. reservedAttachments: 25,
  300. });
  301. actualTotalBudget = getTotalBudget(
  302. parseOnDemandBudgetsFromSubscription(subscription)
  303. );
  304. expect(actualTotalBudget).toBe(0);
  305. });
  306. it('returns total budget of per-category on-demand budget for AM plans', function () {
  307. const organization = OrganizationFixture();
  308. const subscription = SubscriptionFixture({
  309. organization,
  310. plan: 'am1_business',
  311. planTier: 'am1',
  312. reservedEvents: 200000,
  313. reservedTransactions: 250000,
  314. reservedAttachments: 25,
  315. onDemandMaxSpend: 100 + 200 + 300,
  316. onDemandBudgets: {
  317. enabled: true,
  318. budgetMode: OnDemandBudgetMode.PER_CATEGORY,
  319. errorsBudget: 100,
  320. transactionsBudget: 200,
  321. attachmentsBudget: 300,
  322. replaysBudget: 0,
  323. budgets: {errors: 100, transactions: 200, attachments: 300, uptime: 400},
  324. attachmentSpendUsed: 0,
  325. errorSpendUsed: 0,
  326. transactionSpendUsed: 0,
  327. usedSpends: {errors: 0, transactions: 0, attachments: 0, replays: 0},
  328. },
  329. });
  330. subscription.categories.errors!.reserved = 200000;
  331. subscription.categories.transactions!.reserved = 250000;
  332. subscription.categories.attachments!.reserved = 25;
  333. const actualTotalBudget = getTotalBudget(
  334. parseOnDemandBudgetsFromSubscription(subscription)
  335. );
  336. expect(actualTotalBudget).toEqual(100 + 200 + 300 + 400);
  337. });
  338. it('returns total on-demand budget if onDemandBudgets is missing', function () {
  339. const organization = OrganizationFixture();
  340. const subscription = SubscriptionFixture({
  341. organization,
  342. plan: 'am1_business',
  343. planTier: 'am1',
  344. reservedEvents: 200000,
  345. reservedTransactions: 250000,
  346. reservedAttachments: 25,
  347. onDemandMaxSpend: 123,
  348. });
  349. const actualTotalBudget = getTotalBudget(
  350. parseOnDemandBudgetsFromSubscription(subscription)
  351. );
  352. expect(actualTotalBudget).toBe(123);
  353. });
  354. });
  355. describe('exceedsInvoicedBudgetLimit', function () {
  356. it('returns false for non-invoiced subscription', function () {
  357. const organization = OrganizationFixture();
  358. const subscription = SubscriptionFixture({organization});
  359. const ondemandBudget: OnDemandBudgets = {
  360. budgetMode: OnDemandBudgetMode.SHARED,
  361. sharedMaxBudget: 3_000_000,
  362. };
  363. expect(exceedsInvoicedBudgetLimit(subscription, ondemandBudget)).toBe(false);
  364. });
  365. it('returns false for invoiced subscriptions without flag', function () {
  366. const organization = OrganizationFixture();
  367. const subscription = InvoicedSubscriptionFixture({organization});
  368. const ondemandBudget: OnDemandBudgets = {
  369. budgetMode: OnDemandBudgetMode.SHARED,
  370. sharedMaxBudget: 0,
  371. };
  372. expect(exceedsInvoicedBudgetLimit(subscription, ondemandBudget)).toBe(false);
  373. });
  374. it('returns false for invoiced subscriptions with budget and with onDemandInvoiced flag', function () {
  375. // no limit for CC-invoiced on-demand
  376. const organization = OrganizationFixture();
  377. const subscription = InvoicedSubscriptionFixture({
  378. organization,
  379. onDemandInvoiced: true,
  380. supportsOnDemand: true,
  381. });
  382. const ondemandBudget: OnDemandBudgets = {
  383. budgetMode: OnDemandBudgetMode.SHARED,
  384. sharedMaxBudget: 1000,
  385. };
  386. expect(exceedsInvoicedBudgetLimit(subscription, ondemandBudget)).toBe(false);
  387. });
  388. it('returns true for invoiced subscriptions with budget and without any flags', function () {
  389. // if an invoiced customer is somehow setting OD budget without either onDemandInvoicedManual or onDemandInvoiced, always stop them
  390. const organization = OrganizationFixture();
  391. const subscription = InvoicedSubscriptionFixture({organization});
  392. const ondemandBudget: OnDemandBudgets = {
  393. budgetMode: OnDemandBudgetMode.SHARED,
  394. sharedMaxBudget: 1000,
  395. };
  396. expect(exceedsInvoicedBudgetLimit(subscription, ondemandBudget)).toBe(true);
  397. });
  398. it('returns false for invoiced subscriptions with flag and budget lower than or equal to 5x custom price', function () {
  399. const organization = OrganizationFixture();
  400. const subscription = InvoicedSubscriptionFixture({
  401. organization,
  402. onDemandInvoicedManual: true,
  403. supportsOnDemand: true,
  404. customPrice: 12000,
  405. });
  406. const ondemandBudget: OnDemandBudgets = {
  407. budgetMode: OnDemandBudgetMode.SHARED,
  408. sharedMaxBudget: 5000,
  409. };
  410. expect(exceedsInvoicedBudgetLimit(subscription, ondemandBudget)).toBe(false);
  411. ondemandBudget.sharedMaxBudget = 800;
  412. expect(exceedsInvoicedBudgetLimit(subscription, ondemandBudget)).toBe(false);
  413. });
  414. it('returns false for invoiced subscriptions with flag and budget lower than or equal to 5x acv', function () {
  415. const organization = OrganizationFixture();
  416. const subscription = InvoicedSubscriptionFixture({
  417. organization,
  418. onDemandInvoicedManual: true,
  419. supportsOnDemand: true,
  420. acv: 12000,
  421. });
  422. const ondemandBudget: OnDemandBudgets = {
  423. budgetMode: OnDemandBudgetMode.SHARED,
  424. sharedMaxBudget: 5000,
  425. };
  426. expect(exceedsInvoicedBudgetLimit(subscription, ondemandBudget)).toBe(false);
  427. ondemandBudget.sharedMaxBudget = 800;
  428. expect(exceedsInvoicedBudgetLimit(subscription, ondemandBudget)).toBe(false);
  429. });
  430. it('returns true for invoiced subscriptions with flag and budget greater than 5x custom price', function () {
  431. const organization = OrganizationFixture();
  432. const subscription = InvoicedSubscriptionFixture({
  433. organization,
  434. onDemandInvoicedManual: true,
  435. supportsOnDemand: true,
  436. customPrice: 12000,
  437. });
  438. const ondemandBudget: OnDemandBudgets = {
  439. budgetMode: OnDemandBudgetMode.SHARED,
  440. sharedMaxBudget: 5001,
  441. };
  442. expect(exceedsInvoicedBudgetLimit(subscription, ondemandBudget)).toBe(true);
  443. });
  444. it('returns false for invoiced subscriptions with flag and budget greater than 5x acv', function () {
  445. const organization = OrganizationFixture();
  446. const subscription = InvoicedSubscriptionFixture({
  447. organization,
  448. onDemandInvoicedManual: true,
  449. supportsOnDemand: true,
  450. acv: 12000,
  451. });
  452. const ondemandBudget: OnDemandBudgets = {
  453. budgetMode: OnDemandBudgetMode.SHARED,
  454. sharedMaxBudget: 5001,
  455. };
  456. expect(exceedsInvoicedBudgetLimit(subscription, ondemandBudget)).toBe(true);
  457. });
  458. });