feat(core-flows,link-modules,modules-sdk): add cart <> promotion link as source of truth (#6561)

what:

- adds promotion cart link
- update steps to create and remove links
This commit is contained in:
Riqwan Thamir
2024-03-04 15:20:49 +05:30
committed by GitHub
parent 8dad2b51a2
commit d550be3685
14 changed files with 272 additions and 51 deletions

View File

@@ -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

View File

@@ -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"
@@ -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, "..", "..", ".."))
@@ -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 () => {
@@ -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`, {
@@ -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,
[

View File

@@ -1,4 +1,9 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
LinkModuleUtils,
ModuleRegistrationName,
Modules,
RemoteLink,
} from "@medusajs/modules-sdk"
import {
ICartModuleService,
ICustomerModuleService,
@@ -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
@@ -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)
})
@@ -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
@@ -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,

View File

@@ -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"
@@ -18,6 +23,7 @@ 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, "..", "..", ".."))
@@ -25,6 +31,7 @@ describe("Store Carts API: Remove promotions from cart", () => {
shutdownServer = await startBootstrapApp({ cwd, env })
appContainer = getContainer()
cartModuleService = appContainer.resolve(ModuleRegistrationName.CART)
remoteLinkService = appContainer.resolve(LinkModuleUtils.REMOTE_LINK)
promotionModuleService = appContainer.resolve(
ModuleRegistrationName.PROMOTION
)
@@ -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`, {
@@ -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`, {

View File

@@ -1,15 +1,9 @@
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 =
@@ -17,46 +11,26 @@ export const getActionsToComputeFromPromotionsStepId =
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 existingPromotions = await promotionService.list(
{ id: existingCartPromotionLinks.map((l) => l.promotion_id) },
{ take: null, select: ["code"] }
)
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 actionsToCompute = await promotionModuleService.computeActions(
promotionCodesToApply,
const actionsToCompute = await promotionService.computeActions(
existingPromotions.map((p) => p.code!),
cart as any
)

View File

@@ -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"

View File

@@ -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)
}
}
)

View File

@@ -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"

View File

@@ -12,6 +12,7 @@ import {
removeLineItemAdjustmentsStep,
removeShippingMethodAdjustmentsStep,
retrieveCartStep,
updateCartPromotionsStep,
} from "../steps"
type WorkflowInput = {
@@ -39,11 +40,15 @@ export const updateCartPromotionsWorkflow = createWorkflow(
},
}
updateCartPromotionsStep({
id: input.cartId,
promo_codes: input.promoCodes,
action: input.action || PromotionActions.ADD,
})
const cart = retrieveCartStep(retrieveCartInput)
const actions = getActionsToComputeFromPromotionsStep({
cart,
promoCodes: input.promoCodes,
action: input.action || PromotionActions.ADD,
})
const {

View File

@@ -9,7 +9,7 @@
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"sourceMap": false,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,

View File

@@ -0,0 +1,55 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
export const CartPromotion: ModuleJoinerConfig = {
serviceName: LINKS.CartPromotion,
isLink: true,
databaseConfig: {
tableName: "cart_promotion",
idPrefix: "cartpromo",
},
alias: [
{
name: ["cart_promotion", "cart_promotions"],
args: {
entity: "LinkCartPromotion",
},
},
],
primaryKeys: ["id", "cart_id", "promotion_id"],
relationships: [
{
serviceName: Modules.CART,
primaryKey: "id",
foreignKey: "cart_id",
alias: "cart",
},
{
serviceName: Modules.PROMOTION,
primaryKey: "id",
foreignKey: "promotion_id",
alias: "promotion",
},
],
extends: [
{
serviceName: Modules.CART,
relationship: {
serviceName: LINKS.CartPromotion,
primaryKey: "cart_id",
foreignKey: "id",
alias: "cart_link",
},
},
{
serviceName: Modules.PROMOTION,
relationship: {
serviceName: LINKS.CartPromotion,
primaryKey: "promotion_id",
foreignKey: "id",
alias: "promotion_link",
},
},
],
}

View File

@@ -1,5 +1,6 @@
export * from "./cart-customer"
export * from "./cart-payment-collection"
export * from "./cart-promotion"
export * from "./cart-region"
export * from "./cart-sales-channel"
export * from "./inventory-level-stock-location"

View File

@@ -20,6 +20,12 @@ export const LINKS = {
Modules.PAYMENT,
"payment_collection_id"
),
CartPromotion: composeLinkName(
Modules.CART,
"cart_id",
Modules.PROMOTION,
"promotion_id"
),
// Internal services
ProductShippingProfile: composeLinkName(

View File

@@ -6,6 +6,11 @@ import {
import { upperCaseFirst } from "@medusajs/utils"
export enum LinkModuleUtils {
REMOTE_QUERY = "remoteQuery",
REMOTE_LINK = "remoteLink",
}
export enum Modules {
AUTH = "auth",
CACHE = "cacheService",