feat(core-flows, types): update shipping methods upon cart ops (#10382)

* feat(core-flows,framework,medusa): list shipping options pass in cart as pricing context

* chore: add test for shipping options returning free shipping

* feat(core-flows, types): update shipping methods upon cart ops

* chore: fix specs

* chore: fix bugs + specs

* Update update-shipping-methods.ts

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>

* Update mutations.ts

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>

* chore: undo refresh changes

* chore: merge with latest

* chore: address PR comments

* chore: fix conflicts

* chore: fix specs

* chore: address reviews

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Riqwan Thamir
2024-12-08 14:06:50 +01:00
committed by GitHub
parent f95c4e240c
commit 9e797dc3d2
21 changed files with 1169 additions and 1244 deletions

View File

@@ -302,7 +302,107 @@ medusaIntegrationTestRunner({
})
describe("POST /store/carts/:id/line-items", () => {
let shippingOption
beforeEach(async () => {
const stockLocation = (
await api.post(
`/admin/stock-locations`,
{ name: "test location" },
adminHeaders
)
).data.stock_location
await api.post(
`/admin/stock-locations/${stockLocation.id}/sales-channels`,
{ add: [salesChannel.id] },
adminHeaders
)
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: `test-${stockLocation.id}`, type: "default" },
adminHeaders
)
).data.shipping_profile
const fulfillmentSets = (
await api.post(
`/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`,
{
name: `Test-${shippingProfile.id}`,
type: "test-type",
},
adminHeaders
)
).data.stock_location.fulfillment_sets
const fulfillmentSet = (
await api.post(
`/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`,
{
name: `Test-${shippingProfile.id}`,
geo_zones: [
{ type: "country", country_code: "it" },
{ type: "country", country_code: "us" },
],
},
adminHeaders
)
).data.fulfillment_set
await api.post(
`/admin/stock-locations/${stockLocation.id}/fulfillment-providers`,
{ add: ["manual_test-provider"] },
adminHeaders
)
shippingOption = (
await api.post(
`/admin/shipping-options`,
{
name: `Shipping`,
service_zone_id: fulfillmentSet.service_zones[0].id,
shipping_profile_id: shippingProfile.id,
provider_id: "manual_test-provider",
price_type: "flat",
type: {
label: "Test type",
description: "Test description",
code: "test-code",
},
prices: [
{ currency_code: "usd", amount: 1000 },
{
currency_code: "usd",
amount: 0,
rules: [
{
attribute: "item_total",
operator: "gt",
value: 5000,
},
],
},
],
rules: [
{
attribute: "enabled_in_store",
value: '"true"',
operator: "eq",
},
{
attribute: "is_return",
value: "false",
operator: "eq",
},
],
},
adminHeaders
)
).data.shipping_option
cart = (
await api.post(
`/store/carts`,
@@ -362,6 +462,101 @@ medusaIntegrationTestRunner({
)
})
describe("with custom shipping options prices", () => {
beforeEach(async () => {
cart = (
await api.post(
`/store/carts`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
promo_codes: [promotion.code],
},
storeHeadersWithCustomer
)
).data.cart
})
it("should update shipping method amount when cart totals change", async () => {
let response = await api.post(
`/store/carts/${cart.id}/shipping-methods`,
{ option_id: shippingOption.id },
storeHeaders
)
expect(response.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
shipping_methods: expect.arrayContaining([
expect.objectContaining({
shipping_option_id: shippingOption.id,
amount: 1000,
is_tax_inclusive: true,
}),
]),
})
)
response = await api.post(
`/store/carts/${cart.id}/line-items`,
{
variant_id: product.variants[0].id,
quantity: 100,
},
storeHeaders
)
expect(response.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
shipping_methods: expect.arrayContaining([
expect.objectContaining({
shipping_option_id: shippingOption.id,
amount: 0,
is_tax_inclusive: true,
}),
]),
})
)
})
it("should remove shipping methods when they are no longer valid for the cart", async () => {
let response = await api.post(
`/store/carts/${cart.id}/shipping-methods`,
{ option_id: shippingOption.id },
storeHeaders
)
expect(response.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
shipping_methods: expect.arrayContaining([
expect.objectContaining({
shipping_option_id: shippingOption.id,
amount: 1000,
is_tax_inclusive: true,
}),
]),
})
)
response = await api.post(
`/store/carts/${cart.id}`,
{ region_id: noAutomaticRegion.id },
storeHeaders
)
expect(response.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
shipping_methods: expect.arrayContaining([]),
})
)
})
})
it("should add item to cart with tax lines multiple times", async () => {
let response = await api.post(
`/store/carts/${cart.id}/line-items`,
@@ -1529,6 +1724,222 @@ medusaIntegrationTestRunner({
)
})
})
describe("POST /store/carts/:id/shipping-methods", () => {
let shippingOption
beforeEach(async () => {
const stockLocation = (
await api.post(
`/admin/stock-locations`,
{ name: "test location" },
adminHeaders
)
).data.stock_location
await api.post(
`/admin/stock-locations/${stockLocation.id}/sales-channels`,
{ add: [salesChannel.id] },
adminHeaders
)
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: `test-${stockLocation.id}`, type: "default" },
adminHeaders
)
).data.shipping_profile
const fulfillmentSets = (
await api.post(
`/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`,
{
name: `Test-${shippingProfile.id}`,
type: "test-type",
},
adminHeaders
)
).data.stock_location.fulfillment_sets
const fulfillmentSet = (
await api.post(
`/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`,
{
name: `Test-${shippingProfile.id}`,
geo_zones: [
{ type: "country", country_code: "it" },
{ type: "country", country_code: "us" },
],
},
adminHeaders
)
).data.fulfillment_set
await api.post(
`/admin/stock-locations/${stockLocation.id}/fulfillment-providers`,
{ add: ["manual_test-provider"] },
adminHeaders
)
shippingOption = (
await api.post(
`/admin/shipping-options`,
{
name: `Test shipping option ${fulfillmentSet.id}`,
service_zone_id: fulfillmentSet.service_zones[0].id,
shipping_profile_id: shippingProfile.id,
provider_id: "manual_test-provider",
price_type: "flat",
type: {
label: "Test type",
description: "Test description",
code: "test-code",
},
prices: [
{ currency_code: "usd", amount: 1000 },
{
currency_code: "usd",
amount: 500,
rules: [
{
attribute: "item_total",
operator: "gt",
value: 3000,
},
],
},
],
rules: [
{
attribute: "enabled_in_store",
value: '"true"',
operator: "eq",
},
{
attribute: "is_return",
value: "false",
operator: "eq",
},
],
},
adminHeaders
)
).data.shipping_option
cart = (
await api.post(
`/store/carts?fields=+total`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
},
storeHeadersWithCustomer
)
).data.cart
})
it("should add shipping method to cart", async () => {
let response = await api.post(
`/store/carts/${cart.id}/shipping-methods`,
{ option_id: shippingOption.id },
storeHeaders
)
expect(response.status).toEqual(200)
expect(response.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
shipping_methods: expect.arrayContaining([
expect.objectContaining({
shipping_option_id: shippingOption.id,
amount: 1000,
is_tax_inclusive: true,
}),
]),
})
)
// Total is over the amount 3000 to enable the second pricing rule
const cart2 = (
await api.post(
`/store/carts?fields=+total`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
items: [{ variant_id: product.variants[0].id, quantity: 5 }],
},
storeHeadersWithCustomer
)
).data.cart
response = await api.post(
`/store/carts/${cart2.id}/shipping-methods`,
{ option_id: shippingOption.id },
storeHeaders
)
expect(response.data.cart).toEqual(
expect.objectContaining({
id: cart2.id,
shipping_methods: expect.arrayContaining([
expect.objectContaining({
shipping_option_id: shippingOption.id,
amount: 500,
is_tax_inclusive: true,
}),
]),
})
)
})
it("should throw when prices are not setup for shipping option", async () => {
cart = (
await api.post(
`/store/carts?fields=+total`,
{
currency_code: "eur",
sales_channel_id: salesChannel.id,
region_id: region.id,
items: [{ variant_id: product.variants[0].id, quantity: 5 }],
},
storeHeadersWithCustomer
)
).data.cart
let { response } = await api
.post(
`/store/carts/${cart.id}/shipping-methods`,
{ option_id: shippingOption.id },
storeHeaders
)
.catch((e) => e)
expect(response.data).toEqual({
type: "invalid_data",
message: `Shipping options with IDs ${shippingOption.id} do not have a price`,
})
})
it("should throw when shipping option id is not found", async () => {
let { response } = await api
.post(
`/store/carts/${cart.id}/shipping-methods`,
{ option_id: "does-not-exist" },
storeHeaders
)
.catch((e) => e)
expect(response.status).toEqual(400)
expect(response.data).toEqual({
type: "invalid_data",
message: "Shipping Options are invalid for cart.",
})
})
})
})
},
})

