Skip to content

Commit

Permalink
feat(core-flows,link-modules,modules-sdk): add cart <> promotion link…
Browse files Browse the repository at this point in the history
… as source of truth (#6561)

what:

- adds promotion cart link
- update steps to create and remove links
  • Loading branch information
riqwan authored Mar 4, 2024
1 parent 8dad2b5 commit d550be3
Show file tree
Hide file tree
Showing 14 changed files with 272 additions and 51 deletions.
7 changes: 7 additions & 0 deletions .changeset/silly-clouds-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@medusajs/link-modules": patch
"@medusajs/modules-sdk": patch
"@medusajs/core-flows": patch
---

feat(core-flows,link-modules,modules-sdk): add cart <> promotion link as source of truth
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
LinkModuleUtils,
ModuleRegistrationName,
Modules,
RemoteLink,
} from "@medusajs/modules-sdk"
import { ICartModuleService, IPromotionModuleService } from "@medusajs/types"
import { PromotionType } from "@medusajs/utils"
import path from "path"
Expand All @@ -18,6 +23,7 @@ describe("Store Carts API: Add promotions to cart", () => {
let shutdownServer
let cartModuleService: ICartModuleService
let promotionModuleService: IPromotionModuleService
let remoteLinkService: RemoteLink

beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
Expand All @@ -28,6 +34,7 @@ describe("Store Carts API: Add promotions to cart", () => {
promotionModuleService = appContainer.resolve(
ModuleRegistrationName.PROMOTION
)
remoteLinkService = appContainer.resolve(LinkModuleUtils.REMOTE_LINK)
})

afterAll(async () => {
Expand Down Expand Up @@ -119,6 +126,11 @@ describe("Store Carts API: Add promotions to cart", () => {
},
])

await remoteLinkService.create({
[Modules.CART]: { cart_id: cart.id },
[Modules.PROMOTION]: { promotion_id: appliedPromotion.id },
})

const api = useApi() as any

const created = await api.post(`/store/carts/${cart.id}/promotions`, {
Expand Down Expand Up @@ -247,6 +259,11 @@ describe("Store Carts API: Add promotions to cart", () => {
]
)

await remoteLinkService.create({
[Modules.CART]: { cart_id: cart.id },
[Modules.PROMOTION]: { promotion_id: appliedPromotion.id },
})

const [adjustment] = await cartModuleService.addShippingMethodAdjustments(
cart.id,
[
Expand Down
21 changes: 18 additions & 3 deletions integration-tests/modules/__tests__/cart/store/carts.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
LinkModuleUtils,
ModuleRegistrationName,
Modules,
RemoteLink,
} from "@medusajs/modules-sdk"
import {
ICartModuleService,
ICustomerModuleService,
Expand Down Expand Up @@ -31,7 +36,7 @@ describe("Store Carts API", () => {
let customerModule: ICustomerModuleService
let productModule: IProductModuleService
let pricingModule: IPricingModuleService
let remoteLink
let remoteLink: RemoteLink
let promotionModule: IPromotionModuleService

let defaultRegion
Expand All @@ -47,7 +52,7 @@ describe("Store Carts API", () => {
customerModule = appContainer.resolve(ModuleRegistrationName.CUSTOMER)
productModule = appContainer.resolve(ModuleRegistrationName.PRODUCT)
pricingModule = appContainer.resolve(ModuleRegistrationName.PRICING)
remoteLink = appContainer.resolve("remoteLink")
remoteLink = appContainer.resolve(LinkModuleUtils.REMOTE_LINK)
promotionModule = appContainer.resolve(ModuleRegistrationName.PROMOTION)
})

Expand Down Expand Up @@ -351,6 +356,11 @@ describe("Store Carts API", () => {
},
])

await remoteLink.create({
[Modules.CART]: { cart_id: cart.id },
[Modules.PROMOTION]: { promotion_id: appliedPromotion.id },
})

const api = useApi() as any

// Should remove earlier adjustments from other promocodes
Expand Down Expand Up @@ -629,6 +639,11 @@ describe("Store Carts API", () => {
},
])

await remoteLink.create({
[Modules.CART]: { cart_id: cart.id },
[Modules.PROMOTION]: { promotion_id: appliedPromotion.id },
})

const api = useApi() as any
const response = await api.post(`/store/carts/${cart.id}/line-items`, {
variant_id: product.variants[0].id,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
LinkModuleUtils,
ModuleRegistrationName,
Modules,
RemoteLink,
} from "@medusajs/modules-sdk"
import { ICartModuleService, IPromotionModuleService } from "@medusajs/types"
import { PromotionType } from "@medusajs/utils"
import path from "path"
Expand All @@ -18,13 +23,15 @@ describe("Store Carts API: Remove promotions from cart", () => {
let shutdownServer
let cartModuleService: ICartModuleService
let promotionModuleService: IPromotionModuleService
let remoteLinkService: RemoteLink

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)
remoteLinkService = appContainer.resolve(LinkModuleUtils.REMOTE_LINK)
promotionModuleService = appContainer.resolve(
ModuleRegistrationName.PROMOTION
)
Expand Down Expand Up @@ -129,6 +136,17 @@ describe("Store Carts API: Remove promotions from cart", () => {
},
])

await remoteLinkService.create([
{
[Modules.CART]: { cart_id: cart.id },
[Modules.PROMOTION]: { promotion_id: appliedPromotion.id },
},
{
[Modules.CART]: { cart_id: cart.id },
[Modules.PROMOTION]: { promotion_id: appliedPromotionToRemove.id },
},
])

const api = useApi() as any

const response = await api.delete(`/store/carts/${cart.id}/promotions`, {
Expand Down Expand Up @@ -263,6 +281,17 @@ describe("Store Carts API: Remove promotions from cart", () => {
},
])

await remoteLinkService.create([
{
[Modules.CART]: { cart_id: cart.id },
[Modules.PROMOTION]: { promotion_id: appliedPromotion.id },
},
{
[Modules.CART]: { cart_id: cart.id },
[Modules.PROMOTION]: { promotion_id: appliedPromotionToRemove.id },
},
])

const api = useApi() as any

const response = await api.delete(`/store/carts/${cart.id}/promotions`, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,62 +1,36 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { LinkModuleUtils, ModuleRegistrationName } from "@medusajs/modules-sdk"
import { CartDTO, IPromotionModuleService } from "@medusajs/types"
import { PromotionActions, deduplicate, isString } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"

interface StepInput {
cart: CartDTO
promoCodes?: string[]
action:
| PromotionActions.ADD
| PromotionActions.REMOVE
| PromotionActions.REPLACE
}

export const getActionsToComputeFromPromotionsStepId =
"get-actions-to-compute-from-promotions"
export const getActionsToComputeFromPromotionsStep = createStep(
getActionsToComputeFromPromotionsStepId,
async (data: StepInput, { container }) => {
const promotionModuleService: IPromotionModuleService = container.resolve(
const { cart } = data
const remoteQuery = container.resolve(LinkModuleUtils.REMOTE_QUERY)
const promotionService = container.resolve<IPromotionModuleService>(
ModuleRegistrationName.PROMOTION
)

const { action = PromotionActions.ADD, promoCodes, cart } = data
const existingCartPromotionLinks = await remoteQuery({
cart_promotion: {
__args: { cart_id: [cart.id] },
fields: ["id", "cart_id", "promotion_id", "deleted_at"],
},
})

if (!Array.isArray(promoCodes)) {
return new StepResponse([])
}

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 (action === PromotionActions.REMOVE) {
promotionCodesToApply = promotionCodesToApply.filter(
(code) => !promoCodes.includes(code)
)
}

if (action === PromotionActions.REPLACE) {
promotionCodesToApply = promoCodes
}
const existingPromotions = await promotionService.list(
{ id: existingCartPromotionLinks.map((l) => l.promotion_id) },
{ take: null, select: ["code"] }
)

const actionsToCompute = await promotionModuleService.computeActions(
promotionCodesToApply,
const actionsToCompute = await promotionService.computeActions(
existingPromotions.map((p) => p.code!),
cart as any
)

Expand Down
1 change: 1 addition & 0 deletions packages/core-flows/src/definition/cart/steps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ 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-cart-promotions"
export * from "./update-carts"
export * from "./validate-variants-existence"
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
LinkModuleUtils,
ModuleRegistrationName,
Modules,
} from "@medusajs/modules-sdk"
import { IPromotionModuleService } from "@medusajs/types"
import { PromotionActions } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"

interface StepInput {
id: string
promo_codes?: string[]
action?:
| PromotionActions.ADD
| PromotionActions.REMOVE
| PromotionActions.REPLACE
}

export const updateCartPromotionsStepId = "update-cart-promotions"
export const updateCartPromotionsStep = createStep(
updateCartPromotionsStepId,
async (data: StepInput, { container }) => {
const { promo_codes = [], id, action = PromotionActions.ADD } = data

const remoteLink = container.resolve(LinkModuleUtils.REMOTE_LINK)
const remoteQuery = container.resolve(LinkModuleUtils.REMOTE_QUERY)
const promotionService = container.resolve<IPromotionModuleService>(
ModuleRegistrationName.PROMOTION
)

const existingCartPromotionLinks = await remoteQuery({
cart_promotion: {
__args: { cart_id: [id] },
fields: ["cart_id", "promotion_id"],
},
})

const promotionLinkMap = new Map<string, any>(
existingCartPromotionLinks.map((link) => [link.promotion_id, link])
)

const promotions = await promotionService.list(
{ code: promo_codes },
{ select: ["id"] }
)

const linksToCreate: any[] = []
const linksToDismiss: any[] = []

for (const promotion of promotions) {
const linkObject = {
[Modules.CART]: { cart_id: id },
[Modules.PROMOTION]: { promotion_id: promotion.id },
}

if ([PromotionActions.ADD, PromotionActions.REPLACE].includes(action)) {
linksToCreate.push(linkObject)
}

if (action === PromotionActions.REMOVE) {
const link = promotionLinkMap.get(promotion.id)

if (link) {
linksToDismiss.push(linkObject)
}
}
}

if (action === PromotionActions.REPLACE) {
for (const link of existingCartPromotionLinks) {
linksToDismiss.push({
[Modules.CART]: { cart_id: link.cart_id },
[Modules.PROMOTION]: { promotion_id: link.promotion_id },
})
}
}

const linksToDismissPromise = linksToDismiss.length
? remoteLink.dismiss(linksToDismiss)
: []

const linksToCreatePromise = linksToCreate.length
? remoteLink.create(linksToCreate)
: []

const [_, createdLinks] = await Promise.all([
linksToDismissPromise,
linksToCreatePromise,
])

return new StepResponse(null, {
createdLinkIds: createdLinks.map((link) => link.id),
dismissedLinks: linksToDismiss,
})
},
async (revertData, { container }) => {
const remoteLink = container.resolve(LinkModuleUtils.REMOTE_LINK)

if (revertData?.dismissedLinks?.length) {
await remoteLink.create(revertData.dismissedLinks)
}

if (revertData?.createdLinkIds?.length) {
await remoteLink.delete(revertData.createdLinkIds)
}
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { prepareLineItemData } from "../utils/prepare-line-item-data"
// TODO: The AddToCartWorkflow are missing the following steps:
// - Confirm inventory exists (inventory module)
// - Refresh/delete shipping methods (fulfillment module)
// - Create line item adjustments (promotion module)
// - Update payment sessions (payment module)

export const addToCartWorkflowId = "add-to-cart"
Expand Down
Loading

0 comments on commit d550be3

Please sign in to comment.