Skip to content

Commit c779e60

Browse files
committed
fix(pci-workflow): display distant region backup price
ref: #TAPC-4894 Signed-off-by: Adrien Turmo <[email protected]>
1 parent 3db90bc commit c779e60

File tree

10 files changed

+287
-50
lines changed

10 files changed

+287
-50
lines changed

packages/manager/apps/pci-workflow/src/api/hooks/order.tsx renamed to packages/manager/apps/pci-workflow/src/api/hooks/order/order.tsx

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo } from 'react';
1+
import { useCallback, useMemo } from 'react';
22

33
import { usePrefetchQuery, useQueries } from '@tanstack/react-query';
44

@@ -10,12 +10,15 @@ import {
1010

1111
import { useIsDistantBackupAvailable } from '@/api/hooks/feature';
1212
import { TInstance } from '@/api/hooks/instance/selector/instances.selector';
13+
import { getRegionPricing } from '@/api/hooks/order/selector/order.selector';
1314
import { useRegionTranslation } from '@/api/hooks/region';
1415
import { useMe } from '@/api/hooks/user';
1516
import { isSnapshotConsumption } from '@/pages/new/utils/is-snapshot-consumption';
17+
import { groupBy } from '@/utils';
1618

1719
export type ContinentRegion = Pick<TProductAvailabilityRegion, 'enabled' | 'name' | 'type'> & {
1820
label: string;
21+
price: number | null;
1922
};
2023

2124
export const useInstanceSnapshotPricing = (projectId: string, instanceId: TInstance['id']) => {
@@ -32,33 +35,25 @@ export const useInstanceSnapshotPricing = (projectId: string, instanceId: TInsta
3235
],
3336
});
3437

35-
const snapshotPlan = useMemo(
36-
() =>
37-
snapshotAvailabilities?.plans.find(
38-
({ code, regions }) =>
39-
isSnapshotConsumption(code) && regions.find((r) => r.name === instanceId.region),
40-
),
41-
[snapshotAvailabilities, instanceId.region],
42-
);
38+
const currentRegion = useMemo(() => {
39+
const currentPlan = snapshotAvailabilities?.plans.find(
40+
({ code, regions }) =>
41+
isSnapshotConsumption(code) && regions.find((r) => r.name === instanceId.region),
42+
);
4343

44-
const catalogAddon = useMemo(
45-
() => catalog?.addons.find(({ planCode }) => planCode === snapshotPlan.code),
46-
[catalog, snapshotPlan],
47-
);
44+
return currentPlan?.regions.find((r) => r.name === instanceId.region);
45+
}, [snapshotAvailabilities, instanceId]);
4846

49-
const currentRegion = useMemo(
50-
() => snapshotPlan?.regions.find((r) => r.name === instanceId.region),
51-
[snapshotPlan, instanceId],
52-
);
47+
const regionPriceCalculator = useCallback(getRegionPricing(snapshotAvailabilities, catalog), [
48+
snapshotAvailabilities,
49+
catalog,
50+
]);
5351

