From deed3c23c0edfbc46c7a1ac01b3d4b54822b2d2d Mon Sep 17 00:00:00 2001 From: David LY <153214527+dlymonkai@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:27:03 +0200 Subject: [PATCH] Added Pricing API routes (#855) * Added state management for pricing api routes * Added post / delete / patch API routes to @monkvision/network --- .../src/state/actions/createdOnePricing.ts | 59 +++++++ .../src/state/actions/deletedOnePricing.ts | 64 +++++++ packages/common/src/state/actions/index.ts | 4 + .../common/src/state/actions/monkAction.ts | 24 ++- .../updatedOneInspectionAdditionalData.ts | 61 +++++++ .../src/state/actions/updatedOnePricing.ts | 56 +++++++ packages/common/src/state/reducer.ts | 25 +++ packages/common/src/state/state.ts | 6 + .../state/actions/createdOnePricing.test.ts | 57 +++++++ .../state/actions/deletedOnePricing.test.ts | 53 ++++++ .../state/actions/gotOneInspection.test.ts | 10 ++ ...updatedOneInspectionAdditionalData.test.ts | 52 ++++++ .../state/actions/updatedOnePricing.test.ts | 58 +++++++ packages/common/test/state/reducer.test.ts | 28 ++++ .../hooks/usePhotoCaptureSightState.test.ts | 1 + packages/network/README.md | 53 ++++++ packages/network/src/api/api.ts | 7 +- .../network/src/api/inspection/mappers.ts | 38 +++-- .../network/src/api/inspection/requests.ts | 59 ++++++- packages/network/src/api/models/pricingV2.ts | 9 + packages/network/src/api/pricing/index.ts | 2 + packages/network/src/api/pricing/mappers.ts | 31 ++++ packages/network/src/api/pricing/requests.ts | 156 ++++++++++++++++++ packages/network/src/api/pricing/types.ts | 41 +++++ packages/network/src/api/react.ts | 27 +++ .../data/apiInspectionGet.data.json | 35 +++- .../inspection/data/apiInspectionGet.data.ts | 44 ++++- .../test/api/inspection/requests.test.ts | 69 +++++++- .../network/test/api/pricing/mappers.test.ts | 31 ++++ .../network/test/api/pricing/requests.test.ts | 136 +++++++++++++++ packages/network/test/api/react.test.ts | 30 ++++ packages/types/src/state/entity.ts | 1 + packages/types/src/state/inspection.ts | 3 +- packages/types/src/state/pricingV2.ts | 18 +- 34 files changed, 1297 insertions(+), 51 deletions(-) create mode 100644 packages/common/src/state/actions/createdOnePricing.ts create mode 100644 packages/common/src/state/actions/deletedOnePricing.ts create mode 100644 packages/common/src/state/actions/updatedOneInspectionAdditionalData.ts create mode 100644 packages/common/src/state/actions/updatedOnePricing.ts create mode 100644 packages/common/test/state/actions/createdOnePricing.test.ts create mode 100644 packages/common/test/state/actions/deletedOnePricing.test.ts create mode 100644 packages/common/test/state/actions/updatedOneInspectionAdditionalData.test.ts create mode 100644 packages/common/test/state/actions/updatedOnePricing.test.ts create mode 100644 packages/network/src/api/pricing/index.ts create mode 100644 packages/network/src/api/pricing/mappers.ts create mode 100644 packages/network/src/api/pricing/requests.ts create mode 100644 packages/network/src/api/pricing/types.ts create mode 100644 packages/network/test/api/pricing/mappers.test.ts create mode 100644 packages/network/test/api/pricing/requests.test.ts diff --git a/packages/common/src/state/actions/createdOnePricing.ts b/packages/common/src/state/actions/createdOnePricing.ts new file mode 100644 index 000000000..6e584a723 --- /dev/null +++ b/packages/common/src/state/actions/createdOnePricing.ts @@ -0,0 +1,59 @@ +import { PricingV2 } from '@monkvision/types'; +import { MonkAction, MonkActionType } from './monkAction'; +import { MonkState } from '../state'; + +/** + * The payload of a MonkCreatedOnePricingPayload. + */ +export interface MonkCreatedOnePricingPayload { + /** + * The pricing created. + */ + pricing: PricingV2; +} + +/** + * Action dispatched when a vehicle have been updated. + */ +export interface MonkCreatedOnePricingAction extends MonkAction { + /** + * The type of the action : `MonkActionType.CREATED_ONE_PRICING`. + */ + type: MonkActionType.CREATED_ONE_PRICING; + /** + * The payload of the action containing the fetched entities. + */ + payload: MonkCreatedOnePricingPayload; +} + +/** + * Matcher function that matches a CreatedOnePricing while also inferring its type using TypeScript's type predicate + * feature. + */ +export function isCreatedOnePricingAction( + action: MonkAction, +): action is MonkCreatedOnePricingAction { + return action.type === MonkActionType.CREATED_ONE_PRICING; +} + +/** + * Reducer function for a createdOnePricing action. + */ +export function createdOnePricing( + state: MonkState, + action: MonkCreatedOnePricingAction, +): MonkState { + const { pricings, inspections } = state; + const { payload } = action; + + const inspection = inspections.find((value) => value.id === payload.pricing.inspectionId); + if (inspection) { + inspection.pricings?.push(action.payload.pricing.id); + } + pricings.push(action.payload.pricing); + return { + ...state, + pricings: [...pricings], + inspections: [...inspections], + }; +} diff --git a/packages/common/src/state/actions/deletedOnePricing.ts b/packages/common/src/state/actions/deletedOnePricing.ts new file mode 100644 index 000000000..d2be9e5f2 --- /dev/null +++ b/packages/common/src/state/actions/deletedOnePricing.ts @@ -0,0 +1,64 @@ +import { MonkAction, MonkActionType } from './monkAction'; +import { MonkState } from '../state'; + +/** + * The payload of a MonkDeletedOnePricingPayload. + */ +export interface MonkDeletedtedOnePricingPayload { + /** + * The ID of the inspection to which the pricing was deleted. + */ + inspectionId: string; + /** + * The pricing ID deleted. + */ + pricingId: string; +} + +/** + * Action dispatched when a pricing have been deleted. + */ +export interface MonkDeletedOnePricingAction extends MonkAction { + /** + * The type of the action : `MonkActionType.DELETED_ONE_PRICING`. + */ + type: MonkActionType.DELETED_ONE_PRICING; + /** + * The payload of the action containing the fetched entities. + */ + payload: MonkDeletedtedOnePricingPayload; +} + +/** + * Matcher function that matches a DeletedOnePricing while also inferring its type using TypeScript's type predicate + * feature. + */ +export function isDeletedOnePricingAction( + action: MonkAction, +): action is MonkDeletedOnePricingAction { + return action.type === MonkActionType.DELETED_ONE_PRICING; +} + +/** + * Reducer function for a deletedOnePricing action. + */ +export function deletedOnePricing( + state: MonkState, + action: MonkDeletedOnePricingAction, +): MonkState { + const { pricings, inspections } = state; + const { payload } = action; + + const inspection = inspections.find((value) => value.id === payload.inspectionId); + if (inspection) { + inspection.pricings = inspection.pricings?.filter( + (pricingId) => pricingId !== payload.pricingId, + ); + } + const newPricings = pricings.filter((pricing) => pricing.id !== payload.pricingId); + return { + ...state, + pricings: newPricings, + inspections: [...inspections], + }; +} diff --git a/packages/common/src/state/actions/index.ts b/packages/common/src/state/actions/index.ts index eb8f83e06..496420e68 100644 --- a/packages/common/src/state/actions/index.ts +++ b/packages/common/src/state/actions/index.ts @@ -4,3 +4,7 @@ export * from './gotOneInspection'; export * from './createdOneImage'; export * from './updatedManyTasks'; export * from './updatedVehicle'; +export * from './createdOnePricing'; +export * from './deletedOnePricing'; +export * from './updatedOnePricing'; +export * from './updatedOneInspectionAdditionalData'; diff --git a/packages/common/src/state/actions/monkAction.ts b/packages/common/src/state/actions/monkAction.ts index 33fc16d41..53ad2625c 100644 --- a/packages/common/src/state/actions/monkAction.ts +++ b/packages/common/src/state/actions/monkAction.ts @@ -6,6 +6,10 @@ export enum MonkActionType { * An inspection has been fetched from the API. */ GOT_ONE_INSPECTION = 'got_one_inspection', + /** + * An inspection additional data has been updated. + */ + UPDATED_ONE_INSPECTION_ADDITIONAL_DATA = 'updated_one_inspection_additional_data', /** * An image has been uploaded to the API. */ @@ -14,14 +18,26 @@ export enum MonkActionType { * One or more tasks have been updated. */ UPDATED_MANY_TASKS = 'updated_many_tasks', - /** - * Clear and reset the state. - */ - RESET_STATE = 'reset_state', /** * A vehicle has been updated. */ UPDATED_VEHICLE = 'updated_vehicle', + /** + * A pricing has been uploaded to the API. + */ + CREATED_ONE_PRICING = 'created_one_pricing', + /** + * A pricing has been updated. + */ + UPDATED_ONE_PRICING = 'updated_one_pricing', + /** + * A pricing has been deleted. + */ + DELETED_ONE_PRICING = 'deleted_one_pricing', + /** + * Clear and reset the state. + */ + RESET_STATE = 'reset_state', } /** diff --git a/packages/common/src/state/actions/updatedOneInspectionAdditionalData.ts b/packages/common/src/state/actions/updatedOneInspectionAdditionalData.ts new file mode 100644 index 000000000..a96d30ce4 --- /dev/null +++ b/packages/common/src/state/actions/updatedOneInspectionAdditionalData.ts @@ -0,0 +1,61 @@ +import { AdditionalData } from '@monkvision/types'; +import { MonkAction, MonkActionType } from './monkAction'; +import { MonkState } from '../state'; + +/** + * The payload of a MonkUpdatedOneInspectionAdditionalDataPayload. + */ +export interface MonkUpdatedOneInspectionAdditionalDataPayload { + /** + * The ID of the inspection to which the pricing was updated. + */ + inspectionId: string; + /** + * Additional data used for the update operation. + */ + additionalData?: AdditionalData; +} + +/** + * Action dispatched when a inspection have been updated. + */ +export interface MonkUpdatedOneInspectionAdditionalDataAction extends MonkAction { + /** + * The type of the action : `MonkActionType.UPDATED_ONE_INSPECTION_ADDITIONAL_DATA`. + */ + type: MonkActionType.UPDATED_ONE_INSPECTION_ADDITIONAL_DATA; + /** + * The payload of the action containing the fetched entities. + */ + payload: MonkUpdatedOneInspectionAdditionalDataPayload; +} + +/** + * Matcher function that matches a UpdatedOneInspection while also inferring its type using TypeScript's type predicate + * feature. + */ +export function isUpdatedOneInspectionAdditionalDataAction( + action: MonkAction, +): action is MonkUpdatedOneInspectionAdditionalDataAction { + return action.type === MonkActionType.UPDATED_ONE_INSPECTION_ADDITIONAL_DATA; +} + +/** + * Reducer function for a UpdatedOneInspection action. + */ +export function updatedOneInspectionAdditionalData( + state: MonkState, + action: MonkUpdatedOneInspectionAdditionalDataAction, +): MonkState { + const { inspections } = state; + const { payload } = action; + + const inspection = inspections.find((value) => value.id === payload.inspectionId); + if (inspection) { + inspection.additionalData = payload.additionalData; + } + return { + ...state, + inspections: [...inspections], + }; +} diff --git a/packages/common/src/state/actions/updatedOnePricing.ts b/packages/common/src/state/actions/updatedOnePricing.ts new file mode 100644 index 000000000..3b6a026c5 --- /dev/null +++ b/packages/common/src/state/actions/updatedOnePricing.ts @@ -0,0 +1,56 @@ +import { PricingV2 } from '@monkvision/types'; +import { MonkAction, MonkActionType } from './monkAction'; +import { MonkState } from '../state'; + +/** + * The payload of a MonkUpdatedOnePricingPayload. + */ +export interface MonkUpdatedOnePricingPayload { + /** + * The pricing created. + */ + pricing: PricingV2; +} + +/** + * Action dispatched when a pricing have been updated. + */ +export interface MonkUpdatedOnePricingAction extends MonkAction { + /** + * The type of the action : `MonkActionType.UPDATED_ONE_PRICING`. + */ + type: MonkActionType.UPDATED_ONE_PRICING; + /** + * The payload of the action containing the fetched entities. + */ + payload: MonkUpdatedOnePricingPayload; +} + +/** + * Matcher function that matches a updatedOnePricing while also inferring its type using TypeScript's type predicate + * feature. + */ +export function isUpdatedOnePricingAction( + action: MonkAction, +): action is MonkUpdatedOnePricingAction { + return action.type === MonkActionType.UPDATED_ONE_PRICING; +} + +/** + * Reducer function for a updatedOnePricing action. + */ +export function updatedOnePricing( + state: MonkState, + action: MonkUpdatedOnePricingAction, +): MonkState { + const { pricings } = state; + const { payload } = action; + + const updatedPricings = pricings.map((pricing) => + pricing.id === payload.pricing.id ? { ...pricing, ...payload.pricing } : pricing, + ); + return { + ...state, + pricings: updatedPricings, + }; +} diff --git a/packages/common/src/state/reducer.ts b/packages/common/src/state/reducer.ts index d8ad50b23..713b6bd7b 100644 --- a/packages/common/src/state/reducer.ts +++ b/packages/common/src/state/reducer.ts @@ -5,9 +5,19 @@ import { isGotOneInspectionAction, isResetStateAction, isUpdatedManyTasksAction, + isCreatedOnePricingAction, + isDeletedOnePricingAction, + isUpdatedOnePricingAction, + isUpdatedOneInspectionAdditionalDataAction, + isUpdatedVehicleAction, MonkAction, resetState, updatedManyTasks, + createdOnePricing, + deletedOnePricing, + updatedOnePricing, + updatedOneInspectionAdditionalData, + updatedVehicle, } from './actions'; import { MonkState } from './state'; @@ -21,11 +31,26 @@ export function monkReducer(state: MonkState, action: MonkAction): MonkState { if (isGotOneInspectionAction(action)) { return gotOneInspection(state, action); } + if (isUpdatedOneInspectionAdditionalDataAction(action)) { + return updatedOneInspectionAdditionalData(state, action); + } if (isCreatedOneImageAction(action)) { return createdOneImage(state, action); } if (isUpdatedManyTasksAction(action)) { return updatedManyTasks(state, action); } + if (isCreatedOnePricingAction(action)) { + return createdOnePricing(state, action); + } + if (isDeletedOnePricingAction(action)) { + return deletedOnePricing(state, action); + } + if (isUpdatedOnePricingAction(action)) { + return updatedOnePricing(state, action); + } + if (isUpdatedVehicleAction(action)) { + return updatedVehicle(state, action); + } return state; } diff --git a/packages/common/src/state/state.ts b/packages/common/src/state/state.ts index 6db82f8cd..fb0aa044a 100644 --- a/packages/common/src/state/state.ts +++ b/packages/common/src/state/state.ts @@ -4,6 +4,7 @@ import { Inspection, Part, PartOperation, + PricingV2, RenderedOutput, SeverityResult, Task, @@ -55,6 +56,10 @@ export interface MonkState { * The views created during inspections. */ views: View[]; + /** + * The pricings created during inspections. + */ + pricings: PricingV2[]; } /** @@ -72,5 +77,6 @@ export function createEmptyMonkState(): MonkState { tasks: [], vehicles: [], views: [], + pricings: [], }; } diff --git a/packages/common/test/state/actions/createdOnePricing.test.ts b/packages/common/test/state/actions/createdOnePricing.test.ts new file mode 100644 index 000000000..abf0a867b --- /dev/null +++ b/packages/common/test/state/actions/createdOnePricing.test.ts @@ -0,0 +1,57 @@ +import { + createEmptyMonkState, + MonkActionType, + createdOnePricing, + MonkCreatedOnePricingAction, + isCreatedOnePricingAction, +} from '../../../src'; +import { Inspection, MonkEntityType, PricingV2RelatedItemType } from '@monkvision/types'; + +const action: MonkCreatedOnePricingAction = { + type: MonkActionType.CREATED_ONE_PRICING, + payload: { + pricing: { + entityType: MonkEntityType.PRICING, + id: 'test-id', + inspectionId: 'inspections-test', + relatedItemType: PricingV2RelatedItemType.PART, + pricing: 10, + }, + }, +}; + +describe('CreatedOnePricing action handlers', () => { + describe('Action matcher', () => { + it('should return true if the action has the proper type', () => { + expect(isCreatedOnePricingAction({ type: MonkActionType.CREATED_ONE_PRICING })).toBe(true); + }); + + it('should return false if the action does not have the proper type', () => { + expect(isCreatedOnePricingAction({ type: MonkActionType.RESET_STATE })).toBe(false); + }); + }); + + describe('Action handler', () => { + it('should return a new state', () => { + const state = createEmptyMonkState(); + expect(Object.is(createdOnePricing(state, action), state)).toBe(false); + }); + + it('should create pricing in the state', () => { + const state = createEmptyMonkState(); + state.inspections.push({ + id: 'inspections-test', + pricings: [] as string[], + } as Inspection); + const newState = createdOnePricing(state, action); + const inspectionPricing = newState.inspections.find( + (ins) => ins.id === action.payload.pricing.inspectionId, + )?.pricings; + expect(inspectionPricing?.length).toBe(1); + expect(inspectionPricing).toContainEqual(action.payload.pricing.id); + expect(newState.pricings).toContainEqual({ + ...action.payload.pricing, + }); + }); + }); +}); diff --git a/packages/common/test/state/actions/deletedOnePricing.test.ts b/packages/common/test/state/actions/deletedOnePricing.test.ts new file mode 100644 index 000000000..86364cc27 --- /dev/null +++ b/packages/common/test/state/actions/deletedOnePricing.test.ts @@ -0,0 +1,53 @@ +import { + createEmptyMonkState, + MonkActionType, + MonkDeletedOnePricingAction, + isDeletedOnePricingAction, + deletedOnePricing, +} from '../../../src'; +import { Inspection, PricingV2 } from '@monkvision/types'; + +const action: MonkDeletedOnePricingAction = { + type: MonkActionType.DELETED_ONE_PRICING, + payload: { + inspectionId: 'inspections-test', + pricingId: 'pricing-id-test', + }, +}; + +describe('DeletedOnePricing action handlers', () => { + describe('Action matcher', () => { + it('should return true if the action has the proper type', () => { + expect(isDeletedOnePricingAction({ type: MonkActionType.DELETED_ONE_PRICING })).toBe(true); + }); + + it('should return false if the action does not have the proper type', () => { + expect(isDeletedOnePricingAction({ type: MonkActionType.RESET_STATE })).toBe(false); + }); + }); + + describe('Action handler', () => { + it('should return a new state', () => { + const state = createEmptyMonkState(); + expect(Object.is(deletedOnePricing(state, action), state)).toBe(false); + }); + + it('should delete pricing in the state', () => { + const state = createEmptyMonkState(); + state.inspections.push({ + id: 'inspections-test', + pricings: [action.payload.pricingId], + } as Inspection); + state.pricings.push({ id: action.payload.pricingId } as PricingV2); + const newState = deletedOnePricing(state, action); + const inspectionPricing = newState.inspections.find( + (ins) => ins.id === action.payload.inspectionId, + )?.pricings; + expect(inspectionPricing?.length).toBe(0); + expect(inspectionPricing).not.toContainEqual(action.payload.pricingId); + expect( + newState.pricings.find((pricing) => pricing.id === action.payload.pricingId), + ).toBeUndefined(); + }); + }); +}); diff --git a/packages/common/test/state/actions/gotOneInspection.test.ts b/packages/common/test/state/actions/gotOneInspection.test.ts index 83d67fe31..86ff94f6e 100644 --- a/packages/common/test/state/actions/gotOneInspection.test.ts +++ b/packages/common/test/state/actions/gotOneInspection.test.ts @@ -13,6 +13,8 @@ import { Inspection, Part, PartOperation, + PricingV2, + PricingV2RelatedItemType, RenderedOutput, SeverityResult, Task, @@ -34,6 +36,7 @@ const action: MonkGotOneInspectionAction = { tasks: [{ id: 'tasks-test' } as Task], vehicles: [{ id: 'vehicles-test' } as Vehicle], views: [{ id: 'views-test' } as View], + pricings: [{ id: 'pricings-test' } as PricingV2], }, }; @@ -66,6 +69,7 @@ describe('GotOneInspection action handlers', () => { tasks: [{ id: 'tasks-test-111111' } as Task], vehicles: [{ id: 'vehicles-test-111111' } as Vehicle], views: [{ id: 'views-test-111111' } as View], + pricings: [{ id: 'pricings-test-111111' } as PricingV2], }; const newState = gotOneInspection(state, action); Object.keys(createEmptyMonkState()).forEach((key) => { @@ -87,6 +91,12 @@ describe('GotOneInspection action handlers', () => { tasks: [{ id: 'tasks-test', images: ['ok'] } as Task], vehicles: [{ id: 'vehicles-test', type: 'nice' } as Vehicle], views: [{ id: 'views-test', elementId: 'ww' } as View], + pricings: [ + { + id: 'pricings-test', + relatedItemType: PricingV2RelatedItemType.PART, + } as PricingV2, + ], }; const newState = gotOneInspection(state, action); Object.keys(createEmptyMonkState()).forEach((key) => { diff --git a/packages/common/test/state/actions/updatedOneInspectionAdditionalData.test.ts b/packages/common/test/state/actions/updatedOneInspectionAdditionalData.test.ts new file mode 100644 index 000000000..fd4faf5b8 --- /dev/null +++ b/packages/common/test/state/actions/updatedOneInspectionAdditionalData.test.ts @@ -0,0 +1,52 @@ +import { + createEmptyMonkState, + MonkActionType, + MonkUpdatedOneInspectionAdditionalDataAction, + isUpdatedOneInspectionAdditionalDataAction, + updatedOneInspectionAdditionalData, +} from '../../../src'; +import { Inspection } from '@monkvision/types'; + +const action: MonkUpdatedOneInspectionAdditionalDataAction = { + type: MonkActionType.UPDATED_ONE_INSPECTION_ADDITIONAL_DATA, + payload: { + inspectionId: 'inspections-test', + additionalData: { 'add-data-test': 'additionalData-test' }, + }, +}; + +describe('UpdatedOne action handlers', () => { + describe('Action matcher', () => { + it('should return true if the action has the proper type', () => { + expect( + isUpdatedOneInspectionAdditionalDataAction({ + type: MonkActionType.UPDATED_ONE_INSPECTION_ADDITIONAL_DATA, + }), + ).toBe(true); + }); + + it('should return false if the action does not have the proper type', () => { + expect(isUpdatedOneInspectionAdditionalDataAction({ type: MonkActionType.RESET_STATE })).toBe( + false, + ); + }); + }); + + describe('Action handler', () => { + it('should return a new state', () => { + const state = createEmptyMonkState(); + expect(Object.is(updatedOneInspectionAdditionalData(state, action), state)).toBe(false); + }); + + it('should update pricing in the state', () => { + const state = createEmptyMonkState(); + state.inspections.push({ + id: 'inspections-test', + additionalData: {}, + } as Inspection); + const newState = updatedOneInspectionAdditionalData(state, action); + const inspection = newState.inspections.find((ins) => ins.id === action.payload.inspectionId); + expect(inspection?.additionalData).toEqual(action.payload.additionalData); + }); + }); +}); diff --git a/packages/common/test/state/actions/updatedOnePricing.test.ts b/packages/common/test/state/actions/updatedOnePricing.test.ts new file mode 100644 index 000000000..e084427f6 --- /dev/null +++ b/packages/common/test/state/actions/updatedOnePricing.test.ts @@ -0,0 +1,58 @@ +import { + createEmptyMonkState, + MonkActionType, + MonkUpdatedOnePricingAction, + isUpdatedOnePricingAction, + updatedOnePricing, +} from '../../../src'; +import { Inspection, MonkEntityType, PricingV2RelatedItemType } from '@monkvision/types'; + +const action: MonkUpdatedOnePricingAction = { + type: MonkActionType.UPDATED_ONE_PRICING, + payload: { + pricing: { + entityType: MonkEntityType.PRICING, + id: 'test-id', + inspectionId: 'inspections-test', + relatedItemType: PricingV2RelatedItemType.PART, + pricing: 10, + }, + }, +}; + +describe('UpdatedOnePricing action handlers', () => { + describe('Action matcher', () => { + it('should return true if the action has the proper type', () => { + expect(isUpdatedOnePricingAction({ type: MonkActionType.UPDATED_ONE_PRICING })).toBe(true); + }); + + it('should return false if the action does not have the proper type', () => { + expect(isUpdatedOnePricingAction({ type: MonkActionType.RESET_STATE })).toBe(false); + }); + }); + + describe('Action handler', () => { + it('should return a new state', () => { + const state = createEmptyMonkState(); + expect(Object.is(updatedOnePricing(state, action), state)).toBe(false); + }); + + it('should update pricing in the state', () => { + const state = createEmptyMonkState(); + state.inspections.push({ + id: 'inspections-test', + pricings: [action.payload.pricing.id], + } as Inspection); + state.pricings.push({ ...action.payload.pricing, pricing: 90 }); + const newState = updatedOnePricing(state, action); + const inspectionPricing = newState.inspections.find( + (ins) => ins.id === action.payload.pricing.inspectionId, + )?.pricings; + expect(inspectionPricing?.length).toBe(1); + expect(inspectionPricing).toContainEqual(action.payload.pricing.id); + expect(newState.pricings).toContainEqual({ + ...action.payload.pricing, + }); + }); + }); +}); diff --git a/packages/common/test/state/reducer.test.ts b/packages/common/test/state/reducer.test.ts index 1441c8190..57c804fc8 100644 --- a/packages/common/test/state/reducer.test.ts +++ b/packages/common/test/state/reducer.test.ts @@ -3,19 +3,39 @@ jest.mock('../../src/state/actions', () => ({ isGotOneInspectionAction: jest.fn(() => false), isResetStateAction: jest.fn(() => false), isUpdatedManyTasksAction: jest.fn(() => false), + isCreatedOnePricingAction: jest.fn(() => false), + isDeletedOnePricingAction: jest.fn(() => false), + isUpdatedOnePricingAction: jest.fn(() => false), + isUpdatedOneInspectionAdditionalDataAction: jest.fn(() => false), + isUpdatedVehicleAction: jest.fn(() => false), createdOneImage: jest.fn(() => null), gotOneInspection: jest.fn(() => null), resetState: jest.fn(() => null), updatedManyTasks: jest.fn(() => null), + createdOnePricing: jest.fn(() => null), + deletedOnePricing: jest.fn(() => null), + updatedOnePricing: jest.fn(() => null), + updatedOneInspectionAdditionalData: jest.fn(() => null), + updatedVehicle: jest.fn(() => null), })); import { createdOneImage, gotOneInspection, + createdOnePricing, + deletedOnePricing, + updatedOnePricing, + updatedOneInspectionAdditionalData, + updatedVehicle, isCreatedOneImageAction, isGotOneInspectionAction, isResetStateAction, isUpdatedManyTasksAction, + isCreatedOnePricingAction, + isDeletedOnePricingAction, + isUpdatedOnePricingAction, + isUpdatedOneInspectionAdditionalDataAction, + isUpdatedVehicleAction, MonkAction, monkReducer, MonkState, @@ -28,6 +48,14 @@ const actions = [ { matcher: isGotOneInspectionAction, handler: gotOneInspection }, { matcher: isCreatedOneImageAction, handler: createdOneImage }, { matcher: isUpdatedManyTasksAction, handler: updatedManyTasks }, + { matcher: isCreatedOnePricingAction, handler: createdOnePricing }, + { matcher: isDeletedOnePricingAction, handler: deletedOnePricing }, + { matcher: isUpdatedOnePricingAction, handler: updatedOnePricing }, + { + matcher: isUpdatedOneInspectionAdditionalDataAction, + handler: updatedOneInspectionAdditionalData, + }, + { matcher: isUpdatedVehicleAction, handler: updatedVehicle }, ] as unknown as { matcher: jest.Mock; handler: jest.Mock; noParams?: boolean }[]; describe('Monk state reducer', () => { diff --git a/packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureSightState.test.ts b/packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureSightState.test.ts index c0da54ed5..762caf340 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureSightState.test.ts +++ b/packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureSightState.test.ts @@ -76,6 +76,7 @@ function mockGetInspectionResponse( severityResults: [], vehicles: [], views: [], + pricings: [], }, }; takenSights.forEach((sight, index) => { diff --git a/packages/network/README.md b/packages/network/README.md index 71a6eaba8..46906a5e7 100644 --- a/packages/network/README.md +++ b/packages/network/README.md @@ -117,6 +117,59 @@ Update the vehicle of an inspection. |-----------|--------------------------------|-----------------------------|----------| | options | UpdateInspectionVehicleOptions | The options of the request. | ✔️ | +### createPricing +```typescript +import { MonkApi } from '@monkvision/network'; + +MonkApi.createPricing(options, apiConfig, dispatch); +``` + +Create a new pricing of an inspection. + +| Parameter | Type | Description | Required | +|-----------|----------------------|-----------------------------|----------| +| options | CreatePricingOptions | The options of the request. | ✔️ | + +### updatePricing +```typescript +import { MonkApi } from '@monkvision/network'; + +MonkApi.updatePricing(options, apiConfig, dispatch); +``` + +Update a pricing of an inspection. + +| Parameter | Type | Description | Required | +|-----------|----------------------|-----------------------------|----------| +| options | UpdatePricingOptions | The options of the request. | ✔️ | + +### deletePricing +```typescript +import { MonkApi } from '@monkvision/network'; + +MonkApi.deletePricing(options, apiConfig, dispatch); +``` + +Update a pricing of an inspection. + +| Parameter | Type | Description | Required | +|-----------|----------------------|-----------------------------|----------| +| options | DeletePricingOptions | The options of the request. | ✔️ | + +### updateAdditionalData +```typescript +import { MonkApi } from '@monkvision/network'; + +MonkApi.updateAdditionalData(options, apiConfig, dispatch); +``` + +Update the additional data of an inspection. + +| Parameter | Type | Description | Required | +|-----------|-----------------------------|-----------------------------|----------| +| options | UpdateAdditionalDataOptions | The options of the request. | ✔️ | + + # React Tools In order to simply integrate the Monk Api requests into your React app, you can make use of the `useMonkApi` hook. This custom hook returns a custom version of the `MonkApi` object described in the section above, in which the requests do diff --git a/packages/network/src/api/api.ts b/packages/network/src/api/api.ts index b4a968301..e24edca7a 100644 --- a/packages/network/src/api/api.ts +++ b/packages/network/src/api/api.ts @@ -1,8 +1,9 @@ -import { getInspection, createInspection } from './inspection'; +import { getInspection, createInspection, updateAdditionalData } from './inspection'; import { addImage } from './image'; import { startInspectionTasks, updateTaskStatus } from './task'; import { getLiveConfig } from './liveConfigs'; import { updateInspectionVehicle } from './vehicle'; +import { createPricing, deletePricing, updatePricing } from './pricing'; /** * Object regrouping the different API requests available to communicate with the API using the `@monkvision/network` @@ -16,4 +17,8 @@ export const MonkApi = { startInspectionTasks, getLiveConfig, updateInspectionVehicle, + updateAdditionalData, + createPricing, + deletePricing, + updatePricing, }; diff --git a/packages/network/src/api/inspection/mappers.ts b/packages/network/src/api/inspection/mappers.ts index cc1ca729a..c8a872093 100644 --- a/packages/network/src/api/inspection/mappers.ts +++ b/packages/network/src/api/inspection/mappers.ts @@ -15,7 +15,6 @@ import { MonkEntityType, Part, PricingV2, - PricingV2Details, PricingV2RelatedItemType, ProgressStatus, RenderedOutput, @@ -193,10 +192,12 @@ function mapParts(response: ApiInspectionGet): { parts: Part[]; partIds: string[ function mapPricingV2Details( apiPricingV2Details: ApiPricingV2Details | undefined, inspectionId: string, -): PricingV2Details { +): PricingV2 { const details = apiPricingV2Details as ApiPricingV2Details; return { inspectionId, + id: details.id, + entityType: MonkEntityType.PRICING, relatedItemType: details.related_item_type as PricingV2RelatedItemType, relatedItemId: details.related_item_id, pricing: details.pricing, @@ -205,22 +206,21 @@ function mapPricingV2Details( }; } -function mapPricingV2(response: ApiInspectionGet): PricingV2 | undefined { +function mapPricingV2(response: ApiInspectionGet): { + pricings: PricingV2[]; + pricingIds: string[]; +} { + const pricings: PricingV2[] = []; + const pricingIds: string[] = []; + if (!response.pricing) { - return undefined; + return { pricings, pricingIds }; } - return { - details: response?.pricing.details - ? Object.keys(response.pricing.details).reduce( - (prev, curr) => ({ - ...prev, - [curr]: mapPricingV2Details(response.pricing?.details[curr], response.id), - }), - {} as Record, - ) - : {}, - totalPrice: response.pricing.total_price, - }; + Object.values(response.pricing.details).forEach((details) => { + pricingIds.push(details.id); + pricings.push(mapPricingV2Details(details, response.id)); + }); + return { pricings, pricingIds }; } function mapSeverityResultRepairOperation( @@ -353,6 +353,7 @@ function mapInspection( severityResultIds: string[]; taskIds: string[]; vehicleId?: string; + pricingIds: string[]; }, ): Inspection { return { @@ -365,7 +366,7 @@ function mapInspection( vehicle: ids.vehicleId, wheelAnalysis: mapWheelAnalysis(response), severityResults: ids.severityResultIds, - pricing: mapPricingV2(response), + pricings: ids.pricingIds, additionalData: response.additional_data, }; } @@ -384,6 +385,7 @@ export function mapApiInspectionGet( const { parts, partIds } = mapParts(response); const { severityResults, severityResultIds } = mapSeverityResults(response); const { tasks, taskIds } = mapTasks(response); + const { pricings, pricingIds } = mapPricingV2(response); const vehicle = mapVehicle(response); const inspection = mapInspection(response, { imageIds, @@ -393,6 +395,7 @@ export function mapApiInspectionGet( partIds, severityResultIds, taskIds, + pricingIds, }); return { @@ -405,6 +408,7 @@ export function mapApiInspectionGet( tasks, vehicles: vehicle ? [vehicle] : [], views, + pricings, partOperations: [], }; } diff --git a/packages/network/src/api/inspection/requests.ts b/packages/network/src/api/inspection/requests.ts index 5c30d81cd..7461a3ebc 100644 --- a/packages/network/src/api/inspection/requests.ts +++ b/packages/network/src/api/inspection/requests.ts @@ -4,8 +4,9 @@ import { MonkActionType, MonkGotOneInspectionAction, MonkState, + MonkUpdatedOneInspectionAdditionalDataAction, } from '@monkvision/common'; -import { ComplianceOptions, CreateInspectionOptions } from '@monkvision/types'; +import { AdditionalData, ComplianceOptions, CreateInspectionOptions } from '@monkvision/types'; import { Dispatch } from 'react'; import { getDefaultOptions, MonkApiConfig } from '../config'; import { ApiIdColumn, ApiInspectionGet } from '../models'; @@ -86,3 +87,59 @@ export async function createInspection( body, }; } + +/** + * Options passed to the `updateAdditionalData` API request. + */ +export interface UpdateAdditionalDataOptions { + /** + * The ID of the inspection to update via the API. + */ + id: string; + /** + * Callback function that takes optional additional data and returns the updated additional data. + */ + callback: (additionalData?: AdditionalData) => AdditionalData; +} + +/** + * Update the additional data of inspection with the given options. + * See the `UpdateAdditionalDataOptions` interface for more details. + * + * @param options The options of the request. + * @param config The API config. + * @param [dispatch] Optional MonkState dispatch function that you can pass if you want this request to handle React + * state management for you. + * @see UpdateAdditionalDataOptions + */ +export async function updateAdditionalData( + options: UpdateAdditionalDataOptions, + config: MonkApiConfig, + dispatch?: Dispatch, +): Promise { + const { entities } = await getInspection({ id: options.id }, config); + const inspection = entities.inspections.find((i) => i.id === options.id); + if (!inspection) { + throw new Error('Inspection does not exist'); + } + const newAdditionalData = options.callback(inspection.additionalData); + + const kyOptions = getDefaultOptions(config); + const response = await ky.patch(`inspections/${options.id}`, { + ...kyOptions, + json: { additional_data: newAdditionalData }, + }); + const body = await response.json(); + dispatch?.({ + type: MonkActionType.UPDATED_ONE_INSPECTION_ADDITIONAL_DATA, + payload: { + inspectionId: options.id, + additionalData: newAdditionalData, + }, + }); + return { + id: body.id, + response, + body, + }; +} diff --git a/packages/network/src/api/models/pricingV2.ts b/packages/network/src/api/models/pricingV2.ts index d6594865d..b7fb248b1 100644 --- a/packages/network/src/api/models/pricingV2.ts +++ b/packages/network/src/api/models/pricingV2.ts @@ -1,3 +1,5 @@ +import { PricingV2RelatedItemType, VehiclePart } from '@monkvision/types'; + export type ApiRelatedItemType = 'part' | 'vehicle'; export type ApiRepairOperationsTypes = @@ -15,6 +17,7 @@ export type ApiRepairOperationsTypes = export type ApiHours = Record; export interface ApiPricingV2Details { + id: string; hours?: ApiHours; operations?: ApiRepairOperationsTypes[]; pricing?: number; @@ -28,3 +31,9 @@ export interface ApiPricingV2 { details: ApiDetails; total_price?: number; } + +export interface ApiPricingPost { + pricing: number; + related_item_type: PricingV2RelatedItemType; + part_type: VehiclePart | undefined; +} diff --git a/packages/network/src/api/pricing/index.ts b/packages/network/src/api/pricing/index.ts new file mode 100644 index 000000000..96f11f1f1 --- /dev/null +++ b/packages/network/src/api/pricing/index.ts @@ -0,0 +1,2 @@ +export * from './requests'; +export * from './types'; diff --git a/packages/network/src/api/pricing/mappers.ts b/packages/network/src/api/pricing/mappers.ts new file mode 100644 index 000000000..f2a48ce7e --- /dev/null +++ b/packages/network/src/api/pricing/mappers.ts @@ -0,0 +1,31 @@ +import { + MonkEntityType, + PricingV2, + PricingV2RelatedItemType, + RepairOperationType, + VehiclePart, +} from '@monkvision/types'; +import { ApiPricingPost, ApiPricingV2Details } from '../models'; +import { PricingOptions } from './types'; + +export function mapApiPricingPost(inspectionId: string, response: ApiPricingV2Details): PricingV2 { + return { + inspectionId, + id: response.id, + entityType: MonkEntityType.PRICING, + relatedItemType: response.related_item_type as PricingV2RelatedItemType, + relatedItemId: response.related_item_id, + pricing: response.pricing, + operations: response.operations as RepairOperationType[] | undefined, + hours: response.hours, + }; +} + +export function mapApiPricingPostRequest(options: PricingOptions): ApiPricingPost { + return { + pricing: options.pricing >= 0 ? options.pricing : 0, + related_item_type: options.type, + part_type: + options.type === PricingV2RelatedItemType.PART ? options.vehiclePart : VehiclePart.IGNORE, + }; +} diff --git a/packages/network/src/api/pricing/requests.ts b/packages/network/src/api/pricing/requests.ts new file mode 100644 index 000000000..36084b608 --- /dev/null +++ b/packages/network/src/api/pricing/requests.ts @@ -0,0 +1,156 @@ +import { Dispatch } from 'react'; +import { + MonkActionType, + MonkCreatedOnePricingAction, + MonkDeletedOnePricingAction, + MonkUpdatedOnePricingAction, +} from '@monkvision/common'; +import ky from 'ky'; +import { getDefaultOptions, MonkApiConfig } from '../config'; +import { MonkApiResponse } from '../types'; +import { ApiIdColumn, ApiPricingV2Details } from '../models'; +import { mapApiPricingPost, mapApiPricingPostRequest } from './mappers'; +import { PricingOptions } from './types'; + +/** + * Options passed to the `createPricing` API request. + */ +export interface CreatePricingOptions { + /** + * The ID of the inspection to update via the API. + */ + id: string; + /** + * Pricing used for the update operation. + */ + pricing: PricingOptions; +} + +/** + * Create a new pricing with the given options. See the `CreatePricingOptions` interface for more details. + * + * @param options The options of the inspection. + * @param config The API config. + * @param [dispatch] Optional MonkState dispatch function that you can pass if you want this request to handle React + * state management for you. + * @see CreatePricingOptions + */ + +export async function createPricing( + options: CreatePricingOptions, + config: MonkApiConfig, + dispatch?: Dispatch, +): Promise { + const kyOptions = getDefaultOptions(config); + const response = await ky.post(`inspections/${options.id}/pricing`, { + ...kyOptions, + json: mapApiPricingPostRequest(options.pricing), + }); + const body = await response.json(); + const pricing = mapApiPricingPost(options.id, body); + dispatch?.({ + type: MonkActionType.CREATED_ONE_PRICING, + payload: { pricing }, + }); + return { + id: body.id, + response, + body, + }; +} + +/** + * Options passed to the `deletePricing` API request. + */ +export interface DeletePricingOptions { + /** + * The ID of the inspection to update via the API. + */ + id: string; + /** + * Pricing ID that will be deleted. + */ + pricingId: string; +} + +/** + * Delete a pricing with the given options. See the `DeletePricingOptions` interface for more details. + * + * @param options The options of the inspection. + * @param config The API config. + * @param [dispatch] Optional MonkState dispatch function that you can pass if you want this request to handle React + * state management for you. + * @see DeletePricingOptions + */ + +export async function deletePricing( + options: DeletePricingOptions, + config: MonkApiConfig, + dispatch?: Dispatch, +): Promise { + const kyOptions = getDefaultOptions(config); + const response = await ky.delete(`inspections/${options.id}/pricing/${options.pricingId}`, { + ...kyOptions, + }); + const body = await response.json(); + dispatch?.({ + type: MonkActionType.DELETED_ONE_PRICING, + payload: { inspectionId: options.id, pricingId: body.id }, + }); + return { + id: body.id, + response, + body, + }; +} + +/** + * Options passed to the `updatePricing` API request. + */ +export interface UpdatePricingOptions { + /** + * The ID of the inspection to update via the API. + */ + id: string; + /** + * Pricing ID that will be update. + */ + pricingId: string; + /** + * The new price value. + */ + price: number; +} + +/** + * Update a pricing with the given options. See the `UpdatePricingOptions` interface for more details. + * + * @param options The options of the inspection. + * @param config The API config. + * @param [dispatch] Optional MonkState dispatch function that you can pass if you want this request to handle React + * state management for you. + * @see UpdatePricingOptions + */ + +export async function updatePricing( + options: UpdatePricingOptions, + config: MonkApiConfig, + dispatch?: Dispatch, +): Promise { + const kyOptions = getDefaultOptions(config); + const response = await ky.patch(`inspections/${options.id}/pricing/${options.pricingId}`, { + ...kyOptions, + json: { pricing: options.price }, + }); + const body = await response.json(); + const pricing = mapApiPricingPost(options.id, body); + dispatch?.({ + type: MonkActionType.UPDATED_ONE_PRICING, + payload: { pricing }, + }); + return { + id: body.id, + response, + body, + }; +} diff --git a/packages/network/src/api/pricing/types.ts b/packages/network/src/api/pricing/types.ts new file mode 100644 index 000000000..f11ab01d1 --- /dev/null +++ b/packages/network/src/api/pricing/types.ts @@ -0,0 +1,41 @@ +import { PricingV2RelatedItemType, VehiclePart } from '@monkvision/types'; + +/** + * Options for part-specific pricing. + */ +export interface PricingPartOptions { + /** + * The type of pricing, in this case for a part. + */ + type: PricingV2RelatedItemType.PART; + + /** + * The pricing value for the part. Must be a non-negative float. + */ + pricing: number; + + /** + * The specific vehicle part this pricing applies to. + */ + vehiclePart: VehiclePart; +} + +/** + * Options for car-wide pricing. + */ +export interface PricingVehicleOptions { + /** + * The type of pricing, in this case for the whole car. + */ + type: PricingV2RelatedItemType.VEHICLE; + + /** + * The pricing value for the entire car. + */ + pricing: number; +} + +/** + * Union type representing pricing options for either a car or a part. + */ +export type PricingOptions = PricingVehicleOptions | PricingPartOptions; diff --git a/packages/network/src/api/react.ts b/packages/network/src/api/react.ts index 9024ef5f8..a8bc474a3 100644 --- a/packages/network/src/api/react.ts +++ b/packages/network/src/api/react.ts @@ -111,5 +111,32 @@ export function useMonkApi(config: MonkApiConfig) { dispatch, handleError, ), + /** + * Update the additional data of an inspection. + * + * @param options The options of the request. + */ + updateAdditionalData: reactify(MonkApi.updateAdditionalData, config, dispatch, handleError), + /** + * Create a new pricing with the given options. See the `CreatePricingOptions` interface for more details. + * + * @param options The options of the inspection. + * @see CreatePricingOptions + */ + createPricing: reactify(MonkApi.createPricing, config, dispatch, handleError), + /** + * Delete a pricing with the given options. See the `DeletePricingOptions` interface for more details. + * + * @param options The options of the inspection. + * @see DeletePricingOptions + */ + deletePricing: reactify(MonkApi.deletePricing, config, dispatch, handleError), + /** + * Update a pricing with the given options. See the `UpdatePricingOptions` interface for more details. + * + * @param options The options of the inspection. + * @see UpdatePricingOptions + */ + updatePricing: reactify(MonkApi.updatePricing, config, dispatch, handleError), }; } diff --git a/packages/network/test/api/inspection/data/apiInspectionGet.data.json b/packages/network/test/api/inspection/data/apiInspectionGet.data.json index b41ba4538..d3a1ade48 100644 --- a/packages/network/test/api/inspection/data/apiInspectionGet.data.json +++ b/packages/network/test/api/inspection/data/apiInspectionGet.data.json @@ -3941,8 +3941,39 @@ ], "pdf_generation_ready": true, "pricing": { - "details": {}, - "total_price": 0 + "details": { + "bumper_back": { + "hours": { + "INGREDIENT": 2, + "PAINT": 2 + }, + "id": "8bbfcba0-fdee-4268-8bd5-69dffac87aea", + "inspection_id": "5843de96-21d3-282b-5829-7ce926f510a9", + "operations": ["painting"], + "pricing": 200, + "related_item_id": "fd6dd94c-d202-c9c4-fd07-7b33d524f14e", + "related_item_type": "part" + }, + "roof": { + "hours": null, + "id": "fed5af09-1e6d-0aac-febf-0d76194b3256", + "inspection_id": "5843de96-21d3-282b-5829-7ce926f510a9", + "operations": null, + "pricing": 200, + "related_item_id": "f78040b7-5562-55cf-f7ea-e2c852446d45", + "related_item_type": "part" + }, + "wheel_front_left": { + "hours": null, + "id": "b5d64933-4bce-9e9e-b5bc-eb4c4ce8a61c", + "inspection_id": "5843de96-21d3-282b-5829-7ce926f510a9", + "operations": null, + "pricing": 100, + "related_item_id": "41c29e52-6780-63e9-41a8-3c2d60a65b63", + "related_item_type": "part" + } + }, + "total_price": 500 }, "related_inspection_id": null, "severity_results": [ diff --git a/packages/network/test/api/inspection/data/apiInspectionGet.data.ts b/packages/network/test/api/inspection/data/apiInspectionGet.data.ts index 376381538..e55f972a2 100644 --- a/packages/network/test/api/inspection/data/apiInspectionGet.data.ts +++ b/packages/network/test/api/inspection/data/apiInspectionGet.data.ts @@ -262,10 +262,11 @@ export default { 'ca8b97ee-a50f-919b-cae1-3591a229bddc', '5dfffeae-6b5f-a699-5d95-5cd16c798ade', ], - pricing: { - details: {}, - totalPrice: 0, - }, + pricings: [ + '8bbfcba0-fdee-4268-8bd5-69dffac87aea', + 'fed5af09-1e6d-0aac-febf-0d76194b3256', + 'b5d64933-4bce-9e9e-b5bc-eb4c4ce8a61c', + ], additionalData: { damage_detection_version: 'v2', environment: { @@ -3516,5 +3517,40 @@ export default { renderedOutputs: ['662bc970-70d1-83de-6641-6b0f77f7af99'], }, ], + pricings: [ + { + entityType: 'PRICING', + hours: { + INGREDIENT: 2, + PAINT: 2, + }, + id: '8bbfcba0-fdee-4268-8bd5-69dffac87aea', + inspectionId: '42242f0b-378b-1325-424e-8d7430ad3f62', + operations: ['painting'], + pricing: 200, + relatedItemId: 'fd6dd94c-d202-c9c4-fd07-7b33d524f14e', + relatedItemType: 'part', + }, + { + entityType: 'PRICING', + hours: null, + id: 'fed5af09-1e6d-0aac-febf-0d76194b3256', + inspectionId: '42242f0b-378b-1325-424e-8d7430ad3f62', + operations: null, + pricing: 200, + relatedItemId: 'f78040b7-5562-55cf-f7ea-e2c852446d45', + relatedItemType: 'part', + }, + { + entityType: 'PRICING', + hours: null, + id: 'b5d64933-4bce-9e9e-b5bc-eb4c4ce8a61c', + inspectionId: '42242f0b-378b-1325-424e-8d7430ad3f62', + operations: null, + pricing: 100, + relatedItemId: '41c29e52-6780-63e9-41a8-3c2d60a65b63', + relatedItemType: 'part', + }, + ], partOperations: [], }; diff --git a/packages/network/test/api/inspection/requests.test.ts b/packages/network/test/api/inspection/requests.test.ts index 19d032077..d37237b61 100644 --- a/packages/network/test/api/inspection/requests.test.ts +++ b/packages/network/test/api/inspection/requests.test.ts @@ -1,16 +1,47 @@ +import { + AdditionalData, + ComplianceIssue, + ComplianceOptions, + Inspection, + MonkEntityType, + TaskName, +} from '@monkvision/types'; +import { MonkActionType } from '@monkvision/common'; +import ky from 'ky'; + +const additionalData = { + country: 'USA', + other_damages: [ + { + area: 'seats', + damage_type: 'scratch', + repair_cost: 544, + }, + ], +}; +const mockInspection: Inspection = { + id: 'test-inspection-id', + additionalData, + damages: [], + entityType: MonkEntityType.INSPECTION, + images: [], + parts: [], + tasks: [], +}; + jest.mock('../../../src/api/config', () => ({ getDefaultOptions: jest.fn(() => ({ prefixUrl: 'getDefaultOptionsTest' })), })); jest.mock('../../../src/api/inspection/mappers', () => ({ - mapApiInspectionGet: jest.fn(() => ({ test: 'hello' })), + mapApiInspectionGet: jest.fn(() => ({ + inspections: [mockInspection] as unknown as Inspection[], + parts: [], + })), mapApiInspectionPost: jest.fn(() => ({ test: 'ok-ok-ok' })), })); -import { ComplianceIssue, ComplianceOptions, TaskName } from '@monkvision/types'; -import ky from 'ky'; -import { MonkActionType } from '@monkvision/common'; import { getDefaultOptions } from '../../../src/api/config'; -import { createInspection, getInspection } from '../../../src/api/inspection'; +import { createInspection, getInspection, updateAdditionalData } from '../../../src/api/inspection'; import { mapApiInspectionGet, mapApiInspectionPost } from '../../../src/api/inspection/mappers'; const apiConfig = { @@ -74,4 +105,32 @@ describe('Inspection requests', () => { }); }); }); + + describe('updateAdditionalData request', () => { + it('should make the proper API call', async () => { + const id = 'test-inspection-id'; + const callback = (addData?: AdditionalData) => { + const newAddData = { + ...addData, + ...additionalData, + }; + return newAddData; + }; + const options = { id, callback }; + const result = await updateAdditionalData(options, apiConfig); + const response = await (ky.patch as jest.Mock).mock.results[0].value; + const body = await response.json(); + + expect(getDefaultOptions).toHaveBeenCalledWith(apiConfig); + expect(ky.patch).toHaveBeenCalledWith(`inspections/${id}`, { + ...getDefaultOptions(apiConfig), + json: { additional_data: additionalData }, + }); + expect(result).toEqual({ + id: body.id, + response, + body, + }); + }); + }); }); diff --git a/packages/network/test/api/pricing/mappers.test.ts b/packages/network/test/api/pricing/mappers.test.ts new file mode 100644 index 000000000..a540cf28f --- /dev/null +++ b/packages/network/test/api/pricing/mappers.test.ts @@ -0,0 +1,31 @@ +import { MonkEntityType, PricingV2RelatedItemType } from '@monkvision/types'; +import { mapApiPricingPost } from '../../../src/api/pricing/mappers'; +import { ApiPricingV2Details } from '../../../src/api/models'; + +function createApiPricingPost(): ApiPricingV2Details { + return { + id: 'id-test', + related_item_type: PricingV2RelatedItemType.PART, + pricing: 10, + }; +} + +describe('Pricing API Mappers', () => { + const inspectionId = 'test-inspection-id'; + describe('ApiPricingPost mapper', () => { + it('should properly map the ApiPricingPost object', () => { + const apiPricingPostData = createApiPricingPost(); + const result = mapApiPricingPost(inspectionId, createApiPricingPost()); + expect(result).toEqual({ + id: apiPricingPostData.id, + entityType: MonkEntityType.PRICING, + hours: apiPricingPostData.hours, + inspectionId, + operations: apiPricingPostData.operations, + pricing: apiPricingPostData.pricing, + relatedItemId: apiPricingPostData.related_item_id, + relatedItemType: apiPricingPostData.related_item_type, + }); + }); + }); +}); diff --git a/packages/network/test/api/pricing/requests.test.ts b/packages/network/test/api/pricing/requests.test.ts new file mode 100644 index 000000000..8c1605b20 --- /dev/null +++ b/packages/network/test/api/pricing/requests.test.ts @@ -0,0 +1,136 @@ +jest.mock('../../../src/api/config', () => ({ + getDefaultOptions: jest.fn(() => ({ prefixUrl: 'getDefaultOptionsTest' })), +})); +jest.mock('../../../src/api/pricing/mappers', () => ({ + mapApiPricingPost: jest.fn(() => ({ test: 'hello' })), + mapApiPricingPostRequest: jest.fn(() => ({ test: 'hello' })), +})); +jest.mock('ky', () => ({ + post: jest.fn(() => + Promise.resolve({ json: jest.fn(() => Promise.resolve({ id: 'test-fake-id' })) }), + ), + delete: jest.fn(() => + Promise.resolve({ json: jest.fn(() => Promise.resolve({ id: 'test-fake-id' })) }), + ), + patch: jest.fn(() => + Promise.resolve({ json: jest.fn(() => Promise.resolve({ id: 'test-fake-id' })) }), + ), +})); + +import { PricingV2RelatedItemType, VehiclePart } from '@monkvision/types'; +import { + createPricing, + deletePricing, + PricingOptions, + updatePricing, +} from '../../../src/api/pricing'; +import { MonkActionType } from '@monkvision/common'; +import ky from 'ky'; +import { getDefaultOptions } from '../../../src/api/config'; +import { mapApiPricingPost, mapApiPricingPostRequest } from '../../../src/api/pricing/mappers'; + +const apiConfig = { + apiDomain: 'apiDomain', + authToken: 'authToken', + thumbnailDomain: 'thumbnailDomain', +}; + +describe('Pricing requests', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('createPricing request', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('should make the proper API call and map the resulting response', async () => { + const id = 'test-inspection-id'; + const dispatch = jest.fn(); + const pricing: PricingOptions = { + type: PricingV2RelatedItemType.PART, + vehiclePart: VehiclePart.ROOF, + pricing: 50, + }; + const result = await createPricing({ id, pricing }, apiConfig, dispatch); + const response = await (ky.post as jest.Mock).mock.results[0].value; + const body = await response.json(); + + const apiPricing = (mapApiPricingPost as jest.Mock).mock.results[0].value; + const apiPricingPost = (mapApiPricingPostRequest as jest.Mock).mock.results[0].value; + expect(mapApiPricingPost).toHaveBeenCalledWith(id, body); + expect(getDefaultOptions).toHaveBeenCalledWith(apiConfig); + const kyOptions = getDefaultOptions(apiConfig); + expect(ky.post).toHaveBeenCalledWith(`inspections/${id}/pricing`, { + ...kyOptions, + json: apiPricingPost, + }); + expect(dispatch).toHaveBeenCalledWith({ + type: MonkActionType.CREATED_ONE_PRICING, + payload: { pricing: apiPricing }, + }); + expect(result).toEqual({ + id: body.id, + response, + body, + }); + }); + }); + + describe('deletePricing request', () => { + it('should make the proper API call and map the resulting response', async () => { + const id = 'test-inspection-id'; + const pricingId = 'test-pricing-id'; + const dispatch = jest.fn(); + const result = await deletePricing({ id, pricingId }, apiConfig, dispatch); + const response = await (ky.delete as jest.Mock).mock.results[0].value; + const body = await response.json(); + + expect(getDefaultOptions).toHaveBeenCalledWith(apiConfig); + const kyOptions = getDefaultOptions(apiConfig); + expect(ky.delete).toHaveBeenCalledWith(`inspections/${id}/pricing/${pricingId}`, { + ...kyOptions, + }); + expect(dispatch).toHaveBeenCalledWith({ + type: MonkActionType.DELETED_ONE_PRICING, + payload: { inspectionId: id, pricingId: body.id }, + }); + expect(result).toEqual({ + id: body.id, + response, + body, + }); + }); + }); + + describe('updatePricing request', () => { + it('should make the proper API call and map the resulting response', async () => { + const id = 'test-inspection-id'; + const pricingId = 'test-pricing-id'; + const pricingMock = 50; + const dispatch = jest.fn(); + const result = await updatePricing({ id, pricingId, price: 50 }, apiConfig, dispatch); + const response = await (ky.patch as jest.Mock).mock.results[0].value; + const body = await response.json(); + + const apiPricing = (mapApiPricingPost as jest.Mock).mock.results[0].value; + expect(getDefaultOptions).toHaveBeenCalledWith(apiConfig); + const kyOptions = getDefaultOptions(apiConfig); + expect(ky.patch).toHaveBeenCalledWith(`inspections/${id}/pricing/${pricingId}`, { + ...kyOptions, + json: { + pricing: pricingMock, + }, + }); + expect(dispatch).toHaveBeenCalledWith({ + type: MonkActionType.UPDATED_ONE_PRICING, + payload: { pricing: apiPricing }, + }); + expect(result).toEqual({ + id: body.id, + response, + body, + }); + }); + }); +}); diff --git a/packages/network/test/api/react.test.ts b/packages/network/test/api/react.test.ts index 1d3c6b22f..064b76a86 100644 --- a/packages/network/test/api/react.test.ts +++ b/packages/network/test/api/react.test.ts @@ -2,6 +2,9 @@ jest.mock('../../src/api/api', () => ({ MonkApi: { getInspection: jest.fn(() => Promise.resolve({ test: 'getInspection' })), createInspection: jest.fn(() => Promise.resolve({ test: 'createInspection' })), + createPricing: jest.fn(() => Promise.resolve({ test: 'createPricing' })), + deletePricing: jest.fn(() => Promise.resolve({ test: 'deletePricing' })), + updatePricing: jest.fn(() => Promise.resolve({ test: 'updatePricing' })), }, })); @@ -47,6 +50,33 @@ describe('Monk API React utilities', () => { requestResultMock = await requestMock.mock.results[0].value; expect(resultMock).toBe(requestResultMock); + dispatchMock.mockClear(); + + param = 'test-createPricing'; + resultMock = await (result.current.createPricing as any)(param); + requestMock = MonkApi.createPricing as jest.Mock; + expect(requestMock).toHaveBeenCalledWith(param, config, dispatchMock); + requestResultMock = await requestMock.mock.results[0].value; + expect(resultMock).toBe(requestResultMock); + + dispatchMock.mockClear(); + + param = 'test-deletePricing'; + resultMock = await (result.current.deletePricing as any)(param); + requestMock = MonkApi.deletePricing as jest.Mock; + expect(requestMock).toHaveBeenCalledWith(param, config, dispatchMock); + requestResultMock = await requestMock.mock.results[0].value; + expect(resultMock).toBe(requestResultMock); + + dispatchMock.mockClear(); + + param = 'test-updatePricing'; + resultMock = await (result.current.updatePricing as any)(param); + requestMock = MonkApi.updatePricing as jest.Mock; + expect(requestMock).toHaveBeenCalledWith(param, config, dispatchMock); + requestResultMock = await requestMock.mock.results[0].value; + expect(resultMock).toBe(requestResultMock); + unmount(); }); diff --git a/packages/types/src/state/entity.ts b/packages/types/src/state/entity.ts index 336bf8849..49215cb4b 100644 --- a/packages/types/src/state/entity.ts +++ b/packages/types/src/state/entity.ts @@ -13,6 +13,7 @@ export enum MonkEntityType { TASK = 'TASK', VEHICLE = 'VEHICLE', VIEW = 'VIEW', + PRICING = 'PRICING', } /** diff --git a/packages/types/src/state/inspection.ts b/packages/types/src/state/inspection.ts index cfe631149..94c5fc4fc 100644 --- a/packages/types/src/state/inspection.ts +++ b/packages/types/src/state/inspection.ts @@ -1,6 +1,5 @@ import { AdditionalData } from './common'; import { MonkEntity, MonkEntityType } from './entity'; -import { PricingV2 } from './pricingV2'; import { WheelAnalysis } from './wheelAnalysis'; /** @@ -44,7 +43,7 @@ export interface Inspection extends MonkEntity { /** * The details about the cost of the vehicle reparations using the PricingV2 API if it was requested. */ - pricing?: PricingV2; + pricings?: string[]; /** * Additional data added during the creation of the inspection. */ diff --git a/packages/types/src/state/pricingV2.ts b/packages/types/src/state/pricingV2.ts index 5946d8453..2a374047d 100644 --- a/packages/types/src/state/pricingV2.ts +++ b/packages/types/src/state/pricingV2.ts @@ -1,3 +1,5 @@ +import { MonkEntity } from './entity'; + /** * Enumeration of the types of items that a PricingV2 object can refer to. */ @@ -32,7 +34,7 @@ export enum RepairOperationType { * Details of the pricing using the expanded pricing feature. Provides details about the operations and the hours of * labour required and the cost of repairs for a specific part or on the entirety of the vehicle. */ -export interface PricingV2Details { +export interface PricingV2 extends MonkEntity { /** * The ID of the inspection associated with this pricing information. */ @@ -59,17 +61,3 @@ export interface PricingV2Details { */ hours?: Record; } - -/** - * Pricing information for the reparation of a vehicle, in its entirety and for each part. - */ -export interface PricingV2 { - /** - * The details of the pricing information. It associates each element name to its pricing details if it has some. - */ - details: Record; - /** - * The total cost of the reparations. - */ - totalPrice?: number; -}