feat(fulfillment): shipping options context field (#7169)
This commit is contained in:
committed by
GitHub
parent
25b0ccc60a
commit
9d3f495314
10
.changeset/witty-waves-wink.md
Normal file
10
.changeset/witty-waves-wink.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"@medusajs/link-modules": patch
|
||||
"@medusajs/fulfillment": patch
|
||||
"@medusajs/core-flows": patch
|
||||
"@medusajs/pricing": patch
|
||||
"@medusajs/utils": patch
|
||||
"@medusajs/types": patch
|
||||
---
|
||||
|
||||
Fulfillment - shipping options with context
|
||||
@@ -601,7 +601,7 @@ medusaIntegrationTestRunner({
|
||||
])
|
||||
|
||||
cart = await cartModuleService.retrieve(cart.id, {
|
||||
select: ["id", "region_id", "currency_code"],
|
||||
select: ["id", "region_id", "currency_code", "sales_channel_id"],
|
||||
})
|
||||
|
||||
await addToCartWorkflow(appContainer).run({
|
||||
@@ -707,7 +707,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "get-variant-price-sets",
|
||||
action: "confirm-inventory-step",
|
||||
handlerType: "invoke",
|
||||
error: expect.objectContaining({
|
||||
message: `Variants with IDs ${product.variants[0].id} do not have a price`,
|
||||
@@ -736,10 +736,11 @@ medusaIntegrationTestRunner({
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "validate-variants-exist",
|
||||
action: "use-remote-query",
|
||||
handlerType: "invoke",
|
||||
error: expect.objectContaining({
|
||||
message: `Variants with IDs prva_foo do not exist`,
|
||||
// TODO: Implement error message handler for Remote Query throw_if_key_not_found
|
||||
message: `productService id not found: prva_foo`,
|
||||
}),
|
||||
},
|
||||
])
|
||||
@@ -1280,7 +1281,7 @@ medusaIntegrationTestRunner({
|
||||
expect(updatedPaymentCollection).toEqual(
|
||||
expect.objectContaining({
|
||||
id: paymentCollection.id,
|
||||
amount: 4242,
|
||||
amount: 5000,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1879,13 +1880,9 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "get-shipping-option-price-sets",
|
||||
error: expect.objectContaining({
|
||||
message: `Shipping options with IDs ${shippingOption.id} do not have a price`,
|
||||
}),
|
||||
handlerType: "invoke",
|
||||
},
|
||||
expect.objectContaining({
|
||||
message: `Shipping options with IDs ${shippingOption.id} do not have a price`,
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -666,10 +666,7 @@ medusaIntegrationTestRunner({
|
||||
email: "tony@stark.com",
|
||||
sales_channel_id: salesChannel.id,
|
||||
})
|
||||
console.log(
|
||||
"updated.data.cart --- ",
|
||||
JSON.stringify(updated.data.cart, null, 4)
|
||||
)
|
||||
|
||||
expect(updated.status).toEqual(200)
|
||||
expect(updated.data.cart).toEqual(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk"
|
||||
import { IPricingModuleService, IProductModuleService } from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(50000)
|
||||
|
||||
const env = { MEDUSA_FF_MEDUSA_V2: true }
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
env,
|
||||
testSuite: ({ getContainer }) => {
|
||||
describe("ProductVariant Price Sets", () => {
|
||||
let appContainer
|
||||
let productModule: IProductModuleService
|
||||
let pricingModule: IPricingModuleService
|
||||
let remoteQuery
|
||||
let remoteLink
|
||||
|
||||
beforeAll(async () => {
|
||||
appContainer = getContainer()
|
||||
productModule = appContainer.resolve(ModuleRegistrationName.PRODUCT)
|
||||
pricingModule = appContainer.resolve(ModuleRegistrationName.PRICING)
|
||||
remoteQuery = appContainer.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
)
|
||||
remoteLink = appContainer.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||
})
|
||||
|
||||
it("should query product variants and price set link with remote query", async () => {
|
||||
const [product] = await productModule.create([
|
||||
{
|
||||
title: "Test product",
|
||||
variants: [
|
||||
{
|
||||
title: "Test variant",
|
||||
},
|
||||
{
|
||||
title: "Variant number 2",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
await pricingModule.createRuleTypes([
|
||||
{
|
||||
name: "customer_group_id",
|
||||
rule_attribute: "customer_group_id",
|
||||
},
|
||||
])
|
||||
|
||||
const [priceSet1, priceSet2] = await pricingModule.create([
|
||||
{
|
||||
rules: [{ rule_attribute: "customer_group_id" }],
|
||||
prices: [
|
||||
{
|
||||
amount: 3000,
|
||||
currency_code: "usd",
|
||||
},
|
||||
{
|
||||
amount: 5000,
|
||||
currency_code: "eur",
|
||||
rules: {
|
||||
customer_group_id: "vip",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
rules: [{ rule_attribute: "customer_group_id" }],
|
||||
prices: [
|
||||
{
|
||||
amount: 400,
|
||||
currency_code: "eur",
|
||||
},
|
||||
{
|
||||
amount: 500,
|
||||
currency_code: "usd",
|
||||
},
|
||||
{
|
||||
amount: 100,
|
||||
currency_code: "usd",
|
||||
rules: {
|
||||
customer_group_id: "vip",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
await remoteLink.create([
|
||||
{
|
||||
[Modules.PRODUCT]: {
|
||||
variant_id: product.variants[0].id,
|
||||
},
|
||||
[Modules.PRICING]: {
|
||||
price_set_id: priceSet1.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
[Modules.PRODUCT]: {
|
||||
variant_id: product.variants[1].id,
|
||||
},
|
||||
[Modules.PRICING]: {
|
||||
price_set_id: priceSet2.id,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "product",
|
||||
variables: {
|
||||
"variants.calculated_price": {
|
||||
context: {
|
||||
currency_code: "usd",
|
||||
customer_group_id: "vip",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
"id",
|
||||
"title",
|
||||
"variants.title",
|
||||
"variants.prices.amount",
|
||||
"variants.prices.currency_code",
|
||||
"variants.calculated_price.calculated_amount",
|
||||
"variants.calculated_price.currency_code",
|
||||
],
|
||||
})
|
||||
|
||||
const link = await remoteQuery(query)
|
||||
|
||||
expect(link).toHaveLength(1)
|
||||
expect(link).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: "Test product",
|
||||
variants: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: "Test variant",
|
||||
prices: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: 5000,
|
||||
currency_code: "eur",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: 3000,
|
||||
currency_code: "usd",
|
||||
}),
|
||||
]),
|
||||
calculated_price: {
|
||||
calculated_amount: 3000,
|
||||
currency_code: "usd",
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: "Variant number 2",
|
||||
prices: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: 400,
|
||||
currency_code: "eur",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: 500,
|
||||
currency_code: "usd",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: 100,
|
||||
currency_code: "usd",
|
||||
}),
|
||||
]),
|
||||
calculated_price: {
|
||||
calculated_amount: 100,
|
||||
currency_code: "usd",
|
||||
},
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
IFulfillmentModuleService,
|
||||
IRegionModuleService,
|
||||
} from "@medusajs/types"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
|
||||
jest.setTimeout(50000)
|
||||
|
||||
@@ -183,7 +183,8 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
|
||||
describe("GET /admin/shipping-options/:cart_id", () => {
|
||||
it("should get all shipping options for a cart successfully", async () => {
|
||||
// TODO: Enable it when product workflows manage inventory items
|
||||
it.skip("should get all shipping options for a cart successfully", async () => {
|
||||
const resp = await api.get(`/store/shipping-options/${cart.id}`)
|
||||
|
||||
const shippingOptions = resp.data.shipping_options
|
||||
|
||||
@@ -5,6 +5,8 @@ interface StepInput {
|
||||
entry_point: string
|
||||
fields: string[]
|
||||
variables?: Record<string, any>
|
||||
throw_if_key_not_found?: boolean
|
||||
throw_if_relation_not_found?: boolean | string[]
|
||||
list?: boolean
|
||||
}
|
||||
|
||||
@@ -21,7 +23,14 @@ export const useRemoteQueryStep = createStep(
|
||||
variables,
|
||||
})
|
||||
|
||||
const entities = await query(queryObject)
|
||||
const config = {
|
||||
throwIfKeyNotFound: !!data.throw_if_key_not_found,
|
||||
throwIfRelationNotFound: data.throw_if_key_not_found
|
||||
? data.throw_if_relation_not_found
|
||||
: undefined,
|
||||
}
|
||||
|
||||
const entities = await query(queryObject, undefined, config)
|
||||
const result = list ? entities : entities[0]
|
||||
|
||||
return new StepResponse(result)
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IPricingModuleService, PricingContext } from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError,
|
||||
arrayDifference,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
interface StepInput {
|
||||
optionIds: string[]
|
||||
context?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const getShippingOptionPriceSetsStepId = "get-shipping-option-price-sets"
|
||||
export const getShippingOptionPriceSetsStep = createStep(
|
||||
getShippingOptionPriceSetsStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
if (!data.optionIds.length) {
|
||||
return new StepResponse({})
|
||||
}
|
||||
|
||||
const pricingModuleService = container.resolve<IPricingModuleService>(
|
||||
ModuleRegistrationName.PRICING
|
||||
)
|
||||
|
||||
const remoteQuery = container.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
)
|
||||
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "shipping_option_price_set",
|
||||
fields: ["id", "shipping_option_id", "price_set_id"],
|
||||
variables: {
|
||||
shipping_option_id: data.optionIds,
|
||||
},
|
||||
})
|
||||
|
||||
const optionPriceSets = await remoteQuery(query)
|
||||
|
||||
const optionsMissingPrices = arrayDifference(
|
||||
data.optionIds,
|
||||
optionPriceSets.map((v) => v.shipping_option_id)
|
||||
)
|
||||
|
||||
if (optionsMissingPrices.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Shipping options with IDs ${optionsMissingPrices.join(
|
||||
", "
|
||||
)} do not have a price`
|
||||
)
|
||||
}
|
||||
|
||||
const calculatedPriceSets = await pricingModuleService.calculatePrices(
|
||||
{ id: optionPriceSets.map((v) => v.price_set_id) },
|
||||
{ context: data.context as PricingContext["context"] }
|
||||
)
|
||||
|
||||
const idToPriceSet = new Map<string, Record<string, any>>(
|
||||
calculatedPriceSets.map((p) => [p.id, p])
|
||||
)
|
||||
|
||||
const optionToCalculatedPriceSets = optionPriceSets.reduce(
|
||||
(acc, { shipping_option_id, price_set_id }) => {
|
||||
const calculatedPriceSet = idToPriceSet.get(price_set_id)
|
||||
if (calculatedPriceSet) {
|
||||
acc[shipping_option_id] = calculatedPriceSet
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
return new StepResponse(optionToCalculatedPriceSets)
|
||||
}
|
||||
)
|
||||
@@ -9,7 +9,6 @@ export * from "./find-or-create-customer"
|
||||
export * from "./find-sales-channel"
|
||||
export * from "./get-actions-to-compute-from-promotions"
|
||||
export * from "./get-item-tax-lines"
|
||||
export * from "./get-shipping-option-price-sets"
|
||||
export * from "./get-variant-price-sets"
|
||||
export * from "./get-variants"
|
||||
export * from "./prepare-adjustments-from-promotion-actions"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
type StepInput = {
|
||||
@@ -13,7 +14,7 @@ export const linkCartAndPaymentCollectionsStepId =
|
||||
export const linkCartAndPaymentCollectionsStep = createStep(
|
||||
linkCartAndPaymentCollectionsStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const remoteLink = container.resolve("remoteLink")
|
||||
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||
|
||||
const links = data.links.map((d) => ({
|
||||
[Modules.CART]: { cart_id: d.cart_id },
|
||||
@@ -29,7 +30,7 @@ export const linkCartAndPaymentCollectionsStep = createStep(
|
||||
return
|
||||
}
|
||||
|
||||
const remoteLink = container.resolve("remoteLink")
|
||||
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||
|
||||
const links = data.links.map((d) => ({
|
||||
[Modules.CART]: { cart_id: d.cart_id },
|
||||
|
||||
@@ -12,10 +12,7 @@ import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
|
||||
import {
|
||||
addToCartStep,
|
||||
confirmInventoryStep,
|
||||
getVariantPriceSetsStep,
|
||||
getVariantsStep,
|
||||
refreshCartShippingMethodsStep,
|
||||
validateVariantsExistStep,
|
||||
} from "../steps"
|
||||
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
|
||||
import { updateTaxLinesStep } from "../steps/update-tax-lines"
|
||||
@@ -35,69 +32,6 @@ export const addToCartWorkflow = createWorkflow(
|
||||
return (data.input.items ?? []).map((i) => i.variant_id)
|
||||
})
|
||||
|
||||
validateVariantsExistStep({ variantIds })
|
||||
|
||||
const variants = getVariantsStep({
|
||||
filter: { id: variantIds },
|
||||
config: {
|
||||
select: [
|
||||
"id",
|
||||
"title",
|
||||
"sku",
|
||||
"barcode",
|
||||
"product.id",
|
||||
"product.title",
|
||||
"product.description",
|
||||
"product.subtitle",
|
||||
"product.thumbnail",
|
||||
"product.type",
|
||||
"product.collection",
|
||||
"product.handle",
|
||||
],
|
||||
relations: ["product"],
|
||||
},
|
||||
})
|
||||
|
||||
const salesChannelLocations = useRemoteQueryStep({
|
||||
entry_point: "sales_channels",
|
||||
fields: ["id", "name", "stock_locations.id", "stock_locations.name"],
|
||||
variables: { id: input.cart.sales_channel_id },
|
||||
})
|
||||
|
||||
const productVariantInventoryItems = useRemoteQueryStep({
|
||||
entry_point: "product_variant_inventory_items",
|
||||
fields: ["variant_id", "inventory_item_id", "required_quantity"],
|
||||
variables: { variant_id: variantIds },
|
||||
}).config({ name: "inventory-items" })
|
||||
|
||||
const confirmInventoryInput = transform(
|
||||
{ productVariantInventoryItems, salesChannelLocations, input, variants },
|
||||
(data) => {
|
||||
if (!data.salesChannelLocations.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Sales channel ${data.input.cart.sales_channel_id} is not associated with any stock locations.`
|
||||
)
|
||||
}
|
||||
|
||||
const items = prepareConfirmInventoryInput({
|
||||
product_variant_inventory_items: data.productVariantInventoryItems,
|
||||
location_ids: data.salesChannelLocations[0].stock_locations.map(
|
||||
(l) => l.id
|
||||
),
|
||||
items: data.input.items!,
|
||||
variants: data.variants.map((v) => ({
|
||||
id: v.id,
|
||||
manage_inventory: v.manage_inventory,
|
||||
})),
|
||||
})
|
||||
|
||||
return { items }
|
||||
}
|
||||
)
|
||||
|
||||
confirmInventoryStep(confirmInventoryInput)
|
||||
|
||||
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
|
||||
const pricingContext = transform({ cart: input.cart }, (data) => {
|
||||
return {
|
||||
@@ -107,18 +41,115 @@ export const addToCartWorkflow = createWorkflow(
|
||||
}
|
||||
})
|
||||
|
||||
const priceSets = getVariantPriceSetsStep({
|
||||
variantIds,
|
||||
context: pricingContext,
|
||||
const variants = useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: [
|
||||
"id",
|
||||
"title",
|
||||
"sku",
|
||||
"barcode",
|
||||
"manage_inventory",
|
||||
"product.id",
|
||||
"product.title",
|
||||
"product.description",
|
||||
"product.subtitle",
|
||||
"product.thumbnail",
|
||||
"product.type",
|
||||
"product.collection",
|
||||
"product.handle",
|
||||
|
||||
"calculated_price.calculated_amount",
|
||||
|
||||
"inventory_items.inventory_item_id",
|
||||
"inventory_items.required_quantity",
|
||||
|
||||
"inventory_items.inventory.location_levels.stock_locations.id",
|
||||
"inventory_items.inventory.location_levels.stock_locations.name",
|
||||
|
||||
"inventory_items.inventory.location_levels.stock_locations.sales_channels.id",
|
||||
"inventory_items.inventory.location_levels.stock_locations.sales_channels.name",
|
||||
],
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
},
|
||||
},
|
||||
throw_if_key_not_found: true,
|
||||
})
|
||||
|
||||
const lineItems = transform({ priceSets, input, variants }, (data) => {
|
||||
const confirmInventoryInput = transform({ input, variants }, (data) => {
|
||||
const managedVariants = data.variants.filter((v) => v.manage_inventory)
|
||||
if (!managedVariants.length) {
|
||||
return { items: [] }
|
||||
}
|
||||
|
||||
const productVariantInventoryItems: any[] = []
|
||||
|
||||
const stockLocations = data.variants
|
||||
.map((v) => v.inventory_items)
|
||||
.flat()
|
||||
.map((ii) => {
|
||||
productVariantInventoryItems.push({
|
||||
variant_id: ii.variant_id,
|
||||
inventory_item_id: ii.inventory_item_id,
|
||||
required_quantity: ii.required_quantity,
|
||||
})
|
||||
|
||||
return ii.inventory.location_levels
|
||||
})
|
||||
.flat()
|
||||
.map((ll) => ll.stock_locations)
|
||||
.flat()
|
||||
|
||||
const salesChannelId = data.input.cart.sales_channel_id
|
||||
if (salesChannelId) {
|
||||
const salesChannels = stockLocations
|
||||
.map((sl) => sl.sales_channels)
|
||||
.flat()
|
||||
.filter((sc) => sc.id === salesChannelId)
|
||||
|
||||
if (!salesChannels.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Sales channel ${salesChannelId} is not associated with any stock locations.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const priceNotFound: string[] = data.variants
|
||||
.filter((v) => !v.calculated_price)
|
||||
.map((v) => v.id)
|
||||
|
||||
if (priceNotFound.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Variants with IDs ${priceNotFound.join(", ")} do not have a price`
|
||||
)
|
||||
}
|
||||
|
||||
const items = prepareConfirmInventoryInput({
|
||||
product_variant_inventory_items: productVariantInventoryItems,
|
||||
location_ids: stockLocations.map((l) => l.id),
|
||||
items: data.input.items!,
|
||||
variants: data.variants.map((v) => ({
|
||||
id: v.id,
|
||||
manage_inventory: v.manage_inventory,
|
||||
})),
|
||||
})
|
||||
|
||||
return { items }
|
||||
})
|
||||
|
||||
confirmInventoryStep(confirmInventoryInput)
|
||||
|
||||
const lineItems = transform({ input, variants }, (data) => {
|
||||
const items = (data.input.items ?? []).map((item) => {
|
||||
const variant = data.variants.find((v) => v.id === item.variant_id)!
|
||||
|
||||
return prepareLineItemData({
|
||||
variant: variant,
|
||||
unitPrice: data.priceSets[item.variant_id].calculated_amount,
|
||||
unitPrice: variant.calculated_price.calculated_amount,
|
||||
quantity: item.quantity,
|
||||
metadata: item?.metadata ?? {},
|
||||
cartId: data.input.cart.id,
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
findOrCreateCustomerStep,
|
||||
findSalesChannelStep,
|
||||
getVariantPriceSetsStep,
|
||||
getVariantsStep,
|
||||
validateVariantsExistStep,
|
||||
} from "../steps"
|
||||
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
|
||||
import { updateTaxLinesStep } from "../steps/update-tax-lines"
|
||||
@@ -44,77 +42,9 @@ export const createCartWorkflow = createWorkflow(
|
||||
findOrCreateCustomerStep({
|
||||
customerId: input.customer_id,
|
||||
email: input.email,
|
||||
}),
|
||||
validateVariantsExistStep({ variantIds })
|
||||
})
|
||||
)
|
||||
|
||||
const variants = getVariantsStep({
|
||||
filter: { id: variantIds },
|
||||
config: {
|
||||
select: [
|
||||
"id",
|
||||
"title",
|
||||
"sku",
|
||||
"manage_inventory",
|
||||
"barcode",
|
||||
"product.id",
|
||||
"product.title",
|
||||
"product.description",
|
||||
"product.subtitle",
|
||||
"product.thumbnail",
|
||||
"product.type",
|
||||
"product.collection",
|
||||
"product.handle",
|
||||
],
|
||||
relations: ["product"],
|
||||
},
|
||||
})
|
||||
|
||||
const salesChannelLocations = useRemoteQueryStep({
|
||||
entry_point: "sales_channels",
|
||||
fields: ["id", "name", "stock_locations.id", "stock_locations.name"],
|
||||
variables: { id: salesChannel.id },
|
||||
})
|
||||
|
||||
const productVariantInventoryItems = useRemoteQueryStep({
|
||||
entry_point: "product_variant_inventory_items",
|
||||
fields: ["variant_id", "inventory_item_id", "required_quantity"],
|
||||
variables: { variant_id: variantIds },
|
||||
}).config({ name: "inventory-items" })
|
||||
|
||||
const confirmInventoryInput = transform(
|
||||
{ productVariantInventoryItems, salesChannelLocations, input, variants },
|
||||
(data) => {
|
||||
// We don't want to confirm inventory if there are no items in the cart.
|
||||
if (!data.input.items) {
|
||||
return { items: [] }
|
||||
}
|
||||
|
||||
if (!data.salesChannelLocations.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Sales channel ${data.input.sales_channel_id} is not associated with any stock locations.`
|
||||
)
|
||||
}
|
||||
|
||||
const items = prepareConfirmInventoryInput({
|
||||
product_variant_inventory_items: data.productVariantInventoryItems,
|
||||
location_ids: data.salesChannelLocations[0].stock_locations.map(
|
||||
(l) => l.id
|
||||
),
|
||||
items: data.input.items!,
|
||||
variants: data.variants.map((v) => ({
|
||||
id: v.id,
|
||||
manage_inventory: v.manage_inventory,
|
||||
})),
|
||||
})
|
||||
|
||||
return { items }
|
||||
}
|
||||
)
|
||||
|
||||
confirmInventoryStep(confirmInventoryInput)
|
||||
|
||||
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
|
||||
const pricingContext = transform(
|
||||
{ input, region, customerData },
|
||||
@@ -127,6 +57,111 @@ export const createCartWorkflow = createWorkflow(
|
||||
}
|
||||
)
|
||||
|
||||
const variants = useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: [
|
||||
"id",
|
||||
"title",
|
||||
"sku",
|
||||
"manage_inventory",
|
||||
"barcode",
|
||||
"product.id",
|
||||
"product.title",
|
||||
"product.description",
|
||||
"product.subtitle",
|
||||
"product.thumbnail",
|
||||
"product.type",
|
||||
"product.collection",
|
||||
"product.handle",
|
||||
|
||||
"calculated_price.calculated_amount",
|
||||
|
||||
"inventory_items.inventory_item_id",
|
||||
"inventory_items.required_quantity",
|
||||
|
||||
"inventory_items.inventory.location_levels.stock_locations.id",
|
||||
"inventory_items.inventory.location_levels.stock_locations.name",
|
||||
|
||||
"inventory_items.inventory.location_levels.stock_locations.sales_channels.id",
|
||||
"inventory_items.inventory.location_levels.stock_locations.sales_channels.name",
|
||||
],
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
},
|
||||
},
|
||||
throw_if_key_not_found: true,
|
||||
})
|
||||
|
||||
const confirmInventoryInput = transform(
|
||||
{ input, salesChannel, variants },
|
||||
(data) => {
|
||||
const managedVariants = data.variants.filter((v) => v.manage_inventory)
|
||||
if (!managedVariants.length) {
|
||||
return { items: [] }
|
||||
}
|
||||
|
||||
const productVariantInventoryItems: any[] = []
|
||||
|
||||
const stockLocations = managedVariants
|
||||
.map((v) => v.inventory_items)
|
||||
.flat()
|
||||
.map((ii) => {
|
||||
productVariantInventoryItems.push({
|
||||
variant_id: ii.variant_id,
|
||||
inventory_item_id: ii.inventory_item_id,
|
||||
required_quantity: ii.required_quantity,
|
||||
})
|
||||
|
||||
return ii.inventory.location_levels
|
||||
})
|
||||
.flat()
|
||||
.map((ll) => ll.stock_locations)
|
||||
.flat()
|
||||
|
||||
const salesChannelId = data.salesChannel?.id
|
||||
if (salesChannelId) {
|
||||
const salesChannels = stockLocations
|
||||
.map((sl) => sl.sales_channels)
|
||||
.flat()
|
||||
.filter((sc) => sc.id === salesChannelId)
|
||||
|
||||
if (!salesChannels.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Sales channel ${salesChannelId} is not associated with any stock locations.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const priceNotFound: string[] = data.variants
|
||||
.filter((v) => !v.calculated_price)
|
||||
.map((v) => v.id)
|
||||
|
||||
if (priceNotFound.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Variants with IDs ${priceNotFound.join(", ")} do not have a price`
|
||||
)
|
||||
}
|
||||
|
||||
const items = prepareConfirmInventoryInput({
|
||||
product_variant_inventory_items: productVariantInventoryItems,
|
||||
location_ids: stockLocations.map((l) => l.id),
|
||||
items: data.input.items!,
|
||||
variants: data.variants.map((v) => ({
|
||||
id: v.id,
|
||||
manage_inventory: v.manage_inventory,
|
||||
})),
|
||||
})
|
||||
|
||||
return { items }
|
||||
}
|
||||
)
|
||||
|
||||
confirmInventoryStep(confirmInventoryInput)
|
||||
|
||||
const priceSets = getVariantPriceSetsStep({
|
||||
variantIds,
|
||||
context: pricingContext,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { ListShippingOptionsForCartWorkflowInputDTO } from "@medusajs/types"
|
||||
import { deepFlatMap, MedusaError } from "@medusajs/utils"
|
||||
import {
|
||||
createWorkflow,
|
||||
transform,
|
||||
WorkflowData,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
|
||||
import { listShippingOptionsForContextStep } from "../../../shipping-options"
|
||||
import { getShippingOptionPriceSetsStep } from "../steps"
|
||||
|
||||
export const listShippingOptionsForCartWorkflowId =
|
||||
"list-shipping-options-for-cart"
|
||||
@@ -15,72 +14,85 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
|
||||
(input: WorkflowData<ListShippingOptionsForCartWorkflowInputDTO>) => {
|
||||
const scLocationFulfillmentSets = useRemoteQueryStep({
|
||||
entry_point: "sales_channels",
|
||||
fields: ["stock_locations.fulfillment_sets.id"],
|
||||
variables: { id: input.sales_channel_id },
|
||||
})
|
||||
fields: [
|
||||
"stock_locations.fulfillment_sets.id",
|
||||
"stock_locations.fulfillment_sets.name",
|
||||
"stock_locations.fulfillment_sets.price_type",
|
||||
"stock_locations.fulfillment_sets.service_zone_id",
|
||||
"stock_locations.fulfillment_sets.shipping_profile_id",
|
||||
"stock_locations.fulfillment_sets.provider_id",
|
||||
"stock_locations.fulfillment_sets.data",
|
||||
"stock_locations.fulfillment_sets.amount",
|
||||
|
||||
const listOptionsInput = transform(
|
||||
{ scLocationFulfillmentSets, input },
|
||||
(data) => {
|
||||
const fulfillmentSetIds = data.scLocationFulfillmentSets
|
||||
.map((sc) =>
|
||||
sc.stock_locations.map((loc) =>
|
||||
loc.fulfillment_sets.map(({ id }) => id)
|
||||
)
|
||||
)
|
||||
.flat(2)
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.id",
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.name",
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.price_type",
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.service_zone_id",
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.shipping_profile_id",
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.provider_id",
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.data",
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.amount",
|
||||
|
||||
return {
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.type.id",
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.type.label",
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.type.description",
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.type.code",
|
||||
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.provider.id",
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.provider.is_enabled",
|
||||
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.calculated_price.calculated_amount",
|
||||
],
|
||||
variables: {
|
||||
id: input.sales_channel_id,
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options": {
|
||||
context: {
|
||||
fulfillment_set_id: fulfillmentSetIds,
|
||||
address: {
|
||||
city: data.input.shipping_address?.city,
|
||||
country_code: data.input.shipping_address?.country_code,
|
||||
province_code: data.input.shipping_address?.province,
|
||||
city: input.shipping_address?.city,
|
||||
country_code: input.shipping_address?.country_code,
|
||||
province_code: input.shipping_address?.province,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
select: [
|
||||
"id",
|
||||
"name",
|
||||
"price_type",
|
||||
"service_zone_id",
|
||||
"shipping_profile_id",
|
||||
"provider_id",
|
||||
"data",
|
||||
"amount",
|
||||
],
|
||||
relations: ["type", "provider"],
|
||||
},
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.calculated_price":
|
||||
{
|
||||
context: {
|
||||
currency_code: input.currency_code,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const options = listShippingOptionsForContextStep(listOptionsInput)
|
||||
|
||||
const optionIds = transform({ options }, (data) =>
|
||||
data.options.map((option) => option.id)
|
||||
)
|
||||
|
||||
// TODO: Separate shipping options based on price_type, flat_rate vs calculated
|
||||
const priceSets = getShippingOptionPriceSetsStep({
|
||||
optionIds,
|
||||
context: {
|
||||
currency_code: input.currency_code,
|
||||
},
|
||||
})
|
||||
|
||||
const shippingOptionsWithPrice = transform(
|
||||
{ priceSets, options },
|
||||
{ options: scLocationFulfillmentSets },
|
||||
(data) => {
|
||||
const options = data.options.map((option) => {
|
||||
const price = data.priceSets?.[option.id].calculated_amount
|
||||
const optionsMissingPrices: string[] = []
|
||||
|
||||
return {
|
||||
...option,
|
||||
amount: price,
|
||||
const options = deepFlatMap(
|
||||
data.options,
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.calculated_price",
|
||||
({ shipping_options }) => {
|
||||
const { calculated_price, ...options } = shipping_options ?? {}
|
||||
|
||||
if (!calculated_price) {
|
||||
optionsMissingPrices.push(options.id)
|
||||
}
|
||||
|
||||
return {
|
||||
...options,
|
||||
amount: calculated_price?.calculated_amount,
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if (optionsMissingPrices.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Shipping options with IDs ${optionsMissingPrices.join(
|
||||
", "
|
||||
)} do not have a price`
|
||||
)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
WorkflowData,
|
||||
createStep,
|
||||
createWorkflow,
|
||||
transform,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
|
||||
import {
|
||||
@@ -49,21 +50,20 @@ export const refreshPaymentCollectionForCartWorkflow = createWorkflow(
|
||||
"payment_collection.payment_sessions.id",
|
||||
],
|
||||
variables: { id: input.cart_id },
|
||||
throw_if_key_not_found: true,
|
||||
})
|
||||
|
||||
const cart = transform({ carts }, (data) => data.carts[0])
|
||||
|
||||
deletePaymentSessionStep({
|
||||
payment_session_id: carts[0].payment_collection.payment_sessions?.[0].id,
|
||||
payment_session_id: cart.payment_collection.payment_sessions?.[0].id,
|
||||
})
|
||||
|
||||
// TODO: Temporary fixed cart total, so we can test the workflow.
|
||||
// This will be removed when the totals utilities are built.
|
||||
const cartTotal = 4242
|
||||
|
||||
updatePaymentCollectionStep({
|
||||
selector: { id: carts[0].payment_collection.id },
|
||||
selector: { id: cart.payment_collection.id },
|
||||
update: {
|
||||
amount: cartTotal,
|
||||
currency_code: carts[0].currency_code,
|
||||
amount: cart.total,
|
||||
currency_code: cart.currency_code,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,12 +7,7 @@ import {
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
|
||||
import { updateLineItemsStep } from "../../line-item/steps"
|
||||
import {
|
||||
confirmInventoryStep,
|
||||
getVariantPriceSetsStep,
|
||||
getVariantsStep,
|
||||
refreshCartShippingMethodsStep,
|
||||
} from "../steps"
|
||||
import { confirmInventoryStep, refreshCartShippingMethodsStep } from "../steps"
|
||||
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
|
||||
import { cartFieldsForRefreshSteps } from "../utils/fields"
|
||||
import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory-input"
|
||||
@@ -25,9 +20,12 @@ export const updateLineItemInCartWorkflowId = "update-line-item-in-cart"
|
||||
export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
updateLineItemInCartWorkflowId,
|
||||
(input: WorkflowData<UpdateLineItemInCartWorkflowInputDTO>) => {
|
||||
const item = transform({ input }, (data) => data.input.item)
|
||||
const variantIds = transform({ input }, (data) => {
|
||||
return [data.input.item.variant_id]
|
||||
})
|
||||
|
||||
const pricingContext = transform({ cart: input.cart, item }, (data) => {
|
||||
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
|
||||
const pricingContext = transform({ cart: input.cart }, (data) => {
|
||||
return {
|
||||
currency_code: data.cart.currency_code,
|
||||
region_id: data.cart.region_id,
|
||||
@@ -35,78 +33,124 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
}
|
||||
})
|
||||
|
||||
const variantIds = transform({ input }, (data) => [
|
||||
data.input.item.variant_id!,
|
||||
])
|
||||
const variants = useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: [
|
||||
"id",
|
||||
"title",
|
||||
"sku",
|
||||
"barcode",
|
||||
"manage_inventory",
|
||||
"product.id",
|
||||
"product.title",
|
||||
"product.description",
|
||||
"product.subtitle",
|
||||
"product.thumbnail",
|
||||
"product.type",
|
||||
"product.collection",
|
||||
"product.handle",
|
||||
|
||||
const salesChannelLocations = useRemoteQueryStep({
|
||||
entry_point: "sales_channels",
|
||||
fields: ["id", "name", "stock_locations.id", "stock_locations.name"],
|
||||
variables: { id: input.cart.sales_channel_id },
|
||||
"calculated_price.calculated_amount",
|
||||
|
||||
"inventory_items.inventory_item_id",
|
||||
"inventory_items.required_quantity",
|
||||
|
||||
"inventory_items.inventory.location_levels.stock_locations.id",
|
||||
"inventory_items.inventory.location_levels.stock_locations.name",
|
||||
|
||||
"inventory_items.inventory.location_levels.stock_locations.sales_channels.id",
|
||||
"inventory_items.inventory.location_levels.stock_locations.sales_channels.name",
|
||||
],
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
},
|
||||
},
|
||||
throw_if_key_not_found: true,
|
||||
})
|
||||
|
||||
const productVariantInventoryItems = useRemoteQueryStep({
|
||||
entry_point: "product_variant_inventory_items",
|
||||
fields: ["variant_id", "inventory_item_id", "required_quantity"],
|
||||
variables: { variant_id: variantIds },
|
||||
}).config({ name: "inventory-items" })
|
||||
const confirmInventoryInput = transform({ input, variants }, (data) => {
|
||||
const managedVariants = data.variants.filter((v) => v.manage_inventory)
|
||||
if (!managedVariants.length) {
|
||||
return { items: [] }
|
||||
}
|
||||
|
||||
const variants = getVariantsStep({
|
||||
filter: { id: variantIds },
|
||||
config: { select: ["id", "manage_inventory"] },
|
||||
})
|
||||
const productVariantInventoryItems: any[] = []
|
||||
|
||||
const confirmInventoryInput = transform(
|
||||
{ productVariantInventoryItems, salesChannelLocations, input, variants },
|
||||
(data) => {
|
||||
if (!data.salesChannelLocations.length) {
|
||||
const stockLocations = data.variants
|
||||
.map((v) => v.inventory_items)
|
||||
.flat()
|
||||
.map((ii) => {
|
||||
productVariantInventoryItems.push({
|
||||
variant_id: ii.variant_id,
|
||||
inventory_item_id: ii.inventory_item_id,
|
||||
required_quantity: ii.required_quantity,
|
||||
})
|
||||
|
||||
return ii.inventory.location_levels
|
||||
})
|
||||
.flat()
|
||||
.map((ll) => ll.stock_locations)
|
||||
.flat()
|
||||
|
||||
const salesChannelId = data.input.cart.sales_channel_id
|
||||
if (salesChannelId) {
|
||||
const salesChannels = stockLocations
|
||||
.map((sl) => sl.sales_channels)
|
||||
.flat()
|
||||
.filter((sc) => sc.id === salesChannelId)
|
||||
|
||||
if (!salesChannels.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Sales channel ${data.input.cart.sales_channel_id} is not associated with any stock locations.`
|
||||
`Sales channel ${salesChannelId} is not associated with any stock locations.`
|
||||
)
|
||||
}
|
||||
|
||||
const items = prepareConfirmInventoryInput({
|
||||
product_variant_inventory_items: data.productVariantInventoryItems,
|
||||
location_ids: data.salesChannelLocations[0].stock_locations.map(
|
||||
(l) => l.id
|
||||
),
|
||||
items: [data.input.item],
|
||||
variants: data.variants.map((v) => ({
|
||||
id: v.id,
|
||||
manage_inventory: v.manage_inventory,
|
||||
})),
|
||||
})
|
||||
|
||||
return { items }
|
||||
}
|
||||
)
|
||||
|
||||
const priceNotFound: string[] = data.variants
|
||||
.filter((v) => !v.calculated_price)
|
||||
.map((v) => v.id)
|
||||
|
||||
if (priceNotFound.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Variants with IDs ${priceNotFound.join(", ")} do not have a price`
|
||||
)
|
||||
}
|
||||
|
||||
const items = prepareConfirmInventoryInput({
|
||||
product_variant_inventory_items: productVariantInventoryItems,
|
||||
location_ids: stockLocations.map((l) => l.id),
|
||||
items: [data.input.item!],
|
||||
variants: data.variants.map((v) => ({
|
||||
id: v.id,
|
||||
manage_inventory: v.manage_inventory,
|
||||
})),
|
||||
})
|
||||
|
||||
return { items }
|
||||
})
|
||||
|
||||
confirmInventoryStep(confirmInventoryInput)
|
||||
|
||||
const priceSets = getVariantPriceSetsStep({
|
||||
variantIds,
|
||||
context: pricingContext,
|
||||
})
|
||||
|
||||
const lineItemUpdate = transform({ input, priceSets, item }, (data) => {
|
||||
const price = data.priceSets[data.item.variant_id!].calculated_amount
|
||||
const lineItemUpdate = transform({ input, variants }, (data) => {
|
||||
const variant = data.variants[0]
|
||||
const item = data.input.item
|
||||
|
||||
return {
|
||||
data: {
|
||||
...data.input.update,
|
||||
unit_price: price,
|
||||
unit_price: variant.calculated_price.calculated_amount,
|
||||
},
|
||||
selector: {
|
||||
id: data.input.item.id,
|
||||
id: item.id,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const result = updateLineItemsStep({
|
||||
data: lineItemUpdate.data,
|
||||
selector: lineItemUpdate.selector,
|
||||
})
|
||||
const result = updateLineItemsStep(lineItemUpdate)
|
||||
|
||||
const cart = useRemoteQueryStep({
|
||||
entry_point: "cart",
|
||||
|
||||
@@ -137,6 +137,51 @@ export default class FulfillmentModuleService<
|
||||
return joinerConfig
|
||||
}
|
||||
|
||||
private setupShippingOptionsConfig_(
|
||||
filters,
|
||||
config
|
||||
):
|
||||
| FulfillmentTypes.FilterableShippingOptionForContextProps["context"]
|
||||
| undefined {
|
||||
const fieldIdx = config.relations?.indexOf("shipping_options_context")
|
||||
const shouldCalculatePrice = fieldIdx > -1
|
||||
|
||||
const shippingOptionsContext = filters.context ?? {}
|
||||
|
||||
delete filters.context
|
||||
|
||||
if (!shouldCalculatePrice) {
|
||||
return
|
||||
}
|
||||
|
||||
// cleanup virtual field "shipping_options_context"
|
||||
config.relations?.splice(fieldIdx, 1)
|
||||
|
||||
return shippingOptionsContext
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
// @ts-ignore
|
||||
async listShippingOptions(
|
||||
filters: FulfillmentTypes.FilterableShippingOptionForContextProps = {},
|
||||
config: FindConfig<FulfillmentTypes.ShippingOptionDTO> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<FulfillmentTypes.ShippingOptionDTO[]> {
|
||||
const optionsContext = this.setupShippingOptionsConfig_(filters, config)
|
||||
|
||||
if (optionsContext) {
|
||||
filters.context = optionsContext
|
||||
|
||||
return await this.listShippingOptionsForContext(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
return await super.listShippingOptions(filters, config, sharedContext)
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async listShippingOptionsForContext(
|
||||
filters: FulfillmentTypes.FilterableShippingOptionForContextProps,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LINKS } from "@medusajs/utils"
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { LINKS } from "@medusajs/utils"
|
||||
|
||||
export const ProductVariantInventoryItem: ModuleJoinerConfig = {
|
||||
serviceName: LINKS.ProductVariantInventoryItem,
|
||||
@@ -48,6 +48,9 @@ export const ProductVariantInventoryItem: ModuleJoinerConfig = {
|
||||
extends: [
|
||||
{
|
||||
serviceName: Modules.PRODUCT,
|
||||
fieldAlias: {
|
||||
inventory: "inventory_items.inventory",
|
||||
},
|
||||
relationship: {
|
||||
serviceName: LINKS.ProductVariantInventoryItem,
|
||||
primaryKey: "variant_id",
|
||||
|
||||
@@ -43,6 +43,7 @@ export const ProductVariantPriceSet: ModuleJoinerConfig = {
|
||||
serviceName: Modules.PRODUCT,
|
||||
fieldAlias: {
|
||||
price_set: "price_set_link.price_set",
|
||||
prices: "price_set_link.price_set.prices",
|
||||
calculated_price: {
|
||||
path: "price_set_link.price_set.calculated_price",
|
||||
forwardArgumentsOnPath: ["price_set_link.price_set"],
|
||||
|
||||
@@ -159,15 +159,16 @@ export default class PricingModuleService<
|
||||
): PricingContext["context"] | undefined {
|
||||
const fieldIdx = config.relations?.indexOf("calculated_price")
|
||||
const shouldCalculatePrice = fieldIdx > -1
|
||||
|
||||
const pricingContext = filters.context ?? {}
|
||||
|
||||
delete filters.context
|
||||
if (!shouldCalculatePrice) {
|
||||
return
|
||||
}
|
||||
|
||||
let pricingContext = filters.context ?? {}
|
||||
|
||||
// cleanup virtual field "calculated_price"
|
||||
config.relations?.splice(fieldIdx, 1)
|
||||
delete filters.context
|
||||
|
||||
return pricingContext
|
||||
}
|
||||
@@ -746,9 +747,9 @@ export default class PricingModuleService<
|
||||
) {
|
||||
const input = Array.isArray(data) ? data : [data]
|
||||
|
||||
const ruleAttributes = data
|
||||
.map((d) => d.rules?.map((r) => r.rule_attribute) ?? [])
|
||||
.flat()
|
||||
const ruleAttributes = deduplicate(
|
||||
data.map((d) => d.rules?.map((r) => r.rule_attribute) ?? []).flat()
|
||||
)
|
||||
|
||||
const ruleTypes = await this.ruleTypeService_.list(
|
||||
{ rule_attribute: ruleAttributes },
|
||||
|
||||
@@ -1090,7 +1090,7 @@ export interface IFulfillmentModuleService extends IModuleService {
|
||||
* ```
|
||||
*/
|
||||
listShippingOptions(
|
||||
filters?: FilterableShippingOptionProps,
|
||||
filters?: FilterableShippingOptionForContextProps,
|
||||
config?: FindConfig<ShippingOptionDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<ShippingOptionDTO[]>
|
||||
@@ -1156,6 +1156,7 @@ export interface IFulfillmentModuleService extends IModuleService {
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
|
||||
listShippingOptionsForContext(
|
||||
filters: FilterableShippingOptionForContextProps,
|
||||
config?: FindConfig<ShippingOptionDTO>,
|
||||
|
||||
203
packages/utils/src/common/__tests__/deep-flat-map.ts
Normal file
203
packages/utils/src/common/__tests__/deep-flat-map.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { deepFlatMap } from "../deep-flat-map"
|
||||
|
||||
describe("deepFlatMap", function () {
|
||||
it("should return flat map of nested objects", function () {
|
||||
const data = [
|
||||
{
|
||||
id: "sales_channel_1",
|
||||
stock_locations: [
|
||||
{
|
||||
id: "location_1",
|
||||
fulfillment_sets: [
|
||||
{
|
||||
id: "fset_1",
|
||||
name: "Test 123",
|
||||
service_zones: [
|
||||
{
|
||||
id: "zone_123",
|
||||
shipping_options: [
|
||||
{
|
||||
id: "so_zone_123 1111",
|
||||
calculated_price: {
|
||||
calculated_amount: 3000,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "so_zone_123 22222",
|
||||
calculated_price: {
|
||||
calculated_amount: 6000,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "zone_567",
|
||||
shipping_options: [
|
||||
{
|
||||
id: "zone 567 11111",
|
||||
calculated_price: {
|
||||
calculated_amount: 1230,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "zone 567 22222",
|
||||
calculated_price: {
|
||||
calculated_amount: 1230,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "location_2",
|
||||
fulfillment_sets: [
|
||||
{
|
||||
id: "fset_2",
|
||||
name: "fset name 2",
|
||||
service_zones: [
|
||||
{
|
||||
id: "zone_ABC",
|
||||
shipping_options: [
|
||||
{
|
||||
id: "zone_abc_unique",
|
||||
calculated_price: {
|
||||
calculated_amount: 70,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: "sales_channel_2",
|
||||
stock_locations: [
|
||||
{
|
||||
id: "location_5",
|
||||
fulfillment_sets: [
|
||||
{
|
||||
id: "fset_aaa",
|
||||
name: "Test aaa",
|
||||
service_zones: [
|
||||
{
|
||||
id: "zone_aaa",
|
||||
shipping_options: [
|
||||
{
|
||||
id: "so_zone_aaa aaaa",
|
||||
calculated_price: {
|
||||
calculated_amount: 500,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "so_zone_aaa bbbb",
|
||||
calculated_price: {
|
||||
calculated_amount: 12,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = deepFlatMap(
|
||||
data,
|
||||
"stock_locations.fulfillment_sets.service_zones.shipping_options.calculated_price",
|
||||
({
|
||||
root_,
|
||||
stock_locations,
|
||||
fulfillment_sets,
|
||||
service_zones,
|
||||
shipping_options,
|
||||
calculated_price,
|
||||
}) => {
|
||||
return {
|
||||
sales_channel_id: root_.id,
|
||||
stock_location_id: stock_locations.id,
|
||||
fulfillment_set_id: fulfillment_sets.id,
|
||||
fulfillment_set_name: fulfillment_sets.name,
|
||||
service_zone_id: service_zones.id,
|
||||
shipping_option_id: shipping_options.id,
|
||||
price: calculated_price.calculated_amount,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
sales_channel_id: "sales_channel_1",
|
||||
stock_location_id: "location_1",
|
||||
fulfillment_set_id: "fset_1",
|
||||
fulfillment_set_name: "Test 123",
|
||||
service_zone_id: "zone_123",
|
||||
shipping_option_id: "so_zone_123 1111",
|
||||
price: 3000,
|
||||
},
|
||||
{
|
||||
sales_channel_id: "sales_channel_1",
|
||||
stock_location_id: "location_1",
|
||||
fulfillment_set_id: "fset_1",
|
||||
fulfillment_set_name: "Test 123",
|
||||
service_zone_id: "zone_123",
|
||||
shipping_option_id: "so_zone_123 22222",
|
||||
price: 6000,
|
||||
},
|
||||
{
|
||||
sales_channel_id: "sales_channel_1",
|
||||
stock_location_id: "location_1",
|
||||
fulfillment_set_id: "fset_1",
|
||||
fulfillment_set_name: "Test 123",
|
||||
service_zone_id: "zone_567",
|
||||
shipping_option_id: "zone 567 11111",
|
||||
price: 1230,
|
||||
},
|
||||
{
|
||||
sales_channel_id: "sales_channel_1",
|
||||
stock_location_id: "location_1",
|
||||
fulfillment_set_id: "fset_1",
|
||||
fulfillment_set_name: "Test 123",
|
||||
service_zone_id: "zone_567",
|
||||
shipping_option_id: "zone 567 22222",
|
||||
price: 1230,
|
||||
},
|
||||
{
|
||||
sales_channel_id: "sales_channel_1",
|
||||
stock_location_id: "location_2",
|
||||
fulfillment_set_id: "fset_2",
|
||||
fulfillment_set_name: "fset name 2",
|
||||
service_zone_id: "zone_ABC",
|
||||
shipping_option_id: "zone_abc_unique",
|
||||
price: 70,
|
||||
},
|
||||
{
|
||||
sales_channel_id: "sales_channel_2",
|
||||
stock_location_id: "location_5",
|
||||
fulfillment_set_id: "fset_aaa",
|
||||
fulfillment_set_name: "Test aaa",
|
||||
service_zone_id: "zone_aaa",
|
||||
shipping_option_id: "so_zone_aaa aaaa",
|
||||
price: 500,
|
||||
},
|
||||
{
|
||||
sales_channel_id: "sales_channel_2",
|
||||
stock_location_id: "location_5",
|
||||
fulfillment_set_id: "fset_aaa",
|
||||
fulfillment_set_name: "Test aaa",
|
||||
service_zone_id: "zone_aaa",
|
||||
shipping_option_id: "so_zone_aaa bbbb",
|
||||
price: 12,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
103
packages/utils/src/common/deep-flat-map.ts
Normal file
103
packages/utils/src/common/deep-flat-map.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { isDefined } from "./is-defined"
|
||||
import { isObject } from "./is-object"
|
||||
|
||||
/**
|
||||
* @description
|
||||
* This function is used to flatten nested objects and arrays
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* const data = {
|
||||
* root_level_property: "root level",
|
||||
* products: [
|
||||
* {
|
||||
* id: "1",
|
||||
* name: "product 1",
|
||||
* variants: [
|
||||
* { id: "1.1", name: "variant 1.1" },
|
||||
* { id: "1.2", name: "variant 1.2" },
|
||||
* ],
|
||||
* },
|
||||
* {
|
||||
* id: "2",
|
||||
* name: "product 2",
|
||||
* variants: [
|
||||
* { id: "2.1", name: "variant 2.1" },
|
||||
* { id: "2.2", name: "variant 2.2" },
|
||||
* ],
|
||||
* },
|
||||
* ],
|
||||
* }
|
||||
*
|
||||
* const flat = deepFlatMap(
|
||||
* data,
|
||||
* "products.variants",
|
||||
* ({ root_, products, variants }) => {
|
||||
* return {
|
||||
* root_level_property: root_.root_level_property,
|
||||
* product_id: products.id,
|
||||
* product_name: products.name,
|
||||
* variant_id: variants.id,
|
||||
* variant_name: variants.name,
|
||||
* }
|
||||
* }
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
|
||||
export function deepFlatMap(
|
||||
data: any,
|
||||
path: string,
|
||||
callback: (context: Record<string, any>) => any
|
||||
) {
|
||||
const ROOT_LEVEL = "root_"
|
||||
const keys = path.split(".")
|
||||
keys.unshift(ROOT_LEVEL)
|
||||
|
||||
const lastKey = keys[keys.length - 1]
|
||||
const stack: {
|
||||
element: any
|
||||
path: string[]
|
||||
context: Record<string, any>
|
||||
}[] = [{ element: { [ROOT_LEVEL]: data }, path: keys, context: {} }]
|
||||
|
||||
const results: any[] = []
|
||||
while (stack.length > 0) {
|
||||
const { element, path, context } = stack.shift()!
|
||||
const currentKey = path[0]
|
||||
const remainingPath = path.slice(1)
|
||||
|
||||
if (!isDefined(element[currentKey])) {
|
||||
callback({ ...context })
|
||||
continue
|
||||
}
|
||||
|
||||
if (remainingPath.length === 0) {
|
||||
if (Array.isArray(element[currentKey])) {
|
||||
element[currentKey].forEach((item) => {
|
||||
results.push(callback({ ...context, [lastKey]: item }))
|
||||
})
|
||||
} else if (isObject(element[currentKey])) {
|
||||
results.push(callback({ ...context, [lastKey]: element[currentKey] }))
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(element[currentKey])) {
|
||||
element[currentKey].forEach((item) => {
|
||||
stack.push({
|
||||
element: item,
|
||||
path: remainingPath,
|
||||
context: { ...context, [currentKey]: item },
|
||||
})
|
||||
})
|
||||
} else if (isObject(element[currentKey])) {
|
||||
stack.push({
|
||||
element: element[currentKey],
|
||||
path: remainingPath,
|
||||
context: { ...context, [currentKey]: element[currentKey] },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export * from "./create-psql-index-helper"
|
||||
export * from "./deduplicate"
|
||||
export * from "./deep-copy"
|
||||
export * from "./deep-equal-obj"
|
||||
export * from "./deep-flat-map"
|
||||
export * from "./errors"
|
||||
export * from "./generate-entity-id"
|
||||
export * from "./generate-linkable-keys-map"
|
||||
|
||||
Reference in New Issue
Block a user