5452
return {
5553
isPending: !snapshotAvailabilities || !catalog,
5654
pricing: useMemo(
57-
() =>
58-
catalogAddon?.pricings.find(
59-
({ intervalUnit }) => intervalUnit === 'none' || intervalUnit === 'hour',
60-
) ?? null,
61-
[catalogAddon],
55+
() => regionPriceCalculator(instanceId.region),
56+
[instanceId, regionPriceCalculator],
6257
),
6358
distantContinents: useMemo(() => {
6459
if (
@@ -70,7 +65,7 @@ export const useInstanceSnapshotPricing = (projectId: string, instanceId: TInsta
7065
)
7166
return new Map<string, ContinentRegion[]>();
7267

73-
return Map.groupBy(
68+
return groupBy(
7469
snapshotAvailabilities.plans
7570
.filter(({ code }) => isSnapshotConsumption(code))
7671
.flatMap((p) => p.regions)
@@ -83,6 +78,7 @@ export const useInstanceSnapshotPricing = (projectId: string, instanceId: TInsta
8378
.map((r) => ({
8479
...r,
8580
label: translateMicroRegion(r.name) || r.name,
81+
price: regionPriceCalculator(r.name)?.price,
8682
})),
8783
(r) => translateContinent(r.name) || 'Internal',
8884
);
@@ -93,6 +89,7 @@ export const useInstanceSnapshotPricing = (projectId: string, instanceId: TInsta
9389
translateMicroRegion,
9490
translateContinent,
9591
isDistantBackupAvailable,
92+
regionPriceCalculator,
9693
]),
9794
};
9895
};
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { describe } from 'vitest';
2+
3+
import { TCatalog, TProductAvailability } from '@ovh-ux/manager-pci-common';
4+
5+
import { getRegionPricing } from '@/api/hooks/order/selector/order.selector';
6+
7+
describe('order selector', () => {
8+
describe('getRegionPricing', () => {
9+
it('should get the princing for the snapshot.consumption plan corresponding to the region', () => {
10+
const region = 'currentRegion';
11+
12+
const snapshotAvailabilities = {
13+
plans: [
14+
{
15+
code: 'snapshot.consumption.3AZ',
16+
regions: [{ name: region }],
17+
},
18+
{
19+
code: 'snapshot.consumption',
20+
regions: [{ name: 'otherRegion' }],
21+
},
22+
{
23+
code: 'volume.snapshot.consumption',
24+
regions: [{ name: region }],
25+
},
26+
],
27+
} as TProductAvailability;
28+
29+
const catalog = {
30+
addons: [
31+
{
32+
planCode: 'snapshot.consumption.3AZ',
33+
pricings: [
34+
{ intervalUnit: 'month', price: 10500 },
35+
{ intervalUnit: 'hour', price: 2500 },
36+
],
37+
},
38+
{
39+
planCode: 'snapshot.consumption',
40+
pricings: [{ intervalUnit: 'month', price: 20000 }],
41+
},
42+
],
43+
} as TCatalog;
44+
45+
const result = getRegionPricing(snapshotAvailabilities, catalog)(region);
46+
47+
expect(result).toEqual({ intervalUnit: 'hour', price: 2500 });
48+
});
49+
50+
it('should return null if no plan correspond to the region', () => {
51+
const region = 'currentRegion';
52+
53+
const snapshotAvailabilities = {
54+
plans: [
55+
{
56+
code: 'snapshot.consumption',
57+
regions: [{ name: 'otherRegion' }],
58+
},
59+
],
60+
} as TProductAvailability;
61+
62+
const catalog = {
63+
addons: [
64+
{
65+
planCode: 'snapshot.consumption.3AZ',
66+
pricings: [
67+
{ intervalUnit: 'month', price: 10500 },
68+
{ intervalUnit: 'hour', price: 2500 },
69+
],
70+
},
71+
{
72+
planCode: 'snapshot.consumption',
73+
pricings: [{ intervalUnit: 'month', price: 20000 }],
74+
},
75+
],
76+
} as TCatalog;
77+
78+
const result = getRegionPricing(snapshotAvailabilities, catalog)(region);
79+
80+
expect(result).toBeNull();
81+
});
82+
83+
it('should return null if no pricing correspond to plan', () => {
84+
const region = 'currentRegion';
85+
86+
const snapshotAvailabilities = {
87+
plans: [
88+
{
89+
code: 'snapshot.consumption.3AZ',
90+
regions: [{ name: region }],
91+
},
92+
{
93+
code: 'snapshot.consumption',
94+
regions: [{ name: 'otherRegion' }],
95+
},
96+
{
97+
code: 'volume.snapshot.consumption',
98+
regions: [{ name: region }],
99+
},
100+
],
101+
} as TProductAvailability;
102+
103+
const catalog = {
104+
addons: [
105+
{
106+
planCode: 'snapshot.consumption',
107+
pricings: [{ intervalUnit: 'month', price: 20000 }],
108+
},
109+
],
110+
} as TCatalog;
111+
112+
const result = getRegionPricing(snapshotAvailabilities, catalog)(region);
113+
114+
expect(result).toBeNull();
115+
});
116+
});
117+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { TCatalog, TProductAvailability } from '@ovh-ux/manager-pci-common';
2+
3+
import { isSnapshotConsumption } from '@/pages/new/utils/is-snapshot-consumption';
4+
5+
export const getRegionPricing =
6+
(snapshotAvailabilities: TProductAvailability, catalog: TCatalog) => (region: string) => {
7+
const regionPlan =
8+
snapshotAvailabilities?.plans.find(
9+
(plan) => isSnapshotConsumption(plan.code) && plan.regions.some((r) => r.name === region),
10+
) || null;
11+
12+
if (!catalog || !regionPlan) return null;
13+
14+
return (
15+
catalog.addons
16+
.find((addon) => addon.planCode === regionPlan.code)
17+
?.pricings.find(({ intervalUnit }) => intervalUnit === 'none' || intervalUnit === 'hour') ??
18+
null
19+
);
20+
};

packages/manager/apps/pci-workflow/src/pages/new/New.page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
} from '@ovh-ux/manager-react-components';
2222

2323
import { usePrefetchInstances } from '@/api/hooks/instance/useInstances';
24-
import { usePrefetchSnapshotPricing } from '@/api/hooks/order';
24+
import { usePrefetchSnapshotPricing } from '@/api/hooks/order/order';
2525
import { useAddWorkflow } from '@/api/hooks/workflows';
2626
import { useSafeParam } from '@/hooks/useSafeParam';
2727

packages/manager/apps/pci-workflow/src/pages/new/steps/WorkflowName.component.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { vi } from 'vitest';
44
import { useCatalogPrice, useMe } from '@ovh-ux/manager-react-components';
55

66
import { buildInstanceId } from '@/api/hooks/instance/selector/instances.selector';
7-
import { useInstanceSnapshotPricing } from '@/api/hooks/order';
7+
import { useInstanceSnapshotPricing } from '@/api/hooks/order/order';
88

99
import { WorkflowName } from './WorkflowName.component';
1010

1111
vi.mock('@ovh-ux/manager-react-components');
12-
vi.mock('@/api/hooks/order');
12+
vi.mock('@/api/hooks/order/order');
1313

1414
describe('WorkflowName', () => {
1515
beforeEach(() => {

packages/manager/apps/pci-workflow/src/pages/new/steps/WorkflowName.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
import { convertHourlyPriceToMonthly, useCatalogPrice } from '@ovh-ux/manager-react-components';
2222

2323
import { TInstance } from '@/api/hooks/instance/selector/instances.selector';
24-
import { useInstanceSnapshotPricing } from '@/api/hooks/order';
24+
import { useInstanceSnapshotPricing } from '@/api/hooks/order/order';
2525
import { StepState } from '@/pages/new/hooks/useStep';
2626

2727
interface WorkflowNameProps {

packages/manager/apps/pci-workflow/src/pages/new/steps/WorkflowScheduling.component.spec.tsx

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@ import { describe, it, vi } from 'vitest';
44

55
import { renderWithMockedWrappers } from '@/__tests__/renderWithMockedWrappers';
66
import { buildInstanceId } from '@/api/hooks/instance/selector/instances.selector';
7-
import { ContinentRegion, useInstanceSnapshotPricing } from '@/api/hooks/order';
7+
import { ContinentRegion, useInstanceSnapshotPricing } from '@/api/hooks/order/order';
88
import { StepState } from '@/pages/new/hooks/useStep';
99

1010
import { WorkflowScheduling } from './WorkflowScheduling.component';
1111

12-
vi.mock('@/api/hooks/order');
12+
vi.mock('@/api/hooks/order/order');
13+
vi.mock('@ovh-ux/manager-react-components', () => ({
14+
convertHourlyPriceToMonthly: vi.fn(),
15+
useCatalogPrice: vi.fn().mockReturnValue({
16+
getFormattedCatalogPrice: (input: number) => `${input}`,
17+
}),
18+
}));
1319

1420
describe('WorkflowScheduling Component', () => {
1521
const mockOnSubmit = vi.fn();
@@ -90,14 +96,24 @@ describe('WorkflowScheduling Component', () => {
9096
it('can select distant region when distantContinents is provided', async () => {
9197
vi.mocked(useInstanceSnapshotPricing).mockReturnValue({
9298
distantContinents: new Map<string, ContinentRegion[]>([
93-
['Europe', [{ label: 'Region 1', name: 'region1', enabled: true } as ContinentRegion]],
99+
[
100+
'Europe',
101+
[
102+
{
103+
label: 'Region 1',
104+
name: 'region1',
105+
enabled: true,
106+
price: 1500,
107+
} as ContinentRegion,
108+
],
109+
],
94110
]),
95111
pricing: null,
96112
isPending: false,
97113
});
98114

99115
const user = userEvent.setup();
100-
const { getByLabelText, getByRole } = renderWithMockedWrappers(
116+
const { getByLabelText, getByRole, findByText } = renderWithMockedWrappers(
101117
<WorkflowScheduling
102118
step={stepUnlocked as StepState}
103119
onSubmit={mockOnSubmit}
@@ -106,22 +122,27 @@ describe('WorkflowScheduling Component', () => {
106122
);
107123

108124
const distantToggle = getByLabelText(/pci_workflow_create_distant_label/);
109-
expect(distantToggle).toBeInTheDocument();
125+
expect(distantToggle).toBeVisible();
110126

111127
await act(async () => {
112128
await user.click(distantToggle);
113129
});
114130

115131
// ODS 19 combobox labels are broken so we get by role hidden instead
116132
const distantRegion = getByRole('combobox');
117-
expect(distantRegion).toBeInTheDocument();
133+
expect(distantRegion).toBeVisible();
118134

119135
await act(async () => {
120136
await user.click(distantRegion);
121137
});
122138

123139
// We only test if the option is here because ODS a11y doesn't work and doesn't trigger a value change
124-
const region1Option = getByRole('option', { name: 'Region 1', hidden: true });
125-
expect(region1Option).toBeInTheDocument();
140+
const region1Option = getByRole('option', { name: 'Region 1' });
141+
expect(region1Option).toBeVisible();
142+
143+
// It would have been better to use user.click on the element but it doesn't work
144+
act(() => region1Option.click());
145+
146+
expect(await findByText('pci_workflow_create_price_monthly')).toBeVisible();
126147
});
127148
});

0 commit comments

Comments
 (0)