feat(workflows-sdk,core-flows,medusa,types): add workflow to update promotions to cart (#6474)
what: - adds API + workflow to add/remove promotions in a cart - minor fixes in promotions module - minor type fixes in cart module - typing fix in workflows-sdk (Thanks @adrien2p) - fix step result in workflows-sdk (Thanks @adrien2p) RESOLVES CORE-1768 Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
co-authored by
Adrien de Peretti
parent
63be07031b
commit
ac86362e81
@@ -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
|
||||
@@ -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,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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: [],
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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] })
|
||||
|
||||
|
||||
@@ -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!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
)
|
||||
+54
@@ -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)
|
||||
}
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
+75
@@ -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<string, PromotionDTO>(
|
||||
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,
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -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<CartDTO>
|
||||
}
|
||||
|
||||
export const retrieveCartStepId = "retrieve-cart"
|
||||
export const retrieveCartStep = createStep(
|
||||
retrieveCartStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const cartModuleService = container.resolve<ICartModuleService>(
|
||||
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)
|
||||
}
|
||||
)
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from "./create-carts"
|
||||
export * from "./update-cart-promotions"
|
||||
export * from "./update-carts"
|
||||
|
||||
|
||||
@@ -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<WorkflowInput>): WorkflowData<CartDTO> => {
|
||||
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",
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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)],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -52,6 +52,18 @@ export class StorePostCartReq {
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export class StorePostCartsCartPromotionsReq {
|
||||
@IsArray()
|
||||
@Type(() => String)
|
||||
promo_codes: string[]
|
||||
}
|
||||
|
||||
export class StoreDeleteCartsCartPromotionsReq {
|
||||
@IsArray()
|
||||
@Type(() => String)
|
||||
promo_codes: string[]
|
||||
}
|
||||
|
||||
export class StorePostCartsCartReq {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -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<PromotionTypes.ComputeActions[]> {
|
||||
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<string, number>()
|
||||
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,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<LineItemAdjustmentDTO[]>
|
||||
|
||||
addLineItemAdjustments(
|
||||
data: CreateAdjustmentDTO[]
|
||||
data: CreateLineItemAdjustmentDTO[]
|
||||
): Promise<LineItemAdjustmentDTO[]>
|
||||
addLineItemAdjustments(
|
||||
data: CreateAdjustmentDTO
|
||||
data: CreateLineItemAdjustmentDTO
|
||||
): Promise<LineItemAdjustmentDTO[]>
|
||||
addLineItemAdjustments(
|
||||
cartId: string,
|
||||
data: CreateAdjustmentDTO[]
|
||||
data: CreateLineItemAdjustmentDTO[]
|
||||
): Promise<LineItemAdjustmentDTO[]>
|
||||
|
||||
setLineItemAdjustments(
|
||||
|
||||
@@ -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<any> = {},
|
||||
sharedContext: Context = {}
|
||||
): Promise<T[]> {
|
||||
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<T[]>(entities, {
|
||||
populate: true,
|
||||
|
||||
@@ -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<string, any>
|
||||
@@ -96,7 +96,10 @@ export function loadModuleServices({
|
||||
const finalService = moduleServicesMap.get(mappedServiceName)
|
||||
|
||||
if (!finalService) {
|
||||
moduleServicesMap.set(mappedServiceName, internalModuleServiceFactory(Model))
|
||||
moduleServicesMap.set(
|
||||
mappedServiceName,
|
||||
internalModuleServiceFactory(Model)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -42,6 +42,7 @@ export type WorkflowData<T = unknown> = (T extends object
|
||||
[Key in keyof T]: WorkflowData<T[Key]>
|
||||
}
|
||||
: T & WorkflowDataProperties<T>) &
|
||||
T &
|
||||
WorkflowDataProperties<T> & {
|
||||
config(
|
||||
config: { name?: string } & Omit<
|
||||
|
||||
Reference in New Issue
Block a user