View File

@@ -152,12 +152,12 @@ medusaIntegrationTestRunner({
amount: 500,
rules: [
{
attribute: "total",
attribute: "item_total",
operator: "gte",
value: 100,
},
{
attribute: "total",
attribute: "item_total",
operator: "lte",
value: 200,
},
@@ -220,12 +220,12 @@ medusaIntegrationTestRunner({
rules_count: 2,
price_rules: expect.arrayContaining([
expect.objectContaining({
attribute: "total",
attribute: "item_total",
operator: "gte",
value: "100",
}),
expect.objectContaining({
attribute: "total",
attribute: "item_total",
operator: "lte",
value: "200",
}),
@@ -329,7 +329,7 @@ medusaIntegrationTestRunner({
amount: 500,
rules: [
{
attribute: "total",
attribute: "item_total",
operator: "gt",
value: 200,
},
@@ -380,7 +380,7 @@ medusaIntegrationTestRunner({
rules_count: 2,
price_rules: expect.arrayContaining([
expect.objectContaining({
attribute: "total",
attribute: "item_total",
operator: "gt",
value: "200",
}),
@@ -460,7 +460,7 @@ medusaIntegrationTestRunner({
amount: 500,
rules: [
{
attribute: "total",
attribute: "item_total",
operator: "not_whitelisted",
value: 100,
},
@@ -498,7 +498,7 @@ medusaIntegrationTestRunner({
amount: 500,
rules: [
{
attribute: "total",
attribute: "item_total",
operator: "gt",
value: "string",
},
@@ -628,7 +628,7 @@ medusaIntegrationTestRunner({
amount: 5,
rules: [
{
attribute: "total",
attribute: "item_total",
operator: "gt",
value: 200,
},
@@ -704,7 +704,7 @@ medusaIntegrationTestRunner({
amount: 5,
price_rules: [
expect.objectContaining({
attribute: "total",
attribute: "item_total",
operator: "gt",
value: "200",
}),

View File

@@ -1,9 +1,4 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import {
IFulfillmentModuleService,
IRegionModuleService,
} from "@medusajs/types"
import { ContainerRegistrationKeys, Modules } from "@medusajs/utils"
import {
createAdminUser,
generatePublishableKey,
@@ -20,9 +15,6 @@ medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => {
describe("Store: Shipping Option API", () => {
let appContainer
let fulfillmentModule: IFulfillmentModuleService
let regionService: IRegionModuleService
let salesChannel
let region
let regionTwo
@@ -36,8 +28,6 @@ medusaIntegrationTestRunner({
beforeAll(async () => {
appContainer = getContainer()
fulfillmentModule = appContainer.resolve(Modules.FULFILLMENT)
regionService = appContainer.resolve(Modules.REGION)
})
beforeEach(async () => {
@@ -45,31 +35,27 @@ medusaIntegrationTestRunner({
storeHeaders = generateStoreHeaders({ publishableKey })
await createAdminUser(dbConnection, adminHeaders, appContainer)
const remoteLinkService = appContainer.resolve(
ContainerRegistrationKeys.REMOTE_LINK
)
region = await regionService.createRegions({
name: "Test region",
countries: ["US"],
currency_code: "usd",
})
region = (
await api.post(
"/admin/regions",
{ name: "US", currency_code: "usd", countries: ["US"] },
adminHeaders
)
).data.region
regionTwo = await regionService.createRegions({
name: "Test region two",
countries: ["DK"],
currency_code: "dkk",
})
await api.post(
"/admin/price-preferences",
{
attribute: "region_id",
value: regionTwo.id,
is_tax_inclusive: true,
},
adminHeaders
)
regionTwo = (
await api.post(
"/admin/regions",
{
name: "Test region two",
currency_code: "dkk",
countries: ["DK"],
is_tax_inclusive: true,
},
adminHeaders
)
).data.region
salesChannel = (
await api.post(
@@ -116,22 +102,39 @@ medusaIntegrationTestRunner({
stockLocation = (
await api.post(
`/admin/stock-locations`,
{
name: "test location",
},
{ name: "test location" },
adminHeaders
)
).data.stock_location
shippingProfile = await fulfillmentModule.createShippingProfiles({
name: "Test",
type: "default",
})
await api.post(
`/admin/stock-locations/${stockLocation.id}/sales-channels`,
{ add: [salesChannel.id] },
adminHeaders
)
fulfillmentSet = await fulfillmentModule.createFulfillmentSets({
name: "Test",
type: "test-type",
service_zones: [
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
const fulfillmentSets = (
await api.post(
`/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`,
{
name: "Test",
type: "test-type",
},
adminHeaders
)
).data.stock_location.fulfillment_sets
fulfillmentSet = (
await api.post(
`/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`,
{
name: "Test",
geo_zones: [
@@ -139,27 +142,9 @@ medusaIntegrationTestRunner({
{ type: "country", country_code: "dk" },
],
},
],
})
await remoteLinkService.create([
{
[Modules.SALES_CHANNEL]: {
sales_channel_id: salesChannel.id,
},
[Modules.STOCK_LOCATION]: {
stock_location_id: stockLocation.id,
},
},
{
[Modules.STOCK_LOCATION]: {
stock_location_id: stockLocation.id,
},
[Modules.FULFILLMENT]: {
fulfillment_set_id: fulfillmentSet.id,
},
},
])
adminHeaders
)
).data.fulfillment_set
await api.post(
`/admin/stock-locations/${stockLocation.id}/fulfillment-providers`,
@@ -196,7 +181,7 @@ medusaIntegrationTestRunner({
rules: [
{
operator: "gt",
attribute: "total",
attribute: "item_total",
value: 2000,
},
],
@@ -246,8 +231,11 @@ medusaIntegrationTestRunner({
expect.objectContaining({
id: shippingOption.id,
name: "Test shipping option",
amount: 1100,
price_type: "flat",
amount: 1100,
calculated_price: expect.objectContaining({
calculated_amount: 1100,
}),
})
)
@@ -272,8 +260,12 @@ medusaIntegrationTestRunner({
id: shippingOption.id,
name: "Test shipping option",
amount: 500,
price_type: "flat",
is_tax_inclusive: true,
calculated_price: expect.objectContaining({
calculated_amount: 500,
is_calculated_price_tax_inclusive: true,
}),
price_type: "flat",
})
)
})
@@ -313,6 +305,9 @@ medusaIntegrationTestRunner({
name: "Test shipping option",
// Free shipping due to cart total being greater than 2000
amount: 0,
calculated_price: expect.objectContaining({
calculated_amount: 0,
}),
price_type: "flat",
})
)

View File

@@ -20,7 +20,6 @@ import {
Modules,
ProductStatus,
PromotionType,
RuleOperator,
} from "@medusajs/utils"
import {
createAdminUser,
@@ -715,221 +714,6 @@ medusaIntegrationTestRunner({
})
})
it("should add item to cart", async () => {
const customer = await customerModule.createCustomers({
email: "tony@stark-industries.com",
})
const salesChannel = await scModule.createSalesChannels({
name: "Webshop",
})
const [productWithSpecialTax] = await productModule.createProducts([
{
// This product ID is setup in the tax structure fixture (setupTaxStructure)
id: "product_id_1",
title: "Test product",
variants: [{ title: "Test variant", manage_inventory: false }],
} as any,
])
const [productWithDefaultTax] = await productModule.createProducts([
{
title: "Test product default tax",
variants: [
{ title: "Test variant default tax", manage_inventory: false },
],
},
])
await api.post(
"/admin/price-preferences",
{
attribute: "currency_code",
value: "usd",
is_tax_inclusive: true,
},
adminHeaders
)
const cart = await cartModule.createCarts({
currency_code: "usd",
customer_id: customer.id,
sales_channel_id: salesChannel.id,
region_id: region.id,
shipping_address: {
customer_id: customer.id,
address_1: "test address 1",
address_2: "test address 2",
city: "SF",
country_code: "US",
province: "CA",
postal_code: "94016",
},
items: [
{
id: "item-1",
unit_price: 2000,
quantity: 1,
title: "Test item",
product_id: "prod_mat",
} as any,
],
})
const appliedPromotion = await promotionModule.createPromotions({
code: "PROMOTION_APPLIED",
type: PromotionType.STANDARD,
application_method: {
type: "fixed",
target_type: "items",
allocation: "across",
value: 300,
apply_to_quantity: 2,
currency_code: "usd",
target_rules: [
{
attribute: "product_id",
operator: "in",
values: ["prod_mat", productWithSpecialTax.id],
},
],
},
})
const [lineItemAdjustment] = await cartModule.addLineItemAdjustments([
{
code: appliedPromotion.code!,
amount: 300,
item_id: "item-1",
promotion_id: appliedPromotion.id,
},
])
const [priceSet, priceSetDefaultTax] =
await pricingModule.createPriceSets([
{
prices: [{ amount: 3000, currency_code: "usd" }],
},
{
prices: [{ amount: 2000, currency_code: "usd" }],
},
])
await remoteLink.create([
{
[Modules.PRODUCT]: {
variant_id: productWithSpecialTax.variants[0].id,
},
[Modules.PRICING]: { price_set_id: priceSet.id },
},
{
[Modules.PRODUCT]: {
variant_id: productWithDefaultTax.variants[0].id,
},
[Modules.PRICING]: { price_set_id: priceSetDefaultTax.id },
},
{
[Modules.CART]: { cart_id: cart.id },
[Modules.PROMOTION]: { promotion_id: appliedPromotion.id },
},
])
let response = await api.post(
`/store/carts/${cart.id}/line-items`,
{
variant_id: productWithSpecialTax.variants[0].id,
quantity: 1,
},
storeHeaders
)
expect(response.status).toEqual(200)
expect(response.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
currency_code: "usd",
items: expect.arrayContaining([
expect.objectContaining({
unit_price: 3000,
is_tax_inclusive: true,
quantity: 1,
title: "Test variant",
tax_lines: [
expect.objectContaining({
description: "CA Reduced Rate for Products",
code: "CAREDUCE_PROD",
rate: 3,
provider_id: "system",
}),
],
adjustments: [
expect.objectContaining({
code: "PROMOTION_APPLIED",
amount: 177.86561264822134,
}),
],
}),
expect.objectContaining({
unit_price: 2000,
is_tax_inclusive: false,
quantity: 1,
title: "Test item",
tax_lines: [
expect.objectContaining({
code: "CADEFAULT",
description: "CA Default Rate",
provider_id: "system",
rate: 5,
}),
],
adjustments: [
expect.objectContaining({
id: expect.not.stringContaining(lineItemAdjustment.id),
code: "PROMOTION_APPLIED",
amount: 122.13438735177866,
}),
],
}),
]),
})
)
response = await api.post(
`/store/carts/${cart.id}/line-items`,
{
variant_id: productWithDefaultTax.variants[0].id,
quantity: 1,
},
storeHeaders
)
expect(response.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
currency_code: "usd",
items: expect.arrayContaining([
expect.objectContaining({
unit_price: 2000,
is_tax_inclusive: true,
quantity: 1,
title: "Test variant default tax",
tax_lines: [
// Uses the california default rate
expect.objectContaining({
description: "CA Default Rate",
code: "CADEFAULT",
rate: 5,
provider_id: "system",
}),
],
}),
]),
})
)
})
it("adding an existing variant should update or create line item depending on metadata", async () => {
const product = (
await api.post(`/admin/products`, productData, adminHeaders)
@@ -1304,102 +1088,6 @@ medusaIntegrationTestRunner({
})
})
describe("POST /store/carts/:id/shipping-methods", () => {
it("should add a shipping methods to a cart", async () => {
const cart = await cartModule.createCarts({
currency_code: "usd",
shipping_address: { country_code: "us" },
items: [],
})
const shippingProfile =
await fulfillmentModule.createShippingProfiles({
name: "Test",
type: "default",
})
const fulfillmentSet = await fulfillmentModule.createFulfillmentSets({
name: "Test",
type: "test-type",
service_zones: [
{
name: "Test",
geo_zones: [{ type: "country", country_code: "us" }],
},
],
})
await api.post(
"/admin/price-preferences",
{
attribute: "currency_code",
value: "usd",
is_tax_inclusive: true,
},
adminHeaders
)
const priceSet = await pricingModule.createPriceSets({
prices: [{ amount: 3000, currency_code: "usd" }],
})
const shippingOption = await fulfillmentModule.createShippingOptions({
name: "Test shipping option",
service_zone_id: fulfillmentSet.service_zones[0].id,
shipping_profile_id: shippingProfile.id,
provider_id: "manual_test-provider",
price_type: "flat",
type: {
label: "Test type",
description: "Test description",
code: "test-code",
},
rules: [
{
operator: RuleOperator.EQ,
attribute: "is_return",
value: "false",
},
{
operator: RuleOperator.EQ,
attribute: "enabled_in_store",
value: "true",
},
],
})
await remoteLink.create([
{
[Modules.FULFILLMENT]: { shipping_option_id: shippingOption.id },
[Modules.PRICING]: { price_set_id: priceSet.id },
},
])
let response = await api.post(
`/store/carts/${cart.id}/shipping-methods`,
{ option_id: shippingOption.id },
storeHeaders
)
expect(response.status).toEqual(200)
expect(response.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
shipping_methods: [
{
shipping_option_id: shippingOption.id,
amount: 3000,
is_tax_inclusive: true,
id: expect.any(String),
tax_lines: [],
adjustments: [],
},
],
})
)
})
})
describe("POST /store/carts/:id/complete", () => {
let salesChannel
let product

View File

@@ -14,7 +14,6 @@ export * from "./get-promotion-codes-to-apply"
export * from "./get-variant-price-sets"
export * from "./get-variants"
export * from "./prepare-adjustments-from-promotion-actions"
export * from "./refresh-cart-shipping-methods"
export * from "./remove-line-item-adjustments"
export * from "./remove-shipping-method-adjustments"
export * from "./remove-shipping-method-from-cart"
@@ -27,4 +26,3 @@ export * from "./update-line-items"
export * from "./validate-cart-payments"
export * from "./validate-cart-shipping-options"
export * from "./validate-variant-prices"

View File

@@ -1,73 +0,0 @@
import {
CartDTO,
ICartModuleService,
IFulfillmentModuleService,
} from "@medusajs/framework/types"
import { Modules, arrayDifference } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
export interface RefreshCartShippingMethodsStepInput {
cart: CartDTO
}
export const refreshCartShippingMethodsStepId = "refresh-cart-shipping-methods"
/**
* This step refreshes the shipping methods of a cart.
*/
export const refreshCartShippingMethodsStep = createStep(
refreshCartShippingMethodsStepId,
async (data: RefreshCartShippingMethodsStepInput, { container }) => {
const { cart } = data
const { shipping_methods: shippingMethods = [] } = cart
if (!shippingMethods?.length) {
return new StepResponse(void 0, [])
}
const fulfillmentModule = container.resolve<IFulfillmentModuleService>(
Modules.FULFILLMENT
)
const cartModule = container.resolve<ICartModuleService>(Modules.CART)
const shippingOptionIds: string[] = shippingMethods.map(
(sm) => sm.shipping_option_id!
)
const validShippingOptions =
await fulfillmentModule.listShippingOptionsForContext(
{
id: shippingOptionIds,
context: { ...cart, is_return: "false", enabled_in_store: "true" },
address: {
country_code: cart.shipping_address?.country_code,
province_code: cart.shipping_address?.province,
city: cart.shipping_address?.city,
postal_expression: cart.shipping_address?.postal_code,
},
},
{ relations: ["rules"] }
)
const validShippingOptionIds = validShippingOptions.map((o) => o.id)
const invalidShippingOptionIds = arrayDifference(
shippingOptionIds,
validShippingOptionIds
)
const shippingMethodsToDelete = shippingMethods
.filter((sm) => invalidShippingOptionIds.includes(sm.shipping_option_id!))
.map((sm) => sm.id)
await cartModule.softDeleteShippingMethods(shippingMethodsToDelete)
return new StepResponse(void 0, shippingMethodsToDelete)
},
async (shippingMethodsToRestore, { container }) => {
if (shippingMethodsToRestore?.length) {
const cartModule = container.resolve<ICartModuleService>(Modules.CART)
await cartModule.restoreShippingMethods(shippingMethodsToRestore)
}
}
)

View File

@@ -16,6 +16,10 @@ export const removeShippingMethodFromCartStep = createStep(
async (data: RemoveShippingMethodFromCartStepInput, { container }) => {
const cartService = container.resolve<ICartModuleService>(Modules.CART)
if (!data?.shipping_method_ids?.length) {
return new StepResponse(null, [])
}
const methods = await cartService.softDeleteShippingMethods(
data.shipping_method_ids
)
@@ -23,7 +27,7 @@ export const removeShippingMethodFromCartStep = createStep(
return new StepResponse(methods, data.shipping_method_ids)
},
async (ids, { container }) => {
if (!ids) {
if (!ids?.length) {
return
}

View File

@@ -0,0 +1,43 @@
import {
ICartModuleService,
UpdateShippingMethodDTO,
} from "@medusajs/framework/types"
import {
Modules,
getSelectsAndRelationsFromObjectArray,
} from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
export const updateShippingMethodsStepId = "update-shipping-methods-step"
/**
* This step updates a cart's shipping methods.
*/
export const updateShippingMethodsStep = createStep(
updateShippingMethodsStepId,
async (data: UpdateShippingMethodDTO[], { container }) => {
if (!data?.length) {
return new StepResponse([], [])
}
const cartModule = container.resolve<ICartModuleService>(Modules.CART)
const { selects, relations } = getSelectsAndRelationsFromObjectArray(data)
const dataBeforeUpdate = await cartModule.listShippingMethods(
{ id: data.map((d) => d.id!) },
{ select: selects, relations }
)
const updatedItems = await cartModule.updateShippingMethods(data)
return new StepResponse(updatedItems, dataBeforeUpdate)
},
async (dataBeforeUpdate, { container }) => {
if (!dataBeforeUpdate?.length) {
return
}
const cartModule: ICartModuleService = container.resolve(Modules.CART)
await cartModule.updateShippingMethods(dataBeforeUpdate)
}
)

View File

@@ -0,0 +1,37 @@
import { isDefined, MedusaError } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
export const validateCartShippingOptionsStepId =
"validate-cart-shipping-options"
/**
* This step validates shipping options to ensure they have a price.
*/
export const validateCartShippingOptionsPriceStep = createStep(
"validate-cart-shipping-options-price",
async (data: { shippingOptions: any[] }, { container }) => {
const { shippingOptions = [] } = data
const optionsMissingPrices: string[] = []
for (const shippingOption of shippingOptions) {
const { calculated_price, ...options } = shippingOption
if (
shippingOption?.id &&
!isDefined(calculated_price?.calculated_amount)
) {
optionsMissingPrices.push(options.id)
}
}
if (optionsMissingPrices.length) {
const ids = optionsMissingPrices.join(", ")
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Shipping options with IDs ${ids} do not have a price`
)
}
return new StepResponse(void 0)
}
)

View File

@@ -3,6 +3,7 @@ export const cartFieldsForRefreshSteps = [
"currency_code",
"quantity",
"subtotal",
"total",
"item_subtotal",
"shipping_subtotal",
"region_id",

View File

@@ -14,7 +14,9 @@ import {
} from "../steps"
import { validateCartStep } from "../steps/validate-cart"
import { validateAndReturnShippingMethodsDataStep } from "../steps/validate-shipping-methods-data"
import { validateCartShippingOptionsPriceStep } from "../steps/validate-shipping-options-price"
import { cartFieldsForRefreshSteps } from "../utils/fields"
import { listShippingOptionsForCartWorkflow } from "./list-shipping-options-for-cart"
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
import { updateTaxLinesWorkflow } from "./update-tax-lines"
@@ -54,30 +56,24 @@ export const addShippingMethodToCartWorkflow = createWorkflow(
shippingOptionsContext: { is_return: "false", enabled_in_store: "true" },
})
const shippingOptions = useRemoteQueryStep({
entry_point: "shipping_option",
fields: [
"id",
"name",
"calculated_price.calculated_amount",
"calculated_price.is_calculated_price_tax_inclusive",
"provider_id",
],
variables: {
id: optionIds,
calculated_price: {
context: { currency_code: cart.currency_code },
},
const shippingOptions = listShippingOptionsForCartWorkflow.runAsStep({
input: {
option_ids: optionIds,
cart_id: cart.id,
is_return: false,
},
}).config({ name: "fetch-shipping-option" })
})
validateCartShippingOptionsPriceStep({ shippingOptions })
const validateShippingMethodsDataInput = transform(
{ input, shippingOptions },
(data) => {
return data.input.options.map((inputOption) => {
const shippingOption = data.shippingOptions.find(
({ input, shippingOptions }) => {
return input.options.map((inputOption) => {
const shippingOption = shippingOptions.find(
(so) => so.id === inputOption.id
)
return {
id: inputOption.id,
provider_id: shippingOption?.provider_id,

View File

@@ -8,27 +8,20 @@ import {
parallelize,
transform,
WorkflowData,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { emitEventStep } from "../../common/steps/emit-event"
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
import {
createLineItemsStep,
getLineItemActionsStep,
refreshCartShippingMethodsStep,
updateLineItemsStep,
} from "../steps"
import { validateCartStep } from "../steps/validate-cart"
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
import {
cartFieldsForRefreshSteps,
productVariantsFields,
} from "../utils/fields"
import { productVariantsFields } from "../utils/fields"
import { prepareLineItemData } from "../utils/prepare-line-item-data"
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection"
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
import { updateTaxLinesWorkflow } from "./update-tax-lines"
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
export const addToCartWorkflowId = "add-to-cart"
/**
@@ -44,6 +37,7 @@ export const addToCartWorkflow = createWorkflow(
})
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
// TODO: create a common workflow to fetch variants and its prices
const pricingContext = transform({ cart: input.cart }, (data) => {
return {
currency_code: data.cart.currency_code,
@@ -100,7 +94,7 @@ export const addToCartWorkflow = createWorkflow(
},
})
const [createdItems, updatedItems] = parallelize(
parallelize(
createLineItemsStep({
id: input.cart.id,
items: itemsToCreate,
@@ -111,43 +105,13 @@ export const addToCartWorkflow = createWorkflow(
})
)
const items = transform({ createdItems, updatedItems }, (data) => {
return [...(data.createdItems || []), ...(data.updatedItems || [])]
refreshCartItemsWorkflow.runAsStep({
input: { cart_id: input.cart.id },
})
const cart = useRemoteQueryStep({
entry_point: "cart",
fields: cartFieldsForRefreshSteps,
variables: { id: input.cart.id },
list: false,
}).config({ name: "refetchcart" })
parallelize(
refreshCartShippingMethodsStep({ cart }),
emitEventStep({
eventName: CartWorkflowEvents.UPDATED,
data: { id: input.cart.id },
})
)
updateTaxLinesWorkflow.runAsStep({
input: {
cart_id: input.cart.id,
},
emitEventStep({
eventName: CartWorkflowEvents.UPDATED,
data: { id: input.cart.id },
})
updateCartPromotionsWorkflow.runAsStep({
input: {
cart_id: input.cart.id,
},
})
refreshPaymentCollectionForCartWorkflow.runAsStep({
input: {
cart_id: input.cart.id,
},
})
return new WorkflowResponse(items)
}
)

View File

@@ -1,8 +1,7 @@
import { deepFlatMap, isPresent, MedusaError } from "@medusajs/framework/utils"
import { deepFlatMap } from "@medusajs/framework/utils"
import {
createWorkflow,
transform,
when,
WorkflowData,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
@@ -16,7 +15,14 @@ export const listShippingOptionsForCartWorkflowId =
*/
export const listShippingOptionsForCartWorkflow = createWorkflow(
listShippingOptionsForCartWorkflowId,
(input: WorkflowData<{ cart_id: string; is_return?: boolean }>) => {
(
input: WorkflowData<{
cart_id: string
option_ids?: string[]
is_return?: boolean
enabled_in_store?: boolean
}>
) => {
const cartQuery = useQueryGraphStep({
entity: "cart",
filters: { id: input.cart_id },
@@ -28,6 +34,8 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
"shipping_address.city",
"shipping_address.country_code",
"shipping_address.province",
"shipping_address.postal_code",
"item_total",
"total",
],
options: { throwIfKeyNotFound: true },
@@ -70,42 +78,31 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
}
)
const customerGroupIds = when(
"get-customer-group",
{ cart },
({ cart }) => {
return !!cart.id
}
).then(() => {
const customerQuery = useQueryGraphStep({
entity: "customer",
filters: { id: cart.customer_id },
fields: ["groups.id"],
}).config({ name: "get-customer" })
const queryVariables = transform(
{ input, fulfillmentSetIds, cart },
({ input, fulfillmentSetIds, cart }) => ({
id: input.option_ids,
return transform({ customerQuery }, ({ customerQuery }) => {
const customer = customerQuery.data[0]
context: {
is_return: input.is_return ?? false,
enabled_in_store: input.enabled_in_store ?? true,
},
if (!isPresent(customer)) {
return []
}
filters: {
fulfillment_set_id: fulfillmentSetIds,
const { groups = [] } = customer
address: {
country_code: cart.shipping_address?.country_code,
province_code: cart.shipping_address?.province,
city: cart.shipping_address?.city,
postal_expression: cart.shipping_address?.postal_code,
},
},
return groups.map((group) => group.id)
})
})
const pricingContext = transform(
{ cart, customerGroupIds },
({ cart, customerGroupIds }) => ({
...cart,
customer_group_id: customerGroupIds,
calculated_price: { context: cart },
})
)
const isReturn = transform({ input }, ({ input }) => !!input.is_return)
const shippingOptions = useRemoteQueryStep({
entry_point: "shipping_options",
fields: [
@@ -116,7 +113,6 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
"shipping_profile_id",
"provider_id",
"data",
"amount",
"type.id",
"type.label",
@@ -132,55 +128,22 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
"calculated_price.*",
],
variables: {
context: {
is_return: isReturn,
enabled_in_store: "true",
},
filters: {
fulfillment_set_id: fulfillmentSetIds,
address: {
city: cart.shipping_address?.city,
country_code: cart.shipping_address?.country_code,
province_code: cart.shipping_address?.province,
},
},
calculated_price: {
context: pricingContext,
},
},
variables: queryVariables,
}).config({ name: "shipping-options-query" })
const shippingOptionsWithPrice = transform({ shippingOptions }, (data) => {
const optionsMissingPrices: string[] = []
const shippingOptionsWithPrice = transform(
{ shippingOptions },
({ shippingOptions }) =>
shippingOptions.map((shippingOption) => {
const price = shippingOption.calculated_price
const options = data.shippingOptions.map((shippingOption) => {
const { calculated_price, ...options } = shippingOption ?? {}
if (options?.id && !isPresent(calculated_price?.calculated_amount)) {
optionsMissingPrices.push(options.id)
}
return {
...options,
amount: calculated_price?.calculated_amount,
is_tax_inclusive:
!!calculated_price?.is_calculated_price_tax_inclusive,
}
})
if (optionsMissingPrices.length) {
const ids = optionsMissingPrices.join(", ")
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Shipping options with IDs ${ids} do not have a price`
)
}
return options
})
return {
...shippingOption,
amount: price?.calculated_amount,
is_tax_inclusive: !!price?.is_calculated_price_tax_inclusive,
}
})
)
return new WorkflowResponse(shippingOptionsWithPrice)
}

View File

@@ -6,13 +6,14 @@ import {
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
import { refreshCartShippingMethodsStep, updateLineItemsStep } from "../steps"
import { updateLineItemsStep } from "../steps"
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
import {
cartFieldsForRefreshSteps,
productVariantsFields,
} from "../utils/fields"
import { prepareLineItemData } from "../utils/prepare-line-item-data"
import { refreshCartShippingMethodsWorkflow } from "./refresh-cart-shipping-methods"
import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection"
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
import { updateTaxLinesWorkflow } from "./update-tax-lines"
@@ -100,7 +101,9 @@ export const refreshCartItemsWorkflow = createWorkflow(
list: false,
}).config({ name: "refetchcart" })
refreshCartShippingMethodsStep({ cart: refetchedCart })
refreshCartShippingMethodsWorkflow.runAsStep({
input: { cart_id: cart.id },
})
updateTaxLinesWorkflow.runAsStep({
input: { cart_id: cart.id },

View File

@@ -0,0 +1,125 @@
import { isDefined, isPresent } from "@medusajs/framework/utils"
import {
createWorkflow,
parallelize,
transform,
when,
WorkflowData,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "../../common"
import { removeShippingMethodFromCartStep } from "../steps"
import { updateShippingMethodsStep } from "../steps/update-shipping-methods"
import { listShippingOptionsForCartWorkflow } from "./list-shipping-options-for-cart"
export const refreshCartShippingMethodsWorkflowId =
"refresh-cart-shipping-methods"
/**
* This workflow refreshes a cart's shipping methods
*/
export const refreshCartShippingMethodsWorkflow = createWorkflow(
refreshCartShippingMethodsWorkflowId,
(input: WorkflowData<{ cart_id: string }>) => {
const cartQuery = useQueryGraphStep({
entity: "cart",
filters: { id: input.cart_id },
fields: [
"id",
"sales_channel_id",
"currency_code",
"region_id",
"shipping_methods.*",
"shipping_address.city",
"shipping_address.country_code",
"shipping_address.province",
"shipping_methods.shipping_option_id",
"total",
],
options: { throwIfKeyNotFound: true },
}).config({ name: "get-cart" })
const cart = transform({ cartQuery }, ({ cartQuery }) => cartQuery.data[0])
const shippingOptionIds: string[] = transform({ cart }, ({ cart }) =>
(cart.shipping_methods || [])
.map((shippingMethod) => shippingMethod.shipping_option_id)
.filter(Boolean)
)
when({ shippingOptionIds }, ({ shippingOptionIds }) => {
return !!shippingOptionIds?.length
}).then(() => {
const shippingOptions = listShippingOptionsForCartWorkflow.runAsStep({
input: {
option_ids: shippingOptionIds,
cart_id: cart.id,
is_return: false,
},
})
// Creates an object on which shipping methods to remove or update depending
// on the validity of the shipping options for the cart
const shippingMethodsData = transform(
{ cart, shippingOptions },
({ cart, shippingOptions }) => {
const { shipping_methods: shippingMethods = [] } = cart
const validShippingMethods = shippingMethods.filter(
(shippingMethod) => {
// Fetch the available shipping options for the cart context and find the one associated
// with the current shipping method
const shippingOption = shippingOptions.find(
(shippingOption) =>
shippingOption.id === shippingMethod.shipping_option_id
)
const shippingOptionPrice =
shippingOption?.calculated_price?.calculated_amount
// The shipping method is only valid if both the shipping option and the price is found
// for the context of the cart. The invalid options will lead to a deleted shipping method
if (isPresent(shippingOption) && isDefined(shippingOptionPrice)) {
return true
}
return false
}
)
const shippingMethodIds = shippingMethods.map((sm) => sm.id)
const validShippingMethodIds = validShippingMethods.map((sm) => sm.id)
const invalidShippingMethodIds = shippingMethodIds.filter(
(id) => !validShippingMethodIds.includes(id)
)
const shippingMethodsToUpdate = validShippingMethods.map(
(shippingMethod) => {
const shippingOption = shippingOptions.find(
(s) => s.id === shippingMethod.shipping_option_id
)!
return {
id: shippingMethod.id,
shipping_option_id: shippingOption.id,
amount: shippingOption.calculated_price.calculated_amount,
is_tax_inclusive:
shippingOption.calculated_price
.is_calculated_price_tax_inclusive,
}
}
)
return {
shippingMethodsToRemove: invalidShippingMethodIds,
shippingMethodsToUpdate,
}
}
)
parallelize(
removeShippingMethodFromCartStep({
shipping_method_ids: shippingMethodsData.shippingMethodsToRemove,
}),
updateShippingMethodsStep(shippingMethodsData.shippingMethodsToUpdate)
)
})
}
)

View File

@@ -1,28 +1,16 @@
import { UpdateLineItemInCartWorkflowInputDTO } from "@medusajs/framework/types"
import { CartWorkflowEvents } from "@medusajs/framework/utils"
import {
WorkflowData,
WorkflowResponse,
createWorkflow,
parallelize,
transform,
} from "@medusajs/framework/workflows-sdk"
import { emitEventStep } from "../../common/steps/emit-event"
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
import { updateLineItemsStepWithSelector } from "../../line-item/steps"
import { refreshCartShippingMethodsStep } from "../steps"
import { validateCartStep } from "../steps/validate-cart"
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
import {
cartFieldsForRefreshSteps,
productVariantsFields,
} from "../utils/fields"
import { productVariantsFields } from "../utils/fields"
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection"
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
// TODO: The UpdateLineItemsWorkflow are missing the following steps:
// - Validate shipping methods for new items (fulfillment module)
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
export const updateLineItemInCartWorkflowId = "update-line-item-in-cart"
/**
@@ -89,35 +77,10 @@ export const updateLineItemInCartWorkflow = createWorkflow(
}
})
const result = updateLineItemsStepWithSelector(lineItemUpdate)
updateLineItemsStepWithSelector(lineItemUpdate)
const cart = useRemoteQueryStep({
entry_point: "cart",
fields: cartFieldsForRefreshSteps,
variables: { id: input.cart.id },
list: false,
}).config({ name: "refetchcart" })
refreshCartShippingMethodsStep({ cart })
updateCartPromotionsWorkflow.runAsStep({
input: {
cart_id: input.cart.id,
},
refreshCartItemsWorkflow.runAsStep({
input: { cart_id: input.cart.id },
})
parallelize(
refreshPaymentCollectionForCartWorkflow.runAsStep({
input: { cart_id: input.cart.id },
}),
emitEventStep({
eventName: CartWorkflowEvents.UPDATED,
data: { id: input.cart.id },
})
)
const updatedItem = transform({ result }, (data) => data.result?.[0])
return new WorkflowResponse(updatedItem)
}
)

View File

@@ -767,6 +767,11 @@ export interface UpdateShippingMethodDTO {
*/
amount?: BigNumberInput
/**
* The tax inclusivity setting of the shipping method.
*/
is_tax_inclusive?: boolean
/**
* The data of the shipping method.
*/

View File

@@ -38,6 +38,7 @@ import {
UpdateLineItemTaxLineDTO,
UpdateLineItemWithSelectorDTO,
UpdateShippingMethodAdjustmentDTO,
UpdateShippingMethodDTO,
UpdateShippingMethodTaxLineDTO,
UpsertLineItemAdjustmentDTO,
} from "./mutations"
@@ -822,6 +823,46 @@ export interface ICartModuleService extends IModuleService {
sharedContext?: Context
): Promise<CartShippingMethodDTO[]>
/**
* This method updates existing shipping methods.
*
* @param {UpdateShippingMethodDTO[]} data - A list of shipping methods to update
* @returns {Promise<CartShippingMethodDTO[]>} The updated shipping methods.
*
* @example
* const shippingMethods = await cartModuleService.updateShippingMethods([
* {
* id: "casm_123",
* amount: 2,
* },
* ])
*/
updateShippingMethods(
data: UpdateShippingMethodDTO[]
): Promise<CartShippingMethodDTO[]>
/**
* This method updates an existing shipping method.
*
* @param {string} shippingMethodId - The shipping methods's ID.
* @param {Partial<UpdateShippingMethodDTO>} data - The attributes to update in the shipping method.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<CartShippingMethodDTO>} The updated shipping method.
*
* @example
* const lineItem = await cartModuleService.updateShippingMethods(
* "casm_123",
* {
* amount: 3000,
* }
* )
*/
updateShippingMethods(
shippingMethodId: string,
data: Partial<UpdateShippingMethodDTO>,
sharedContext?: Context
): Promise<CartShippingMethodDTO>
/**
* This method retrieves a paginated list of line item adjustments based on optional filters and configuration.
*

View File

@@ -86,7 +86,7 @@ export const AdminCreateShippingOptionTypeObject = z
const AdminPriceRules = z.array(
z.object({
attribute: z.literal("total"),
attribute: z.literal("item_total"),
operator: z.nativeEnum(PricingRuleOperator),
value: z.number(),
})

View File

@@ -8,11 +8,11 @@ import {
GeoZoneType,
Modules,
} from "@medusajs/framework/utils"
import { FulfillmentProviderService } from "@services"
import {
MockEventBusService,
moduleIntegrationTestRunner,
} from "@medusajs/test-utils"
import { FulfillmentProviderService } from "@services"
import { resolve } from "path"
import {
buildExpectedEventMessageShape,