From 5e953f8cf35e909af5147b987666355818d59019 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Mon, 8 Apr 2024 09:23:23 +0200 Subject: [PATCH] chore: add batch update prices for price lists (#6999) --- .../price-lists/admin/price-lists.spec.ts | 112 ++++++++-------- .../core-flows/src/price-list/steps/index.ts | 1 - .../steps/upsert-price-list-prices.ts | 123 ------------------ .../steps/validate-variant-price-links.ts | 9 +- .../src/price-list/workflows/index.ts | 2 +- .../workflows/update-price-list-prices.ts | 31 +++++ .../workflows/update-price-lists.ts | 53 +------- .../workflows/upsert-price-list-prices.ts | 31 ----- .../[id]/prices/batch/update/route.ts | 38 ++++++ .../api-v2/admin/price-lists/middlewares.ts | 6 + .../api-v2/admin/price-lists/validators.ts | 20 +-- packages/types/src/pricing/workflows.ts | 33 ++--- 12 files changed, 166 insertions(+), 293 deletions(-) delete mode 100644 packages/core-flows/src/price-list/steps/upsert-price-list-prices.ts create mode 100644 packages/core-flows/src/price-list/workflows/update-price-list-prices.ts delete mode 100644 packages/core-flows/src/price-list/workflows/upsert-price-list-prices.ts create mode 100644 packages/medusa/src/api-v2/admin/price-lists/[id]/prices/batch/update/route.ts diff --git a/integration-tests/modules/__tests__/price-lists/admin/price-lists.spec.ts b/integration-tests/modules/__tests__/price-lists/admin/price-lists.spec.ts index 4fae4cd2d8..05a8dd85dc 100644 --- a/integration-tests/modules/__tests__/price-lists/admin/price-lists.spec.ts +++ b/integration-tests/modules/__tests__/price-lists/admin/price-lists.spec.ts @@ -422,7 +422,7 @@ medusaIntegrationTestRunner({ ) }) - it("should update price lists and set prices successfully", async () => { + it("should update price lists", async () => { await createVariantPriceSet({ container: appContainer, variantId: variant.id, @@ -446,14 +446,6 @@ medusaIntegrationTestRunner({ rules: { customer_group_id: [customerGroup.id], }, - prices: [ - { - amount: 400, - variant_id: variant.id, - currency_code: "usd", - rules: { region_id: region.id }, - }, - ], } let response = await api.post( @@ -471,49 +463,6 @@ medusaIntegrationTestRunner({ rules: { customer_group_id: [customerGroup.id], }, - prices: [ - { - id: expect.any(String), - currency_code: "usd", - amount: 400, - min_quantity: null, - max_quantity: null, - variant_id: variant.id, - rules: { - region_id: region.id, - }, - }, - ], - }) - ) - - // Updating prices should remove existing prices and create new ones - response = await api.post( - `admin/price-lists/${priceList.id}`, - { - prices: [ - { - amount: 600, - variant_id: variant.id, - currency_code: "usd", - rules: { region_id: region.id }, - }, - ], - }, - adminHeaders - ) - - expect(response.data.price_list).toEqual( - expect.objectContaining({ - prices: [ - expect.objectContaining({ - id: expect.any(String), - currency_code: "usd", - amount: 600, - variant_id: variant.id, - rules: { region_id: region.id }, - }), - ], }) ) }) @@ -582,6 +531,65 @@ medusaIntegrationTestRunner({ }) }) + describe("POST /admin/price-lists/:id/prices/batch/update", () => { + it("should update price list prices successfully", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [{ amount: 3000, currency_code: "usd" }], + }) + + const [priceList] = await pricingModule.createPriceLists([ + { + title: "test price list", + description: "test", + prices: [ + { + id: "test-price-id", + amount: 5000, + currency_code: "usd", + price_set_id: priceSet.id, + rules: { region_id: region.id }, + }, + ], + }, + ]) + + const data = { + prices: [ + { + id: "test-price-id", + amount: 400, + variant_id: variant.id, + currency_code: "usd", + rules: { region_id: region.id }, + }, + ], + } + + const response = await api.post( + `admin/price-lists/${priceList.id}/prices/batch/update`, + data, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.price_list.prices.length).toEqual(1) + expect(response.data.price_list).toEqual( + expect.objectContaining({ + id: expect.any(String), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + currency_code: "usd", + amount: 400, + }), + ]), + }) + ) + }) + }) + describe("POST /admin/price-lists/:id/prices/batch/remove", () => { it("should remove price list prices successfully", async () => { const priceSet = await createVariantPriceSet({ diff --git a/packages/core-flows/src/price-list/steps/index.ts b/packages/core-flows/src/price-list/steps/index.ts index c542e3e0f4..74f4aebfeb 100644 --- a/packages/core-flows/src/price-list/steps/index.ts +++ b/packages/core-flows/src/price-list/steps/index.ts @@ -5,6 +5,5 @@ export * from "./get-existing-price-lists-price-ids" export * from "./remove-price-list-prices" export * from "./update-price-list-prices" export * from "./update-price-lists" -export * from "./upsert-price-list-prices" export * from "./validate-price-lists" export * from "./validate-variant-price-links" diff --git a/packages/core-flows/src/price-list/steps/upsert-price-list-prices.ts b/packages/core-flows/src/price-list/steps/upsert-price-list-prices.ts deleted file mode 100644 index dfc7355657..0000000000 --- a/packages/core-flows/src/price-list/steps/upsert-price-list-prices.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { - AddPriceListPricesDTO, - CreatePriceListPriceDTO, - CreatePriceListPriceWorkflowDTO, - IPricingModuleService, - PriceDTO, - UpdatePriceListPriceDTO, - UpdatePriceListPriceWorkflowDTO, - UpdatePriceListPricesDTO, - UpsertPriceListPricesWorkflowStepDTO, -} from "@medusajs/types" -import { buildPriceSetPricesForModule, promiseAll } from "@medusajs/utils" -import { StepResponse, createStep } from "@medusajs/workflows-sdk" - -export const upsertPriceListPricesStepId = "upsert-price-list-prices" -export const upsertPriceListPricesStep = createStep( - upsertPriceListPricesStepId, - async (stepInput: UpsertPriceListPricesWorkflowStepDTO, { container }) => { - const { data, variant_price_map: variantPriceSetMap } = stepInput - - const priceListPricesToUpdate: UpdatePriceListPricesDTO[] = [] - const priceListPricesToAdd: AddPriceListPricesDTO[] = [] - const pricingModule = container.resolve( - ModuleRegistrationName.PRICING - ) - - for (const upsertPriceListPricesData of data) { - const { prices = [], id } = upsertPriceListPricesData - - const pricesToAdd: CreatePriceListPriceDTO[] = [] - const pricesToUpdate: UpdatePriceListPriceDTO[] = [] - - for (const price of prices) { - const priceSetId = variantPriceSetMap[price.variant_id!] - - if (isPriceUpdate(price)) { - pricesToUpdate.push({ ...price, price_set_id: priceSetId }) - } else { - pricesToAdd.push({ ...price, price_set_id: priceSetId }) - } - } - - if (pricesToUpdate.length) { - priceListPricesToUpdate.push({ - price_list_id: id, - prices: pricesToUpdate, - }) - } - - if (pricesToAdd.length) { - priceListPricesToAdd.push({ - price_list_id: id, - prices: pricesToAdd, - }) - } - } - - const updatedPrices = await pricingModule.listPrices( - { - id: priceListPricesToUpdate - .map((priceListData) => priceListData.prices.map((price) => price.id)) - .filter(Boolean) - .flat(1), - }, - { relations: ["price_list"] } - ) - - const priceListPricesMap = new Map() - const dataBeforePriceUpdate: UpdatePriceListPricesDTO[] = [] - - for (const updatedPrice of updatedPrices) { - const priceListId = updatedPrice.price_list!.id - const prices = priceListPricesMap.get(priceListId) || [] - - priceListPricesMap.set(priceListId, prices) - } - - for (const [priceListId, prices] of Object.entries(priceListPricesMap)) { - dataBeforePriceUpdate.push({ - price_list_id: priceListId, - prices: buildPriceSetPricesForModule(prices), - }) - } - - // TODO: `addPriceListPrices` will return a list of price lists - // This should be reworked to return prices instead, as we need to - // do a revert incase this step fails - const [createdPriceListPrices, _] = await promiseAll([ - pricingModule.addPriceListPrices(priceListPricesToAdd), - pricingModule.updatePriceListPrices(priceListPricesToUpdate), - ]) - - return new StepResponse(null, { - createdPriceListPrices, - updatedPriceListPrices: dataBeforePriceUpdate, - }) - }, - async (data, { container }) => { - if (!data) { - return - } - - const { createdPriceListPrices = [], updatedPriceListPrices = [] } = data - const pricingModule = container.resolve( - ModuleRegistrationName.PRICING - ) - - if (createdPriceListPrices.length) { - await pricingModule.removePrices(createdPriceListPrices.map((p) => p.id)) - } - - if (updatedPriceListPrices.length) { - await pricingModule.updatePriceListPrices(updatedPriceListPrices) - } - } -) - -function isPriceUpdate( - data: UpdatePriceListPriceWorkflowDTO | CreatePriceListPriceWorkflowDTO -): data is UpdatePriceListPriceWorkflowDTO { - return "id" in data -} diff --git a/packages/core-flows/src/price-list/steps/validate-variant-price-links.ts b/packages/core-flows/src/price-list/steps/validate-variant-price-links.ts index c195a81120..2799065b5c 100644 --- a/packages/core-flows/src/price-list/steps/validate-variant-price-links.ts +++ b/packages/core-flows/src/price-list/steps/validate-variant-price-links.ts @@ -1,4 +1,3 @@ -import { UpdatePriceListWorkflowInputDTO } from "@medusajs/types" import { ContainerRegistrationKeys, MedusaError, @@ -10,7 +9,11 @@ export const validateVariantPriceLinksStepId = "validate-variant-price-links" export const validateVariantPriceLinksStep = createStep( validateVariantPriceLinksStepId, async ( - data: Pick[], + data: { + prices: { + variant_id: string + }[] + }[], { container } ) => { const remoteQuery = container.resolve( @@ -18,7 +21,7 @@ export const validateVariantPriceLinksStep = createStep( ) const variantIds: string[] = data - .map((pl) => pl?.prices?.map((price) => price.variant_id!) || []) + .map((pl) => pl?.prices?.map((price) => price.variant_id) || []) .filter(Boolean) .flat(1) diff --git a/packages/core-flows/src/price-list/workflows/index.ts b/packages/core-flows/src/price-list/workflows/index.ts index ffa4e400a3..a1050b6fa7 100644 --- a/packages/core-flows/src/price-list/workflows/index.ts +++ b/packages/core-flows/src/price-list/workflows/index.ts @@ -2,5 +2,5 @@ export * from "./create-price-list-prices" export * from "./create-price-lists" export * from "./delete-price-lists" export * from "./remove-price-list-prices" +export * from "./update-price-list-prices" export * from "./update-price-lists" -export * from "./upsert-price-list-prices" diff --git a/packages/core-flows/src/price-list/workflows/update-price-list-prices.ts b/packages/core-flows/src/price-list/workflows/update-price-list-prices.ts new file mode 100644 index 0000000000..d1a044d6cc --- /dev/null +++ b/packages/core-flows/src/price-list/workflows/update-price-list-prices.ts @@ -0,0 +1,31 @@ +import { UpdatePriceListPricesWorkflowDTO } from "@medusajs/types" +import { + WorkflowData, + createWorkflow, + parallelize, +} from "@medusajs/workflows-sdk" +import { + updatePriceListPricesStep, + validatePriceListsStep, + validateVariantPriceLinksStep, +} from "../steps" + +export const updatePriceListPricesWorkflowId = "update-price-list-prices" +export const updatePriceListPricesWorkflow = createWorkflow( + updatePriceListPricesWorkflowId, + ( + input: WorkflowData<{ + data: UpdatePriceListPricesWorkflowDTO[] + }> + ): WorkflowData => { + const [_, variantPriceMap] = parallelize( + validatePriceListsStep(input.data), + validateVariantPriceLinksStep(input.data) + ) + + updatePriceListPricesStep({ + data: input.data, + variant_price_map: variantPriceMap, + }) + } +) diff --git a/packages/core-flows/src/price-list/workflows/update-price-lists.ts b/packages/core-flows/src/price-list/workflows/update-price-lists.ts index 0b6fb4b51d..9c87002e23 100644 --- a/packages/core-flows/src/price-list/workflows/update-price-lists.ts +++ b/packages/core-flows/src/price-list/workflows/update-price-lists.ts @@ -1,18 +1,6 @@ import { UpdatePriceListWorkflowInputDTO } from "@medusajs/types" -import { - WorkflowData, - createWorkflow, - parallelize, - transform, -} from "@medusajs/workflows-sdk" -import { - createPriceListPricesStep, - getExistingPriceListsPriceIdsStep, - removePriceListPricesStep, - updatePriceListsStep, - validatePriceListsStep, - validateVariantPriceLinksStep, -} from "../steps" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { updatePriceListsStep, validatePriceListsStep } from "../steps" export const updatePriceListsWorkflowId = "update-price-lists" export const updatePriceListsWorkflow = createWorkflow( @@ -20,41 +8,8 @@ export const updatePriceListsWorkflow = createWorkflow( ( input: WorkflowData<{ price_lists_data: UpdatePriceListWorkflowInputDTO[] }> ): WorkflowData => { - const [priceListsMap, variantPriceMap] = parallelize( - validatePriceListsStep(input.price_lists_data), - validateVariantPriceLinksStep(input.price_lists_data) - ) + validatePriceListsStep(input.price_lists_data) - const getPriceListPricesInput = transform({ priceListsMap }, (data) => ({ - price_list_ids: Object.keys(data.priceListsMap), - })) - - const priceListPriceIdMap = getExistingPriceListsPriceIdsStep( - getPriceListPricesInput - ) - - const removePriceListPricesInput = transform( - { priceListPriceIdMap }, - (data) => Object.values(data.priceListPriceIdMap).flat(1) - ) - - removePriceListPricesStep(removePriceListPricesInput) - - const updatePricesInput = transform({ variantPriceMap, input }, (data) => ({ - data: data.input.price_lists_data, - variant_price_map: data.variantPriceMap, - })) - - createPriceListPricesStep(updatePricesInput) - - const updatePriceListInput = transform({ input }, (data) => { - return data.input.price_lists_data.map((priceListData) => { - delete priceListData.prices - - return priceListData - }) - }) - - updatePriceListsStep(updatePriceListInput) + updatePriceListsStep(input.price_lists_data) } ) diff --git a/packages/core-flows/src/price-list/workflows/upsert-price-list-prices.ts b/packages/core-flows/src/price-list/workflows/upsert-price-list-prices.ts deleted file mode 100644 index b56a91b45b..0000000000 --- a/packages/core-flows/src/price-list/workflows/upsert-price-list-prices.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { UpdatePriceListWorkflowInputDTO } from "@medusajs/types" -import { - WorkflowData, - createWorkflow, - parallelize, -} from "@medusajs/workflows-sdk" -import { - upsertPriceListPricesStep, - validatePriceListsStep, - validateVariantPriceLinksStep, -} from "../steps" - -export const upsertPriceListPricesWorkflowId = "upsert-price-list-prices" -export const upsertPriceListPricesWorkflow = createWorkflow( - upsertPriceListPricesWorkflowId, - ( - input: WorkflowData<{ - price_lists_data: Pick[] - }> - ): WorkflowData => { - const [_, variantPriceMap] = parallelize( - validatePriceListsStep(input.price_lists_data), - validateVariantPriceLinksStep(input.price_lists_data) - ) - - upsertPriceListPricesStep({ - data: input.price_lists_data, - variant_price_map: variantPriceMap, - }) - } -) diff --git a/packages/medusa/src/api-v2/admin/price-lists/[id]/prices/batch/update/route.ts b/packages/medusa/src/api-v2/admin/price-lists/[id]/prices/batch/update/route.ts new file mode 100644 index 0000000000..c5218aa290 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/price-lists/[id]/prices/batch/update/route.ts @@ -0,0 +1,38 @@ +import { updatePriceListPricesWorkflow } from "@medusajs/core-flows" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../../../types/routing" +import { getPriceList } from "../../../../queries" +import { + adminPriceListRemoteQueryFields, + defaultAdminPriceListFields, +} from "../../../../query-config" +import { AdminPostPriceListPriceBatchUpdate } from "../../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { prices } = req.validatedBody + + const id = req.params.id + const workflow = updatePriceListPricesWorkflow(req.scope) + const { errors } = await workflow.run({ + input: { data: [{ id, prices }] }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const priceList = await getPriceList({ + id, + container: req.scope, + remoteQueryFields: adminPriceListRemoteQueryFields, + apiFields: defaultAdminPriceListFields, + }) + + res.status(200).json({ price_list: priceList }) +} diff --git a/packages/medusa/src/api-v2/admin/price-lists/middlewares.ts b/packages/medusa/src/api-v2/admin/price-lists/middlewares.ts index 777348c617..4b951fd2cd 100644 --- a/packages/medusa/src/api-v2/admin/price-lists/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/price-lists/middlewares.ts @@ -5,6 +5,7 @@ import * as QueryConfig from "./query-config" import { AdminGetPriceListsParams, AdminGetPriceListsPriceListParams, + AdminPostPriceListPriceBatchUpdate, AdminPostPriceListsPriceListPricesBatchAddReq, AdminPostPriceListsPriceListPricesBatchRemoveReq, AdminPostPriceListsPriceListReq, @@ -59,4 +60,9 @@ export const adminPriceListsRoutesMiddlewares: MiddlewareRoute[] = [ transformBody(AdminPostPriceListsPriceListPricesBatchRemoveReq), ], }, + { + method: ["POST"], + matcher: "/admin/price-lists/:id/prices/batch/update", + middlewares: [transformBody(AdminPostPriceListPriceBatchUpdate)], + }, ] diff --git a/packages/medusa/src/api-v2/admin/price-lists/validators.ts b/packages/medusa/src/api-v2/admin/price-lists/validators.ts index 0c2772c740..bce61c8add 100644 --- a/packages/medusa/src/api-v2/admin/price-lists/validators.ts +++ b/packages/medusa/src/api-v2/admin/price-lists/validators.ts @@ -7,7 +7,6 @@ import { IsObject, IsOptional, IsString, - ValidateIf, ValidateNested, } from "class-validator" import { FindParams } from "../../../types/common" @@ -111,32 +110,33 @@ export class AdminPostPriceListsPriceListPricesBatchAddReq { prices: AdminPriceListPricesCreateReq[] } +export class AdminPostPriceListPriceBatchUpdate { + @IsOptional() + @IsArray() + prices: AdminPostPriceListPriceUpdate[] +} + export class AdminPostPriceListsPriceListPricesBatchRemoveReq { @IsArray() @IsString({ each: true }) ids: string[] } -export class AdminPriceListPricesUpdateReq { - @IsOptional() +export class AdminPostPriceListPriceUpdate { @IsString() id: string + @IsString() + variant_id: string + @IsOptional() - @ValidateIf((object) => !object.id) @IsString() currency_code?: string @IsOptional() - @ValidateIf((object) => !object.id) @IsInt() amount?: number - @IsOptional() - @ValidateIf((object) => !object.id) - @IsString() - variant_id: string - @IsOptional() @IsInt() min_quantity?: number diff --git a/packages/types/src/pricing/workflows.ts b/packages/types/src/pricing/workflows.ts index f03d1c34d7..9562ae8c1f 100644 --- a/packages/types/src/pricing/workflows.ts +++ b/packages/types/src/pricing/workflows.ts @@ -9,16 +9,6 @@ export interface CreatePriceListPriceWorkflowDTO { rules?: Record } -export interface UpdatePriceListPriceWorkflowDTO { - id: string - amount?: number - currency_code?: string - variant_id?: string - max_quantity?: number - min_quantity?: number - rules?: Record -} - export interface CreatePriceListWorkflowInputDTO { title: string description: string @@ -26,7 +16,7 @@ export interface CreatePriceListWorkflowInputDTO { ends_at?: string | null status?: PriceListStatus rules?: Record - prices?: CreatePriceListPriceWorkflowDTO[] + prices: CreatePriceListPriceWorkflowDTO[] } export interface UpdatePriceListWorkflowInputDTO { @@ -37,12 +27,19 @@ export interface UpdatePriceListWorkflowInputDTO { ends_at?: string | null status?: PriceListStatus rules?: Record - prices?: CreatePriceListPriceWorkflowDTO[] } export interface UpdatePriceListPricesWorkflowDTO { id: string - prices: UpdatePriceListPriceWorkflowDTO[] + prices: { + id: string + variant_id: string + amount?: number + currency_code?: string + max_quantity?: number + min_quantity?: number + rules?: Record + }[] } export interface CreatePriceListPricesWorkflowDTO { @@ -50,21 +47,11 @@ export interface CreatePriceListPricesWorkflowDTO { prices: CreatePriceListPriceWorkflowDTO[] } -export interface UpdatePriceListPriceWorkflowDTO { - data: Pick[] - variant_price_map: Record -} - export interface UpdatePriceListPriceWorkflowStepDTO { data?: UpdatePriceListPricesWorkflowDTO[] variant_price_map: Record } -export interface UpsertPriceListPricesWorkflowStepDTO { - data: Pick[] - variant_price_map: Record -} - export interface CreatePriceListsWorkflowStepDTO { data: CreatePriceListWorkflowInputDTO[] variant_price_map: Record