provisionSubscriptionAction.tsx 65 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793
  1. import {Component, Fragment} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  5. import {openModal} from 'sentry/actionCreators/modal';
  6. import type {Client} from 'sentry/api';
  7. import {Alert} from 'sentry/components/core/alert';
  8. import BooleanField from 'sentry/components/deprecatedforms/booleanField';
  9. import DateTimeField from 'sentry/components/deprecatedforms/dateTimeField';
  10. import Form from 'sentry/components/deprecatedforms/form';
  11. import InputField from 'sentry/components/deprecatedforms/inputField';
  12. import NumberField from 'sentry/components/deprecatedforms/numberField';
  13. import SelectField from 'sentry/components/deprecatedforms/selectField';
  14. import {space} from 'sentry/styles/space';
  15. import withApi from 'sentry/utils/withApi';
  16. import {prettyDate} from 'admin/utils';
  17. import {CPE_MULTIPLIER_TO_CENTS, RESERVED_BUDGET_QUOTA} from 'getsentry/constants';
  18. import type {ReservedBudgetMetricHistory, Subscription} from 'getsentry/types';
  19. import {
  20. isAm3DsPlan,
  21. isAm3Plan,
  22. isAmEnterprisePlan,
  23. isAmPlan,
  24. } from 'getsentry/utils/billing';
  25. const CPE_DECIMAL_PRECISION = 8;
  26. // TODO: replace with modern fields so we don't need these workarounds
  27. class DateField extends DateTimeField {
  28. getType() {
  29. return 'date';
  30. }
  31. }
  32. type DollarsAndCentsFieldProps = {
  33. max?: number;
  34. min?: number;
  35. step?: any;
  36. } & NumberField['props'];
  37. class DollarsField extends NumberField {
  38. getField() {
  39. return (
  40. <div className="dollars-field-container">
  41. <span className="dollar-sign">$</span>
  42. {super.getField()}
  43. </div>
  44. );
  45. }
  46. }
  47. class DollarsAndCentsField extends InputField<DollarsAndCentsFieldProps> {
  48. getField() {
  49. return (
  50. <div className="dollars-cents-field-container">
  51. <span className="dollar-sign">$</span>
  52. {super.getField()}
  53. </div>
  54. );
  55. }
  56. coerceValue(value: any): number | '' {
  57. const floatValue = parseFloat(value);
  58. if (isNaN(floatValue)) {
  59. return '';
  60. }
  61. return floatValue;
  62. }
  63. getType() {
  64. return 'number';
  65. }
  66. getAttributes() {
  67. return {
  68. min: this.props.min || undefined,
  69. max: this.props.max || undefined,
  70. step: this.props.step || undefined,
  71. };
  72. }
  73. }
  74. type Props = {
  75. api: Client;
  76. onSuccess: () => void;
  77. orgId: string;
  78. subscription: Subscription;
  79. canProvisionDsPlan?: boolean; // TODO(DS Spans): remove once we need to provision DS plans
  80. };
  81. type ModalProps = ModalRenderProps & Props;
  82. type ModalState = {
  83. data: any; // TODO(ts), TODO:categories get data.plan categories to dynamically create fields
  84. effectiveAtDisabled: boolean;
  85. };
  86. function toAnnualDollars(
  87. cents: number | null | undefined,
  88. billingInterval: string | null | undefined,
  89. decimals = 0
  90. ) {
  91. if (typeof cents !== 'number') {
  92. return cents;
  93. }
  94. if (billingInterval === 'monthly') {
  95. return parseFloat(((cents * 12) / 100).toFixed(decimals));
  96. }
  97. return parseFloat((cents / 100).toFixed(decimals));
  98. }
  99. /**
  100. * Convert dollars to 0.000001 cents
  101. * @param dollars - dollars to convert
  102. * @returns dollars in units of 0.000001 cents
  103. */
  104. function toCpeCents(dollars: number | null | undefined) {
  105. if (typeof dollars !== 'number') {
  106. return dollars;
  107. }
  108. return parseInt(((dollars * 100) / CPE_MULTIPLIER_TO_CENTS).toFixed(0), 10);
  109. }
  110. function toCents(dollars: number | null | undefined, decimals = 0) {
  111. if (typeof dollars !== 'number') {
  112. return dollars;
  113. }
  114. return parseFloat((dollars * 100).toFixed(decimals));
  115. }
  116. class ProvisionSubscriptionModal extends Component<ModalProps, ModalState> {
  117. state: ModalState = {
  118. data: {},
  119. effectiveAtDisabled: false,
  120. };
  121. componentDidMount() {
  122. const {subscription} = this.props;
  123. const existingPlanWithoutSuffix = subscription.plan.endsWith('_auf')
  124. ? subscription.plan.slice(0, subscription.plan.length - 4)
  125. : subscription.plan.endsWith('_ac')
  126. ? subscription.plan.slice(0, subscription.plan.length - 3)
  127. : subscription.plan;
  128. const existingPlanIsEnterprise = this.provisionablePlans.some(
  129. plan => plan[0] === existingPlanWithoutSuffix
  130. );
  131. const reservedBudgets = subscription.reservedBudgets;
  132. const reservedBudgetMetricHistories: Record<string, ReservedBudgetMetricHistory> = {};
  133. reservedBudgets?.forEach(budget => {
  134. Object.entries(budget.categories).forEach(([category, info]) => {
  135. reservedBudgetMetricHistories[category] = info;
  136. });
  137. });
  138. if (existingPlanIsEnterprise) {
  139. this.setState(state => ({
  140. ...state,
  141. data: {
  142. ...state.data,
  143. plan: existingPlanWithoutSuffix,
  144. billingInterval: subscription.billingInterval,
  145. retainOnDemandBudget: false,
  146. type: subscription.type,
  147. onDemandInvoicedManual: subscription.onDemandInvoicedManual
  148. ? subscription.onDemandBudgets?.budgetMode.toString().toUpperCase()
  149. : subscription.onDemandInvoicedManual === null
  150. ? null
  151. : 'DISABLE',
  152. managed: subscription.isManaged,
  153. reservedErrors: subscription.categories.errors?.reserved,
  154. reservedTransactions: subscription.categories.transactions?.reserved,
  155. reservedReplays: subscription.categories.replays?.reserved,
  156. reservedMonitorSeats: subscription.categories.monitorSeats?.reserved,
  157. reservedUptime: subscription.categories.uptime?.reserved,
  158. reservedSpans: subscription.categories.spans?.reserved,
  159. reservedSpansIndexed: subscription.categories.spansIndexed?.reserved,
  160. reservedAttachments: subscription.categories.attachments?.reserved,
  161. reservedProfileDuration: subscription.categories.profileDuration?.reserved,
  162. softCapTypeErrors: subscription.categories.errors?.softCapType,
  163. softCapTypeTransactions: subscription.categories.transactions?.softCapType,
  164. softCapTypeReplays: subscription.categories.replays?.softCapType,
  165. softCapTypeMonitorSeats: subscription.categories.monitorSeats?.softCapType,
  166. softCapTypeUptime: subscription.categories.uptime?.softCapType,
  167. softCapTypeSpans: subscription.categories.spans?.softCapType,
  168. softCapTypeSpansIndexed: subscription.categories.spansIndexed?.softCapType,
  169. softCapTypeAttachments: subscription.categories.attachments?.softCapType,
  170. softCapTypeProfileDuration:
  171. subscription.categories.profileDuration?.softCapType,
  172. customPriceErrors: toAnnualDollars(
  173. subscription.categories.errors?.customPrice,
  174. subscription.billingInterval
  175. ),
  176. customPriceTransactions: toAnnualDollars(
  177. subscription.categories.transactions?.customPrice,
  178. subscription.billingInterval
  179. ),
  180. customPriceReplays: toAnnualDollars(
  181. subscription.categories.replays?.customPrice,
  182. subscription.billingInterval
  183. ),
  184. customPriceMonitorSeats: toAnnualDollars(
  185. subscription.categories.monitorSeats?.customPrice,
  186. subscription.billingInterval
  187. ),
  188. customPriceUptime: toAnnualDollars(
  189. subscription.categories.uptime?.customPrice,
  190. subscription.billingInterval
  191. ),
  192. customPriceSpans: toAnnualDollars(
  193. subscription.categories.spans?.customPrice,
  194. subscription.billingInterval
  195. ),
  196. customPriceSpansIndexed: toAnnualDollars(
  197. subscription.categories.spansIndexed?.customPrice,
  198. subscription.billingInterval
  199. ),
  200. customPriceAttachments: toAnnualDollars(
  201. subscription.categories.attachments?.customPrice,
  202. subscription.billingInterval
  203. ),
  204. customPriceProfileDuration: toAnnualDollars(
  205. subscription.categories.profileDuration?.customPrice,
  206. subscription.billingInterval
  207. ),
  208. customPricePcss: toAnnualDollars(
  209. subscription.customPricePcss,
  210. subscription.billingInterval
  211. ),
  212. customPrice: toAnnualDollars(
  213. subscription.customPrice,
  214. subscription.billingInterval
  215. ),
  216. onDemandCpeErrors: toAnnualDollars(
  217. subscription.categories.errors?.onDemandCpe,
  218. null,
  219. CPE_DECIMAL_PRECISION
  220. ),
  221. onDemandCpeTransactions: toAnnualDollars(
  222. subscription.categories.transactions?.onDemandCpe,
  223. null,
  224. CPE_DECIMAL_PRECISION
  225. ),
  226. onDemandCpeReplays: toAnnualDollars(
  227. subscription.categories.replays?.onDemandCpe,
  228. null,
  229. CPE_DECIMAL_PRECISION
  230. ),
  231. onDemandCpeMonitorSeats: toAnnualDollars(
  232. subscription.categories.monitorSeats?.onDemandCpe,
  233. null,
  234. CPE_DECIMAL_PRECISION
  235. ),
  236. onDemandCpeUptime: toAnnualDollars(
  237. subscription.categories.uptime?.onDemandCpe,
  238. null,
  239. CPE_DECIMAL_PRECISION
  240. ),
  241. onDemandCpeSpans: toAnnualDollars(
  242. subscription.categories.spans?.onDemandCpe,
  243. null,
  244. CPE_DECIMAL_PRECISION
  245. ),
  246. onDemandCpeSpansIndexed: toAnnualDollars(
  247. subscription.categories.spansIndexed?.onDemandCpe,
  248. null,
  249. CPE_DECIMAL_PRECISION
  250. ),
  251. onDemandCpeAttachments: toAnnualDollars(
  252. subscription.categories.attachments?.onDemandCpe,
  253. null,
  254. CPE_DECIMAL_PRECISION
  255. ),
  256. onDemandCpeProfileDuration: toAnnualDollars(
  257. subscription.categories.profileDuration?.onDemandCpe,
  258. null,
  259. CPE_DECIMAL_PRECISION
  260. ),
  261. // coming from the API, reservedCpe is in cents
  262. reservedCpeSpans: toAnnualDollars(
  263. reservedBudgetMetricHistories.spans?.reservedCpe,
  264. null,
  265. CPE_DECIMAL_PRECISION
  266. ),
  267. reservedCpeSpansIndexed: toAnnualDollars(
  268. reservedBudgetMetricHistories.spansIndexed?.reservedCpe,
  269. null,
  270. CPE_DECIMAL_PRECISION
  271. ),
  272. },
  273. }));
  274. }
  275. }
  276. get endpoint() {
  277. return `/customers/${this.props.orgId}/provision-subscription/`;
  278. }
  279. isEnablingOnDemandMaxSpend = () =>
  280. this.state.data.onDemandInvoicedManual === 'SHARED' ||
  281. this.state.data.onDemandInvoicedManual === 'PER_CATEGORY';
  282. isEnablingSoftCap = () =>
  283. this.state.data.softCapTypeErrors ||
  284. this.state.data.softCapTypeTransactions ||
  285. this.state.data.softCapTypeReplays ||
  286. this.state.data.softCapTypeMonitorSeats ||
  287. this.state.data.softCapTypeUptime ||
  288. this.state.data.softCapTypeSpans ||
  289. this.state.data.softCapTypeSpansIndexed ||
  290. this.state.data.softCapTypeAttachments;
  291. isSettingSpansBudget = () =>
  292. isAm3DsPlan(this.state.data.plan) &&
  293. this.state.data.reservedCpeSpans &&
  294. this.state.data.reservedCpeSpansIndexed;
  295. hasCompleteSpansBudget = () =>
  296. this.isSettingSpansBudget() &&
  297. this.state.data.reservedSpans === RESERVED_BUDGET_QUOTA &&
  298. this.state.data.reservedSpansIndexed === RESERVED_BUDGET_QUOTA &&
  299. this.state.data.customPriceSpans;
  300. disableRetainOnDemand = () => {
  301. if (this.state.data.onDemandInvoicedManual === null) {
  302. // don't show the toggle if there is no ondemand type
  303. return true;
  304. }
  305. const original = this.props.subscription.onDemandInvoicedManual
  306. ? this.props.subscription.onDemandBudgets?.budgetMode.toString().toUpperCase()
  307. : this.props.subscription.onDemandInvoicedManual === null
  308. ? null
  309. : 'DISABLE';
  310. return (
  311. this.state.data.onDemandInvoicedManual !== original ||
  312. this.state.data.onDemandInvoicedManual === 'DISABLE'
  313. );
  314. };
  315. onSubmit: Form['props']['onSubmit'] = (formData, _onSubmitSuccess, onSubmitError) => {
  316. const postData = {...this.state.data};
  317. for (const k in formData) {
  318. if (formData[k] !== '' && formData[k] !== null) {
  319. postData[k] = formData[k];
  320. }
  321. }
  322. // clear disabled fields
  323. if (postData.atPeriodEnd || postData.coterm) {
  324. delete postData.effectiveAt;
  325. }
  326. if (!postData.coterm) {
  327. delete postData.coterm;
  328. }
  329. const hasCustomSkuPrices = isAmEnterprisePlan(postData.plan);
  330. if (!hasCustomSkuPrices) {
  331. delete postData.customPriceErrors;
  332. delete postData.customPriceTransactions;
  333. delete postData.customPriceAttachments;
  334. delete postData.customPricePcss;
  335. delete postData.customPriceReplays;
  336. delete postData.customPriceMonitorSeats;
  337. delete postData.customPriceUptime;
  338. delete postData.customPriceSpans;
  339. delete postData.customPriceSpansIndexed;
  340. delete postData.customPriceProfileDuration;
  341. }
  342. // only set reserved & custom price for spans OR transactions
  343. if (isAm3Plan(postData.plan)) {
  344. delete postData.reservedTransactions;
  345. delete postData.customPriceTransactions;
  346. } else {
  347. delete postData.reservedSpans;
  348. delete postData.customPriceSpans;
  349. }
  350. if (postData.type !== 'invoiced') {
  351. delete postData.onDemandInvoicedManual;
  352. delete postData.onDemandCpeErrors;
  353. delete postData.onDemandCpeTransactions;
  354. delete postData.onDemandCpeReplays;
  355. delete postData.onDemandCpeAttachments;
  356. delete postData.onDemandCpeMonitorSeats;
  357. delete postData.onDemandCpeUptime;
  358. delete postData.onDemandCpeSpans;
  359. delete postData.onDemandCpeProfileDuration;
  360. // clear corresponding state
  361. this.setState(state => ({
  362. ...state,
  363. data: {
  364. ...state.data,
  365. onDemandInvoicedManual: null,
  366. },
  367. }));
  368. }
  369. if (this.isEnablingOnDemandMaxSpend()) {
  370. postData.softCapTypeErrors = null;
  371. postData.softCapTypeTransactions = null;
  372. postData.softCapTypeReplays = null;
  373. postData.softCapTypeAttachments = null;
  374. postData.softCapTypeMonitorSeats = null;
  375. postData.softCapTypeUptime = null;
  376. postData.softCapTypeSpans = null;
  377. postData.softCapTypeProfileDuration = null;
  378. this.setState(state => ({
  379. ...state,
  380. data: {
  381. ...state.data,
  382. softCapTypeErrors: null,
  383. softCapTypeTransactions: null,
  384. softCapTypeReplays: null,
  385. softCapTypeAttachments: null,
  386. softCapTypeMonitorSeats: null,
  387. softCapTypeUptime: null,
  388. softCapTypeSpans: null,
  389. softCapTypeProfileDuration: null,
  390. },
  391. }));
  392. } else {
  393. delete postData.onDemandCpeErrors;
  394. delete postData.onDemandCpeTransactions;
  395. delete postData.onDemandCpeReplays;
  396. delete postData.onDemandCpeAttachments;
  397. delete postData.onDemandCpeMonitorSeats;
  398. delete postData.onDemandCpeUptime;
  399. delete postData.onDemandCpeSpans;
  400. delete postData.onDemandCpeProfileDuration;
  401. }
  402. if (this.isEnablingSoftCap()) {
  403. postData.onDemandInvoicedManual = 'DISABLE';
  404. delete postData.onDemandCpeErrors;
  405. delete postData.onDemandCpeTransactions;
  406. delete postData.onDemandCpeReplays;
  407. delete postData.onDemandCpeAttachments;
  408. delete postData.onDemandCpeMonitorSeats;
  409. delete postData.onDemandCpeUptime;
  410. delete postData.onDemandCpeSpans;
  411. delete postData.onDemandCpeProfileDuration;
  412. }
  413. if (!isNaN(postData.onDemandCpeErrors)) {
  414. postData.onDemandCpeErrors = toCents(
  415. postData.onDemandCpeErrors,
  416. CPE_DECIMAL_PRECISION
  417. );
  418. }
  419. if (!isNaN(postData.onDemandCpeTransactions)) {
  420. postData.onDemandCpeTransactions = toCents(
  421. postData.onDemandCpeTransactions,
  422. CPE_DECIMAL_PRECISION
  423. );
  424. }
  425. if (!isNaN(postData.onDemandCpeReplays)) {
  426. postData.onDemandCpeReplays = toCents(
  427. postData.onDemandCpeReplays,
  428. CPE_DECIMAL_PRECISION
  429. );
  430. }
  431. if (!isNaN(postData.onDemandCpeAttachments)) {
  432. postData.onDemandCpeAttachments = toCents(
  433. postData.onDemandCpeAttachments,
  434. CPE_DECIMAL_PRECISION
  435. );
  436. }
  437. if (!isNaN(postData.onDemandCpeMonitorSeats)) {
  438. postData.onDemandCpeMonitorSeats = toCents(
  439. postData.onDemandCpeMonitorSeats,
  440. CPE_DECIMAL_PRECISION
  441. );
  442. }
  443. if (!isNaN(postData.onDemandCpeUptime)) {
  444. postData.onDemandCpeUptime = toCents(
  445. postData.onDemandCpeUptime,
  446. CPE_DECIMAL_PRECISION
  447. );
  448. }
  449. if (!isNaN(postData.onDemandCpeSpans)) {
  450. postData.onDemandCpeSpans = toCents(
  451. postData.onDemandCpeSpans,
  452. CPE_DECIMAL_PRECISION
  453. );
  454. }
  455. if (!isNaN(postData.onDemandCpeSpansIndexed)) {
  456. postData.onDemandCpeSpansIndexed = toCents(
  457. postData.onDemandCpeSpansIndexed,
  458. CPE_DECIMAL_PRECISION
  459. );
  460. }
  461. if (!isNaN(postData.onDemandCpeProfileDuration)) {
  462. postData.onDemandCpeProfileDuration = toCents(
  463. postData.onDemandCpeProfileDuration,
  464. CPE_DECIMAL_PRECISION
  465. );
  466. }
  467. if (!isNaN(postData.reservedCpeSpans)) {
  468. postData.reservedCpeSpans = toCpeCents(postData.reservedCpeSpans);
  469. }
  470. if (!isNaN(postData.reservedCpeSpansIndexed)) {
  471. postData.reservedCpeSpansIndexed = toCpeCents(postData.reservedCpeSpansIndexed);
  472. }
  473. postData.retainOnDemandBudget = postData.retainOnDemandBudget
  474. ? !this.disableRetainOnDemand()
  475. : false;
  476. const hasCustomPrice = hasCustomSkuPrices || postData.managed;
  477. if (!hasCustomPrice) {
  478. delete postData.hasCustomPrice;
  479. }
  480. if (!isNaN(postData.customPriceErrors)) {
  481. postData.customPriceErrors *= 100; // Price should be in cents
  482. }
  483. if (!isNaN(postData.customPriceTransactions)) {
  484. postData.customPriceTransactions *= 100; // Price should be in cents
  485. }
  486. if (!isNaN(postData.customPriceReplays)) {
  487. postData.customPriceReplays *= 100; // Price should be in cents
  488. }
  489. if (!isNaN(postData.customPriceSpans)) {
  490. postData.customPriceSpans *= 100; // Price should be in cents
  491. }
  492. if (!isNaN(postData.customPriceSpansIndexed)) {
  493. postData.customPriceSpansIndexed *= 100; // Price should be in cents
  494. }
  495. if (!isNaN(postData.customPriceMonitorSeats)) {
  496. postData.customPriceMonitorSeats *= 100; // Price should be in cents
  497. }
  498. if (!isNaN(postData.customPriceUptime)) {
  499. postData.customPriceUptime *= 100; // Price should be in cents
  500. }
  501. if (!isNaN(postData.customPriceAttachments)) {
  502. postData.customPriceAttachments *= 100; // Price should be in cents
  503. }
  504. if (!isNaN(postData.customPricePcss)) {
  505. postData.customPricePcss *= 100; // Price should be in cents
  506. }
  507. if (!isNaN(postData.customPriceProfileDuration)) {
  508. postData.customPriceProfileDuration *= 100; // Price should be in cents
  509. }
  510. if (!isNaN(postData.customPrice)) {
  511. postData.customPrice *= 100; // Price should be in cents
  512. // For AM only: If customPrice is set, ensure that it is equal to sum of SKU prices
  513. if (
  514. hasCustomSkuPrices &&
  515. postData.customPrice !==
  516. postData.customPriceErrors +
  517. (isAm3Plan(postData.plan)
  518. ? (postData.customPriceSpans ?? 0)
  519. : (postData.customPriceTransactions ?? 0)) +
  520. (postData.customPriceReplays ?? 0) +
  521. (postData.customPriceMonitorSeats ?? 0) +
  522. (postData.customPriceUptime ?? 0) +
  523. postData.customPriceAttachments +
  524. postData.customPricePcss +
  525. (postData.customPriceProfileDuration ?? 0) +
  526. (isAm3DsPlan(postData.plan) ? (postData.customPriceSpansIndexed ?? 0) : 0)
  527. ) {
  528. onSubmitError({
  529. responseJSON: {
  530. customPrice: ['Custom Price must be equal to sum of SKU prices'],
  531. },
  532. });
  533. return;
  534. }
  535. }
  536. if (isAmPlan(postData.plan)) {
  537. // Setting soft cap types to null if not `ON_DEMAND` or `TRUE_FORWARD` ensures soft cap type
  538. // is disabled if it was set but is not set with the new provisioning request.
  539. if (!postData.softCapTypeErrors) {
  540. postData.softCapTypeErrors = null;
  541. }
  542. if (!postData.softCapTypeReplays) {
  543. postData.softCapTypeReplays = null;
  544. }
  545. if (!postData.softCapTypeAttachments) {
  546. postData.softCapTypeAttachments = null;
  547. }
  548. if (!postData.softCapTypeMonitorSeats) {
  549. postData.softCapTypeMonitorSeats = null;
  550. }
  551. if (!postData.softCapTypeUptime) {
  552. postData.softCapTypeUptime = null;
  553. }
  554. if (!postData.softCapTypeProfileDuration) {
  555. postData.softCapTypeProfileDuration = null;
  556. }
  557. // If a data category has a set soft cap type, trueForwad will also need to be set to true for that category
  558. // until the true forward fields are fully deprecated and soft cap types are used in their place.
  559. postData.trueForward = {
  560. errors: postData.softCapTypeErrors ? true : false,
  561. replays: postData.softCapTypeReplays ? true : false,
  562. attachments: postData.softCapTypeAttachments ? true : false,
  563. monitor_seats: postData.softCapTypeMonitorSeats ? true : false,
  564. uptime: postData.softCapTypeUptime ? true : false,
  565. profile_duration: postData.softCapTypeProfileDuration ? true : false,
  566. };
  567. if (isAm3Plan(postData.plan)) {
  568. postData.trueForward = {
  569. ...postData.trueForward,
  570. spans: postData.softCapTypeSpans ? true : false,
  571. };
  572. delete postData.softCapTypeTransactions;
  573. if (!postData.softCapTypeSpans) {
  574. postData.softCapTypeSpans = null;
  575. }
  576. } else {
  577. postData.trueForward = {
  578. ...postData.trueForward,
  579. transactions: postData.softCapTypeTransactions ? true : false,
  580. };
  581. delete postData.softCapTypeSpans;
  582. delete postData.softCapTypeSpansIndexed;
  583. if (!postData.softCapTypeTransactions) {
  584. postData.softCapTypeTransactions = null;
  585. }
  586. }
  587. }
  588. if (isAm3DsPlan(postData.plan)) {
  589. postData.trueForward = {
  590. ...postData.trueForward,
  591. spansIndexed: postData.softCapTypeSpansIndexed ? true : false,
  592. };
  593. if (!postData.softCapTypeSpansIndexed) {
  594. postData.softCapTypeSpansIndexed = null;
  595. }
  596. if (this.hasCompleteSpansBudget()) {
  597. postData.reservedBudgets = [
  598. {
  599. categories: ['spans', 'spansIndexed'],
  600. budget: postData.customPriceSpans,
  601. },
  602. ];
  603. } else {
  604. onSubmitError({
  605. responseJSON: {
  606. customPriceSpans: [
  607. 'Dynamic Sampling plans require reserved spans budget with reserved CPEs for both accepted and stored spans',
  608. ],
  609. },
  610. });
  611. return;
  612. }
  613. } else {
  614. for (const k in postData) {
  615. if (k.endsWith('SpansIndexed')) {
  616. delete postData[k];
  617. }
  618. }
  619. delete postData.reservedCpeSpans;
  620. }
  621. this.props.api.request(this.endpoint, {
  622. method: 'POST',
  623. data: postData,
  624. success: () => {
  625. this.props.onSuccess();
  626. this.props.closeModal();
  627. },
  628. error: error => {
  629. onSubmitError({
  630. responseJSON: error.responseJSON,
  631. });
  632. },
  633. });
  634. };
  635. provisionablePlans = [
  636. ['am3_business_ent_ds', 'Business with Dynamic Sampling (am3)'],
  637. ['am3_team_ent_ds', 'Team with Dynamic Sampling (am3)'],
  638. ['am3_business_ent', 'Business (am3)'],
  639. ['am3_team_ent', 'Team (am3)'],
  640. ['am2_business_ent', 'Business (am2)'],
  641. ['am2_team_ent', 'Team (am2)'],
  642. ['am1_business_ent', 'Business (am1)'],
  643. ['am1_team_ent', 'Team (am1)'],
  644. ['mm2_a', 'Business (mm2)'],
  645. ['mm2_b', 'Team (mm2)'],
  646. ['e1', 'Enterprise (mm1)'],
  647. ];
  648. render() {
  649. const {Header, Body, closeModal, canProvisionDsPlan = false} = this.props;
  650. const {data} = this.state;
  651. const isAmEnt = isAmEnterprisePlan(data.plan);
  652. const isAm3 = isAm3Plan(data.plan);
  653. const isAm3Ds = isAm3DsPlan(data.plan);
  654. const hasCustomSkuPrices = isAmEnt;
  655. const hasCustomPrice = hasCustomSkuPrices || !!data.managed; // Refers to ACV
  656. if (!canProvisionDsPlan) {
  657. this.provisionablePlans = this.provisionablePlans.filter(
  658. plan => !isAm3DsPlan(plan[0])
  659. );
  660. }
  661. return (
  662. <Fragment>
  663. <Header>Provision Subscription Changes</Header>
  664. <Body>
  665. <Form
  666. onSubmit={this.onSubmit}
  667. onCancel={closeModal}
  668. submitLabel="Submit"
  669. cancelLabel="Cancel"
  670. footerClass="modal-footer"
  671. >
  672. <Columns>
  673. <div>
  674. <SelectField
  675. label="Plan"
  676. name="plan"
  677. clearable={false}
  678. choices={this.provisionablePlans}
  679. onChange={v => {
  680. // Reset price fields if next plan is not AM Enterprise
  681. const isManagedPlan = isAmEnterprisePlan(v as string);
  682. const chosenPlanIsAm3Ds = isAm3DsPlan(v as string);
  683. const nextPrices = isManagedPlan
  684. ? {}
  685. : {
  686. customPriceErrors: '',
  687. customPriceTransactions: '',
  688. customPriceReplays: '',
  689. customPriceMonitorSeats: '',
  690. customPriceUptime: '',
  691. customPriceSpans: '',
  692. customPriceSpansIndexed: '',
  693. customPriceAttachments: '',
  694. customPricePcss: '',
  695. customPrice: '',
  696. };
  697. const nextReservedCpes = chosenPlanIsAm3Ds
  698. ? {}
  699. : {
  700. reservedCpeSpans: '',
  701. reservedCpeSpansIndexed: '',
  702. };
  703. this.setState(state => ({
  704. ...state,
  705. data: {
  706. ...state.data,
  707. plan: v,
  708. ...nextPrices,
  709. ...nextReservedCpes,
  710. },
  711. }));
  712. }}
  713. value={this.state.data.plan}
  714. />
  715. <BooleanField
  716. label={`Apply Changes at the End of the Current Billing Period (${prettyDate(
  717. this.props.subscription.contractPeriodEnd
  718. )})`}
  719. name="atPeriodEnd"
  720. disabled={this.state.data.coterm}
  721. onChange={v =>
  722. this.setState(state => ({
  723. ...state,
  724. effectiveAtDisabled: !!v,
  725. data: {...state.data, atPeriodEnd: v},
  726. }))
  727. }
  728. />
  729. <BooleanField
  730. label="Apply Changes To Current Subscription"
  731. name="coterm"
  732. disabled={this.state.data.atPeriodEnd}
  733. onChange={v =>
  734. this.setState(state => ({
  735. ...state,
  736. data: {...state.data, coterm: v},
  737. effectiveAtDisabled: !!v,
  738. }))
  739. }
  740. />
  741. <DateField
  742. label="Start Date"
  743. name="effectiveAt"
  744. help="The date at which this change should take effect."
  745. disabled={this.state.effectiveAtDisabled}
  746. required={!this.state.effectiveAtDisabled}
  747. />
  748. <SelectField
  749. label="Billing Interval"
  750. name="billingInterval"
  751. choices={[
  752. ['annual', 'Annual'],
  753. ['monthly', 'Monthly'],
  754. ]}
  755. disabled={!this.state.data.plan}
  756. required={!!this.state.data.plan}
  757. value={this.state.data.billingInterval}
  758. onChange={v =>
  759. this.setState(state => ({
  760. ...state,
  761. data: {
  762. ...this.state.data,
  763. billingInterval: v,
  764. },
  765. }))
  766. }
  767. />
  768. <BooleanField
  769. label="Managed Subscription"
  770. name="managed"
  771. value={hasCustomSkuPrices || this.state.data.managed}
  772. onChange={v =>
  773. this.setState(state => ({
  774. ...state,
  775. data: {
  776. ...state.data,
  777. managed: v,
  778. customPrice: v ? state.data.customPrice : '',
  779. },
  780. }))
  781. }
  782. />
  783. <SelectField
  784. label="Billing Type"
  785. name="type"
  786. choices={[
  787. ['invoiced', 'Invoiced'],
  788. ['credit_card', 'Credit Card'],
  789. ]}
  790. onChange={v => {
  791. if (v === 'credit_card') {
  792. this.setState(state => ({
  793. ...state,
  794. data: {...state.data, onDemandInvoicedManual: ''},
  795. }));
  796. }
  797. this.setState(state => ({...state, data: {...state.data, type: v}}));
  798. }}
  799. value={this.state.data.type}
  800. />
  801. {this.state.data.type === 'invoiced' && (
  802. <StyledSelectFieldWithHelpText
  803. label="On-Demand Max Spend Type"
  804. name="onDemandInvoicedManual"
  805. choices={[
  806. ['SHARED', 'Shared'],
  807. ['PER_CATEGORY', 'Per Category'],
  808. ['DISABLE', 'Disable'],
  809. ]}
  810. help="Used to enable (Shared or Per Category) or disable on-demand max spend for invoiced customers. Cannot be provisioned with soft cap."
  811. clearable
  812. disabled={
  813. this.state.data.type === 'credit_card' || this.isEnablingSoftCap()
  814. }
  815. value={this.state.data.onDemandInvoicedManual}
  816. onChange={v =>
  817. this.setState(state => ({
  818. ...state,
  819. data: {...state.data, onDemandInvoicedManual: v},
  820. }))
  821. }
  822. />
  823. )}
  824. {!this.disableRetainOnDemand() && (
  825. <BooleanField
  826. label="Retain On-Demand Budget"
  827. name="retainOnDemandBudget"
  828. value={this.state.data.retainOnDemandBudget}
  829. help="Check to retain the customer's current On-Demand Budget. Otherwise, the customer's On-Demand Budget will be set based on the default calculations (0.5 times the monthly plan price)."
  830. onChange={v =>
  831. this.setState(state => ({
  832. ...state,
  833. data: {
  834. ...state.data,
  835. retainOnDemandBudget: v,
  836. },
  837. }))
  838. }
  839. />
  840. )}
  841. <SectionHeader>Plan Quotas</SectionHeader>
  842. <SectionHeaderDescription>
  843. Monthly quantities for each SKU
  844. </SectionHeaderDescription>
  845. <NumberField
  846. label="Reserved Errors"
  847. name="reservedErrors"
  848. required={!!data.plan}
  849. value={this.state.data.reservedErrors}
  850. onChange={v =>
  851. this.setState(state => ({
  852. ...state,
  853. data: {...state.data, reservedErrors: v},
  854. }))
  855. }
  856. />
  857. <SelectField
  858. label="Soft Cap Type Errors"
  859. name="softCapTypeErrors"
  860. clearable
  861. required={false}
  862. choices={[
  863. ['ON_DEMAND', 'On Demand'],
  864. ['TRUE_FORWARD', 'True Forward'],
  865. ]}
  866. disabled={this.isEnablingOnDemandMaxSpend()}
  867. value={this.state.data.softCapTypeErrors}
  868. onChange={v =>
  869. this.setState(state => ({
  870. ...state,
  871. data: {...state.data, softCapTypeErrors: v},
  872. }))
  873. }
  874. />
  875. <NumberField
  876. label="Reserved Performance Units"
  877. name="reservedTransactions"
  878. required={isAmEnt}
  879. disabled={!isAmEnt || isAm3}
  880. value={this.state.data.reservedTransactions}
  881. onChange={v =>
  882. this.setState(state => ({
  883. ...state,
  884. data: {...state.data, reservedTransactions: v},
  885. }))
  886. }
  887. />
  888. <SelectField
  889. label="Soft Cap Type Performance Units"
  890. name="softCapTypeTransactions"
  891. clearable
  892. required={false}
  893. choices={[
  894. ['ON_DEMAND', 'On Demand'],
  895. ['TRUE_FORWARD', 'True Forward'],
  896. ]}
  897. disabled={this.isEnablingOnDemandMaxSpend() || !isAmEnt || isAm3}
  898. value={this.state.data.softCapTypeTransactions}
  899. onChange={v =>
  900. this.setState(state => ({
  901. ...state,
  902. data: {...state.data, softCapTypeTransactions: v},
  903. }))
  904. }
  905. />
  906. <NumberField
  907. label="Reserved Replays"
  908. name="reservedReplays"
  909. required={isAmEnt}
  910. disabled={!isAmEnt}
  911. value={this.state.data.reservedReplays}
  912. onChange={v =>
  913. this.setState(state => ({
  914. ...state,
  915. data: {...state.data, reservedReplays: v},
  916. }))
  917. }
  918. />
  919. <SelectField
  920. label="Soft Cap Type Replays"
  921. name="softCapTypeReplays"
  922. clearable
  923. required={false}
  924. choices={[
  925. ['ON_DEMAND', 'On Demand'],
  926. ['TRUE_FORWARD', 'True Forward'],
  927. ]}
  928. disabled={this.isEnablingOnDemandMaxSpend() || !isAmEnt}
  929. value={this.state.data.softCapTypeReplays}
  930. onChange={v =>
  931. this.setState(state => ({
  932. ...state,
  933. data: {...state.data, softCapTypeReplays: v},
  934. }))
  935. }
  936. />
  937. <NumberField
  938. label={`Reserved ${isAm3Ds ? 'Accepted Spans' : 'Spans'}`}
  939. name="reservedSpans"
  940. required={isAmEnt}
  941. disabled={!isAm3 || this.state.data.reservedCpeSpans}
  942. value={this.state.data.reservedSpans}
  943. onChange={v =>
  944. this.setState(state => ({
  945. ...state,
  946. data: {...state.data, reservedSpans: v},
  947. }))
  948. }
  949. />
  950. <SelectField
  951. label={`Soft Cap Type ${isAm3Ds ? 'Accepted Spans' : 'Spans'}`}
  952. name="softCapTypeSpans"
  953. clearable
  954. required={false}
  955. choices={[
  956. ['ON_DEMAND', 'On Demand'],
  957. ['TRUE_FORWARD', 'True Forward'],
  958. ]}
  959. disabled={this.isEnablingOnDemandMaxSpend() || !isAm3}
  960. value={this.state.data.softCapTypeSpans}
  961. onChange={v =>
  962. this.setState(state => ({
  963. ...state,
  964. data: {...state.data, softCapTypeSpans: v},
  965. }))
  966. }
  967. />
  968. {isAm3Ds && (
  969. <StyledDollarsAndCentsField
  970. label={`Reserved Cost-Per-${isAm3Ds ? 'Accepted Span' : 'Span'}`}
  971. name="reservedCpeSpans"
  972. disabled={!isAm3Ds}
  973. value={data.reservedCpeSpans}
  974. step={0.00000001}
  975. min={0.00000001}
  976. max={1}
  977. onChange={v =>
  978. this.setState(state => ({
  979. ...state,
  980. data: {
  981. ...state.data,
  982. reservedCpeSpans: v,
  983. reservedSpans: RESERVED_BUDGET_QUOTA,
  984. },
  985. }))
  986. }
  987. onBlur={() => {
  988. const currentValue = parseFloat(this.state.data.reservedCpeSpans);
  989. if (!isNaN(currentValue)) {
  990. this.setState(state => ({
  991. ...state,
  992. data: {
  993. ...state.data,
  994. reservedCpeSpans: currentValue.toFixed(CPE_DECIMAL_PRECISION),
  995. },
  996. }));
  997. }
  998. }}
  999. />
  1000. )}
  1001. {isAm3Ds && (
  1002. <Fragment>
  1003. <NumberField
  1004. label="Reserved Stored Spans"
  1005. name="reservedSpansIndexed"
  1006. required={isAmEnt}
  1007. disabled={!isAm3Ds || this.state.data.reservedCpeSpansIndexed}
  1008. value={this.state.data.reservedSpansIndexed}
  1009. onChange={v =>
  1010. this.setState(state => ({
  1011. ...state,
  1012. data: {...state.data, reservedSpansIndexed: v},
  1013. }))
  1014. }
  1015. />
  1016. <SelectField
  1017. label="Soft Cap Type Stored Spans"
  1018. name="softCapTypeSpansIndexed"
  1019. clearable
  1020. required={false}
  1021. choices={[
  1022. ['ON_DEMAND', 'On Demand'],
  1023. ['TRUE_FORWARD', 'True Forward'],
  1024. ]}
  1025. disabled={this.isEnablingOnDemandMaxSpend() || !isAm3Ds}
  1026. value={this.state.data.softCapTypeSpansIndexed}
  1027. onChange={v =>
  1028. this.setState(state => ({
  1029. ...state,
  1030. data: {...state.data, softCapTypeSpansIndexed: v},
  1031. }))
  1032. }
  1033. />
  1034. <StyledDollarsAndCentsField
  1035. label="Reserved Cost-Per-Stored Span"
  1036. name="reservedCpeSpansIndexed"
  1037. disabled={!isAm3Ds}
  1038. value={data.reservedCpeSpansIndexed}
  1039. step={0.00000001}
  1040. min={0.00000001}
  1041. max={1}
  1042. onChange={v =>
  1043. this.setState(state => ({
  1044. ...state,
  1045. data: {
  1046. ...state.data,
  1047. reservedCpeSpansIndexed: v,
  1048. reservedSpansIndexed: RESERVED_BUDGET_QUOTA,
  1049. },
  1050. }))
  1051. }
  1052. onBlur={() => {
  1053. const currentValue = parseFloat(
  1054. this.state.data.reservedCpeSpansIndexed
  1055. );
  1056. if (!isNaN(currentValue)) {
  1057. this.setState(state => ({
  1058. ...state,
  1059. data: {
  1060. ...state.data,
  1061. reservedCpeSpansIndexed:
  1062. currentValue.toFixed(CPE_DECIMAL_PRECISION),
  1063. },
  1064. }));
  1065. }
  1066. }}
  1067. />
  1068. </Fragment>
  1069. )}
  1070. <NumberField
  1071. label="Reserved Monitor Seats"
  1072. name="reservedMonitorSeats"
  1073. required={isAmEnt}
  1074. disabled={!isAmEnt}
  1075. value={this.state.data.reservedMonitorSeats}
  1076. onChange={v =>
  1077. this.setState(state => ({
  1078. ...state,
  1079. data: {...state.data, reservedMonitorSeats: v},
  1080. }))
  1081. }
  1082. />
  1083. <SelectField
  1084. label="Soft Cap Type Monitor Seats"
  1085. name="softCapTypeMonitorSeats"
  1086. clearable
  1087. required={false}
  1088. choices={[
  1089. ['ON_DEMAND', 'On Demand'],
  1090. ['TRUE_FORWARD', 'True Forward'],
  1091. ]}
  1092. disabled={this.isEnablingOnDemandMaxSpend() || !isAmEnt}
  1093. value={this.state.data.softCapTypeMonitorSeats}
  1094. onChange={v =>
  1095. this.setState(state => ({
  1096. ...state,
  1097. data: {...state.data, softCapTypeMonitorSeats: v},
  1098. }))
  1099. }
  1100. />
  1101. <Fragment>
  1102. <NumberField
  1103. label="Reserved Uptime"
  1104. name="reservedUptime"
  1105. required={isAmEnt}
  1106. disabled={!isAmEnt}
  1107. value={this.state.data.reservedUptime}
  1108. onChange={v =>
  1109. this.setState(state => ({
  1110. ...state,
  1111. data: {...state.data, reservedUptime: v},
  1112. }))
  1113. }
  1114. />
  1115. <SelectField
  1116. label="Soft Cap Type Uptime"
  1117. name="softCapTypeUptime"
  1118. clearable
  1119. required={false}
  1120. choices={[
  1121. ['ON_DEMAND', 'On Demand'],
  1122. ['TRUE_FORWARD', 'True Forward'],
  1123. ]}
  1124. disabled={this.isEnablingOnDemandMaxSpend() || !isAmEnt}
  1125. value={this.state.data.softCapTypeUptime}
  1126. onChange={v =>
  1127. this.setState(state => ({
  1128. ...state,
  1129. data: {...state.data, softCapTypeUptime: v},
  1130. }))
  1131. }
  1132. />
  1133. </Fragment>
  1134. <NumberField
  1135. label="Reserved Attachments (in GB)"
  1136. name="reservedAttachments"
  1137. required={isAmEnt}
  1138. disabled={!isAmEnt}
  1139. value={this.state.data.reservedAttachments}
  1140. onChange={v =>
  1141. this.setState(state => ({
  1142. ...state,
  1143. data: {...state.data, reservedAttachments: v},
  1144. }))
  1145. }
  1146. />
  1147. <SelectField
  1148. label="Soft Cap Type Attachments"
  1149. name="softCapTypeAttachments"
  1150. clearable
  1151. required={false}
  1152. choices={[
  1153. ['ON_DEMAND', 'On Demand'],
  1154. ['TRUE_FORWARD', 'True Forward'],
  1155. ]}
  1156. disabled={this.isEnablingOnDemandMaxSpend() || !isAmEnt}
  1157. value={this.state.data.softCapTypeAttachments}
  1158. onChange={v =>
  1159. this.setState(state => ({
  1160. ...state,
  1161. data: {...state.data, softCapTypeAttachments: v},
  1162. }))
  1163. }
  1164. />
  1165. <NumberField
  1166. label="Reserved Profile Duration (in hours)"
  1167. name="reservedProfileDuration"
  1168. required={isAmEnt}
  1169. disabled={!isAmEnt || true} // TODO: remove this when profile duration is enabled
  1170. value={this.state.data.reservedProfileDuration}
  1171. onChange={v =>
  1172. this.setState(state => ({
  1173. ...state,
  1174. data: {...state.data, reservedProfileDuration: v},
  1175. }))
  1176. }
  1177. />
  1178. <SelectField
  1179. label="Soft Cap Type Profile Duration"
  1180. name="softCapTypeProfileDuration"
  1181. clearable
  1182. required={false}
  1183. choices={[
  1184. ['ON_DEMAND', 'On Demand'],
  1185. ['TRUE_FORWARD', 'True Forward'],
  1186. ]}
  1187. disabled={this.isEnablingOnDemandMaxSpend() || !isAmEnt || true} // TODO: remove this when profile duration is enabled
  1188. value={this.state.data.softCapTypeProfileDuration}
  1189. onChange={v =>
  1190. this.setState(state => ({
  1191. ...state,
  1192. data: {...state.data, softCapTypeProfileDuration: v},
  1193. }))
  1194. }
  1195. />
  1196. </div>
  1197. <div>
  1198. <SectionHeader>Reserved Volume Prices</SectionHeader>
  1199. <SectionHeaderDescription>
  1200. Annual prices for reserved volumes, in whole dollars.
  1201. </SectionHeaderDescription>
  1202. <StyledDollarsField
  1203. label="Price for Errors"
  1204. name="customPriceErrors"
  1205. disabled={!hasCustomSkuPrices}
  1206. required={hasCustomSkuPrices}
  1207. value={data.customPriceErrors}
  1208. onChange={v =>
  1209. this.setState(state => ({
  1210. ...state,
  1211. data: {
  1212. ...state.data,
  1213. customPriceErrors: v,
  1214. },
  1215. }))
  1216. }
  1217. />
  1218. <StyledDollarsField
  1219. label="Price for Performance Units"
  1220. name="customPriceTransactions"
  1221. disabled={!hasCustomSkuPrices || isAm3}
  1222. required={hasCustomSkuPrices}
  1223. value={data.customPriceTransactions}
  1224. onChange={v =>
  1225. this.setState(state => ({
  1226. ...state,
  1227. data: {
  1228. ...state.data,
  1229. customPriceTransactions: v,
  1230. },
  1231. }))
  1232. }
  1233. />
  1234. <StyledDollarsField
  1235. label="Price for Replays"
  1236. name="customPriceReplays"
  1237. disabled={!hasCustomSkuPrices}
  1238. required={hasCustomSkuPrices}
  1239. value={data.customPriceReplays}
  1240. onChange={v =>
  1241. this.setState(state => ({
  1242. ...state,
  1243. data: {
  1244. ...state.data,
  1245. customPriceReplays: v,
  1246. },
  1247. }))
  1248. }
  1249. />
  1250. <StyledDollarsField
  1251. label={`Price for ${isAm3Ds ? 'Accepted Spans' : 'Spans'}${this.isSettingSpansBudget() ? ' (Reserved Spans Budget)' : ''}`}
  1252. name="customPriceSpans"
  1253. disabled={!hasCustomSkuPrices || !isAm3}
  1254. required={hasCustomSkuPrices}
  1255. value={data.customPriceSpans}
  1256. onChange={v =>
  1257. this.setState(state => ({
  1258. ...state,
  1259. data: {
  1260. ...state.data,
  1261. customPriceSpans: v,
  1262. },
  1263. }))
  1264. }
  1265. />
  1266. {isAm3Ds && (
  1267. <StyledDollarsField
  1268. label={`Price for Stored Spans`}
  1269. name="customPriceSpansIndexed"
  1270. disabled={
  1271. !hasCustomSkuPrices || !isAm3Ds || this.isSettingSpansBudget()
  1272. }
  1273. required={hasCustomSkuPrices}
  1274. value={this.isSettingSpansBudget() ? 0 : data.customPriceSpansIndexed}
  1275. onChange={v =>
  1276. this.setState(state => ({
  1277. ...state,
  1278. data: {
  1279. ...state.data,
  1280. customPriceSpansIndexed: v,
  1281. },
  1282. }))
  1283. }
  1284. />
  1285. )}
  1286. <StyledDollarsField
  1287. label="Price for Monitor Seats"
  1288. name="customPriceMonitorSeats"
  1289. disabled={!hasCustomSkuPrices}
  1290. required={hasCustomSkuPrices}
  1291. value={data.customPriceMonitorSeats}
  1292. onChange={v =>
  1293. this.setState(state => ({
  1294. ...state,
  1295. data: {
  1296. ...state.data,
  1297. customPriceMonitorSeats: v,
  1298. },
  1299. }))
  1300. }
  1301. />
  1302. <StyledDollarsField
  1303. label="Price for Uptime"
  1304. name="customPriceUptime"
  1305. disabled={!hasCustomSkuPrices}
  1306. required={hasCustomSkuPrices}
  1307. value={data.customPriceUptime}
  1308. onChange={v =>
  1309. this.setState(state => ({
  1310. ...state,
  1311. data: {
  1312. ...state.data,
  1313. customPriceUptime: v,
  1314. },
  1315. }))
  1316. }
  1317. />
  1318. <StyledDollarsField
  1319. label="Price for Attachments"
  1320. name="customPriceAttachments"
  1321. disabled={!hasCustomSkuPrices}
  1322. required={hasCustomSkuPrices}
  1323. value={data.customPriceAttachments}
  1324. onChange={v =>
  1325. this.setState(state => ({
  1326. ...state,
  1327. data: {
  1328. ...state.data,
  1329. customPriceAttachments: v,
  1330. },
  1331. }))
  1332. }
  1333. />
  1334. <StyledDollarsField
  1335. label="Price for Profile Duration"
  1336. name="customPriceProfileDuration"
  1337. disabled={!hasCustomSkuPrices || true} // TODO: remove this when profile duration is enabled
  1338. required={hasCustomSkuPrices}
  1339. value={data.customPriceProfileDuration}
  1340. onChange={v =>
  1341. this.setState(state => ({
  1342. ...state,
  1343. data: {
  1344. ...state.data,
  1345. customPriceProfileDuration: v,
  1346. },
  1347. }))
  1348. }
  1349. />
  1350. <StyledDollarsField
  1351. label="Price for PCSS"
  1352. name="customPricePcss"
  1353. disabled={!hasCustomSkuPrices}
  1354. required={hasCustomSkuPrices}
  1355. value={data.customPricePcss}
  1356. onChange={v =>
  1357. this.setState(state => ({
  1358. ...state,
  1359. data: {
  1360. ...state.data,
  1361. customPricePcss: v,
  1362. },
  1363. }))
  1364. }
  1365. />
  1366. <StyledDollarsField
  1367. label="Annual Contract Value"
  1368. name="customPrice"
  1369. help="Used as a checksum, must be equal to sum of prices above"
  1370. disabled={!hasCustomPrice}
  1371. value={data.customPrice}
  1372. onChange={v =>
  1373. this.setState(state => ({
  1374. ...state,
  1375. data: {
  1376. ...state.data,
  1377. customPrice: v,
  1378. },
  1379. }))
  1380. }
  1381. />
  1382. {this.isEnablingOnDemandMaxSpend() && (
  1383. <Fragment>
  1384. <SectionHeader>On-Demand Cost-Per-Event (CPE)</SectionHeader>
  1385. <SectionHeaderDescription>
  1386. The cost of on-demand units, in dollars, for invoiced customers with
  1387. on-demand max spend. If not set, the on-demand spend will be
  1388. calculated with the self-serve on-demand pricing.
  1389. </SectionHeaderDescription>
  1390. <Alert.Container>
  1391. <Alert type="warning">
  1392. If the subscription already has on-demand spend in the current
  1393. period, and the new cost-per-event overrides would cause the spend
  1394. to exceed the on-demand budget, the request will fail.
  1395. </Alert>
  1396. </Alert.Container>
  1397. <StyledDollarsAndCentsField
  1398. label="On-Demand Cost-Per-Error"
  1399. name="onDemandCpeErrors"
  1400. disabled={!this.isEnablingOnDemandMaxSpend()}
  1401. value={data.onDemandCpeErrors}
  1402. step={0.00000001}
  1403. min={0.00000001}
  1404. max={1}
  1405. onChange={v =>
  1406. this.setState(state => ({
  1407. ...state,
  1408. data: {
  1409. ...state.data,
  1410. onDemandCpeErrors: v,
  1411. },
  1412. }))
  1413. }
  1414. onBlur={() => {
  1415. const currentValue = parseFloat(
  1416. this.state.data.onDemandCpeErrors
  1417. );
  1418. if (!isNaN(currentValue)) {
  1419. this.setState(state => ({
  1420. ...state,
  1421. data: {
  1422. ...state.data,
  1423. onDemandCpeErrors:
  1424. currentValue.toFixed(CPE_DECIMAL_PRECISION),
  1425. },
  1426. }));
  1427. }
  1428. }}
  1429. />
  1430. <StyledDollarsAndCentsField
  1431. label="On-Demand Cost-Per-Performance Unit"
  1432. name="onDemandCpeTransactions"
  1433. disabled={!this.isEnablingOnDemandMaxSpend() || isAm3}
  1434. value={data.onDemandCpeTransactions}
  1435. step={0.00000001}
  1436. min={0.00000001}
  1437. max={1}
  1438. onChange={v =>
  1439. this.setState(state => ({
  1440. ...state,
  1441. data: {
  1442. ...state.data,
  1443. onDemandCpeTransactions: v,
  1444. },
  1445. }))
  1446. }
  1447. onBlur={() => {
  1448. const currentValue = parseFloat(
  1449. this.state.data.onDemandCpeTransactions
  1450. );
  1451. if (!isNaN(currentValue)) {
  1452. this.setState(state => ({
  1453. ...state,
  1454. data: {
  1455. ...state.data,
  1456. onDemandCpeTransactions:
  1457. currentValue.toFixed(CPE_DECIMAL_PRECISION),
  1458. },
  1459. }));
  1460. }
  1461. }}
  1462. />
  1463. <StyledDollarsAndCentsField
  1464. label="On-Demand Cost-Per-Replay"
  1465. name="onDemandCpeReplays"
  1466. disabled={!this.isEnablingOnDemandMaxSpend()}
  1467. value={data.onDemandCpeReplays}
  1468. step={0.00000001}
  1469. min={0.00000001}
  1470. max={1}
  1471. onChange={v =>
  1472. this.setState(state => ({
  1473. ...state,
  1474. data: {
  1475. ...state.data,
  1476. onDemandCpeReplays: v,
  1477. },
  1478. }))
  1479. }
  1480. onBlur={() => {
  1481. const currentValue = parseFloat(
  1482. this.state.data.onDemandCpeReplays
  1483. );
  1484. if (!isNaN(currentValue)) {
  1485. this.setState(state => ({
  1486. ...state,
  1487. data: {
  1488. ...state.data,
  1489. onDemandCpeReplays:
  1490. currentValue.toFixed(CPE_DECIMAL_PRECISION),
  1491. },
  1492. }));
  1493. }
  1494. }}
  1495. />
  1496. <StyledDollarsAndCentsField
  1497. label="On-Demand Cost-Per-Span"
  1498. name="onDemandCpeSpans"
  1499. disabled={!this.isEnablingOnDemandMaxSpend() || !isAm3}
  1500. value={data.onDemandCpeSpans}
  1501. step={0.00000001}
  1502. min={0.00000001}
  1503. max={1}
  1504. onChange={v =>
  1505. this.setState(state => ({
  1506. ...state,
  1507. data: {
  1508. ...state.data,
  1509. onDemandCpeSpans: v,
  1510. },
  1511. }))
  1512. }
  1513. onBlur={() => {
  1514. const currentValue = parseFloat(this.state.data.onDemandCpeSpans);
  1515. if (!isNaN(currentValue)) {
  1516. this.setState(state => ({
  1517. ...state,
  1518. data: {
  1519. ...state.data,
  1520. onDemandCpeSpans:
  1521. currentValue.toFixed(CPE_DECIMAL_PRECISION),
  1522. },
  1523. }));
  1524. }
  1525. }}
  1526. />
  1527. <StyledDollarsAndCentsField
  1528. label="On-Demand Cost-Per-Attachment"
  1529. name="onDemandCpeAttachments"
  1530. disabled={!this.isEnablingOnDemandMaxSpend()}
  1531. value={data.onDemandCpeAttachments}
  1532. step={0.00000001}
  1533. min={0.00000001}
  1534. max={1}
  1535. onChange={v =>
  1536. this.setState(state => ({
  1537. ...state,
  1538. data: {
  1539. ...state.data,
  1540. onDemandCpeAttachments: v,
  1541. },
  1542. }))
  1543. }
  1544. onBlur={() => {
  1545. const currentValue = parseFloat(
  1546. this.state.data.onDemandCpeAttachments
  1547. );
  1548. if (!isNaN(currentValue)) {
  1549. this.setState(state => ({
  1550. ...state,
  1551. data: {
  1552. ...state.data,
  1553. onDemandCpeAttachments:
  1554. currentValue.toFixed(CPE_DECIMAL_PRECISION),
  1555. },
  1556. }));
  1557. }
  1558. }}
  1559. />
  1560. <StyledDollarsAndCentsField
  1561. label="On-Demand Cost-Per-Profile Duration"
  1562. name="onDemandCpeProfileDuration"
  1563. disabled={!this.isEnablingOnDemandMaxSpend() || true} // TODO: remove this when profile duration is enabled
  1564. value={data.onDemandCpeProfileDuration}
  1565. step={0.00000001}
  1566. min={0.00000001}
  1567. max={1}
  1568. onChange={v =>
  1569. this.setState(state => ({
  1570. ...state,
  1571. data: {
  1572. ...state.data,
  1573. onDemandCpeProfileDuration: v,
  1574. },
  1575. }))
  1576. }
  1577. onBlur={() => {
  1578. const currentValue = parseFloat(
  1579. this.state.data.onDemandCpeProfileDuration
  1580. );
  1581. if (!isNaN(currentValue)) {
  1582. this.setState(state => ({
  1583. ...state,
  1584. data: {
  1585. ...state.data,
  1586. onDemandCpeProfileDuration:
  1587. currentValue.toFixed(CPE_DECIMAL_PRECISION),
  1588. },
  1589. }));
  1590. }
  1591. }}
  1592. />
  1593. <StyledDollarsAndCentsField
  1594. label="On-Demand Cost-Per-Cron Monitor"
  1595. name="onDemandCpeMonitorSeats"
  1596. disabled={!this.isEnablingOnDemandMaxSpend()}
  1597. step={0.00000001}
  1598. min={0.00000001}
  1599. max={1}
  1600. value={data.onDemandCpeMonitorSeats}
  1601. onChange={v =>
  1602. this.setState(state => ({
  1603. ...state,
  1604. data: {
  1605. ...state.data,
  1606. onDemandCpeMonitorSeats: v,
  1607. },
  1608. }))
  1609. }
  1610. onBlur={() => {
  1611. const currentValue = parseFloat(
  1612. this.state.data.onDemandCpeMonitorSeats
  1613. );
  1614. if (!isNaN(currentValue)) {
  1615. this.setState(state => ({
  1616. ...state,
  1617. data: {
  1618. ...state.data,
  1619. onDemandCpeMonitorSeats:
  1620. currentValue.toFixed(CPE_DECIMAL_PRECISION),
  1621. },
  1622. }));
  1623. }
  1624. }}
  1625. />
  1626. <StyledDollarsAndCentsField
  1627. label="On-Demand Cost-Per-Uptime Monitor"
  1628. name="onDemandCpeUptime"
  1629. disabled={!this.isEnablingOnDemandMaxSpend()}
  1630. step={0.00000001}
  1631. min={0.00000001}
  1632. max={1}
  1633. value={data.onDemandCpeUptime}
  1634. onChange={v =>
  1635. this.setState(state => ({
  1636. ...state,
  1637. data: {
  1638. ...state.data,
  1639. onDemandCpeUptime: v,
  1640. },
  1641. }))
  1642. }
  1643. onBlur={() => {
  1644. const currentValue = parseFloat(
  1645. this.state.data.onDemandCpeUptime
  1646. );
  1647. if (!isNaN(currentValue)) {
  1648. this.setState(state => ({
  1649. ...state,
  1650. data: {
  1651. ...state.data,
  1652. onDemandCpeUptime:
  1653. currentValue.toFixed(CPE_DECIMAL_PRECISION),
  1654. },
  1655. }));
  1656. }
  1657. }}
  1658. />
  1659. </Fragment>
  1660. )}
  1661. </div>
  1662. </Columns>
  1663. </Form>
  1664. </Body>
  1665. </Fragment>
  1666. );
  1667. }
  1668. }
  1669. const Columns = styled('div')`
  1670. display: grid;
  1671. grid-template-columns: 1fr 1fr;
  1672. gap: ${space(3)};
  1673. `;
  1674. const SectionHeader = styled('h5')`
  1675. margin-bottom: 0;
  1676. `;
  1677. const SectionHeaderDescription = styled('small')`
  1678. display: block;
  1679. margin-bottom: ${space(3)};
  1680. `;
  1681. const modalCss = css`
  1682. width: 100%;
  1683. max-width: 1200px;
  1684. `;
  1685. const StyledSelectFieldWithHelpText = styled(SelectField)`
  1686. margin-bottom: 15px;
  1687. div[class*='StyledSelectControl'] {
  1688. margin-bottom: 0;
  1689. }
  1690. `;
  1691. const StyledDollarsField = styled(DollarsField)`
  1692. div[class='dollars-field-container'] {
  1693. display: flex;
  1694. }
  1695. span[class='dollar-sign'] {
  1696. padding: 12px;
  1697. }
  1698. `;
  1699. const StyledDollarsAndCentsField = styled(DollarsAndCentsField)`
  1700. div[class='dollars-cents-field-container'] {
  1701. display: flex;
  1702. }
  1703. span[class='dollar-sign'] {
  1704. padding: 12px;
  1705. }
  1706. `;
  1707. const Modal = withApi(ProvisionSubscriptionModal);
  1708. type Options = Pick<Props, 'orgId' | 'subscription' | 'onSuccess' | 'canProvisionDsPlan'>;
  1709. const triggerProvisionSubscription = (opts: Options) =>
  1710. openModal(deps => <Modal {...deps} {...opts} />, {modalCss});
  1711. export default triggerProvisionSubscription;