diff --git a/.changeset/flat-meals-rhyme.md b/.changeset/flat-meals-rhyme.md new file mode 100644 index 0000000000..6127fc05d0 --- /dev/null +++ b/.changeset/flat-meals-rhyme.md @@ -0,0 +1,7 @@ +--- +"@medusajs/core-flows": patch +"@medusajs/workflows-sdk": patch +"integration-tests-modules": patch +--- + +feat: Set pricing context hook diff --git a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts index 5bcd57c8d8..0de6010690 100644 --- a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts @@ -47,6 +47,7 @@ import { } from "../../../../helpers/create-admin-user" import { seedStorefrontDefaults } from "../../../../helpers/seed-storefront-defaults" import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer" +import { StepResponse } from "@medusajs/framework/workflows-sdk" jest.setTimeout(200000) @@ -72,6 +73,7 @@ medusaIntegrationTestRunner({ let salesChannel let defaultRegion let customer, storeHeadersWithCustomer + let setPricingContextHook: any beforeAll(async () => { appContainer = getContainer() @@ -90,6 +92,31 @@ medusaIntegrationTestRunner({ ContainerRegistrationKeys.REMOTE_QUERY ) query = appContainer.resolve(ContainerRegistrationKeys.QUERY) + + createCartWorkflow.hooks.setPricingContext( + (input) => { + if (setPricingContextHook) { + return setPricingContextHook(input) + } + }, + () => {} + ) + addToCartWorkflow.hooks.setPricingContext( + (input) => { + if (setPricingContextHook) { + return setPricingContextHook(input) + } + }, + () => {} + ) + listShippingOptionsForCartWorkflow.hooks.setPricingContext( + (input) => { + if (setPricingContextHook) { + return setPricingContextHook(input) + } + }, + () => {} + ) }) beforeEach(async () => { @@ -627,6 +654,476 @@ medusaIntegrationTestRunner({ ]) }) + describe("setPricingContext hook", () => { + it("should use context provided by the hook", async () => { + const region = await regionModuleService.createRegions({ + name: "US", + currency_code: "usd", + }) + + const salesChannel = await scModuleService.createSalesChannels({ + name: "Webshop", + }) + + const location = await stockLocationModule.createStockLocations({ + name: "Warehouse", + }) + + const [product] = await productModule.createProducts([ + { + title: "Test product", + variants: [ + { + title: "Test variant", + }, + ], + }, + ]) + + const inventoryItem = await inventoryModule.createInventoryItems({ + sku: "inv-1234", + }) + + await inventoryModule.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + stocked_quantity: 2, + reserved_quantity: 0, + }, + ]) + + const priceSet = await pricingModule.createPriceSets({ + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + }) + + await pricingModule.createPricePreferences({ + attribute: "currency_code", + value: "usd", + is_tax_inclusive: true, + }) + + await remoteLink.create([ + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.PRICING]: { + price_set_id: priceSet.id, + }, + }, + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: salesChannel.id, + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + }, + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, + ]) + + setPricingContextHook = function (input) { + expect(input.region).toEqual( + expect.objectContaining({ + id: region.id, + }) + ) + expect(input.variantIds).toEqual([product.variants[0].id]) + expect(input.salesChannel).toEqual( + expect.objectContaining({ + id: salesChannel.id, + }) + ) + expect(input.customerData).toEqual( + expect.objectContaining({ + email: "tony@stark.com", + }) + ) + + return new StepResponse({ + unit_price: 100, + }) + } + + /** + * Tried jest, but for some reasons it is not able to provide + * correct arguments passed to the function + */ + let pricingContext: any + const originalFn = pricingModule.listPriceSets.bind(pricingModule) + pricingModule.listPriceSets = function () { + pricingContext = { ...arguments[0].context } + return originalFn.bind(pricingModule)(...arguments) + } + + const { result } = await createCartWorkflow(appContainer).run({ + input: { + email: "tony@stark.com", + currency_code: "usd", + region_id: region.id, + sales_channel_id: salesChannel.id, + items: [ + { + variant_id: product.variants[0].id, + quantity: 1, + }, + ], + }, + }) + + setPricingContextHook = undefined + pricingModule.listPriceSets = originalFn + + expect(pricingContext).toEqual( + expect.objectContaining({ + unit_price: 100, + region_id: region.id, + currency_code: "usd", + }) + ) + + const cart = await cartModuleService.retrieveCart(result.id, { + relations: ["items"], + }) + + expect(cart).toEqual( + expect.objectContaining({ + currency_code: "usd", + email: "tony@stark.com", + region_id: region.id, + sales_channel_id: salesChannel.id, + customer_id: expect.any(String), + items: expect.arrayContaining([ + expect.objectContaining({ + quantity: 1, + unit_price: 3000, + is_tax_inclusive: true, + }), + ]), + }) + ) + }) + + it("should not be able to override the 'customer_id', 'region_id', and the 'currency_code' from the setPricingContext hook", async () => { + const region = await regionModuleService.createRegions({ + name: "US", + currency_code: "usd", + }) + + const salesChannel = await scModuleService.createSalesChannels({ + name: "Webshop", + }) + + const location = await stockLocationModule.createStockLocations({ + name: "Warehouse", + }) + + const [product] = await productModule.createProducts([ + { + title: "Test product", + variants: [ + { + title: "Test variant", + }, + ], + }, + ]) + + const inventoryItem = await inventoryModule.createInventoryItems({ + sku: "inv-1234", + }) + + await inventoryModule.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + stocked_quantity: 2, + reserved_quantity: 0, + }, + ]) + + const priceSet = await pricingModule.createPriceSets({ + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + }) + + await pricingModule.createPricePreferences({ + attribute: "currency_code", + value: "usd", + is_tax_inclusive: true, + }) + + await remoteLink.create([ + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.PRICING]: { + price_set_id: priceSet.id, + }, + }, + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: salesChannel.id, + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + }, + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, + ]) + + setPricingContextHook = function (input) { + expect(input.region).toEqual( + expect.objectContaining({ + id: region.id, + }) + ) + expect(input.variantIds).toEqual([product.variants[0].id]) + expect(input.salesChannel).toEqual( + expect.objectContaining({ + id: salesChannel.id, + }) + ) + expect(input.customerData).toEqual( + expect.objectContaining({ + email: "tony@stark.com", + }) + ) + + return new StepResponse({ + unit_price: 200, + currency_code: "inr", + customer_id: "1", + region_id: "1", + }) + } + + /** + * Tried jest, but for some reasons it is not able to provide + * correct arguments passed to the function + */ + let pricingContext: any + const originalFn = pricingModule.listPriceSets.bind(pricingModule) + pricingModule.listPriceSets = function () { + pricingContext = { ...arguments[0].context } + return originalFn.bind(pricingModule)(...arguments) + } + + const { result } = await createCartWorkflow(appContainer).run({ + input: { + email: "tony@stark.com", + currency_code: "usd", + region_id: region.id, + sales_channel_id: salesChannel.id, + items: [ + { + variant_id: product.variants[0].id, + quantity: 1, + }, + ], + }, + }) + + setPricingContextHook = undefined + pricingModule.listPriceSets = originalFn + + expect(pricingContext).toEqual( + expect.objectContaining({ + unit_price: 200, + region_id: region.id, + currency_code: "usd", + }) + ) + expect(pricingContext.customer_id).toBeDefined() + expect(pricingContext.customer_id).not.toEqual("1") + + const cart = await cartModuleService.retrieveCart(result.id, { + relations: ["items"], + }) + + expect(cart).toEqual( + expect.objectContaining({ + currency_code: "usd", + email: "tony@stark.com", + region_id: region.id, + sales_channel_id: salesChannel.id, + customer_id: expect.any(String), + items: expect.arrayContaining([ + expect.objectContaining({ + quantity: 1, + unit_price: 3000, + is_tax_inclusive: true, + }), + ]), + }) + ) + }) + + it("should disallow non object response from the setPricingContext hook", async () => { + const region = await regionModuleService.createRegions({ + name: "US", + currency_code: "usd", + }) + + const salesChannel = await scModuleService.createSalesChannels({ + name: "Webshop", + }) + + const location = await stockLocationModule.createStockLocations({ + name: "Warehouse", + }) + + const [product] = await productModule.createProducts([ + { + title: "Test product", + variants: [ + { + title: "Test variant", + }, + ], + }, + ]) + + const inventoryItem = await inventoryModule.createInventoryItems({ + sku: "inv-1234", + }) + + await inventoryModule.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + stocked_quantity: 2, + reserved_quantity: 0, + }, + ]) + + const priceSet = await pricingModule.createPriceSets({ + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + }) + + await pricingModule.createPricePreferences({ + attribute: "currency_code", + value: "usd", + is_tax_inclusive: true, + }) + + await remoteLink.create([ + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.PRICING]: { + price_set_id: priceSet.id, + }, + }, + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: salesChannel.id, + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + }, + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, + ]) + + setPricingContextHook = function (input) { + expect(input.region).toEqual( + expect.objectContaining({ + id: region.id, + }) + ) + expect(input.variantIds).toEqual([product.variants[0].id]) + expect(input.salesChannel).toEqual( + expect.objectContaining({ + id: salesChannel.id, + }) + ) + expect(input.customerData).toEqual( + expect.objectContaining({ + email: "tony@stark.com", + }) + ) + + return new StepResponse([1]) + } + + const { errors } = await createCartWorkflow(appContainer).run({ + throwOnError: false, + input: { + email: "tony@stark.com", + currency_code: "usd", + region_id: region.id, + sales_channel_id: salesChannel.id, + items: [ + { + variant_id: product.variants[0].id, + quantity: 1, + }, + ], + }, + }) + + setPricingContextHook = undefined + + expect(errors).toHaveLength(1) + expect(errors[0]).toEqual( + expect.objectContaining({ + action: "get-setPricingContext-result", + handlerType: "invoke", + error: expect.objectContaining({ + issues: [ + { + code: "invalid_type", + expected: "object", + message: "Expected object, received array", + path: [], + received: "array", + }, + ], + }), + }) + ) + }) + }) + describe("compensation", () => { it("should delete created customer if cart-creation fails", async () => { expect.assertions(2) @@ -1685,6 +2182,309 @@ medusaIntegrationTestRunner({ }, ]) }) + + describe("setPricingContext hook", () => { + it("should use context provided by the hook", async () => { + const salesChannel = await scModuleService.createSalesChannels({ + name: "Webshop", + }) + + const location = await stockLocationModule.createStockLocations({ + name: "Warehouse", + }) + + let cart = await cartModuleService.createCarts({ + currency_code: "usd", + sales_channel_id: salesChannel.id, + }) + + const [product] = await productModule.createProducts([ + { + title: "Test product", + variants: [ + { + title: "Test variant", + }, + ], + }, + ]) + + const inventoryItem = await inventoryModule.createInventoryItems({ + sku: "inv-1234", + }) + + await inventoryModule.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + stocked_quantity: 2, + reserved_quantity: 0, + }, + ]) + + const priceSet = await pricingModule.createPriceSets({ + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + }) + + await pricingModule.createPricePreferences({ + attribute: "currency_code", + value: "usd", + is_tax_inclusive: true, + }) + + await remoteLink.create([ + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.PRICING]: { + price_set_id: priceSet.id, + }, + }, + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: salesChannel.id, + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + }, + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, + ]) + + cart = await cartModuleService.retrieveCart(cart.id, { + select: ["id", "region_id", "currency_code", "sales_channel_id"], + }) + + setPricingContextHook = function (input) { + expect(input.cart).toEqual(expect.objectContaining(cart)) + expect(input.variantIds).toEqual([product.variants[0].id]) + return new StepResponse({ + unit_price: 100, + }) + } + + /** + * Tried jest, but for some reasons it is not able to provide + * correct arguments passed to the function + */ + let pricingContext: any + const originalFn = pricingModule.listPriceSets.bind(pricingModule) + pricingModule.listPriceSets = function () { + pricingContext = { ...arguments[0].context } + return originalFn.bind(pricingModule)(...arguments) + } + + await addToCartWorkflow(appContainer).run({ + input: { + items: [ + { + variant_id: product.variants[0].id, + quantity: 1, + }, + ], + cart_id: cart.id, + }, + }) + + setPricingContextHook = undefined + pricingModule.listPriceSets = originalFn + + expect(pricingContext).toEqual( + expect.objectContaining({ + unit_price: 100, + currency_code: "usd", + }) + ) + + cart = await cartModuleService.retrieveCart(cart.id, { + relations: ["items"], + }) + + expect(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", + }), + ]), + }) + ) + }) + + it("should not be able to override the 'customer_id', 'region_id', and the 'currency_code' from the setPricingContext hook", async () => { + const salesChannel = await scModuleService.createSalesChannels({ + name: "Webshop", + }) + + const location = await stockLocationModule.createStockLocations({ + name: "Warehouse", + }) + + let cart = await cartModuleService.createCarts({ + currency_code: "usd", + sales_channel_id: salesChannel.id, + }) + + const [product] = await productModule.createProducts([ + { + title: "Test product", + variants: [ + { + title: "Test variant", + }, + ], + }, + ]) + + const inventoryItem = await inventoryModule.createInventoryItems({ + sku: "inv-1234", + }) + + await inventoryModule.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + stocked_quantity: 2, + reserved_quantity: 0, + }, + ]) + + const priceSet = await pricingModule.createPriceSets({ + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + }) + + await pricingModule.createPricePreferences({ + attribute: "currency_code", + value: "usd", + is_tax_inclusive: true, + }) + + await remoteLink.create([ + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.PRICING]: { + price_set_id: priceSet.id, + }, + }, + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: salesChannel.id, + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + }, + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, + ]) + + cart = await cartModuleService.retrieveCart(cart.id, { + select: [ + "id", + "region_id", + "currency_code", + "sales_channel_id", + "customer_id", + ], + }) + + setPricingContextHook = function (input) { + expect(input.cart).toEqual(expect.objectContaining(cart)) + expect(input.variantIds).toEqual([product.variants[0].id]) + return new StepResponse({ + unit_price: 200, + currency_code: "inr", + customer_id: "1", + region_id: "1", + }) + } + + /** + * Tried jest, but for some reasons it is not able to provide + * correct arguments passed to the function + */ + let pricingContext: any + const originalFn = pricingModule.listPriceSets.bind(pricingModule) + pricingModule.listPriceSets = function () { + pricingContext = { ...arguments[0].context } + return originalFn.bind(pricingModule)(...arguments) + } + + await addToCartWorkflow(appContainer).run({ + input: { + items: [ + { + variant_id: product.variants[0].id, + quantity: 1, + }, + ], + cart_id: cart.id, + }, + }) + + setPricingContextHook = undefined + pricingModule.listPriceSets = originalFn + + expect(pricingContext).toEqual( + expect.objectContaining({ + unit_price: 200, + region_id: cart.region_id, + customer_id: cart.customer_id, + currency_code: "usd", + }) + ) + + cart = await cartModuleService.retrieveCart(cart.id, { + relations: ["items"], + }) + + expect(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", + }), + ]), + }) + ) + }) + }) }) describe("updateLineItemInCartWorkflow", () => { @@ -3493,6 +4293,174 @@ medusaIntegrationTestRunner({ expect(result).toEqual([]) }) + + describe("setPricingContext hook", () => { + it("should use context provided by the hook", async () => { + const shippingOption = ( + await api.post( + `/admin/shipping-options`, + { + 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", + }, + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + rules: [ + { + operator: RuleOperator.EQ, + attribute: "is_return", + value: "false", + }, + { + operator: RuleOperator.EQ, + attribute: "enabled_in_store", + value: "true", + }, + ], + }, + adminHeaders + ) + ).data.shipping_option + + cart = (await api.get(`/store/carts/${cart.id}`, storeHeaders)).data + .cart + + setPricingContextHook = function (input) { + expect(input.cart.id).toEqual(cart.id) + return new StepResponse({ + unit_price: 100, + }) + } + + /** + * Tried jest, but for some reasons it is not able to provide + * correct arguments passed to the function + */ + let pricingContext: any + const originalFn = pricingModule.listPriceSets.bind(pricingModule) + pricingModule.listPriceSets = function () { + pricingContext = { ...arguments[0].context } + return originalFn.bind(pricingModule)(...arguments) + } + + const { result } = await listShippingOptionsForCartWorkflow( + appContainer + ).run({ input: { cart_id: cart.id } }) + + setPricingContextHook = undefined + pricingModule.listPriceSets = originalFn + + expect(pricingContext).toEqual( + expect.objectContaining({ + unit_price: 100, + }) + ) + + expect(result).toEqual([ + expect.objectContaining({ + amount: 3000, + id: shippingOption.id, + }), + ]) + }) + + it("should not be able to override the 'customer_id', 'region_id', and the 'currency_code' from the setPricingContext hook", async () => { + const shippingOption = ( + await api.post( + `/admin/shipping-options`, + { + 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", + }, + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + rules: [ + { + operator: RuleOperator.EQ, + attribute: "is_return", + value: "false", + }, + { + operator: RuleOperator.EQ, + attribute: "enabled_in_store", + value: "true", + }, + ], + }, + adminHeaders + ) + ).data.shipping_option + + cart = (await api.get(`/store/carts/${cart.id}`, storeHeaders)).data + .cart + + setPricingContextHook = function (input) { + expect(input.cart.id).toEqual(cart.id) + return new StepResponse({ + unit_price: 200, + currency_code: "inr", + customer_id: "1", + region_id: "1", + }) + } + + /** + * Tried jest, but for some reasons it is not able to provide + * correct arguments passed to the function + */ + let pricingContext: any + const originalFn = pricingModule.listPriceSets.bind(pricingModule) + pricingModule.listPriceSets = function () { + pricingContext = { ...arguments[0].context } + return originalFn.bind(pricingModule)(...arguments) + } + + const { result } = await listShippingOptionsForCartWorkflow( + appContainer + ).run({ input: { cart_id: cart.id } }) + + setPricingContextHook = undefined + pricingModule.listPriceSets = originalFn + + expect(pricingContext).toEqual( + expect.objectContaining({ + unit_price: 200, + region_id: cart.region_id, + customer_id: cart.customer_id, + currency_code: "usd", + }) + ) + + expect(result).toEqual([ + expect.objectContaining({ + amount: 3000, + id: shippingOption.id, + }), + ]) + }) + }) }) describe("updateTaxLinesWorkflow", () => { diff --git a/packages/core/core-flows/src/cart/utils/schemas.ts b/packages/core/core-flows/src/cart/utils/schemas.ts new file mode 100644 index 0000000000..053b8f216b --- /dev/null +++ b/packages/core/core-flows/src/cart/utils/schemas.ts @@ -0,0 +1,2 @@ +import z from "zod" +export const pricingContextResult = z.record(z.string(), z.any()).optional() diff --git a/packages/core/core-flows/src/cart/workflows/add-to-cart.ts b/packages/core/core-flows/src/cart/workflows/add-to-cart.ts index 554856dfe3..190a963f94 100644 --- a/packages/core/core-flows/src/cart/workflows/add-to-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/add-to-cart.ts @@ -1,4 +1,5 @@ import { + AdditionalData, AddToCartWorkflowInputDTO, ConfirmVariantInventoryWorkflowInputDTO, } from "@medusajs/framework/types" @@ -33,6 +34,7 @@ import { } from "../utils/prepare-line-item-data" import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory" import { refreshCartItemsWorkflow } from "./refresh-cart-items" +import { pricingContextResult } from "../utils/schemas" const cartFields = ["completed_at"].concat(cartFieldsForPricingContext) @@ -71,7 +73,7 @@ export const addToCartWorkflowId = "add-to-cart" */ export const addToCartWorkflow = createWorkflow( addToCartWorkflowId, - (input: WorkflowData) => { + (input: WorkflowData) => { const cartQuery = useQueryGraphStep({ entity: "cart", filters: { id: input.cart_id }, @@ -93,6 +95,35 @@ export const addToCartWorkflow = createWorkflow( return (data.input.items ?? []).map((i) => i.variant_id).filter(Boolean) }) + const setPricingContext = createHook( + "setPricingContext", + { + cart, + variantIds, + items: input.items, + additional_data: input.additional_data, + }, + { + resultValidator: pricingContextResult, + } + ) + + const setPricingContextResult = setPricingContext.getResult() + const pricingContext = transform( + { cart, setPricingContextResult }, + (data) => { + return { + ...data.cart, + ...(data.setPricingContextResult ? data.setPricingContextResult : {}), + currency_code: data.cart.currency_code, + region_id: data.cart.region_id, + region: data.cart.region, + customer_id: data.cart.customer_id, + customer: data.cart.customer, + } + } + ) + const variants = when({ variantIds }, ({ variantIds }) => { return !!variantIds.length }).then(() => { @@ -102,7 +133,7 @@ export const addToCartWorkflow = createWorkflow( variables: { id: variantIds, calculated_price: { - context: cart, + context: pricingContext, }, }, }) @@ -201,7 +232,7 @@ export const addToCartWorkflow = createWorkflow( }) return new WorkflowResponse(void 0, { - hooks: [validate], + hooks: [validate, setPricingContext] as const, }) } ) diff --git a/packages/core/core-flows/src/cart/workflows/create-carts.ts b/packages/core/core-flows/src/cart/workflows/create-carts.ts index e27ae38ca1..6e68f3b1b5 100644 --- a/packages/core/core-flows/src/cart/workflows/create-carts.ts +++ b/packages/core/core-flows/src/cart/workflows/create-carts.ts @@ -36,6 +36,7 @@ import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-colle import { updateCartPromotionsWorkflow } from "./update-cart-promotions" import { updateTaxLinesWorkflow } from "./update-tax-lines" import { validateSalesChannelStep } from "../steps/validate-sales-channel" +import { pricingContextResult } from "../utils/schemas" /** * The data to create the cart, along with custom data that's passed to the workflow's hooks. @@ -98,16 +99,31 @@ export const createCartWorkflow = createWorkflow( ) validateSalesChannelStep({ salesChannel }) + const setPricingContext = createHook( + "setPricingContext", + { + region, + variantIds, + salesChannel, + customerData, + additional_data: input.additional_data, + }, + { + resultValidator: pricingContextResult, + } + ) + const setPricingContextResult = setPricingContext.getResult() // TODO: This is on par with the context used in v1.*, but we can be more flexible. const pricingContext = transform( - { input, region, customerData }, + { input, region, customerData, setPricingContextResult }, (data) => { if (!data.region) { throw new MedusaError(MedusaError.Types.NOT_FOUND, "No regions found") } return { + ...(data.setPricingContextResult ? data.setPricingContextResult : {}), currency_code: data.input.currency_code ?? data.region.currency_code, region_id: data.region.id, customer_id: data.customerData.customer?.id, @@ -248,7 +264,7 @@ export const createCartWorkflow = createWorkflow( }) return new WorkflowResponse(cart, { - hooks: [validate, cartCreated], + hooks: [validate, cartCreated, setPricingContext] as const, }) } ) diff --git a/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts b/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts index c181a6aaa5..abdbc339d2 100644 --- a/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts @@ -1,4 +1,5 @@ import { + createHook, createWorkflow, transform, WorkflowData, @@ -7,8 +8,12 @@ import { import { useQueryGraphStep, validatePresenceOfStep } from "../../common" import { useRemoteQueryStep } from "../../common/steps/use-remote-query" import { cartFieldsForPricingContext } from "../utils/fields" -import { ListShippingOptionsForCartWorkflowInput } from "@medusajs/types" +import { + AdditionalData, + ListShippingOptionsForCartWorkflowInput, +} from "@medusajs/types" import { isDefined } from "@medusajs/framework/utils" +import { pricingContextResult } from "../utils/schemas" export const listShippingOptionsForCartWorkflowId = "list-shipping-options-for-cart" @@ -41,7 +46,11 @@ export const listShippingOptionsForCartWorkflowId = */ export const listShippingOptionsForCartWorkflow = createWorkflow( listShippingOptionsForCartWorkflowId, - (input: WorkflowData) => { + ( + input: WorkflowData< + ListShippingOptionsForCartWorkflowInput & AdditionalData + > + ) => { const cartQuery = useQueryGraphStep({ entity: "cart", filters: { id: input.cart_id }, @@ -96,9 +105,22 @@ export const listShippingOptionsForCartWorkflow = createWorkflow( } ) + const setPricingContext = createHook( + "setPricingContext", + { + cart: cart, + fulfillmentSetIds, + additional_data: input.additional_data, + }, + { + resultValidator: pricingContextResult, + } + ) + const setPricingContextResult = setPricingContext.getResult() + const queryVariables = transform( - { input, fulfillmentSetIds, cart }, - ({ input, fulfillmentSetIds, cart }) => { + { input, fulfillmentSetIds, cart, setPricingContextResult }, + ({ input, fulfillmentSetIds, cart, setPricingContextResult }) => { return { id: input.option_ids, @@ -122,7 +144,17 @@ export const listShippingOptionsForCartWorkflow = createWorkflow( }, }, - calculated_price: { context: cart }, + calculated_price: { + context: { + ...cart, + ...(setPricingContextResult ? setPricingContextResult : {}), + currency_code: cart.currency_code, + region_id: cart.region_id, + region: cart.region, + customer_id: cart.customer_id, + customer: cart.customer, + }, + }, } } ) @@ -202,6 +234,8 @@ export const listShippingOptionsForCartWorkflow = createWorkflow( }) ) - return new WorkflowResponse(shippingOptionsWithPrice) + return new WorkflowResponse(shippingOptionsWithPrice, { + hooks: [setPricingContext] as const, + }) } ) diff --git a/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts b/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts index 53596a1174..0977daa6d8 100644 --- a/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts +++ b/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts @@ -28,6 +28,8 @@ import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-colle import { updateCartPromotionsWorkflow } from "./update-cart-promotions" import { updateTaxLinesWorkflow } from "./update-tax-lines" import { upsertTaxLinesWorkflow } from "./upsert-tax-lines" +import { AdditionalData } from "@medusajs/types" +import { pricingContextResult } from "../utils/schemas" /** * The details of the cart to refresh. @@ -91,7 +93,20 @@ export const refreshCartItemsWorkflowId = "refresh-cart-items" */ export const refreshCartItemsWorkflow = createWorkflow( refreshCartItemsWorkflowId, - (input: WorkflowData) => { + (input: WorkflowData) => { + const setPricingContext = createHook( + "setPricingContext", + { + cart_id: input.cart_id, + items: input.items, + additional_data: input.additional_data, + }, + { + resultValidator: pricingContextResult, + } + ) + const setPricingContextResult = setPricingContext.getResult() + when({ input }, ({ input }) => { return !!input.force_refresh }).then(() => { @@ -106,9 +121,22 @@ export const refreshCartItemsWorkflow = createWorkflow( return (data.cart.items ?? []).map((i) => i.variant_id).filter(Boolean) }) - const cartPricingContext = transform({ cart }, ({ cart }) => { - return filterObjectByKeys(cart, cartFieldsForPricingContext) - }) + const cartPricingContext = transform( + { cart, setPricingContextResult }, + (data) => { + return { + ...filterObjectByKeys(data.cart, cartFieldsForPricingContext), + ...(data.setPricingContextResult + ? data.setPricingContextResult + : {}), + currency_code: data.cart.currency_code, + region_id: data.cart.region_id, + region: data.cart.region, + customer_id: data.cart.customer_id, + customer: data.cart.customer, + } + } + ) const variants = useRemoteQueryStep({ entry_point: "variants", @@ -235,6 +263,8 @@ export const refreshCartItemsWorkflow = createWorkflow( input: { cart_id: input.cart_id }, }) - return new WorkflowResponse(refetchedCart) + return new WorkflowResponse(refetchedCart, { + hooks: [setPricingContext] as const, + }) } ) diff --git a/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts b/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts index 97631f0c45..af1d4889df 100644 --- a/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts @@ -1,4 +1,7 @@ -import { UpdateLineItemInCartWorkflowInputDTO } from "@medusajs/framework/types" +import { + AdditionalData, + UpdateLineItemInCartWorkflowInputDTO, +} from "@medusajs/framework/types" import { isDefined, MedusaError } from "@medusajs/framework/utils" import { createHook, @@ -19,6 +22,7 @@ import { } from "../utils/fields" import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory" import { refreshCartItemsWorkflow } from "./refresh-cart-items" +import { pricingContextResult } from "../utils/schemas" const cartFields = cartFieldsForPricingContext.concat(["items.*"]) @@ -26,9 +30,9 @@ export const updateLineItemInCartWorkflowId = "update-line-item-in-cart" /** * This workflow updates a line item's details in a cart. You can update the line item's quantity, unit price, and more. This workflow is executed * by the [Update Line Item Store API Route](https://docs.medusajs.com/api/store#carts_postcartsidlineitemsline_id). - * + * * You can use this workflow within your own customizations or custom workflows, allowing you to update a line item's details in your custom flows. - * + * * @example * const { result } = await updateLineItemInCartWorkflow(container) * .run({ @@ -40,16 +44,18 @@ export const updateLineItemInCartWorkflowId = "update-line-item-in-cart" * } * } * }) - * + * * @summary - * + * * Update a cart's line item. * * @property hooks.validate - This hook is executed before all operations. You can consume this hook to perform any custom validation. If validation fails, you can throw an error to stop the workflow execution. */ export const updateLineItemInCartWorkflow = createWorkflow( updateLineItemInCartWorkflowId, - (input: WorkflowData) => { + ( + input: WorkflowData + ) => { const cartQuery = useQueryGraphStep({ entity: "cart", filters: { id: input.cart_id }, @@ -73,6 +79,35 @@ export const updateLineItemInCartWorkflow = createWorkflow( return [item.variant_id].filter(Boolean) }) + const setPricingContext = createHook( + "setPricingContext", + { + cart, + item, + variantIds, + additional_data: input.additional_data, + }, + { + resultValidator: pricingContextResult, + } + ) + + const setPricingContextResult = setPricingContext.getResult() + const pricingContext = transform( + { cart, setPricingContextResult }, + (data) => { + return { + ...data.cart, + ...(data.setPricingContextResult ? data.setPricingContextResult : {}), + currency_code: data.cart.currency_code, + region_id: data.cart.region_id, + region: data.cart.region, + customer_id: data.cart.customer_id, + customer: data.cart.customer, + } + } + ) + const variants = when({ variantIds }, ({ variantIds }) => { return !!variantIds.length }).then(() => { @@ -82,7 +117,7 @@ export const updateLineItemInCartWorkflow = createWorkflow( variables: { id: variantIds, calculated_price: { - context: cart, + context: pricingContext, }, }, }).config({ name: "fetch-variants" }) @@ -98,7 +133,7 @@ export const updateLineItemInCartWorkflow = createWorkflow( confirmVariantInventoryWorkflow.runAsStep({ input: { - sales_channel_id: cart.sales_channel_id, + sales_channel_id: pricingContext.sales_channel_id, variants, items, }, @@ -147,7 +182,7 @@ export const updateLineItemInCartWorkflow = createWorkflow( }) return new WorkflowResponse(void 0, { - hooks: [validate], + hooks: [validate, setPricingContext] as const, }) } ) diff --git a/packages/core/core-flows/src/order/utils/fetch-shipping-option.ts b/packages/core/core-flows/src/order/utils/fetch-shipping-option.ts index 199f228735..c41a16cd71 100644 --- a/packages/core/core-flows/src/order/utils/fetch-shipping-option.ts +++ b/packages/core/core-flows/src/order/utils/fetch-shipping-option.ts @@ -1,4 +1,5 @@ import { + AdditionalData, BigNumberInput, CalculatedRMAShippingContext, CalculateShippingOptionPriceDTO, @@ -6,6 +7,7 @@ import { } from "@medusajs/framework/types" import { WorkflowResponse, + createHook, createWorkflow, transform, when, @@ -13,6 +15,7 @@ import { import { BigNumber, ShippingOptionPriceType } from "@medusajs/framework/utils" import { calculateShippingOptionsPricesStep } from "../../fulfillment/steps" import { useRemoteQueryStep } from "../../common" +import { pricingContextResult } from "../../cart/utils/schemas" const COMMON_OPTIONS_FIELDS = [ "id", @@ -90,9 +93,7 @@ export const createOrderEditShippingMethodWorkflowId = "fetch-shipping-option" */ export const fetchShippingOptionForOrderWorkflow = createWorkflow( createOrderEditShippingMethodWorkflowId, - function ( - input: FetchShippingOptionForOrderWorkflowInput - ): WorkflowResponse { + function (input: FetchShippingOptionForOrderWorkflowInput & AdditionalData) { const initialOption = useRemoteQueryStep({ entry_point: "shipping_option", variables: { id: input.shipping_option_id }, @@ -170,6 +171,20 @@ export const fetchShippingOptionForOrderWorkflow = createWorkflow( return shippingOptionsWithPrice }) + const setPricingContext = createHook("setPricingContext", input, { + resultValidator: pricingContextResult, + }) + const setPricingContextResult = setPricingContext.getResult() + const pricingContext = transform( + { input, setPricingContextResult }, + (data) => { + return { + ...(data.setPricingContextResult ? data.setPricingContextResult : {}), + currency_code: data.input.currency_code, + } + } + ) + const flatRateShippingOption = when( "option-flat", { isCalculatedPriceShippingOption }, @@ -186,7 +201,7 @@ export const fetchShippingOptionForOrderWorkflow = createWorkflow( variables: { id: input.shipping_option_id, calculated_price: { - context: { currency_code: input.currency_code }, + context: pricingContext, }, }, list: false, @@ -195,7 +210,7 @@ export const fetchShippingOptionForOrderWorkflow = createWorkflow( return shippingOption }) - const result = transform( + const result: FetchShippingOptionForOrderWorkflowOutput = transform( { calculatedPriceShippingOption, flatRateShippingOption, @@ -205,6 +220,6 @@ export const fetchShippingOptionForOrderWorkflow = createWorkflow( } ) - return new WorkflowResponse(result) + return new WorkflowResponse(result, { hooks: [setPricingContext] as const }) } ) diff --git a/packages/core/core-flows/src/order/workflows/add-line-items.ts b/packages/core/core-flows/src/order/workflows/add-line-items.ts index b3ad4ca971..9c97518bf3 100644 --- a/packages/core/core-flows/src/order/workflows/add-line-items.ts +++ b/packages/core/core-flows/src/order/workflows/add-line-items.ts @@ -1,6 +1,11 @@ -import { OrderLineItemDTO, OrderWorkflow } from "@medusajs/framework/types" +import { + AdditionalData, + OrderLineItemDTO, + OrderWorkflow, +} from "@medusajs/framework/types" import { isDefined, MedusaError } from "@medusajs/framework/utils" import { + createHook, createWorkflow, parallelize, transform, @@ -21,6 +26,7 @@ import { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-va import { useRemoteQueryStep } from "../../common" import { createOrderLineItemsStep } from "../steps" import { productVariantsFields } from "../utils/fields" +import { pricingContextResult } from "../../cart/utils/schemas" function prepareLineItems(data) { const items = (data.input.items ?? []).map((item) => { @@ -55,12 +61,12 @@ export type OrderAddLineItemWorkflowOutput = OrderLineItemDTO[] export const addOrderLineItemsWorkflowId = "order-add-line-items" /** - * This workflow adds line items to an order. This is useful when making edits to + * This workflow adds line items to an order. This is useful when making edits to * an order. It's used by other workflows, such as {@link orderEditAddNewItemWorkflow}. - * + * * You can use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around adding items to * an order. - * + * * @example * const { result } = await addOrderLineItemsWorkflow(container) * .run({ @@ -74,16 +80,18 @@ export const addOrderLineItemsWorkflowId = "order-add-line-items" * ] * } * }) - * + * * @summary - * + * * Add line items to an order. */ export const addOrderLineItemsWorkflow = createWorkflow( addOrderLineItemsWorkflowId, ( - input: WorkflowData - ): WorkflowResponse => { + input: WorkflowData< + OrderWorkflow.OrderAddLineItemWorkflowInput & AdditionalData + > + ) => { const order = useRemoteQueryStep({ entry_point: "orders", fields: [ @@ -118,14 +126,30 @@ export const addOrderLineItemsWorkflow = createWorkflow( }) ) + const setPricingContext = createHook( + "setPricingContext", + { + order, + variantIds, + region, + customerData, + additional_data: input.additional_data, + }, + { + resultValidator: pricingContextResult, + } + ) + const setPricingContextResult = setPricingContext.getResult() + const pricingContext = transform( - { input, region, customerData, order }, + { input, region, customerData, order, setPricingContextResult }, (data) => { if (!data.region) { throw new MedusaError(MedusaError.Types.NOT_FOUND, "Region not found") } return { + ...(data.setPricingContextResult ? data.setPricingContextResult : {}), currency_code: data.order.currency_code ?? data.region.currency_code, region_id: data.region.id, customer_id: data.customerData.customer?.id, @@ -165,7 +189,10 @@ export const addOrderLineItemsWorkflow = createWorkflow( return new WorkflowResponse( createOrderLineItemsStep({ items: lineItems, - }) + }) satisfies OrderAddLineItemWorkflowOutput, + { + hooks: [setPricingContext] as const, + } ) } ) diff --git a/packages/core/core-flows/src/order/workflows/claim/update-claim-shipping-method.ts b/packages/core/core-flows/src/order/workflows/claim/update-claim-shipping-method.ts index 6a0620b1fc..584b233a4d 100644 --- a/packages/core/core-flows/src/order/workflows/claim/update-claim-shipping-method.ts +++ b/packages/core/core-flows/src/order/workflows/claim/update-claim-shipping-method.ts @@ -1,4 +1,5 @@ import { + AdditionalData, OrderChangeActionDTO, OrderChangeDTO, OrderClaimDTO, @@ -9,6 +10,7 @@ import { ChangeActionType, OrderChangeStatus } from "@medusajs/framework/utils" import { WorkflowData, WorkflowResponse, + createHook, createStep, createWorkflow, parallelize, @@ -26,6 +28,7 @@ import { throwIfOrderChangeIsNotActive, } from "../../utils/order-validation" import { prepareShippingMethodUpdate } from "../../utils/prepare-shipping-method" +import { pricingContextResult } from "../../../cart/utils/schemas" /** * The data to validate that a claim's shipping method can be updated. @@ -42,21 +45,24 @@ export type UpdateClaimShippingMethodValidationStepInput = { /** * The details of updating the shipping method. */ - input: Pick + input: Pick< + OrderWorkflow.UpdateClaimShippingMethodWorkflowInput, + "claim_id" | "action_id" + > } /** * This step validates that a claim's shipping method can be updated. * If the claim is canceled, the order change is not active, the shipping method isn't added to the claim, * or the action is not adding a shipping method, the step will throw an error. - * + * * :::note - * + * * You can retrieve an order claim and order change details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query), * or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep). - * + * * ::: - * + * * @example * const data = updateClaimShippingMethodValidationStep({ * orderChange: { @@ -105,10 +111,10 @@ export const updateClaimShippingMethodWorkflowId = * This workflow updates a claim's inbound (return) or outbound (delivery of new items) shipping method. * It's used by the [Update Inbound Shipping Admin API Route](https://docs.medusajs.com/api/admin#claims_postclaimsidinboundshippingmethodaction_id), * and the [Update Outbound Shipping Admin API Route](https://docs.medusajs.com/api/admin#claims_postclaimsidoutboundshippingmethodaction_id). - * + * * You can use this workflow within your customizations or your own custom workflows, allowing you to update a claim's shipping method * in your own custom flows. - * + * * @example * const { result } = await updateClaimShippingMethodWorkflow(container) * .run({ @@ -120,16 +126,18 @@ export const updateClaimShippingMethodWorkflowId = * } * } * }) - * + * * @summary - * + * * Update an inbound or outbound shipping method of a claim. */ export const updateClaimShippingMethodWorkflow = createWorkflow( updateClaimShippingMethodWorkflowId, function ( - input: WorkflowData - ): WorkflowResponse { + input: WorkflowData< + OrderWorkflow.UpdateClaimShippingMethodWorkflowInput & AdditionalData + > + ) { const orderClaim: OrderClaimDTO = useRemoteQueryStep({ entry_point: "order_claim", fields: [ @@ -157,6 +165,19 @@ export const updateClaimShippingMethodWorkflow = createWorkflow( list: false, }).config({ name: "order-change-query" }) + const setPricingContext = createHook( + "setPricingContext", + { + order_claim: orderClaim, + order_change: orderChange, + additional_data: input.additional_data, + }, + { + resultValidator: pricingContextResult, + } + ) + const setPricingContextResult = setPricingContext.getResult() + const shippingOptions = when({ input }, ({ input }) => { return input.data?.custom_amount === null }).then(() => { @@ -174,6 +195,18 @@ export const updateClaimShippingMethodWorkflow = createWorkflow( } ) + const pricingContext = transform( + { action, setPricingContextResult }, + (data) => { + return { + ...(data.setPricingContextResult + ? data.setPricingContextResult + : {}), + currency_code: data.action.currency_code, + } + } + ) + const shippingMethod = useRemoteQueryStep({ entry_point: "order_shipping_method", fields: ["id", "shipping_option_id"], @@ -194,7 +227,7 @@ export const updateClaimShippingMethodWorkflow = createWorkflow( variables: { id: shippingMethod.shipping_option_id, calculated_price: { - context: { currency_code: action.currency_code }, + context: pricingContext, }, }, }).config({ name: "fetch-shipping-option" }) @@ -212,6 +245,11 @@ export const updateClaimShippingMethodWorkflow = createWorkflow( updateOrderShippingMethodsStep([updateData.shippingMethod!]) ) - return new WorkflowResponse(previewOrderChangeStep(orderClaim.order_id)) + return new WorkflowResponse( + previewOrderChangeStep(orderClaim.order_id) as OrderPreviewDTO, + { + hooks: [setPricingContext] as const, + } + ) } ) diff --git a/packages/core/core-flows/src/order/workflows/create-order.ts b/packages/core/core-flows/src/order/workflows/create-order.ts index c2629b7e9c..228408b1df 100644 --- a/packages/core/core-flows/src/order/workflows/create-order.ts +++ b/packages/core/core-flows/src/order/workflows/create-order.ts @@ -23,6 +23,7 @@ import { useRemoteQueryStep } from "../../common" import { createOrdersStep } from "../steps" import { productVariantsFields } from "../utils/fields" import { updateOrderTaxLinesWorkflow } from "./update-tax-lines" +import { pricingContextResult } from "../../cart/utils/schemas" function prepareLineItems(data) { const items = (data.input.items ?? []).map((item) => { @@ -150,15 +151,30 @@ export const createOrderWorkflow = createWorkflow( }) ) + const setPricingContext = createHook( + "setPricingContext", + { + variantIds, + region, + customerData, + additional_data: input.additional_data, + }, + { + resultValidator: pricingContextResult, + } + ) + const setPricingContextResult = setPricingContext.getResult() + // TODO: This is on par with the context used in v1.*, but we can be more flexible. const pricingContext = transform( - { input, region, customerData }, + { input, region, customerData, setPricingContextResult }, (data) => { if (!data.region) { throw new MedusaError(MedusaError.Types.NOT_FOUND, "Region not found") } return { + ...(data.setPricingContextResult ? data.setPricingContextResult : {}), currency_code: data.input.currency_code ?? data.region.currency_code, region_id: data.region.id, customer_id: data.customerData.customer?.id, @@ -222,7 +238,7 @@ export const createOrderWorkflow = createWorkflow( }) return new WorkflowResponse(order, { - hooks: [orderCreated], + hooks: [orderCreated, setPricingContext] as const, }) } ) diff --git a/packages/core/core-flows/src/order/workflows/exchange/update-exchange-shipping-method.ts b/packages/core/core-flows/src/order/workflows/exchange/update-exchange-shipping-method.ts index e111779081..0ee1e29112 100644 --- a/packages/core/core-flows/src/order/workflows/exchange/update-exchange-shipping-method.ts +++ b/packages/core/core-flows/src/order/workflows/exchange/update-exchange-shipping-method.ts @@ -1,4 +1,5 @@ import { + AdditionalData, OrderChangeActionDTO, OrderChangeDTO, OrderExchangeDTO, @@ -9,6 +10,7 @@ import { ChangeActionType, OrderChangeStatus } from "@medusajs/framework/utils" import { WorkflowData, WorkflowResponse, + createHook, createStep, createWorkflow, parallelize, @@ -26,6 +28,7 @@ import { throwIfOrderChangeIsNotActive, } from "../../utils/order-validation" import { prepareShippingMethodUpdate } from "../../utils/prepare-shipping-method" +import { pricingContextResult } from "../../../cart/utils/schemas" /** * The data to validate that an exchange's shipping method can be updated. @@ -42,7 +45,10 @@ export type UpdateExchangeShippingMethodValidationStepInput = { /** * The details of the shipping method update. */ - input: Pick + input: Pick< + OrderWorkflow.UpdateExchangeShippingMethodWorkflowInput, + "exchange_id" | "action_id" + > } /** @@ -50,14 +56,14 @@ export type UpdateExchangeShippingMethodValidationStepInput = { * If the exchange is canceled, the order change is not active, the shipping method * doesn't exist in the exchange, or the action isn't adding a shipping method, * the step will throw an error. - * + * * :::note - * + * * You can retrieve an order exchange and order change details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query), * or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep). - * + * * ::: - * + * * @example * const data = updateExchangeShippingMethodValidationStep({ * orderChange: { @@ -109,10 +115,10 @@ export const updateExchangeShippingMethodWorkflowId = * This workflow updates an exchange's inbound or outbound shipping method. It's used by the * [Update Inbound Shipping Admin API Route](https://docs.medusajs.com/api/admin#exchanges_postexchangesidinboundshippingmethodaction_id) * or the [Outbound Inbound Shipping Admin API Route](https://docs.medusajs.com/api/admin#exchanges_postexchangesidoutboundshippingmethodaction_id). - * + * * You can use this workflow within your customizations or your own custom workflows, allowing you to update an exchange's * inbound or outbound shipping method in your custom flow. - * + * * @example * const { result } = await updateExchangeShippingMethodWorkflow(container) * .run({ @@ -124,16 +130,18 @@ export const updateExchangeShippingMethodWorkflowId = * } * } * }) - * + * * @summary - * + * * Update an exchange's inbound or outbound shipping method. */ export const updateExchangeShippingMethodWorkflow = createWorkflow( updateExchangeShippingMethodWorkflowId, function ( - input: WorkflowData - ): WorkflowResponse { + input: WorkflowData< + OrderWorkflow.UpdateExchangeShippingMethodWorkflowInput & AdditionalData + > + ) { const orderExchange: OrderExchangeDTO = useRemoteQueryStep({ entry_point: "order_exchange", fields: [ @@ -161,6 +169,19 @@ export const updateExchangeShippingMethodWorkflow = createWorkflow( list: false, }).config({ name: "order-change-query" }) + const setPricingContext = createHook( + "setPricingContext", + { + order_exchange: orderExchange, + order_change: orderChange, + additional_data: input.additional_data, + }, + { + resultValidator: pricingContextResult, + } + ) + const setPricingContextResult = setPricingContext.getResult() + const shippingOptions = when({ input }, ({ input }) => { return input.data?.custom_amount === null }).then(() => { @@ -178,6 +199,18 @@ export const updateExchangeShippingMethodWorkflow = createWorkflow( } ) + const pricingContext = transform( + { action, setPricingContextResult }, + (data) => { + return { + ...(data.setPricingContextResult + ? data.setPricingContextResult + : {}), + currency_code: data.action.currency_code, + } + } + ) + const shippingMethod = useRemoteQueryStep({ entry_point: "order_shipping_method", fields: ["id", "shipping_option_id"], @@ -198,7 +231,7 @@ export const updateExchangeShippingMethodWorkflow = createWorkflow( variables: { id: shippingMethod.shipping_option_id, calculated_price: { - context: { currency_code: action.currency_code }, + context: pricingContext, }, }, }).config({ name: "fetch-shipping-option" }) @@ -220,6 +253,11 @@ export const updateExchangeShippingMethodWorkflow = createWorkflow( updateOrderShippingMethodsStep([updateData.shippingMethod!]) ) - return new WorkflowResponse(previewOrderChangeStep(orderExchange.order_id)) + return new WorkflowResponse( + previewOrderChangeStep(orderExchange.order_id) as OrderPreviewDTO, + { + hooks: [setPricingContext] as const, + } + ) } ) diff --git a/packages/core/core-flows/src/order/workflows/order-edit/create-order-edit-shipping-method.ts b/packages/core/core-flows/src/order/workflows/order-edit/create-order-edit-shipping-method.ts index 0dbd73e6eb..1b40696def 100644 --- a/packages/core/core-flows/src/order/workflows/order-edit/create-order-edit-shipping-method.ts +++ b/packages/core/core-flows/src/order/workflows/order-edit/create-order-edit-shipping-method.ts @@ -1,4 +1,5 @@ import { + AdditionalData, BigNumberInput, OrderChangeDTO, OrderDTO, @@ -7,6 +8,7 @@ import { import { ChangeActionType, OrderChangeStatus } from "@medusajs/framework/utils" import { WorkflowResponse, + createHook, createStep, createWorkflow, transform, @@ -21,6 +23,7 @@ import { import { prepareShippingMethod } from "../../utils/prepare-shipping-method" import { createOrderChangeActionsWorkflow } from "../create-order-change-actions" import { updateOrderTaxLinesWorkflow } from "../update-tax-lines" +import { pricingContextResult } from "../../../cart/utils/schemas" /** * The data to validate that a shipping method can be created for an order edit. @@ -114,8 +117,8 @@ export const createOrderEditShippingMethodWorkflowId = export const createOrderEditShippingMethodWorkflow = createWorkflow( createOrderEditShippingMethodWorkflowId, function ( - input: CreateOrderEditShippingMethodWorkflowInput - ): WorkflowResponse { + input: CreateOrderEditShippingMethodWorkflowInput & AdditionalData + ) { const order: OrderDTO = useRemoteQueryStep({ entry_point: "orders", fields: ["id", "status", "currency_code", "canceled_at"], @@ -124,6 +127,29 @@ export const createOrderEditShippingMethodWorkflow = createWorkflow( throw_if_key_not_found: true, }).config({ name: "order-query" }) + const setPricingContext = createHook( + "setPricingContext", + { + order, + shipping_option_id: input.shipping_option_id, + additional_data: input.additional_data, + }, + { + resultValidator: pricingContextResult, + } + ) + const setPricingContextResult = setPricingContext.getResult() + + const pricingContext = transform( + { order, setPricingContextResult }, + (data) => { + return { + ...(data.setPricingContextResult ? data.setPricingContextResult : {}), + currency_code: data.order.currency_code, + } + } + ) + const shippingOptions = useRemoteQueryStep({ entry_point: "shipping_option", fields: [ @@ -135,7 +161,7 @@ export const createOrderEditShippingMethodWorkflow = createWorkflow( variables: { id: input.shipping_option_id, calculated_price: { - context: { currency_code: order.currency_code }, + context: pricingContext, }, }, }).config({ name: "fetch-shipping-option" }) @@ -213,6 +239,11 @@ export const createOrderEditShippingMethodWorkflow = createWorkflow( input: [orderChangeActionInput], }) - return new WorkflowResponse(previewOrderChangeStep(order.id)) + return new WorkflowResponse( + previewOrderChangeStep(order.id) as OrderPreviewDTO, + { + hooks: [setPricingContext] as const, + } + ) } ) diff --git a/packages/core/core-flows/src/order/workflows/order-edit/update-order-edit-shipping-method.ts b/packages/core/core-flows/src/order/workflows/order-edit/update-order-edit-shipping-method.ts index 05e2233862..2a28031188 100644 --- a/packages/core/core-flows/src/order/workflows/order-edit/update-order-edit-shipping-method.ts +++ b/packages/core/core-flows/src/order/workflows/order-edit/update-order-edit-shipping-method.ts @@ -1,4 +1,5 @@ import { + AdditionalData, OrderChangeActionDTO, OrderChangeDTO, OrderDTO, @@ -9,6 +10,7 @@ import { ChangeActionType, OrderChangeStatus } from "@medusajs/framework/utils" import { WorkflowData, WorkflowResponse, + createHook, createStep, createWorkflow, parallelize, @@ -23,6 +25,7 @@ import { import { previewOrderChangeStep } from "../../steps/preview-order-change" import { throwIfOrderChangeIsNotActive } from "../../utils/order-validation" import { prepareShippingMethodUpdate } from "../../utils/prepare-shipping-method" +import { pricingContextResult } from "../../../cart/utils/schemas" /** * The data to validate that an order edit's shipping method can be updated. @@ -35,21 +38,24 @@ export type UpdateOrderEditShippingMethodValidationStepInput = { /** * The details of the shipping method to be updated. */ - input: Pick + input: Pick< + OrderWorkflow.UpdateOrderEditShippingMethodWorkflowInput, + "order_id" | "action_id" + > } /** * This step validates that an order edit's shipping method can be updated. * If the order change is not active, the shipping method isn't in the order edit, * or the action is not adding a shipping method, the step will throw an error. - * + * * :::note - * + * * You can retrieve an order change details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query), * or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep). - * + * * ::: - * + * * @example * const data = updateOrderEditShippingMethodValidationStep({ * orderChange: { @@ -92,12 +98,12 @@ export const updateOrderEditShippingMethodValidationStep = createStep( export const updateOrderEditShippingMethodWorkflowId = "update-order-edit-shipping-method" /** - * This workflow updates an order edit's shipping method. It's used by the + * This workflow updates an order edit's shipping method. It's used by the * [Update Shipping Method Admin API Route](https://docs.medusajs.com/api/admin#order-edits_postordereditsidshippingmethodaction_id). - * + * * You can use this workflow within your customizations or your own custom workflows, allowing you to update an order edit's shipping method * in your custom flow. - * + * * @example * const { result } = await updateOrderEditShippingMethodWorkflow(container) * .run({ @@ -109,16 +115,18 @@ export const updateOrderEditShippingMethodWorkflowId = * } * } * }) - * + * * @summary - * + * * Update a shipping method of an order edit. */ export const updateOrderEditShippingMethodWorkflow = createWorkflow( updateOrderEditShippingMethodWorkflowId, function ( - input: WorkflowData - ): WorkflowResponse { + input: WorkflowData< + OrderWorkflow.UpdateOrderEditShippingMethodWorkflowInput & AdditionalData + > + ) { const order: OrderDTO = useRemoteQueryStep({ entry_point: "order_claim", fields: ["id", "currency_code"], @@ -139,6 +147,19 @@ export const updateOrderEditShippingMethodWorkflow = createWorkflow( list: false, }).config({ name: "order-change-query" }) + const setPricingContext = createHook( + "setPricingContext", + { + order: order, + order_change: orderChange, + additional_data: input.additional_data, + }, + { + resultValidator: pricingContextResult, + } + ) + const setPricingContextResult = setPricingContext.getResult() + const shippingOptions = when({ input }, ({ input }) => { return input.data?.custom_amount === null }).then(() => { @@ -156,6 +177,18 @@ export const updateOrderEditShippingMethodWorkflow = createWorkflow( } ) + const pricingContext = transform( + { action, setPricingContextResult }, + (data) => { + return { + ...(data.setPricingContextResult + ? data.setPricingContextResult + : {}), + currency_code: data.action.currency_code, + } + } + ) + const shippingMethod = useRemoteQueryStep({ entry_point: "order_shipping_method", fields: ["id", "shipping_option_id"], @@ -176,7 +209,7 @@ export const updateOrderEditShippingMethodWorkflow = createWorkflow( variables: { id: shippingMethod.shipping_option_id, calculated_price: { - context: { currency_code: action.currency_code }, + context: pricingContext, }, }, }).config({ name: "fetch-shipping-option" }) @@ -197,6 +230,11 @@ export const updateOrderEditShippingMethodWorkflow = createWorkflow( updateOrderShippingMethodsStep([updateData.shippingMethod!]) ) - return new WorkflowResponse(previewOrderChangeStep(input.order_id)) + return new WorkflowResponse( + previewOrderChangeStep(input.order_id) as OrderPreviewDTO, + { + hooks: [setPricingContext] as const, + } + ) } ) diff --git a/packages/core/core-flows/src/order/workflows/return/create-complete-return.ts b/packages/core/core-flows/src/order/workflows/return/create-complete-return.ts index 9532affdfa..ebc69471dc 100644 --- a/packages/core/core-flows/src/order/workflows/return/create-complete-return.ts +++ b/packages/core/core-flows/src/order/workflows/return/create-complete-return.ts @@ -7,6 +7,7 @@ import { OrderWorkflow, ShippingOptionDTO, WithCalculatedPrice, + AdditionalData, } from "@medusajs/framework/types" import { MathBN, @@ -18,6 +19,7 @@ import { import { WorkflowData, WorkflowResponse, + createHook, createStep, createWorkflow, parallelize, @@ -36,6 +38,7 @@ import { throwIfOrderIsCancelled, } from "../../utils/order-validation" import { validateReturnReasons } from "../../utils/validate-return-reason" +import { pricingContextResult } from "../../../cart/utils/schemas" function prepareShippingMethodData({ orderId, @@ -186,6 +189,7 @@ function prepareFulfillmentData({ function prepareReturnShippingOptionQueryVariables({ order, input, + setPricingContextResult, }: { order: { currency_code: string @@ -194,11 +198,13 @@ function prepareReturnShippingOptionQueryVariables({ input: { return_shipping?: OrderWorkflow.CreateOrderReturnWorkflowInput["return_shipping"] } + setPricingContextResult?: any }) { const variables = { id: input.return_shipping?.option_id, calculated_price: { context: { + ...(setPricingContextResult ? setPricingContextResult : {}), currency_code: order.currency_code, }, }, @@ -309,8 +315,10 @@ export const createAndCompleteReturnOrderWorkflowId = export const createAndCompleteReturnOrderWorkflow = createWorkflow( createAndCompleteReturnOrderWorkflowId, function ( - input: WorkflowData - ): WorkflowResponse { + input: WorkflowData< + OrderWorkflow.CreateOrderReturnWorkflowInput & AdditionalData + > + ) { const order: OrderDTO = useRemoteQueryStep({ entry_point: "orders", fields: [ @@ -329,8 +337,20 @@ export const createAndCompleteReturnOrderWorkflow = createWorkflow( createCompleteReturnValidationStep({ order, input }) + const setPricingContext = createHook( + "setPricingContext", + { + order, + additional_data: input.additional_data, + }, + { + resultValidator: pricingContextResult, + } + ) + const setPricingContextResult = setPricingContext.getResult() + const returnShippingOptionsVariables = transform( - { input, order }, + { input, order, setPricingContextResult }, prepareReturnShippingOptionQueryVariables ) @@ -415,6 +435,8 @@ export const createAndCompleteReturnOrderWorkflow = createWorkflow( }).config({ name: "emit-return-received-event" }) ) - return new WorkflowResponse(returnCreated) + return new WorkflowResponse(returnCreated as ReturnDTO, { + hooks: [setPricingContext] as const, + }) } ) diff --git a/packages/core/core-flows/src/order/workflows/return/update-return-shipping-method.ts b/packages/core/core-flows/src/order/workflows/return/update-return-shipping-method.ts index 0069ffcb23..595b5520ab 100644 --- a/packages/core/core-flows/src/order/workflows/return/update-return-shipping-method.ts +++ b/packages/core/core-flows/src/order/workflows/return/update-return-shipping-method.ts @@ -1,4 +1,5 @@ import { + AdditionalData, OrderChangeActionDTO, OrderChangeDTO, OrderPreviewDTO, @@ -9,6 +10,7 @@ import { ChangeActionType, OrderChangeStatus } from "@medusajs/framework/utils" import { WorkflowData, WorkflowResponse, + createHook, createStep, createWorkflow, parallelize, @@ -26,6 +28,7 @@ import { throwIfOrderChangeIsNotActive, } from "../../utils/order-validation" import { prepareShippingMethodUpdate } from "../../utils/prepare-shipping-method" +import { pricingContextResult } from "../../../cart/utils/schemas" /** * The data to validate that a return's shipping method can be updated. @@ -42,7 +45,10 @@ export type UpdateReturnShippingMethodValidationStepInput = { /** * The details of updating the shipping method. */ - input: Pick + input: Pick< + OrderWorkflow.UpdateReturnShippingMethodWorkflowInput, + "return_id" | "action_id" + > } /** @@ -50,14 +56,14 @@ export type UpdateReturnShippingMethodValidationStepInput = { * If the return is canceled, the order change is not active, * the shipping method isn't in the return, or the action isn't adding a shipping method, * the step will throw an error. - * + * * :::note - * + * * You can retrieve a return and order change details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query), * or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep). - * + * * ::: - * + * * @example * const data = updateReturnShippingMethodValidationStep({ * orderChange: { @@ -105,10 +111,10 @@ export const updateReturnShippingMethodWorkflowId = /** * This workflow updates the shipping method of a return. It's used by the * [Update Shipping Method Admin API Route](https://docs.medusajs.com/api/admin#returns_postreturnsidshippingmethodaction_id). - * + * * You can use this workflow within your customizations or your own custom workflows, allowing you * to update the shipping method of a return in your custom flows. - * + * * @example * const { result } = await updateReturnShippingMethodWorkflow(container) * .run({ @@ -120,16 +126,18 @@ export const updateReturnShippingMethodWorkflowId = * } * } * }) - * + * * @summary - * + * * Update the shipping method of a return. */ export const updateReturnShippingMethodWorkflow = createWorkflow( updateReturnShippingMethodWorkflowId, function ( - input: WorkflowData - ): WorkflowResponse { + input: WorkflowData< + OrderWorkflow.UpdateReturnShippingMethodWorkflowInput & AdditionalData + > + ) { const orderReturn: ReturnDTO = useRemoteQueryStep({ entry_point: "return", fields: [ @@ -157,6 +165,19 @@ export const updateReturnShippingMethodWorkflow = createWorkflow( list: false, }).config({ name: "order-change-query" }) + const setPricingContext = createHook( + "setPricingContext", + { + order_return: orderReturn, + order_change: orderChange, + additional_data: input.additional_data, + }, + { + resultValidator: pricingContextResult, + } + ) + const setPricingContextResult = setPricingContext.getResult() + const shippingOptions = when({ input }, ({ input }) => { return input.data?.custom_amount === null }).then(() => { @@ -174,6 +195,18 @@ export const updateReturnShippingMethodWorkflow = createWorkflow( } ) + const pricingContext = transform( + { action, setPricingContextResult }, + (data) => { + return { + ...(data.setPricingContextResult + ? data.setPricingContextResult + : {}), + currency_code: data.action.currency_code, + } + } + ) + const shippingMethod = useRemoteQueryStep({ entry_point: "order_shipping_method", fields: ["id", "shipping_option_id"], @@ -194,7 +227,7 @@ export const updateReturnShippingMethodWorkflow = createWorkflow( variables: { id: shippingMethod.shipping_option_id, calculated_price: { - context: { currency_code: action.currency_code }, + context: pricingContext, }, }, }).config({ name: "fetch-shipping-option" }) @@ -216,6 +249,11 @@ export const updateReturnShippingMethodWorkflow = createWorkflow( updateOrderShippingMethodsStep([updateData.shippingMethod!]) ) - return new WorkflowResponse(previewOrderChangeStep(orderReturn.order_id)) + return new WorkflowResponse( + previewOrderChangeStep(orderReturn.order_id) as OrderPreviewDTO, + { + hooks: [setPricingContext] as const, + } + ) } ) diff --git a/packages/core/workflows-sdk/src/utils/composer/create-hook.ts b/packages/core/workflows-sdk/src/utils/composer/create-hook.ts index f18fde57f8..32a3c321a8 100644 --- a/packages/core/workflows-sdk/src/utils/composer/create-hook.ts +++ b/packages/core/workflows-sdk/src/utils/composer/create-hook.ts @@ -85,6 +85,9 @@ export function createHook( if (options.resultValidator) { return options.resultValidator.parse(result) } + if (result === undefined) { + return new StepResponse(undefined) + } return result }, () => void 0