diff --git a/.changeset/clean-llamas-join.md b/.changeset/clean-llamas-join.md new file mode 100644 index 0000000000000..f690bfc57db6a --- /dev/null +++ b/.changeset/clean-llamas-join.md @@ -0,0 +1,8 @@ +--- +"@medusajs/workflows-sdk": patch +"@medusajs/core-flows": patch +"@medusajs/medusa": patch +"@medusajs/types": patch +--- + +feat(workflows-sdk,core-flows,medusa,types): add workflow to add promotions to cart diff --git a/integration-tests/plugins/__tests__/cart/store/add-promotions-to-cart.spec.ts b/integration-tests/plugins/__tests__/cart/store/add-promotions-to-cart.spec.ts new file mode 100644 index 0000000000000..62273d24481ba --- /dev/null +++ b/integration-tests/plugins/__tests__/cart/store/add-promotions-to-cart.spec.ts @@ -0,0 +1,309 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICartModuleService, IPromotionModuleService } from "@medusajs/types" +import { PromotionType } from "@medusajs/utils" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import adminSeeder from "../../../../helpers/admin-seeder" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } + +describe("Store Carts API: Add promotions to cart", () => { + let dbConnection + let appContainer + let shutdownServer + let cartModuleService: ICartModuleService + let promotionModuleService: IPromotionModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + cartModuleService = appContainer.resolve(ModuleRegistrationName.CART) + promotionModuleService = appContainer.resolve( + ModuleRegistrationName.PROMOTION + ) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + describe("POST /store/carts/:id/promotions", () => { + it("should add line item adjustments to a cart based on promotions", async () => { + const appliedPromotion = await promotionModuleService.create({ + code: "PROMOTION_APPLIED", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "300", + apply_to_quantity: 1, + max_quantity: 1, + target_rules: [ + { + attribute: "product_id", + operator: "eq", + values: "prod_tshirt", + }, + ], + }, + }) + + const createdPromotion = await promotionModuleService.create({ + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "1000", + apply_to_quantity: 1, + target_rules: [ + { + attribute: "product_id", + operator: "eq", + values: "prod_mat", + }, + ], + }, + }) + + const cart = await cartModuleService.create({ + currency_code: "usd", + items: [ + // Adjustment to add + { + id: "item-1", + unit_price: 2000, + quantity: 1, + title: "Test item", + product_id: "prod_mat", + } as any, + // This adjustment will be removed and recreated + { + id: "item-2", + unit_price: 1000, + quantity: 1, + title: "Test item", + product_id: "prod_tshirt", + } as any, + ], + }) + + // Adjustment to keep + const [lineItemAdjustment] = + await cartModuleService.addLineItemAdjustments([ + { + code: appliedPromotion.code!, + amount: 300, + item_id: "item-2", + promotion_id: appliedPromotion.id, + }, + ]) + + const api = useApi() as any + + const created = await api.post(`/store/carts/${cart.id}/promotions`, { + promo_codes: [createdPromotion.code], + }) + + expect(created.status).toEqual(200) + expect(created.data.cart).toEqual( + expect.objectContaining({ + id: expect.any(String), + items: expect.arrayContaining([ + expect.objectContaining({ + id: "item-1", + adjustments: expect.arrayContaining([ + expect.objectContaining({ + promotion_id: createdPromotion.id, + code: createdPromotion.code, + amount: 1000, + }), + ]), + }), + expect.objectContaining({ + adjustments: expect.arrayContaining([ + expect.objectContaining({ + id: expect.not.stringContaining(lineItemAdjustment.id), + promotion_id: appliedPromotion.id, + code: appliedPromotion.code, + amount: 300, + }), + ]), + }), + ]), + }) + ) + }) + + it("should add shipping method adjustments to a cart based on promotions", async () => { + const [appliedPromotion] = await promotionModuleService.create([ + { + code: "PROMOTION_APPLIED", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer_id", + operator: "in", + values: ["cus_test"], + }, + { + attribute: "currency_code", + operator: "in", + values: ["eur"], + }, + ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "100", + max_quantity: 1, + target_rules: [ + { + attribute: "name", + operator: "in", + values: ["express"], + }, + ], + }, + }, + ]) + + const [newPromotion] = await promotionModuleService.create([ + { + code: "PROMOTION_NEW", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer_id", + operator: "in", + values: ["cus_test"], + }, + { + attribute: "currency_code", + operator: "in", + values: ["eur"], + }, + ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 1, + target_rules: [ + { + attribute: "name", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const cart = await cartModuleService.create({ + currency_code: "eur", + customer_id: "cus_test", + items: [ + { + unit_price: 2000, + quantity: 1, + title: "Test item", + product_id: "prod_mat", + } as any, + ], + }) + + const [express, standard] = await cartModuleService.addShippingMethods( + cart.id, + [ + { + amount: 500, + name: "express", + }, + { + amount: 500, + name: "standard", + }, + ] + ) + + const [adjustment] = await cartModuleService.addShippingMethodAdjustments( + cart.id, + [ + { + shipping_method_id: express.id, + amount: 100, + code: appliedPromotion.code!, + }, + ] + ) + + const api = useApi() as any + + const created = await api.post(`/store/carts/${cart.id}/promotions`, { + promo_codes: [newPromotion.code], + }) + + expect(created.status).toEqual(200) + expect(created.data.cart).toEqual( + expect.objectContaining({ + id: expect.any(String), + items: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + }), + ]), + shipping_methods: expect.arrayContaining([ + expect.objectContaining({ + id: express.id, + adjustments: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + amount: 200, + code: newPromotion.code, + }), + expect.objectContaining({ + id: expect.not.stringContaining(adjustment.id), + amount: 100, + code: appliedPromotion.code, + }), + ]), + }), + expect.objectContaining({ + id: standard.id, + adjustments: [ + expect.objectContaining({ + id: expect.any(String), + amount: 200, + code: newPromotion.code, + }), + ], + }), + ]), + }) + ) + }) + }) +}) diff --git a/integration-tests/plugins/__tests__/cart/store/remove-promotions-from-cart.spec.ts b/integration-tests/plugins/__tests__/cart/store/remove-promotions-from-cart.spec.ts new file mode 100644 index 0000000000000..286d113160272 --- /dev/null +++ b/integration-tests/plugins/__tests__/cart/store/remove-promotions-from-cart.spec.ts @@ -0,0 +1,300 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICartModuleService, IPromotionModuleService } from "@medusajs/types" +import { PromotionType } from "@medusajs/utils" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import adminSeeder from "../../../../helpers/admin-seeder" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } + +describe("Store Carts API: Remove promotions from cart", () => { + let dbConnection + let appContainer + let shutdownServer + let cartModuleService: ICartModuleService + let promotionModuleService: IPromotionModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + cartModuleService = appContainer.resolve(ModuleRegistrationName.CART) + promotionModuleService = appContainer.resolve( + ModuleRegistrationName.PROMOTION + ) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + describe("DELETE /store/carts/:id/promotions", () => { + it("should remove line item adjustments from a cart based on promotions", async () => { + const appliedPromotion = await promotionModuleService.create({ + code: "PROMOTION_APPLIED", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "300", + apply_to_quantity: 1, + max_quantity: 1, + target_rules: [ + { + attribute: "product_id", + operator: "eq", + values: "prod_tshirt", + }, + ], + }, + }) + + const appliedPromotionToRemove = await promotionModuleService.create({ + code: "PROMOTION_APPLIED_TO_REMOVE", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "300", + apply_to_quantity: 1, + max_quantity: 1, + target_rules: [ + { + attribute: "product_id", + operator: "eq", + values: "prod_tshirt", + }, + ], + }, + }) + + const cart = await cartModuleService.create({ + currency_code: "usd", + items: [ + { + id: "item-1", + unit_price: 2000, + quantity: 1, + title: "Test item", + product_id: "prod_mat", + } as any, + { + id: "item-2", + unit_price: 1000, + quantity: 1, + title: "Test item", + product_id: "prod_tshirt", + } as any, + ], + }) + + const [adjustment1, adjustment2] = + await cartModuleService.addLineItemAdjustments([ + { + code: appliedPromotion.code!, + amount: 300, + item_id: "item-2", + promotion_id: appliedPromotionToRemove.id, + }, + { + code: appliedPromotionToRemove.code!, + amount: 150, + item_id: "item-1", + promotion_id: appliedPromotionToRemove.id, + }, + { + code: appliedPromotionToRemove.code!, + amount: 150, + item_id: "item-2", + promotion_id: appliedPromotionToRemove.id, + }, + ]) + + const api = useApi() as any + + const response = await api.delete(`/store/carts/${cart.id}/promotions`, { + data: { + promo_codes: [appliedPromotionToRemove.code], + }, + }) + + expect(response.status).toEqual(200) + expect(response.data.cart).toEqual( + expect.objectContaining({ + id: expect.any(String), + items: expect.arrayContaining([ + expect.objectContaining({ + id: "item-1", + adjustments: [], + }), + expect.objectContaining({ + id: "item-2", + adjustments: [ + expect.objectContaining({ + amount: 300, + code: appliedPromotion.code, + }), + ], + }), + ]), + }) + ) + }) + + it("should add shipping method adjustments to a cart based on promotions", async () => { + const appliedPromotion = await promotionModuleService.create({ + code: "PROMOTION_APPLIED", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer_id", + operator: "in", + values: ["cus_test"], + }, + { + attribute: "currency_code", + operator: "in", + values: ["eur"], + }, + ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "100", + max_quantity: 1, + target_rules: [ + { + attribute: "name", + operator: "in", + values: ["express"], + }, + ], + }, + }) + + const appliedPromotionToRemove = await promotionModuleService.create({ + code: "PROMOTION_APPLIED_TO_REMOVE", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer_id", + operator: "in", + values: ["cus_test"], + }, + { + attribute: "currency_code", + operator: "in", + values: ["eur"], + }, + ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "100", + max_quantity: 1, + target_rules: [ + { + attribute: "name", + operator: "in", + values: ["express"], + }, + ], + }, + }) + + const cart = await cartModuleService.create({ + currency_code: "eur", + customer_id: "cus_test", + items: [ + { + unit_price: 2000, + quantity: 1, + title: "Test item", + product_id: "prod_mat", + }, + ], + }) + + const [express, standard] = await cartModuleService.addShippingMethods( + cart.id, + [ + { + amount: 500, + name: "express", + }, + { + amount: 500, + name: "standard", + }, + ] + ) + + await cartModuleService.addShippingMethodAdjustments(cart.id, [ + { + shipping_method_id: express.id, + amount: 100, + code: appliedPromotion.code!, + }, + { + shipping_method_id: express.id, + amount: 100, + code: appliedPromotionToRemove.code!, + }, + ]) + + const api = useApi() as any + + const response = await api.delete(`/store/carts/${cart.id}/promotions`, { + data: { promo_codes: [appliedPromotionToRemove.code] }, + }) + + expect(response.status).toEqual(200) + expect(response.data.cart).toEqual( + expect.objectContaining({ + id: expect.any(String), + items: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + }), + ]), + shipping_methods: expect.arrayContaining([ + expect.objectContaining({ + id: express.id, + adjustments: [ + expect.objectContaining({ + amount: 100, + code: appliedPromotion.code, + }), + ], + }), + expect.objectContaining({ + id: standard.id, + adjustments: [], + }), + ]), + }) + ) + }) + }) +}) diff --git a/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts b/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts index b2c2052293e37..5440b9938b3e1 100644 --- a/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts +++ b/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts @@ -291,12 +291,9 @@ describe("Cart Module Service", () => { }, ]) - const updatedCart = await service.update( - createdCart.id, - { - email: "test@email.com", - } - ) + const updatedCart = await service.update(createdCart.id, { + email: "test@email.com", + }) const [cart] = await service.list({ id: [createdCart.id] }) diff --git a/packages/cart/src/models/line-item.ts b/packages/cart/src/models/line-item.ts index b86057b59a5d8..7c27e6a064003 100644 --- a/packages/cart/src/models/line-item.ts +++ b/packages/cart/src/models/line-item.ts @@ -2,12 +2,12 @@ import { BigNumberRawValue, DAL } from "@medusajs/types" import { BigNumber, DALUtils, + MikroOrmBigNumberProperty, createPsqlIndexStatementHelper, generateEntityId, } from "@medusajs/utils" import { BeforeCreate, - BeforeUpdate, Cascade, Collection, Entity, @@ -123,13 +123,13 @@ export default class LineItem { @Property({ columnType: "boolean" }) is_tax_inclusive = false - @Property({ columnType: "numeric", nullable: true }) + @MikroOrmBigNumberProperty({ nullable: true }) compare_at_unit_price?: BigNumber | number | null = null @Property({ columnType: "jsonb", nullable: true }) raw_compare_at_unit_price: BigNumberRawValue | null = null - @Property({ columnType: "numeric" }) + @MikroOrmBigNumberProperty() unit_price: BigNumber | number @Property({ columnType: "jsonb" }) @@ -171,28 +171,10 @@ export default class LineItem { @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "cali") - - const val = new BigNumber(this.raw_unit_price ?? this.unit_price) - - this.unit_price = val.numeric - this.raw_unit_price = val.raw! - } - - @BeforeUpdate() - onUpdate() { - const val = new BigNumber(this.raw_unit_price ?? this.unit_price) - - this.unit_price = val.numeric - this.raw_unit_price = val.raw as BigNumberRawValue } @OnInit() onInit() { this.id = generateEntityId(this.id, "cali") - - const val = new BigNumber(this.raw_unit_price ?? this.unit_price) - - this.unit_price = val.numeric - this.raw_unit_price = val.raw! } } diff --git a/packages/cart/src/models/shipping-method.ts b/packages/cart/src/models/shipping-method.ts index 72157c4fde528..c3e59e432481b 100644 --- a/packages/cart/src/models/shipping-method.ts +++ b/packages/cart/src/models/shipping-method.ts @@ -2,6 +2,7 @@ import { BigNumberRawValue, DAL } from "@medusajs/types" import { BigNumber, DALUtils, + MikroOrmBigNumberProperty, createPsqlIndexStatementHelper, generateEntityId, } from "@medusajs/utils" @@ -19,7 +20,6 @@ import { PrimaryKey, Property, } from "@mikro-orm/core" -import { BeforeUpdate } from "typeorm" import Cart from "./cart" import ShippingMethodAdjustment from "./shipping-method-adjustment" import ShippingMethodTaxLine from "./shipping-method-tax-line" @@ -49,7 +49,7 @@ export default class ShippingMethod { @ManyToOne({ entity: () => Cart, - cascade: [Cascade.REMOVE, Cascade.PERSIST, "soft-remove"] as any, + cascade: [Cascade.REMOVE, Cascade.PERSIST], }) cart: Cart @@ -59,7 +59,7 @@ export default class ShippingMethod { @Property({ columnType: "jsonb", nullable: true }) description: string | null = null - @Property({ columnType: "numeric" }) + @MikroOrmBigNumberProperty() amount: BigNumber | number @Property({ columnType: "jsonb" }) @@ -127,28 +127,10 @@ export default class ShippingMethod { @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "casm") - - const val = new BigNumber(this.raw_amount ?? this.amount) - - this.amount = val.numeric - this.raw_amount = val.raw! - } - - @BeforeUpdate() - onUpdate() { - const val = new BigNumber(this.raw_amount ?? this.amount) - - this.amount = val.numeric - this.raw_amount = val.raw as BigNumberRawValue } @OnInit() onInit() { this.id = generateEntityId(this.id, "casm") - - const val = new BigNumber(this.raw_amount ?? this.amount) - - this.amount = val.numeric - this.raw_amount = val.raw! } } diff --git a/packages/core-flows/src/definition/cart/steps/create-line-item-adjustments.ts b/packages/core-flows/src/definition/cart/steps/create-line-item-adjustments.ts new file mode 100644 index 0000000000000..56ca03ffbb0f2 --- /dev/null +++ b/packages/core-flows/src/definition/cart/steps/create-line-item-adjustments.ts @@ -0,0 +1,41 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + CreateLineItemAdjustmentDTO, + ICartModuleService, +} from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +interface StepInput { + lineItemAdjustmentsToCreate: CreateLineItemAdjustmentDTO[] +} + +export const createLineItemAdjustmentsStepId = "create-line-item-adjustments" +export const createLineItemAdjustmentsStep = createStep( + createLineItemAdjustmentsStepId, + async (data: StepInput, { container }) => { + const { lineItemAdjustmentsToCreate = [] } = data + const cartModuleService: ICartModuleService = container.resolve( + ModuleRegistrationName.CART + ) + + const createdLineItemAdjustments = + await cartModuleService.addLineItemAdjustments( + lineItemAdjustmentsToCreate + ) + + return new StepResponse(void 0, createdLineItemAdjustments) + }, + async (createdLineItemAdjustments, { container }) => { + const cartModuleService: ICartModuleService = container.resolve( + ModuleRegistrationName.CART + ) + + if (!createdLineItemAdjustments?.length) { + return + } + + await cartModuleService.softDeleteLineItemAdjustments( + createdLineItemAdjustments.map((c) => c.id) + ) + } +) diff --git a/packages/core-flows/src/definition/cart/steps/create-shipping-method-adjustments.ts b/packages/core-flows/src/definition/cart/steps/create-shipping-method-adjustments.ts new file mode 100644 index 0000000000000..44ebdd46959a9 --- /dev/null +++ b/packages/core-flows/src/definition/cart/steps/create-shipping-method-adjustments.ts @@ -0,0 +1,42 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + CreateShippingMethodAdjustmentDTO, + ICartModuleService, +} from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +interface StepInput { + shippingMethodAdjustmentsToCreate: CreateShippingMethodAdjustmentDTO[] +} + +export const createShippingMethodAdjustmentsStepId = + "create-shipping-method-adjustments" +export const createShippingMethodAdjustmentsStep = createStep( + createShippingMethodAdjustmentsStepId, + async (data: StepInput, { container }) => { + const { shippingMethodAdjustmentsToCreate = [] } = data + const cartModuleService: ICartModuleService = container.resolve( + ModuleRegistrationName.CART + ) + + const createdShippingMethodAdjustments = + await cartModuleService.addShippingMethodAdjustments( + shippingMethodAdjustmentsToCreate + ) + + return new StepResponse(void 0, createdShippingMethodAdjustments) + }, + async (createdShippingMethodAdjustments, { container }) => { + const cartModuleService: ICartModuleService = container.resolve( + ModuleRegistrationName.CART + ) + + if (!createdShippingMethodAdjustments?.length) { + return + } + + await cartModuleService.softDeleteShippingMethodAdjustments( + createdShippingMethodAdjustments.map((c) => c.id) + ) + } +) diff --git a/packages/core-flows/src/definition/cart/steps/get-actions-to-compute-from-promotions.ts b/packages/core-flows/src/definition/cart/steps/get-actions-to-compute-from-promotions.ts new file mode 100644 index 0000000000000..3e16e27f23648 --- /dev/null +++ b/packages/core-flows/src/definition/cart/steps/get-actions-to-compute-from-promotions.ts @@ -0,0 +1,54 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { CartDTO, IPromotionModuleService } from "@medusajs/types" +import { deduplicate, isString } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +interface StepInput { + cart: CartDTO + promoCodes: string[] + removePromotions: boolean +} + +export const getActionsToComputeFromPromotionsStepId = + "get-actions-to-compute-from-promotions" +export const getActionsToComputeFromPromotionsStep = createStep( + getActionsToComputeFromPromotionsStepId, + async (data: StepInput, { container }) => { + const promotionModuleService: IPromotionModuleService = container.resolve( + ModuleRegistrationName.PROMOTION + ) + + const { removePromotions = false, promoCodes = [], cart } = data + + const appliedItemPromoCodes = cart.items + ?.map((item) => item.adjustments?.map((adjustment) => adjustment.code)) + .flat(1) + .filter(isString) as string[] + + const appliedShippingMethodPromoCodes = cart.shipping_methods + ?.map((shippingMethod) => + shippingMethod.adjustments?.map((adjustment) => adjustment.code) + ) + .flat(1) + .filter(isString) as string[] + + let promotionCodesToApply = deduplicate([ + ...promoCodes, + ...appliedItemPromoCodes, + ...appliedShippingMethodPromoCodes, + ]) + + if (removePromotions) { + promotionCodesToApply = promotionCodesToApply.filter( + (code) => !promoCodes.includes(code) + ) + } + + const actionsToCompute = await promotionModuleService.computeActions( + promotionCodesToApply, + cart as any + ) + + return new StepResponse(actionsToCompute) + } +) diff --git a/packages/core-flows/src/definition/cart/steps/index.ts b/packages/core-flows/src/definition/cart/steps/index.ts index d2b4329db5b4d..2809b76efb4cc 100644 --- a/packages/core-flows/src/definition/cart/steps/index.ts +++ b/packages/core-flows/src/definition/cart/steps/index.ts @@ -1,5 +1,12 @@ export * from "./create-carts" +export * from "./create-line-item-adjustments" +export * from "./create-shipping-method-adjustments" export * from "./find-one-or-any-region" export * from "./find-or-create-customer" export * from "./find-sales-channel" +export * from "./get-actions-to-compute-from-promotions" +export * from "./prepare-adjustments-from-promotion-actions" +export * from "./remove-line-item-adjustments" +export * from "./remove-shipping-method-adjustments" +export * from "./retrieve-cart" export * from "./update-carts" diff --git a/packages/core-flows/src/definition/cart/steps/prepare-adjustments-from-promotion-actions.ts b/packages/core-flows/src/definition/cart/steps/prepare-adjustments-from-promotion-actions.ts new file mode 100644 index 0000000000000..9c12cc9905589 --- /dev/null +++ b/packages/core-flows/src/definition/cart/steps/prepare-adjustments-from-promotion-actions.ts @@ -0,0 +1,75 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + ComputeActions, + IPromotionModuleService, + PromotionDTO, + RemoveItemAdjustmentAction, + RemoveShippingMethodAdjustment, +} from "@medusajs/types" +import { ComputedActions } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +interface StepInput { + actions: ComputeActions[] +} + +export const prepareAdjustmentsFromPromotionActionsStepId = + "prepare-adjustments-from-promotion-actions" +export const prepareAdjustmentsFromPromotionActionsStep = createStep( + prepareAdjustmentsFromPromotionActionsStepId, + async (data: StepInput, { container }) => { + const promotionModuleService: IPromotionModuleService = container.resolve( + ModuleRegistrationName.PROMOTION + ) + + const { actions = [] } = data + const promotions = await promotionModuleService.list( + { code: actions.map((a) => a.code) }, + { select: ["id", "code"] } + ) + + const promotionsMap = new Map( + promotions.map((promotion) => [promotion.code!, promotion]) + ) + + const lineItemAdjustmentsToCreate = actions + .filter((a) => a.action === ComputedActions.ADD_ITEM_ADJUSTMENT) + .map((action) => ({ + code: action.code, + amount: (action as AddItemAdjustmentAction).amount, + item_id: (action as AddItemAdjustmentAction).item_id, + promotion_id: promotionsMap.get(action.code)?.id, + })) + + const lineItemAdjustmentIdsToRemove = actions + .filter((a) => a.action === ComputedActions.REMOVE_ITEM_ADJUSTMENT) + .map((a) => (a as RemoveItemAdjustmentAction).adjustment_id) + + const shippingMethodAdjustmentsToCreate = actions + .filter( + (a) => a.action === ComputedActions.ADD_SHIPPING_METHOD_ADJUSTMENT + ) + .map((action) => ({ + code: action.code, + amount: (action as AddShippingMethodAdjustment).amount, + shipping_method_id: (action as AddShippingMethodAdjustment) + .shipping_method_id, + promotion_id: promotionsMap.get(action.code)?.id, + })) + + const shippingMethodAdjustmentIdsToRemove = actions + .filter( + (a) => a.action === ComputedActions.REMOVE_SHIPPING_METHOD_ADJUSTMENT + ) + .map((a) => (a as RemoveShippingMethodAdjustment).adjustment_id) + + return new StepResponse({ + lineItemAdjustmentsToCreate, + lineItemAdjustmentIdsToRemove, + shippingMethodAdjustmentsToCreate, + shippingMethodAdjustmentIdsToRemove, + }) + } +) diff --git a/packages/core-flows/src/definition/cart/steps/remove-line-item-adjustments.ts b/packages/core-flows/src/definition/cart/steps/remove-line-item-adjustments.ts new file mode 100644 index 0000000000000..8524adf11a2c0 --- /dev/null +++ b/packages/core-flows/src/definition/cart/steps/remove-line-item-adjustments.ts @@ -0,0 +1,37 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICartModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +interface StepInput { + lineItemAdjustmentIdsToRemove: string[] +} + +export const removeLineItemAdjustmentsStepId = "remove-line-item-adjustments" +export const removeLineItemAdjustmentsStep = createStep( + removeLineItemAdjustmentsStepId, + async (data: StepInput, { container }) => { + const { lineItemAdjustmentIdsToRemove = [] } = data + const cartModuleService: ICartModuleService = container.resolve( + ModuleRegistrationName.CART + ) + + await cartModuleService.softDeleteLineItemAdjustments( + lineItemAdjustmentIdsToRemove + ) + + return new StepResponse(void 0, lineItemAdjustmentIdsToRemove) + }, + async (lineItemAdjustmentIdsToRemove, { container }) => { + const cartModuleService: ICartModuleService = container.resolve( + ModuleRegistrationName.CART + ) + + if (!lineItemAdjustmentIdsToRemove?.length) { + return + } + + await cartModuleService.restoreLineItemAdjustments( + lineItemAdjustmentIdsToRemove + ) + } +) diff --git a/packages/core-flows/src/definition/cart/steps/remove-shipping-method-adjustments.ts b/packages/core-flows/src/definition/cart/steps/remove-shipping-method-adjustments.ts new file mode 100644 index 0000000000000..8403ffc7e2f43 --- /dev/null +++ b/packages/core-flows/src/definition/cart/steps/remove-shipping-method-adjustments.ts @@ -0,0 +1,38 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICartModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +interface StepInput { + shippingMethodAdjustmentIdsToRemove: string[] +} + +export const removeShippingMethodAdjustmentsStepId = + "remove-shipping-method-adjustments" +export const removeShippingMethodAdjustmentsStep = createStep( + removeShippingMethodAdjustmentsStepId, + async (data: StepInput, { container }) => { + const { shippingMethodAdjustmentIdsToRemove = [] } = data + const cartModuleService: ICartModuleService = container.resolve( + ModuleRegistrationName.CART + ) + + await cartModuleService.softDeleteShippingMethodAdjustments( + shippingMethodAdjustmentIdsToRemove + ) + + return new StepResponse(void 0, shippingMethodAdjustmentIdsToRemove) + }, + async (shippingMethodAdjustmentIdsToRemove, { container }) => { + const cartModuleService: ICartModuleService = container.resolve( + ModuleRegistrationName.CART + ) + + if (!shippingMethodAdjustmentIdsToRemove?.length) { + return + } + + await cartModuleService.restoreShippingMethodAdjustments( + shippingMethodAdjustmentIdsToRemove + ) + } +) diff --git a/packages/core-flows/src/definition/cart/steps/retrieve-cart.ts b/packages/core-flows/src/definition/cart/steps/retrieve-cart.ts new file mode 100644 index 0000000000000..fc70c4ba09bed --- /dev/null +++ b/packages/core-flows/src/definition/cart/steps/retrieve-cart.ts @@ -0,0 +1,37 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { CartDTO, FindConfig, ICartModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +interface StepInput { + cartId: string + config: FindConfig +} + +export const retrieveCartStepId = "retrieve-cart" +export const retrieveCartStep = createStep( + retrieveCartStepId, + async (data: StepInput, { container }) => { + const cartModuleService = container.resolve( + ModuleRegistrationName.CART + ) + + const cart = await cartModuleService.retrieve(data.cartId, data.config) + + // TODO: remove this when cart handles totals calculation + cart.items = cart.items?.map((item) => { + item.subtotal = item.unit_price + + return item + }) + + // TODO: remove this when cart handles totals calculation + cart.shipping_methods = cart.shipping_methods?.map((shipping_method) => { + // TODO: should we align all amounts/prices fields to be unit_price? + shipping_method.subtotal = shipping_method.amount + + return shipping_method + }) + + return new StepResponse(cart) + } +) diff --git a/packages/core-flows/src/definition/cart/workflows/index.ts b/packages/core-flows/src/definition/cart/workflows/index.ts index 92c4628406e3a..2255e2a619555 100644 --- a/packages/core-flows/src/definition/cart/workflows/index.ts +++ b/packages/core-flows/src/definition/cart/workflows/index.ts @@ -1,3 +1,3 @@ export * from "./create-carts" +export * from "./update-cart-promotions" export * from "./update-carts" - diff --git a/packages/core-flows/src/definition/cart/workflows/update-cart-promotions.ts b/packages/core-flows/src/definition/cart/workflows/update-cart-promotions.ts new file mode 100644 index 0000000000000..c86469f5c72aa --- /dev/null +++ b/packages/core-flows/src/definition/cart/workflows/update-cart-promotions.ts @@ -0,0 +1,69 @@ +import { CartDTO } from "@medusajs/types" +import { + WorkflowData, + createWorkflow, + parallelize, +} from "@medusajs/workflows-sdk" +import { + createLineItemAdjustmentsStep, + createShippingMethodAdjustmentsStep, + getActionsToComputeFromPromotionsStep, + prepareAdjustmentsFromPromotionActionsStep, + removeLineItemAdjustmentsStep, + removeShippingMethodAdjustmentsStep, + retrieveCartStep, +} from "../steps" + +type WorkflowInput = { + promoCodes: string[] + cartId: string + removePromotions?: boolean +} + +export const updateCartPromotionsWorkflowId = "update-cart-promotions" +export const updateCartPromotionsWorkflow = createWorkflow( + updateCartPromotionsWorkflowId, + (input: WorkflowData): WorkflowData => { + const retrieveCartInput = { + cartId: input.cartId, + config: { + relations: [ + "items", + "items.adjustments", + "shipping_methods", + "shipping_methods.adjustments", + ], + }, + } + + const cart = retrieveCartStep(retrieveCartInput) + const actions = getActionsToComputeFromPromotionsStep({ + cart, + promoCodes: input.promoCodes, + removePromotions: input.removePromotions || false, + }) + + const { + lineItemAdjustmentsToCreate, + lineItemAdjustmentIdsToRemove, + shippingMethodAdjustmentsToCreate, + shippingMethodAdjustmentIdsToRemove, + } = prepareAdjustmentsFromPromotionActionsStep({ actions }) + + parallelize( + removeLineItemAdjustmentsStep({ lineItemAdjustmentIdsToRemove }), + removeShippingMethodAdjustmentsStep({ + shippingMethodAdjustmentIdsToRemove, + }) + ) + + parallelize( + createLineItemAdjustmentsStep({ lineItemAdjustmentsToCreate }), + createShippingMethodAdjustmentsStep({ shippingMethodAdjustmentsToCreate }) + ) + + return retrieveCartStep(retrieveCartInput).config({ + name: "retrieve-cart-result-step", + }) + } +) diff --git a/packages/medusa/src/api-v2/store/carts/[id]/promotions/route.ts b/packages/medusa/src/api-v2/store/carts/[id]/promotions/route.ts new file mode 100644 index 0000000000000..7bb51a6955f29 --- /dev/null +++ b/packages/medusa/src/api-v2/store/carts/[id]/promotions/route.ts @@ -0,0 +1,42 @@ +import { updateCartPromotionsWorkflow } from "@medusajs/core-flows" +import { MedusaRequest, MedusaResponse } from "../../../../../types/routing" +import { StorePostCartsCartPromotionsReq } from "../../validators" + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + const workflow = updateCartPromotionsWorkflow(req.scope) + const payload = req.validatedBody as StorePostCartsCartPromotionsReq + + const { result, errors } = await workflow.run({ + input: { + promoCodes: payload.promo_codes, + cartId: req.params.id, + }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ cart: result }) +} + +export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => { + const workflow = updateCartPromotionsWorkflow(req.scope) + const payload = req.validatedBody as StorePostCartsCartPromotionsReq + + const { result, errors } = await workflow.run({ + input: { + promoCodes: payload.promo_codes, + cartId: req.params.id, + removePromotions: true, + }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ cart: result }) +} diff --git a/packages/medusa/src/api-v2/store/carts/middlewares.ts b/packages/medusa/src/api-v2/store/carts/middlewares.ts index b224a56666f59..e38e8d6855001 100644 --- a/packages/medusa/src/api-v2/store/carts/middlewares.ts +++ b/packages/medusa/src/api-v2/store/carts/middlewares.ts @@ -3,8 +3,10 @@ import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { authenticate } from "../../../utils/authenticate-middleware" import * as QueryConfig from "./query-config" import { + StoreDeleteCartsCartPromotionsReq, StoreGetCartsCartParams, StorePostCartReq, + StorePostCartsCartPromotionsReq, StorePostCartsCartReq, } from "./validators" @@ -38,4 +40,14 @@ export const storeCartRoutesMiddlewares: MiddlewareRoute[] = [ matcher: "/store/carts/:id", middlewares: [transformBody(StorePostCartsCartReq)], }, + { + method: ["POST"], + matcher: "/store/carts/:id/promotions", + middlewares: [transformBody(StorePostCartsCartPromotionsReq)], + }, + { + method: ["DELETE"], + matcher: "/store/carts/:id/promotions", + middlewares: [transformBody(StoreDeleteCartsCartPromotionsReq)], + }, ] diff --git a/packages/medusa/src/api-v2/store/carts/validators.ts b/packages/medusa/src/api-v2/store/carts/validators.ts index da22a235f6226..90c6e26495455 100644 --- a/packages/medusa/src/api-v2/store/carts/validators.ts +++ b/packages/medusa/src/api-v2/store/carts/validators.ts @@ -52,6 +52,18 @@ export class StorePostCartReq { metadata?: Record } +export class StorePostCartsCartPromotionsReq { + @IsArray() + @Type(() => String) + promo_codes: string[] +} + +export class StoreDeleteCartsCartPromotionsReq { + @IsArray() + @Type(() => String) + promo_codes: string[] +} + export class StorePostCartsCartReq { @IsOptional() @IsString() diff --git a/packages/promotion/src/services/promotion-module.ts b/packages/promotion/src/services/promotion-module.ts index 008003032e625..2427fba0bb9b7 100644 --- a/packages/promotion/src/services/promotion-module.ts +++ b/packages/promotion/src/services/promotion-module.ts @@ -11,11 +11,11 @@ import { CampaignBudgetType, InjectManager, InjectTransactionManager, - isString, MedusaContext, MedusaError, ModulesSdkUtils, PromotionType, + isString, } from "@medusajs/utils" import { ApplicationMethod, @@ -38,9 +38,9 @@ import { UpdatePromotionDTO, } from "@types" import { + ComputeActionUtils, allowedAllocationForQuantity, areRulesValidForContext, - ComputeActionUtils, validateApplicationMethodAttributes, validatePromotionRuleAttributes, } from "@utils" @@ -166,10 +166,10 @@ export default class PromotionModuleService< if (campaignBudget.type === CampaignBudgetType.SPEND) { const campaignBudgetData = promotionCodeCampaignBudgetMap.get( campaignBudget.id - ) || { id: campaignBudget.id, used: campaignBudget.used || 0 } + ) || { id: campaignBudget.id, used: campaignBudget.used ?? 0 } campaignBudgetData.used = - (campaignBudgetData.used || 0) + computedAction.amount + (campaignBudgetData.used ?? 0) + computedAction.amount if ( campaignBudget.limit && @@ -194,7 +194,7 @@ export default class PromotionModuleService< const campaignBudgetData = { id: campaignBudget.id, - used: (campaignBudget.used || 0) + 1, + used: (campaignBudget.used ?? 0) + 1, } if ( @@ -225,10 +225,12 @@ export default class PromotionModuleService< } } + @InjectManager("baseRepository_") async computeActions( promotionCodes: string[], applicationContext: PromotionTypes.ComputeActionContext, - options: PromotionTypes.ComputeActionOptions = {} + options: PromotionTypes.ComputeActionOptions = {}, + @MedusaContext() sharedContext: Context = {} ): Promise { const { prevent_auto_promotions: preventAutoPromotions } = options const computedActions: PromotionTypes.ComputeActions[] = [] @@ -238,12 +240,16 @@ export default class PromotionModuleService< const appliedShippingCodes: string[] = [] const codeAdjustmentMap = new Map< string, - PromotionTypes.ComputeActionAdjustmentLine + PromotionTypes.ComputeActionAdjustmentLine[] >() const methodIdPromoValueMap = new Map() const automaticPromotions = preventAutoPromotions ? [] - : await this.list({ is_automatic: true }, { select: ["code"] }) + : await this.list( + { is_automatic: true }, + { select: ["code"] }, + sharedContext + ) const automaticPromotionCodes = automaticPromotions.map((p) => p.code!) const promotionCodesToApply = [ @@ -254,7 +260,11 @@ export default class PromotionModuleService< items.forEach((item) => { item.adjustments?.forEach((adjustment) => { if (isString(adjustment.code)) { - codeAdjustmentMap.set(adjustment.code, adjustment) + const adjustments = codeAdjustmentMap.get(adjustment.code) || [] + + adjustments.push(adjustment) + + codeAdjustmentMap.set(adjustment.code, adjustments) appliedItemCodes.push(adjustment.code) } }) @@ -263,7 +273,11 @@ export default class PromotionModuleService< shippingMethods.forEach((shippingMethod) => { shippingMethod.adjustments?.forEach((adjustment) => { if (isString(adjustment.code)) { - codeAdjustmentMap.set(adjustment.code, adjustment) + const adjustments = codeAdjustmentMap.get(adjustment.code) || [] + + adjustments.push(adjustment) + + codeAdjustmentMap.set(adjustment.code, adjustments) appliedShippingCodes.push(adjustment.code) } }) @@ -292,6 +306,7 @@ export default class PromotionModuleService< } ) + const appliedCodes = [...appliedShippingCodes, ...appliedItemCodes] const sortedPermissionsToApply = promotions .filter((p) => promotionCodesToApply.includes(p.code!)) .sort(ComputeActionUtils.sortByBuyGetType) @@ -300,7 +315,7 @@ export default class PromotionModuleService< promotions.map((promotion) => [promotion.code!, promotion]) ) - for (const appliedCode of [...appliedShippingCodes, ...appliedItemCodes]) { + for (const appliedCode of appliedCodes) { const promotion = existingPromotionsMap.get(appliedCode) if (!promotion) { @@ -310,24 +325,28 @@ export default class PromotionModuleService< ) } - if (promotionCodes.includes(appliedCode)) { - continue - } - if (appliedItemCodes.includes(appliedCode)) { - computedActions.push({ - action: "removeItemAdjustment", - adjustment_id: codeAdjustmentMap.get(appliedCode)!.id, - code: appliedCode, - }) + const adjustments = codeAdjustmentMap.get(appliedCode) || [] + + adjustments.forEach((adjustment) => + computedActions.push({ + action: "removeItemAdjustment", + adjustment_id: adjustment.id, + code: appliedCode, + }) + ) } if (appliedShippingCodes.includes(appliedCode)) { - computedActions.push({ - action: "removeShippingMethodAdjustment", - adjustment_id: codeAdjustmentMap.get(appliedCode)!.id, - code: appliedCode, - }) + const adjustments = codeAdjustmentMap.get(appliedCode) || [] + + adjustments.forEach((adjustment) => + computedActions.push({ + action: "removeShippingMethodAdjustment", + adjustment_id: adjustment.id, + code: appliedCode, + }) + ) } } diff --git a/packages/promotion/src/utils/compute-actions/buy-get.ts b/packages/promotion/src/utils/compute-actions/buy-get.ts index b6a718ae1228d..e6c33fc83b5b7 100644 --- a/packages/promotion/src/utils/compute-actions/buy-get.ts +++ b/packages/promotion/src/utils/compute-actions/buy-get.ts @@ -4,10 +4,12 @@ import { ComputedActions, MedusaError, PromotionType, + isPresent, } from "@medusajs/utils" import { areRulesValidForContext } from "../validations" import { computeActionForBudgetExceeded } from "./usage" +// TODO: calculations should eventually move to a totals util outside of the module export function getComputedActionsForBuyGet( promotion: PromotionTypes.PromotionDTO, itemsContext: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.ITEMS], @@ -45,6 +47,7 @@ export function getComputedActionsForBuyGet( const validItemsForTargetRules = itemsContext .filter((item) => areRulesValidForContext(targetRules, item)) + .filter((item) => isPresent(item.subtotal) && isPresent(item.quantity)) .sort((a, b) => { const aPrice = a.subtotal / a.quantity const bPrice = b.subtotal / b.quantity @@ -55,7 +58,7 @@ export function getComputedActionsForBuyGet( let remainingQtyToApply = applyToQuantity for (const method of validItemsForTargetRules) { - const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 + const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0 const multiplier = Math.min(method.quantity, remainingQtyToApply) const amount = (method.subtotal / method.quantity) * multiplier const newRemainingQtyToApply = remainingQtyToApply - multiplier diff --git a/packages/promotion/src/utils/compute-actions/items.ts b/packages/promotion/src/utils/compute-actions/items.ts index 555a27548f612..4168559aaca97 100644 --- a/packages/promotion/src/utils/compute-actions/items.ts +++ b/packages/promotion/src/utils/compute-actions/items.ts @@ -49,6 +49,7 @@ export function getComputedActionsForItems( ) } +// TODO: calculations should eventually move to a totals util outside of the module export function applyPromotionToItems( promotion: PromotionTypes.PromotionDTO, items: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.ITEMS], @@ -63,7 +64,11 @@ export function applyPromotionToItems( [allocation, allocationOverride].includes(ApplicationMethodAllocation.EACH) ) { for (const method of items!) { - const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 + if (!method.subtotal || !method.quantity) { + continue + } + + const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0 const quantityMultiplier = Math.min( method.quantity, applicationMethod?.max_quantity! @@ -111,16 +116,20 @@ export function applyPromotionToItems( ) ) { const totalApplicableValue = items!.reduce((acc, method) => { - const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 - return ( - acc + - (method.subtotal / method.quantity) * method.quantity - - appliedPromoValue - ) + const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0 + const perItemCost = method.subtotal + ? method.subtotal / method.quantity + : 0 + + return acc + perItemCost * method.quantity - appliedPromoValue }, 0) for (const method of items!) { - const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 + if (!method.subtotal || !method.quantity) { + continue + } + + const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0 const promotionValue = parseFloat(applicationMethod!.value!) const applicableTotal = (method.subtotal / method.quantity) * method.quantity - diff --git a/packages/promotion/src/utils/compute-actions/shipping-methods.ts b/packages/promotion/src/utils/compute-actions/shipping-methods.ts index 6e96db1fd2aec..75f0c06cd9c6f 100644 --- a/packages/promotion/src/utils/compute-actions/shipping-methods.ts +++ b/packages/promotion/src/utils/compute-actions/shipping-methods.ts @@ -55,7 +55,11 @@ export function applyPromotionToShippingMethods( if (allocation === ApplicationMethodAllocation.EACH) { for (const method of shippingMethods!) { - const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 + if (!method.subtotal) { + continue + } + + const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0 let promotionValue = parseFloat(applicationMethod!.value!) const applicableTotal = method.subtotal - appliedPromoValue @@ -93,9 +97,9 @@ export function applyPromotionToShippingMethods( if (allocation === ApplicationMethodAllocation.ACROSS) { const totalApplicableValue = shippingMethods!.reduce((acc, method) => { - const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 + const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0 - return acc + method.subtotal - appliedPromoValue + return acc + (method.subtotal ?? 0) - appliedPromoValue }, 0) if (totalApplicableValue <= 0) { @@ -103,9 +107,13 @@ export function applyPromotionToShippingMethods( } for (const method of shippingMethods!) { + if (!method.subtotal) { + continue + } + const promotionValue = parseFloat(applicationMethod!.value!) const applicableTotal = method.subtotal - const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 + const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0 // TODO: should we worry about precision here? let applicablePromotionValue = diff --git a/packages/promotion/src/utils/compute-actions/usage.ts b/packages/promotion/src/utils/compute-actions/usage.ts index f2f5e10e51992..678fb6506448b 100644 --- a/packages/promotion/src/utils/compute-actions/usage.ts +++ b/packages/promotion/src/utils/compute-actions/usage.ts @@ -24,7 +24,7 @@ export function computeActionForBudgetExceeded( return } - const campaignBudgetUsed = campaignBudget.used || 0 + const campaignBudgetUsed = campaignBudget.used ?? 0 const totalUsed = campaignBudget.type === CampaignBudgetType.SPEND ? campaignBudgetUsed + amount diff --git a/packages/types/src/cart/common.ts b/packages/types/src/cart/common.ts index 2fb47b3fe2417..ea10cbe2c7547 100644 --- a/packages/types/src/cart/common.ts +++ b/packages/types/src/cart/common.ts @@ -206,7 +206,7 @@ export interface CartShippingMethodDTO { /** * The price of the shipping method */ - unit_price: number + amount: number /** * Whether the shipping method price is tax inclusive or not diff --git a/packages/types/src/cart/service.ts b/packages/types/src/cart/service.ts index e78316e963ec6..6349b6a9d1606 100644 --- a/packages/types/src/cart/service.ts +++ b/packages/types/src/cart/service.ts @@ -22,8 +22,8 @@ import { } from "./common" import { CreateAddressDTO, - CreateAdjustmentDTO, CreateCartDTO, + CreateLineItemAdjustmentDTO, CreateLineItemDTO, CreateLineItemForCartDTO, CreateLineItemTaxLineDTO, @@ -185,14 +185,14 @@ export interface ICartModuleService extends IModuleService { ): Promise addLineItemAdjustments( - data: CreateAdjustmentDTO[] + data: CreateLineItemAdjustmentDTO[] ): Promise addLineItemAdjustments( - data: CreateAdjustmentDTO + data: CreateLineItemAdjustmentDTO ): Promise addLineItemAdjustments( cartId: string, - data: CreateAdjustmentDTO[] + data: CreateLineItemAdjustmentDTO[] ): Promise setLineItemAdjustments( diff --git a/packages/utils/src/modules-sdk/abstract-module-service-factory.ts b/packages/utils/src/modules-sdk/abstract-module-service-factory.ts index 6a6d50f56564a..cdaf3bd3032ee 100644 --- a/packages/utils/src/modules-sdk/abstract-module-service-factory.ts +++ b/packages/utils/src/modules-sdk/abstract-module-service-factory.ts @@ -12,11 +12,11 @@ import { SoftDeleteReturn, } from "@medusajs/types" import { + MapToConfig, isString, kebabCase, lowerCaseFirst, mapObjectTo, - MapToConfig, pluralize, upperCaseFirst, } from "../common" @@ -311,9 +311,8 @@ export function abstractModuleServiceFactory< config: FindConfig = {}, sharedContext: Context = {} ): Promise { - const entities = await this.__container__[ - serviceRegistrationName - ].list(filters, config, sharedContext) + const service = this.__container__[serviceRegistrationName] + const entities = await service.list(filters, config, sharedContext) return await this.baseRepository_.serialize(entities, { populate: true, diff --git a/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts b/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts index e89cca934a78f..3ae18886feb6f 100644 --- a/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts +++ b/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts @@ -6,10 +6,10 @@ import { ModuleServiceInitializeOptions, RepositoryService, } from "@medusajs/types" -import { lowerCaseFirst } from "../../common" import { asClass } from "awilix" -import { internalModuleServiceFactory } from "../internal-module-service-factory" +import { lowerCaseFirst } from "../../common" import { mikroOrmBaseRepositoryFactory } from "../../dal" +import { internalModuleServiceFactory } from "../internal-module-service-factory" type RepositoryLoaderOptions = { moduleModels: Record @@ -96,7 +96,10 @@ export function loadModuleServices({ const finalService = moduleServicesMap.get(mappedServiceName) if (!finalService) { - moduleServicesMap.set(mappedServiceName, internalModuleServiceFactory(Model)) + moduleServicesMap.set( + mappedServiceName, + internalModuleServiceFactory(Model) + ) } }) diff --git a/packages/workflows-sdk/src/utils/composer/create-workflow.ts b/packages/workflows-sdk/src/utils/composer/create-workflow.ts index c9f6b669641af..674ed887f2d4b 100644 --- a/packages/workflows-sdk/src/utils/composer/create-workflow.ts +++ b/packages/workflows-sdk/src/utils/composer/create-workflow.ts @@ -5,7 +5,7 @@ import { WorkflowManager, } from "@medusajs/orchestration" import { LoadedModule, MedusaContainer } from "@medusajs/types" -import { isString, OrchestrationUtils } from "@medusajs/utils" +import { OrchestrationUtils, isString } from "@medusajs/utils" import { ExportedWorkflow, exportWorkflow } from "../../helper" import { proxify } from "./helpers/proxy" import { diff --git a/packages/workflows-sdk/src/utils/composer/type.ts b/packages/workflows-sdk/src/utils/composer/type.ts index ee7ea958fad8f..0b7ac8acda4b1 100644 --- a/packages/workflows-sdk/src/utils/composer/type.ts +++ b/packages/workflows-sdk/src/utils/composer/type.ts @@ -42,6 +42,7 @@ export type WorkflowData = (T extends object [Key in keyof T]: WorkflowData } : T & WorkflowDataProperties) & + T & WorkflowDataProperties & { config( config: { name?: string } & Omit<