diff --git a/packages/api-v4/.changeset/pr-10468-added-1716219170108.md b/packages/api-v4/.changeset/pr-10468-added-1716219170108.md new file mode 100644 index 00000000000..2a5eab0136d --- /dev/null +++ b/packages/api-v4/.changeset/pr-10468-added-1716219170108.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Added +--- + +New endpoint for `object-storage/types` ([#10468](https://github.com/linode/manager/pull/10468)) diff --git a/packages/api-v4/src/object-storage/index.ts b/packages/api-v4/src/object-storage/index.ts index f4a9bdf8d18..e2985222d3f 100644 --- a/packages/api-v4/src/object-storage/index.ts +++ b/packages/api-v4/src/object-storage/index.ts @@ -9,3 +9,5 @@ export * from './objects'; export * from './objectStorageKeys'; export * from './types'; + +export * from './prices'; diff --git a/packages/api-v4/src/object-storage/prices.ts b/packages/api-v4/src/object-storage/prices.ts new file mode 100644 index 00000000000..2907a6a110c --- /dev/null +++ b/packages/api-v4/src/object-storage/prices.ts @@ -0,0 +1,16 @@ +import { Params, PriceType, ResourcePage } from 'src/types'; +import { API_ROOT } from '../constants'; +import Request, { setMethod, setParams, setURL } from '../request'; + +/** + * getObjectStorageTypes + * + * Return a paginated list of available Object Storage types; used for pricing. + * This endpoint does not require authentication. + */ +export const getObjectStorageTypes = (params?: Params) => + Request>( + setURL(`${API_ROOT}/object-storage/types`), + setMethod('GET'), + setParams(params) + ); diff --git a/packages/manager/.changeset/pr-10468-changed-1716219242980.md b/packages/manager/.changeset/pr-10468-changed-1716219242980.md new file mode 100644 index 00000000000..729048a7d33 --- /dev/null +++ b/packages/manager/.changeset/pr-10468-changed-1716219242980.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Use dynamic pricing with `object-storage/types` endpoint ([#10468](https://github.com/linode/manager/pull/10468)) diff --git a/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts index 85735a3cf48..76459bd7b5b 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts @@ -36,16 +36,6 @@ import { makeFeatureFlagData } from 'support/util/feature-flags'; // Various messages, notes, and warnings that may be shown when enabling Object Storage // under different circumstances. const objNotes = { - // When enabling OBJ using a region with a regular pricing structure, when OBJ DC-specific pricing is disabled. - regularPricing: /Linode Object Storage costs a flat rate of \$5\/month, and includes 250 GB of storage and 1 TB of outbound data transfer. Beyond that, it.*s \$0.02 per GB per month./, - - // When enabling OBJ using a region with special pricing during the free beta period (OBJ DC-specific pricing is disabled). - dcSpecificBetaPricing: /Object Storage for .* is currently in beta\. During the beta period, Object Storage in these regions is free\. After the beta period, customers will be notified before charges for this service begin./, - - // When enabling OBJ without having selected a region, when OBJ DC-specific pricing is disabled. - dcPricingGenericExplanation: - 'Pricing for monthly rate and overage costs will depend on the data center you select for deployment.', - // When enabling OBJ, in both the Access Key flow and Create Bucket flow, when OBJ DC-specific pricing is enabled. objDCPricing: 'Object Storage costs a flat rate of $5/month, and includes 250 GB of storage. When you enable Object Storage, 1 TB of outbound data transfer will be added to your global network transfer pool.', diff --git a/packages/manager/src/factories/types.ts b/packages/manager/src/factories/types.ts index 830f7efdbcd..5229d085b50 100644 --- a/packages/manager/src/factories/types.ts +++ b/packages/manager/src/factories/types.ts @@ -172,3 +172,49 @@ export const volumeTypeFactory = Factory.Sync.makeFactory({ ], transfer: 0, }); + +export const objectStorageTypeFactory = Factory.Sync.makeFactory({ + id: 'objectstorage', + label: 'Object Storage', + price: { + hourly: 0.0075, + monthly: 5.0, + }, + region_prices: [ + { + hourly: 0.0075, + id: 'id-cgk', + monthly: 5.0, + }, + { + hourly: 0.0075, + id: 'br-gru', + monthly: 5.0, + }, + ], + transfer: 1000, +}); + +export const objectStorageOverageTypeFactory = Factory.Sync.makeFactory( + { + id: 'objectstorage-overage', + label: 'Object Storage Overage', + price: { + hourly: 0.02, + monthly: null, + }, + region_prices: [ + { + hourly: 0.024, + id: 'id-cgk', + monthly: null, + }, + { + hourly: 0.028, + id: 'br-gru', + monthly: null, + }, + ], + transfer: 0, + } +); diff --git a/packages/manager/src/features/Account/EnableObjectStorage.tsx b/packages/manager/src/features/Account/EnableObjectStorage.tsx index 51e74662cf0..f6562865e36 100644 --- a/packages/manager/src/features/Account/EnableObjectStorage.tsx +++ b/packages/manager/src/features/Account/EnableObjectStorage.tsx @@ -2,8 +2,8 @@ import { AccountSettings } from '@linode/api-v4/lib/account'; import { cancelObjectStorage } from '@linode/api-v4/lib/object-storage'; import { APIError } from '@linode/api-v4/lib/types'; import Grid from '@mui/material/Unstable_Grid2'; -import * as React from 'react'; import { useQueryClient } from '@tanstack/react-query'; +import * as React from 'react'; import { Accordion } from 'src/components/Accordion'; import { Button } from 'src/components/Button/Button'; diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx index 9b4997e76ed..001bdce16d3 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx @@ -19,6 +19,7 @@ import { useCreateBucketMutation, useObjectStorageBuckets, useObjectStorageClusters, + useObjectStorageTypesQuery, } from 'src/queries/objectStorage'; import { useProfile } from 'src/queries/profile'; import { useRegionsQuery } from 'src/queries/regions/regions'; @@ -26,6 +27,7 @@ import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; import { sendCreateBucketEvent } from 'src/utilities/analytics'; import { getErrorMap } from 'src/utilities/errorUtils'; import { getGDPRDetails } from 'src/utilities/formatRegion'; +import { PRICES_RELOAD_ERROR_NOTICE_TEXT } from 'src/utilities/pricing/constants'; import { EnableObjectStorageModal } from '../EnableObjectStorageModal'; import ClusterSelect from './ClusterSelect'; @@ -74,6 +76,14 @@ export const CreateBucketDrawer = (props: Props) => { : undefined, }); + const { + data: types, + isError: isErrorTypes, + isLoading: isLoadingTypes, + } = useObjectStorageTypesQuery(isOpen); + + const isInvalidPrice = !types || isErrorTypes; + const { error, isLoading, @@ -199,9 +209,15 @@ export const CreateBucketDrawer = (props: Props) => { 'data-testid': 'create-bucket-button', disabled: !formik.values.cluster || - (showGDPRCheckbox && !hasSignedAgreement), + (showGDPRCheckbox && !hasSignedAgreement) || + isErrorTypes, label: 'Create Bucket', - loading: isLoading, + loading: + isLoading || Boolean(clusterRegion?.[0]?.id && isLoadingTypes), + tooltipText: + !isLoadingTypes && isInvalidPrice + ? PRICES_RELOAD_ERROR_NOTICE_TEXT + : '', type: 'submit', }} secondaryButtonProps={{ label: 'Cancel', onClick: onClose }} diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx index efc2669ea5a..43330dcaa49 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx @@ -1,7 +1,14 @@ import { fireEvent } from '@testing-library/react'; import React from 'react'; -import { OBJ_STORAGE_PRICE } from 'src/utilities/pricing/constants'; +import { + objectStorageOverageTypeFactory, + objectStorageTypeFactory, +} from 'src/factories'; +import { + OBJ_STORAGE_PRICE, + UNKNOWN_PRICE, +} from 'src/utilities/pricing/constants'; import { objectStoragePriceIncreaseMap } from 'src/utilities/pricing/dynamicPricing'; import { renderWithTheme } from 'src/utilities/testHelpers'; @@ -11,12 +18,37 @@ import { OveragePricing, } from './OveragePricing'; -describe('OveragePricing', () => { +const mockObjectStorageTypes = [ + objectStorageTypeFactory.build(), + objectStorageOverageTypeFactory.build(), +]; + +const queryMocks = vi.hoisted(() => ({ + useObjectStorageTypesQuery: vi.fn().mockReturnValue({}), +})); + +vi.mock('src/queries/objectStorage', async () => { + const actual = await vi.importActual('src/queries/objectStorage'); + return { + ...actual, + useObjectStorageTypesQuery: queryMocks.useObjectStorageTypesQuery, + }; +}); + +describe('OveragePricing', async () => { + beforeAll(() => { + queryMocks.useObjectStorageTypesQuery.mockReturnValue({ + data: mockObjectStorageTypes, + }); + }); + it('Renders base overage pricing for a region without price increases', () => { const { getByText } = renderWithTheme( ); - getByText(`$${OBJ_STORAGE_PRICE.storage_overage} per GB`, { exact: false }); + getByText(`$${mockObjectStorageTypes[1].price.hourly?.toFixed(2)} per GB`, { + exact: false, + }); getByText(`$${OBJ_STORAGE_PRICE.transfer_overage} per GB`, { exact: false, }); @@ -24,10 +56,9 @@ describe('OveragePricing', () => { it('Renders DC-specific overage pricing for a region with price increases', () => { const { getByText } = renderWithTheme(); - getByText( - `$${objectStoragePriceIncreaseMap['br-gru'].storage_overage} per GB`, - { exact: false } - ); + getByText(`$${mockObjectStorageTypes[1].region_prices[1].hourly} per GB`, { + exact: false, + }); getByText( `$${objectStoragePriceIncreaseMap['br-gru'].transfer_overage} per GB`, { exact: false } @@ -59,4 +90,40 @@ describe('OveragePricing', () => { expect(tooltip).toBeInTheDocument(); expect(getByText(GLOBAL_TRANSFER_POOL_TOOLTIP_TEXT)).toBeVisible(); }); + + it('Renders a loading state while prices are loading', () => { + queryMocks.useObjectStorageTypesQuery.mockReturnValue({ + isLoading: true, + }); + + const { getByRole } = renderWithTheme( + + ); + + expect(getByRole('progressbar')).toBeVisible(); + }); + + it('Renders placeholder unknown pricing when there is an error', () => { + queryMocks.useObjectStorageTypesQuery.mockReturnValue({ + isError: true, + }); + + const { getAllByText } = renderWithTheme( + + ); + + expect(getAllByText(`$${UNKNOWN_PRICE} per GB`)).toHaveLength(1); + }); + + it('Renders placeholder unknown pricing when prices are undefined', () => { + queryMocks.useObjectStorageTypesQuery.mockReturnValue({ + data: undefined, + }); + + const { getAllByText } = renderWithTheme( + + ); + + expect(getAllByText(`$${UNKNOWN_PRICE} per GB`)).toHaveLength(1); + }); }); diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx index 25279f3efc9..727c20d5cdc 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx @@ -2,10 +2,18 @@ import { Region } from '@linode/api-v4'; import { styled } from '@mui/material/styles'; import React from 'react'; +import { CircularProgress } from 'src/components/CircularProgress'; import { TextTooltip } from 'src/components/TextTooltip'; import { Typography } from 'src/components/Typography'; -import { OBJ_STORAGE_PRICE } from 'src/utilities/pricing/constants'; -import { objectStoragePriceIncreaseMap } from 'src/utilities/pricing/dynamicPricing'; +import { useObjectStorageTypesQuery } from 'src/queries/objectStorage'; +import { + OBJ_STORAGE_PRICE, + UNKNOWN_PRICE, +} from 'src/utilities/pricing/constants'; +import { + getDCSpecificPriceByType, + objectStoragePriceIncreaseMap, +} from 'src/utilities/pricing/dynamicPricing'; interface Props { regionId: Region['id']; @@ -18,23 +26,40 @@ export const GLOBAL_TRANSFER_POOL_TOOLTIP_TEXT = export const OveragePricing = (props: Props) => { const { regionId } = props; + + const { data: types, isError, isLoading } = useObjectStorageTypesQuery(); + + const overageType = types?.find( + (type) => type.id === 'objectstorage-overage' + ); + + const storageOveragePrice = getDCSpecificPriceByType({ + decimalPrecision: 3, + interval: 'hourly', + regionId, + type: overageType, + }); + const isDcSpecificPricingRegion = objectStoragePriceIncreaseMap.hasOwnProperty( regionId ); - return ( + return isLoading ? ( + + ) : ( <> For this region, additional storage costs{' '} $ - {isDcSpecificPricingRegion - ? objectStoragePriceIncreaseMap[regionId].storage_overage - : OBJ_STORAGE_PRICE.storage_overage}{' '} + {storageOveragePrice && !isError + ? parseFloat(storageOveragePrice) + : UNKNOWN_PRICE}{' '} per GB . + Outbound transfer will cost{' '} diff --git a/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.test.tsx b/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.test.tsx index e36ba887af3..834cd38f4ec 100644 --- a/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.test.tsx +++ b/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.test.tsx @@ -1,7 +1,11 @@ import { fireEvent, render } from '@testing-library/react'; import * as React from 'react'; -import { OBJ_STORAGE_PRICE } from 'src/utilities/pricing/constants'; +import { + objectStorageOverageTypeFactory, + objectStorageTypeFactory, +} from 'src/factories'; +import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants'; import { wrapWithTheme } from 'src/utilities/testHelpers'; import { @@ -23,7 +27,29 @@ const props: EnableObjectStorageProps = { open: true, }; +const queryMocks = vi.hoisted(() => ({ + useObjectStorageTypesQuery: vi.fn().mockReturnValue({}), +})); + +vi.mock('src/queries/objectStorage', async () => { + const actual = await vi.importActual('src/queries/objectStorage'); + return { + ...actual, + useObjectStorageTypesQuery: queryMocks.useObjectStorageTypesQuery, + }; +}); + describe('EnableObjectStorageModal', () => { + beforeAll(() => { + const mockObjectStorageTypes = [ + objectStorageTypeFactory.build(), + objectStorageOverageTypeFactory.build(), + ]; + queryMocks.useObjectStorageTypesQuery.mockReturnValue({ + data: mockObjectStorageTypes, + }); + }); + it('includes a header', () => { const { getAllByText } = render( wrapWithTheme() @@ -37,7 +63,7 @@ describe('EnableObjectStorageModal', () => { ) ); - getByText(`$${OBJ_STORAGE_PRICE.monthly}/month`, { exact: false }); + getByText(`$5/month`, { exact: false }); getByText(OBJ_STORAGE_STORAGE_AMT, { exact: false }); getByText(OBJ_STORAGE_NETWORK_TRANSFER_AMT, { exact: false }); }); @@ -51,7 +77,7 @@ describe('EnableObjectStorageModal', () => { /> ) ); - getByText(`$${OBJ_STORAGE_PRICE.monthly}/month`, { exact: false }); + getByText(`$5/month`, { exact: false }); getByText(OBJ_STORAGE_STORAGE_AMT, { exact: false }); getByText(OBJ_STORAGE_NETWORK_TRANSFER_AMT, { exact: false }); }); @@ -60,11 +86,27 @@ describe('EnableObjectStorageModal', () => { const { getByText } = render( wrapWithTheme() ); - getByText(`$${OBJ_STORAGE_PRICE.monthly}/month`, { exact: false }); + getByText(`$5/month`, { exact: false }); getByText(OBJ_STORAGE_STORAGE_AMT, { exact: false }); getByText(OBJ_STORAGE_NETWORK_TRANSFER_AMT, { exact: false }); }); + it('displays placeholder unknown pricing and disables the primary action button if pricing is not available', () => { + queryMocks.useObjectStorageTypesQuery.mockReturnValue({ + data: undefined, + isError: true, + }); + + const { getByTestId, getByText } = render( + wrapWithTheme() + ); + + const primaryActionButton = getByTestId('enable-obj'); + + expect(getByText(`${UNKNOWN_PRICE}/month`, { exact: false })).toBeVisible(); + expect(primaryActionButton).toBeDisabled(); + }); + it('includes a link to linode.com/pricing', () => { const { getByText } = render( wrapWithTheme() diff --git a/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx b/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx index 8e990d54e31..fbc6d26cbd4 100644 --- a/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx +++ b/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx @@ -7,7 +7,12 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati import { Link } from 'src/components/Link'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; -import { OBJ_STORAGE_PRICE } from 'src/utilities/pricing/constants'; +import { useObjectStorageTypesQuery } from 'src/queries/objectStorage'; +import { + PRICES_RELOAD_ERROR_NOTICE_TEXT, + UNKNOWN_PRICE, +} from 'src/utilities/pricing/constants'; +import { getDCSpecificPriceByType } from 'src/utilities/pricing/dynamicPricing'; export const OBJ_STORAGE_STORAGE_AMT = '250 GB'; export const OBJ_STORAGE_NETWORK_TRANSFER_AMT = '1 TB'; @@ -22,17 +27,41 @@ export const EnableObjectStorageModal = React.memo( (props: EnableObjectStorageProps) => { const { handleSubmit, onClose, open, regionId } = props; + const { data: types, isError, isLoading } = useObjectStorageTypesQuery( + Boolean(regionId) + ); + + const isInvalidPrice = Boolean(regionId) && (!types || isError); + + const objectStorageType = types?.find( + (type) => type.id === 'objectstorage' + ); + + const price = regionId + ? getDCSpecificPriceByType({ + decimalPrecision: 0, + regionId, + type: objectStorageType, + }) + : objectStorageType?.price.monthly; + return ( ( { onClose(); handleSubmit(); }, + tooltipText: + !isLoading && isInvalidPrice + ? PRICES_RELOAD_ERROR_NOTICE_TEXT + : '', }} secondaryButtonProps={{ 'data-testid': 'cancel', @@ -51,7 +80,7 @@ export const EnableObjectStorageModal = React.memo( Object Storage costs a flat rate of{' '} - ${OBJ_STORAGE_PRICE.monthly}/month, and includes{' '} + ${price ?? UNKNOWN_PRICE}/month, and includes{' '} {OBJ_STORAGE_STORAGE_AMT} of storage. When you enable Object Storage,{' '} {OBJ_STORAGE_NETWORK_TRANSFER_AMT} of outbound data transfer will be added to your global network transfer pool. diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index c0dceb9e76e..d0cc05f9548 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -76,6 +76,8 @@ import { objectStorageBucketFactory, objectStorageClusterFactory, objectStorageKeyFactory, + objectStorageTypeFactory, + objectStorageOverageTypeFactory, paymentFactory, paymentMethodFactory, placementGroupFactory, @@ -919,6 +921,13 @@ export const handlers = [ ]; return HttpResponse.json(makeResourcePage(configs)); }), + http.get('*/v4/object-storage/types', () => { + const objectStorageTypes = [ + objectStorageTypeFactory.build(), + objectStorageOverageTypeFactory.build(), + ]; + return HttpResponse.json(makeResourcePage(objectStorageTypes)); + }), http.get('*object-storage/buckets/*/*/access', async () => { await sleep(2000); return HttpResponse.json({ diff --git a/packages/manager/src/queries/objectStorage.ts b/packages/manager/src/queries/objectStorage.ts index 052a81d7989..44de89ed1a9 100644 --- a/packages/manager/src/queries/objectStorage.ts +++ b/packages/manager/src/queries/objectStorage.ts @@ -20,11 +20,17 @@ import { getClusters, getObjectList, getObjectStorageKeys, + getObjectStorageTypes, getObjectURL, getSSLCert, uploadSSLCert, } from '@linode/api-v4'; -import { APIError, Params, ResourcePage } from '@linode/api-v4/lib/types'; +import { + APIError, + Params, + PriceType, + ResourcePage, +} from '@linode/api-v4/lib/types'; import { QueryClient, useInfiniteQuery, @@ -406,3 +412,16 @@ export const useBucketSSLDeleteMutation = (cluster: string, bucket: string) => { }, }); }; + +const getAllObjectStorageTypes = () => + getAll((params) => getObjectStorageTypes(params))().then( + (data) => data.data + ); + +export const useObjectStorageTypesQuery = (enabled = true) => + useQuery({ + queryFn: getAllObjectStorageTypes, + queryKey: [queryKey, 'types'], + ...queryPresets.oneTimeFetch, + enabled, + }); diff --git a/packages/manager/src/utilities/pricing/dynamicPricing.test.ts b/packages/manager/src/utilities/pricing/dynamicPricing.test.ts index 4381a738abc..56c73482029 100644 --- a/packages/manager/src/utilities/pricing/dynamicPricing.test.ts +++ b/packages/manager/src/utilities/pricing/dynamicPricing.test.ts @@ -75,6 +75,28 @@ describe('getDCSpecificPricingByType', () => { ).toBe('14.00'); }); + it('calculates dynamic pricing for a region without an increase on an hourly interval to the specified decimal', () => { + expect( + getDCSpecificPriceByType({ + decimalPrecision: 3, + interval: 'hourly', + regionId: 'us-east', + type: mockNodeBalancerType, + }) + ).toBe('0.015'); + }); + + it('calculates dynamic pricing for a region with an increase on an hourly interval to the specified decimal', () => { + expect( + getDCSpecificPriceByType({ + decimalPrecision: 3, + interval: 'hourly', + regionId: 'id-cgk', + type: mockNodeBalancerType, + }) + ).toBe('0.018'); + }); + it('calculates dynamic pricing for a volume based on size', () => { expect( getDCSpecificPriceByType({ diff --git a/packages/manager/src/utilities/pricing/dynamicPricing.ts b/packages/manager/src/utilities/pricing/dynamicPricing.ts index 90598a37e6f..b190b0c5d64 100644 --- a/packages/manager/src/utilities/pricing/dynamicPricing.ts +++ b/packages/manager/src/utilities/pricing/dynamicPricing.ts @@ -21,6 +21,16 @@ export interface DataCenterPricingOptions { } export interface DataCenterPricingByTypeOptions { + /** + * The number of decimal places to return for the price. + * @default 2 + */ + decimalPrecision?: number; + /** + * The time period for which to find pricing data for (hourly or monthly). + * @default monthly + */ + interval?: 'hourly' | 'monthly'; /** * The `id` of the region we intended to get the price for. * @example us-east @@ -94,6 +104,8 @@ export const getDCSpecificPrice = ({ * @returns a data center specific price or undefined if this cannot be calculated */ export const getDCSpecificPriceByType = ({ + decimalPrecision = 2, + interval = 'monthly', regionId, size, type, @@ -101,19 +113,18 @@ export const getDCSpecificPriceByType = ({ if (!regionId || !type) { return undefined; } - // Apply the DC-specific price if it exists; otherwise, use the base price. const price = type.region_prices.find((region_price: RegionPrice) => { return region_price.id === regionId; - })?.monthly ?? type.price.monthly; + })?.[interval] ?? type.price?.[interval]; // If pricing is determined by size of the entity if (size && price) { - return (size * price).toFixed(2); + return (size * price).toFixed(decimalPrecision); } - return price?.toFixed(2) ?? undefined; + return price?.toFixed(decimalPrecision) ?? undefined; }; export const renderMonthlyPriceToCorrectDecimalPlace = (