From 86f499de2f31356ab36ad5e93f27345443b3e5f6 Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Fri, 29 Mar 2024 11:23:24 +0100 Subject: [PATCH] feat: Implemented price set update with prices and aligned pricing API (#6872) --- .changeset/fluffy-mugs-sort.md | 7 ++ .../src/pricing/steps/update-price-sets.ts | 32 +++-- .../steps/update-pricing-rule-types.ts | 2 +- .../services/pricing-module/price-set.spec.ts | 69 +++++++++-- .../pricing/src/services/pricing-module.ts | 116 ++++++++++++++++-- packages/pricing/src/types/services/index.ts | 1 + .../pricing/src/types/services/price-set.ts | 5 + .../types/src/pricing/common/price-set.ts | 27 +++- packages/types/src/pricing/service.ts | 116 ++++++++++++++++-- 9 files changed, 326 insertions(+), 49 deletions(-) create mode 100644 .changeset/fluffy-mugs-sort.md create mode 100644 packages/pricing/src/types/services/price-set.ts diff --git a/.changeset/fluffy-mugs-sort.md b/.changeset/fluffy-mugs-sort.md new file mode 100644 index 0000000000..3b2638915f --- /dev/null +++ b/.changeset/fluffy-mugs-sort.md @@ -0,0 +1,7 @@ +--- +"@medusajs/pricing": minor +"@medusajs/types": minor +"@medusajs/core-flows": patch +--- + +Aligned pricing module price set API with convention diff --git a/packages/core-flows/src/pricing/steps/update-price-sets.ts b/packages/core-flows/src/pricing/steps/update-price-sets.ts index ec54f0e1ac..d2e40c95af 100644 --- a/packages/core-flows/src/pricing/steps/update-price-sets.ts +++ b/packages/core-flows/src/pricing/steps/update-price-sets.ts @@ -1,26 +1,36 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IPricingModuleService, UpdatePriceSetDTO } from "@medusajs/types" +import { IPricingModuleService, PricingTypes } from "@medusajs/types" import { convertItemResponseToUpdateRequest, getSelectsAndRelationsFromObjectArray, } from "@medusajs/utils" import { StepResponse, createStep } from "@medusajs/workflows-sdk" +type UpdatePriceSetsStepInput = { + selector: PricingTypes.FilterablePriceSetProps + update: PricingTypes.UpdatePriceSetDTO +} export const updatePriceSetsStepId = "update-price-sets" export const updatePriceSetsStep = createStep( updatePriceSetsStepId, - async (data: UpdatePriceSetDTO[], { container }) => { + async (data: UpdatePriceSetsStepInput, { container }) => { const pricingModule = container.resolve( ModuleRegistrationName.PRICING ) - const { selects, relations } = getSelectsAndRelationsFromObjectArray(data) - const dataBeforeUpdate = await pricingModule.list( - { id: data.map((d) => d.id) }, - { relations, select: selects } - ) + const { selects, relations } = getSelectsAndRelationsFromObjectArray([ + data.update, + ]) - const updatedPriceSets = await pricingModule.update(data) + const dataBeforeUpdate = await pricingModule.list(data.selector, { + select: selects, + relations, + }) + + const updatedPriceSets = await pricingModule.update( + data.selector, + data.update + ) return new StepResponse(updatedPriceSets, { dataBeforeUpdate, @@ -29,17 +39,17 @@ export const updatePriceSetsStep = createStep( }) }, async (revertInput, { container }) => { - if (!revertInput) { + if (!revertInput || !revertInput.dataBeforeUpdate?.length) { return } - const { dataBeforeUpdate = [], selects, relations } = revertInput + const { dataBeforeUpdate, selects, relations } = revertInput const pricingModule = container.resolve( ModuleRegistrationName.PRICING ) - await pricingModule.update( + await pricingModule.upsert( dataBeforeUpdate.map((data) => convertItemResponseToUpdateRequest(data, selects, relations) ) diff --git a/packages/core-flows/src/pricing/steps/update-pricing-rule-types.ts b/packages/core-flows/src/pricing/steps/update-pricing-rule-types.ts index 44c3ea5655..02e6bd1e25 100644 --- a/packages/core-flows/src/pricing/steps/update-pricing-rule-types.ts +++ b/packages/core-flows/src/pricing/steps/update-pricing-rule-types.ts @@ -39,7 +39,7 @@ export const updatePricingRuleTypesStep = createStep( ModuleRegistrationName.PRICING ) - await pricingModule.update( + await pricingModule.updateRuleTypes( dataBeforeUpdate.map((data) => convertItemResponseToUpdateRequest(data, selects, relations) ) diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts index 5748439d26..10c67c9f91 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts @@ -265,21 +265,64 @@ moduleIntegrationTestRunner({ describe("update", () => { const id = "price-set-1" - it("should throw an error when a id does not exist", async () => { - let error + it("should throw an error when an id does not exist", async () => { + let error = await service + .update("does-not-exist", {}) + .catch((e) => e.message) - try { - await service.update([ - { - id: "does-not-exist", - }, + expect(error).toEqual( + "PriceSet with id: does-not-exist was not found" + ) + }) + + it("should create, update, and delete prices to a price set", async () => { + const priceSetBefore = await service.retrieve(id, { + relations: ["prices"], + }) + + const updateResponse = await service.update(priceSetBefore.id, { + prices: [ + { amount: 100, currency_code: "USD" }, + { amount: 200, currency_code: "EUR" }, + ], + }) + + const priceSetAfter = await service.retrieve(id, { + relations: ["prices"], + }) + expect(priceSetBefore.prices).toHaveLength(1) + expect(priceSetBefore.prices?.[0]).toEqual( + expect.objectContaining({ + amount: 500, + currency_code: "USD", + }) + ) + + expect(priceSetAfter.prices).toHaveLength(2) + expect(priceSetAfter.prices).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + amount: 100, + currency_code: "USD", + }), + expect.objectContaining({ + amount: 200, + currency_code: "EUR", + }), + ]) + ) + expect(updateResponse.prices).toHaveLength(2) + expect(updateResponse.prices).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + amount: 100, + currency_code: "USD", + }), + expect.objectContaining({ + amount: 200, + currency_code: "EUR", + }), ]) - } catch (e) { - error = e - } - - expect(error.message).toEqual( - 'PriceSet with id "does-not-exist" not found' ) }) }) diff --git a/packages/pricing/src/services/pricing-module.ts b/packages/pricing/src/services/pricing-module.ts index 76720b96c1..aa0dbd451d 100644 --- a/packages/pricing/src/services/pricing-module.ts +++ b/packages/pricing/src/services/pricing-module.ts @@ -2,6 +2,7 @@ import { AddPricesDTO, Context, CreatePriceListRuleDTO, + CreatePriceSetDTO, CreatePricesDTO, DAL, InternalModuleDeclaration, @@ -13,6 +14,7 @@ import { PricingRepositoryService, PricingTypes, RuleTypeDTO, + UpsertPriceSetDTO, } from "@medusajs/types" import { arrayDifference, @@ -22,6 +24,7 @@ import { InjectManager, InjectTransactionManager, isDefined, + isString, MedusaContext, MedusaError, ModulesSdkUtils, @@ -46,6 +49,7 @@ import { validatePriceListDates } from "@utils" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { PriceSetIdPrefix } from "../models/price-set" import { PriceListIdPrefix } from "../models/price-list" +import { UpdatePriceSetInput } from "src/types/services" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -235,6 +239,7 @@ export default class PricingModuleService< const input = Array.isArray(data) ? data : [data] const priceSets = await this.create_(input, sharedContext) + // TODO: Remove the need to refetch the data here const dbPriceSets = await this.list( { id: priceSets.map((p) => p.id) }, { relations: ["rule_types", "prices", "price_rules"] }, @@ -246,7 +251,104 @@ export default class PricingModuleService< return dbPriceSets.find((p) => p.id === priceSet.id)! }) - return Array.isArray(data) ? results : results[0] + return await this.baseRepository_.serialize( + Array.isArray(data) ? results : results[0] + ) + } + + async upsert( + data: UpsertPriceSetDTO[], + sharedContext?: Context + ): Promise + async upsert( + data: UpsertPriceSetDTO, + sharedContext?: Context + ): Promise + + @InjectManager("baseRepository_") + async upsert( + data: UpsertPriceSetDTO | UpsertPriceSetDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const input = Array.isArray(data) ? data : [data] + const forUpdate = input.filter( + (priceSet): priceSet is UpdatePriceSetInput => !!priceSet.id + ) + const forCreate = input.filter( + (priceSet): priceSet is CreatePriceSetDTO => !priceSet.id + ) + + const operations: Promise[] = [] + + if (forCreate.length) { + operations.push(this.create_(forCreate, sharedContext)) + } + if (forUpdate.length) { + operations.push(this.update_(forUpdate, sharedContext)) + } + + const result = (await promiseAll(operations)).flat() + return await this.baseRepository_.serialize( + Array.isArray(data) ? result : result[0] + ) + } + + async update( + id: string, + data: PricingTypes.UpdatePriceSetDTO, + sharedContext?: Context + ): Promise + async update( + selector: PricingTypes.FilterablePriceSetProps, + data: PricingTypes.UpdatePriceSetDTO, + sharedContext?: Context + ): Promise + + @InjectManager("baseRepository_") + async update( + idOrSelector: string | PricingTypes.FilterablePriceSetProps, + data: PricingTypes.UpdatePriceSetDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise { + let normalizedInput: UpdatePriceSetInput[] = [] + if (isString(idOrSelector)) { + // Check if the ID exists, it will throw if not. + await this.priceSetService_.retrieve(idOrSelector, {}, sharedContext) + normalizedInput = [{ id: idOrSelector, ...data }] + } else { + const priceSets = await this.priceSetService_.list( + idOrSelector, + {}, + sharedContext + ) + + normalizedInput = priceSets.map((priceSet) => ({ + id: priceSet.id, + ...data, + })) + } + + const updateResult = await this.update_(normalizedInput, sharedContext) + const priceSets = await this.baseRepository_.serialize< + PriceSetDTO[] | PriceSetDTO + >(updateResult) + + return isString(idOrSelector) ? priceSets[0] : priceSets + } + + @InjectTransactionManager("baseRepository_") + protected async update_( + data: UpdatePriceSetInput[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + // TODO: We are not handling rule types, rules, etc. here, add support after data models are finalized + // TODO: Since money IDs are rarely passed, this will delete all previous data and insert new entries. + // We can make the `insert` inside upsertWithReplace do an `upsert` instead to avoid this + return this.priceSetService_.upsertWithReplace( + data, + { relations: ["prices"] }, + sharedContext + ) } async addRules( @@ -356,18 +458,6 @@ export default class PricingModuleService< ) } - @InjectTransactionManager("baseRepository_") - async update( - data: PricingTypes.UpdatePriceSetDTO[], - @MedusaContext() sharedContext: Context = {} - ) { - const priceSets = await this.priceSetService_.update(data, sharedContext) - - return await this.baseRepository_.serialize( - priceSets - ) - } - @InjectManager("baseRepository_") async createPriceLists( data: PricingTypes.CreatePriceListDTO[], diff --git a/packages/pricing/src/types/services/index.ts b/packages/pricing/src/types/services/index.ts index dfc795c078..0834669391 100644 --- a/packages/pricing/src/types/services/index.ts +++ b/packages/pricing/src/types/services/index.ts @@ -1 +1,2 @@ export * from "./price-list" +export * from "./price-set" diff --git a/packages/pricing/src/types/services/price-set.ts b/packages/pricing/src/types/services/price-set.ts new file mode 100644 index 0000000000..63a018e290 --- /dev/null +++ b/packages/pricing/src/types/services/price-set.ts @@ -0,0 +1,5 @@ +import { UpdatePriceSetDTO } from "@medusajs/types" + +export interface UpdatePriceSetInput extends UpdatePriceSetDTO { + id: string +} diff --git a/packages/types/src/pricing/common/price-set.ts b/packages/types/src/pricing/common/price-set.ts index f3f9d538d8..e115135df5 100644 --- a/packages/types/src/pricing/common/price-set.ts +++ b/packages/types/src/pricing/common/price-set.ts @@ -55,7 +55,7 @@ export interface PriceSetDTO { /** * The prices that belong to this price set. */ - money_amounts?: MoneyAmountDTO[] + prices?: MoneyAmountDTO[] /** * The rule types applied on this price set. */ @@ -279,6 +279,18 @@ export interface CreatePriceSetDTO { prices?: CreatePricesDTO[] } +/** + * @interface + * + * The data to upsert in a price set. The `id` is used in the case we are doing an update. + */ +export interface UpsertPriceSetDTO extends UpdatePriceSetDTO { + /** + * A string indicating the ID of the price set to update. + */ + id?: string +} + /** * @interface * @@ -286,9 +298,18 @@ export interface CreatePriceSetDTO { */ export interface UpdatePriceSetDTO { /** - * A string indicating the ID of the price set to update. + * The rules to associate with the price set. */ - id: string + rules?: { + /** + * the value of the rule's `rule_attribute` attribute. + */ + rule_attribute: string + }[] + /** + * The prices to create and add to this price set. + */ + prices?: CreatePricesDTO[] } /** diff --git a/packages/types/src/pricing/service.ts b/packages/types/src/pricing/service.ts index bad805f697..d5767f47b2 100644 --- a/packages/types/src/pricing/service.ts +++ b/packages/types/src/pricing/service.ts @@ -31,6 +31,7 @@ import { UpdatePriceRuleDTO, UpdatePriceSetDTO, UpdateRuleTypeDTO, + UpsertPriceSetDTO, } from "./common" import { FindConfig } from "../common" @@ -602,18 +603,117 @@ export interface IPricingModuleService extends IModuleService { ): Promise /** - * @ignore - * @privateRemarks - * The update method shouldn't be documented at the moment + * This method updates existing price sets, or creates new ones if they don't exist. * - * This method is used to update existing price sets. - * - * @param {UpdatePriceSetDTO[]} data - The price sets to update, each having the attributes that should be updated in a price set. + * @param {UpsertPriceSetDTO[]} data - The attributes to update or create for each price set. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise} The list of updated price sets. + * @returns {Promise} The updated and created price sets. + * + * @example + * import { + * initialize as initializePricingModule, + * } from "@medusajs/pricing" + * + * async function upsertPriceSet (title: string) { + * const pricingModule = await initializePricingModule() + * + * const createdPriceSets = await pricingModule.upsert([ + * { + * prices: [{amount: 100, currency_code: "USD"}] + * } + * ]) + * + * // do something with the price sets or return them + * } + */ + upsert( + data: UpsertPriceSetDTO[], + sharedContext?: Context + ): Promise + + /** + * This method updates the price set if it exists, or creates a new ones if it doesn't. + * + * @param {UpsertPriceSetDTO} data - The attributes to update or create for the new price set. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated or created price set. + * + * @example + * import { + * initialize as initializePricingModule, + * } from "@medusajs/pricing" + * + * async function upsertPriceSet (title: string) { + * const pricingModule = await initializePricingModule() + * + * const createdPriceSet = await pricingModule.upsert( + * { + * prices: [{amount: 100, currency_code: "USD"}] + * } + * ) + * + * // do something with the price set or return it + * } + */ + upsert(data: UpsertPriceSetDTO, sharedContext?: Context): Promise + + /** + * This method is used to update a price set. + * + * @param {string} id - The ID of the price set to be updated. + * @param {UpdatePriceSetDTO} data - The attributes of the price set to be updated + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated price set. + * + * @example + * import { + * initialize as initializePricingModule, + * } from "@medusajs/pricing" + * + * async function updatePriceSet (id: string, title: string) { + * const pricingtModule = await initializePricingModule() + * + * const priceSet = await pricingtModule.update(id, { + * prices: [{amount: 100, currency_code: "USD"}] + * } + * ) + * + * // do something with the price set or return it + * } */ update( - data: UpdatePriceSetDTO[], + id: string, + data: UpdatePriceSetDTO, + sharedContext?: Context + ): Promise + + /** + * This method is used to update a list of price sets determined by the selector filters. + * + * @param {FilterablePriceSetProps} selector - The filters that will determine which price sets will be updated. + * @param {UpdatePriceSetDTO} data - The attributes to be updated on the selected price sets + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated price sets. + * + * @example + * import { + * initialize as initializePricingModule, + * } from "@medusajs/pricing" + * + * async function updatePriceSet(id: string, title: string) { + * const pricingModule = await initializePricingModule() + * + * const priceSets = await pricingModule.update({id}, { + * prices: [{amount: 100, currency_code: "USD"}] + * } + * ) + * + * // do something with the price sets or return them + * } + */ + update( + selector: FilterablePriceSetProps, + data: UpdatePriceSetDTO, sharedContext?: Context ): Promise