diff --git a/integration-tests/http/__tests__/product/admin/product-import.spec.ts b/integration-tests/http/__tests__/product/admin/product-import.spec.ts index 2e32557c5d4cc..9f80cac28fb2c 100644 --- a/integration-tests/http/__tests__/product/admin/product-import.spec.ts +++ b/integration-tests/http/__tests__/product/admin/product-import.spec.ts @@ -200,7 +200,7 @@ medusaIntegrationTestRunner({ thumbnail: "test-image.png", status: "draft", description: "test-product-description\ntest line 2", - options: [ + options: expect.arrayContaining([ expect.objectContaining({ title: "size", values: expect.arrayContaining([ @@ -220,7 +220,7 @@ medusaIntegrationTestRunner({ }), ]), }), - ], + ]), images: expect.arrayContaining([ expect.objectContaining({ url: "test-image.png", @@ -243,12 +243,12 @@ medusaIntegrationTestRunner({ collection: expect.objectContaining({ id: baseCollection.id, }), - variants: [ + variants: expect.arrayContaining([ expect.objectContaining({ title: "Test variant", allow_backorder: false, manage_inventory: true, - prices: [ + prices: expect.arrayContaining([ expect.objectContaining({ currency_code: "dkk", amount: 30, @@ -261,21 +261,21 @@ medusaIntegrationTestRunner({ currency_code: "usd", amount: 100, }), - ], - options: [ + ]), + options: expect.arrayContaining([ expect.objectContaining({ value: "large", }), expect.objectContaining({ value: "green", }), - ], + ]), }), expect.objectContaining({ title: "Test variant 2", allow_backorder: false, manage_inventory: true, - prices: [ + prices: expect.arrayContaining([ expect.objectContaining({ currency_code: "dkk", amount: 50, @@ -288,17 +288,17 @@ medusaIntegrationTestRunner({ currency_code: "usd", amount: 200, }), - ], - options: [ + ]), + options: expect.arrayContaining([ expect.objectContaining({ value: "small", }), expect.objectContaining({ value: "green", }), - ], + ]), }), - ], + ]), created_at: expect.any(String), updated_at: expect.any(String), }), @@ -309,7 +309,7 @@ medusaIntegrationTestRunner({ thumbnail: "test-image.png", status: "proposed", description: "test-product-description", - options: [ + options: expect.arrayContaining([ expect.objectContaining({ title: "size", values: expect.arrayContaining([ @@ -326,7 +326,7 @@ medusaIntegrationTestRunner({ }), ]), }), - ], + ]), images: expect.arrayContaining([ expect.objectContaining({ url: "test-image.png", @@ -344,12 +344,12 @@ medusaIntegrationTestRunner({ id: baseType.id, }), collection: null, - variants: [ + variants: expect.arrayContaining([ expect.objectContaining({ title: "Test variant", allow_backorder: false, manage_inventory: true, - prices: [ + prices: expect.arrayContaining([ expect.objectContaining({ currency_code: "dkk", amount: 30, @@ -362,17 +362,17 @@ medusaIntegrationTestRunner({ currency_code: "usd", amount: 100, }), - ], - options: [ + ]), + options: expect.arrayContaining([ expect.objectContaining({ value: "large", }), expect.objectContaining({ value: "green", }), - ], + ]), }), - ], + ]), created_at: expect.any(String), updated_at: expect.any(String), }), @@ -613,7 +613,7 @@ medusaIntegrationTestRunner({ thumbnail: "test-image.png", status: "draft", description: "test-product-description", - options: [ + options: expect.arrayContaining([ expect.objectContaining({ title: "Size", values: expect.arrayContaining([ @@ -628,7 +628,7 @@ medusaIntegrationTestRunner({ }), ]), }), - ], + ]), images: expect.arrayContaining([ expect.objectContaining({ url: "test-image.png", @@ -645,7 +645,7 @@ medusaIntegrationTestRunner({ collection: expect.objectContaining({ id: baseCollection.id, }), - variants: [ + variants: expect.arrayContaining([ expect.objectContaining({ title: "Test variant", sku: "test-sku-2", @@ -689,7 +689,7 @@ medusaIntegrationTestRunner({ barcode: "test-barcode-4", allow_backorder: false, manage_inventory: true, - prices: [ + prices: expect.arrayContaining([ expect.objectContaining({ currency_code: "usd", amount: 100, @@ -703,14 +703,14 @@ medusaIntegrationTestRunner({ currency_code: "dkk", amount: 30, }), - ], + ]), options: [ expect.objectContaining({ value: "Large", }), ], }), - ], + ]), created_at: expect.any(String), updated_at: expect.any(String), }), @@ -723,7 +723,7 @@ medusaIntegrationTestRunner({ status: "draft", description: "Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\\n100% organic cotton, soft and crisp to the touch. Made in Portugal.", - options: [ + options: expect.arrayContaining([ expect.objectContaining({ title: "test-option-1", values: expect.arrayContaining([ @@ -740,7 +740,7 @@ medusaIntegrationTestRunner({ }), ]), }), - ], + ]), images: expect.arrayContaining([ expect.objectContaining({ url: "test-image.png", @@ -748,14 +748,14 @@ medusaIntegrationTestRunner({ ]), tags: [], type: null, - variants: [ + variants: expect.arrayContaining([ expect.objectContaining({ title: "Test variant", sku: "test-sku-1-1", barcode: "test-barcode-1-1", allow_backorder: false, manage_inventory: true, - prices: [ + prices: expect.arrayContaining([ expect.objectContaining({ currency_code: "usd", rules: { @@ -767,17 +767,17 @@ medusaIntegrationTestRunner({ currency_code: "usd", amount: 1.1, }), - ], - options: [ + ]), + options: expect.arrayContaining([ expect.objectContaining({ value: "option 1 value red", }), expect.objectContaining({ value: "option 2 value 1", }), - ], + ]), }), - ], + ]), created_at: expect.any(String), updated_at: expect.any(String), }), diff --git a/integration-tests/modules/__tests__/shipping-options/store/shipping-options.spec.ts b/integration-tests/modules/__tests__/shipping-options/store/shipping-options.spec.ts index b1fd4a7b042c2..0e963277ca637 100644 --- a/integration-tests/modules/__tests__/shipping-options/store/shipping-options.spec.ts +++ b/integration-tests/modules/__tests__/shipping-options/store/shipping-options.spec.ts @@ -200,6 +200,7 @@ medusaIntegrationTestRunner({ ) const shippingOptions = resp.data.shipping_options + expect(shippingOptions).toHaveLength(1) expect(shippingOptions[0]).toEqual( expect.objectContaining({ diff --git a/packages/core/core-flows/src/cart/workflows/add-shipping-method-to-cart.ts b/packages/core/core-flows/src/cart/workflows/add-shipping-method-to-cart.ts index f7b3bdb765690..295188482901b 100644 --- a/packages/core/core-flows/src/cart/workflows/add-shipping-method-to-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/add-shipping-method-to-cart.ts @@ -1,3 +1,4 @@ +import { MedusaError } from "@medusajs/framework/utils" import { WorkflowData, createWorkflow, @@ -74,6 +75,13 @@ export const addShippingMethodToWorkflow = createWorkflow( (so) => so.id === option.id )! + if (!shippingOption?.calculated_price) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Shipping option with ID ${shippingOption.id} do not have a price` + ) + } + return { shipping_option_id: shippingOption.id, amount: shippingOption.calculated_price.calculated_amount, 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 274fee11e934c..8d242a338fe07 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 @@ -18,86 +18,100 @@ export const listShippingOptionsForCartWorkflow = createWorkflow( (input: WorkflowData) => { const scLocationFulfillmentSets = useRemoteQueryStep({ entry_point: "sales_channels", + fields: ["stock_locations.fulfillment_sets.id"], + variables: { + id: input.sales_channel_id, + }, + }).config({ name: "sales_channels-fulfillment-query" }) + + const fulfillmentSetIds = transform( + { options: scLocationFulfillmentSets }, + (data) => { + const fulfillmentSetIds = new Set() + + deepFlatMap( + data.options, + "stock_locations.fulfillment_sets", + ({ fulfillment_sets }) => { + if (fulfillment_sets?.id) { + fulfillmentSetIds.add(fulfillment_sets.id) + } + } + ) + + return Array.from(fulfillmentSetIds) + } + ) + + const shippingOptions = useRemoteQueryStep({ + entry_point: "shipping_options", fields: [ - "stock_locations.fulfillment_sets.id", - "stock_locations.fulfillment_sets.name", - "stock_locations.fulfillment_sets.price_type", - "stock_locations.fulfillment_sets.service_zone_id", - "stock_locations.fulfillment_sets.shipping_profile_id", - "stock_locations.fulfillment_sets.provider_id", - "stock_locations.fulfillment_sets.data", - "stock_locations.fulfillment_sets.amount", - - "stock_locations.fulfillment_sets.service_zones.shipping_options.id", - "stock_locations.fulfillment_sets.service_zones.shipping_options.name", - "stock_locations.fulfillment_sets.service_zones.shipping_options.price_type", - "stock_locations.fulfillment_sets.service_zones.shipping_options.service_zone_id", - "stock_locations.fulfillment_sets.service_zones.shipping_options.shipping_profile_id", - "stock_locations.fulfillment_sets.service_zones.shipping_options.provider_id", - "stock_locations.fulfillment_sets.service_zones.shipping_options.data", - "stock_locations.fulfillment_sets.service_zones.shipping_options.amount", - - "stock_locations.fulfillment_sets.service_zones.shipping_options.type.id", - "stock_locations.fulfillment_sets.service_zones.shipping_options.type.label", - "stock_locations.fulfillment_sets.service_zones.shipping_options.type.description", - "stock_locations.fulfillment_sets.service_zones.shipping_options.type.code", - - "stock_locations.fulfillment_sets.service_zones.shipping_options.provider.id", - "stock_locations.fulfillment_sets.service_zones.shipping_options.provider.is_enabled", - - "stock_locations.fulfillment_sets.service_zones.shipping_options.calculated_price.calculated_amount", - "stock_locations.fulfillment_sets.service_zones.shipping_options.calculated_price.is_calculated_price_tax_inclusive", + "id", + "name", + "price_type", + "service_zone_id", + "shipping_profile_id", + "provider_id", + "data", + "amount", + + "type.id", + "type.label", + "type.description", + "type.code", + + "provider.id", + "provider.is_enabled", + + "rules.attribute", + "rules.value", + "rules.operator", + + "calculated_price.*", ], variables: { - id: input.sales_channel_id, - "stock_locations.fulfillment_sets.service_zones.shipping_options": { - context: { - is_return: "false", - enabled_in_store: "true", - }, - filters: { - address: { - city: input.shipping_address?.city, - country_code: input.shipping_address?.country_code, - province_code: input.shipping_address?.province, - }, + context: { + is_return: input.is_return, + enabled_in_store: "true", + }, + filters: { + fulfillment_set_id: fulfillmentSetIds, + address: { + city: input.shipping_address?.city, + country_code: input.shipping_address?.country_code, + province_code: input.shipping_address?.province, }, }, - "stock_locations.fulfillment_sets.service_zones.shipping_options.calculated_price": - { - context: { - currency_code: input.currency_code, - }, + + calculated_price: { + context: { + currency_code: input.currency_code, }, + }, }, - }) + }).config({ name: "shipping-options-query" }) const shippingOptionsWithPrice = transform( - { options: scLocationFulfillmentSets }, + { + shippingOptions, + }, (data) => { const optionsMissingPrices: string[] = [] - const options = deepFlatMap( - data.options, - "stock_locations.fulfillment_sets.service_zones.shipping_options.calculated_price", - ({ shipping_options }) => { - const { calculated_price, ...options } = shipping_options ?? {} - - if ( - options?.id && - !isPresent(calculated_price?.calculated_amount) - ) { - optionsMissingPrices.push(options.id) - } + const options = data.shippingOptions.map((shippingOption) => { + const { calculated_price, ...options } = shippingOption ?? {} - return { - ...options, - amount: calculated_price?.calculated_amount, - is_tax_inclusive: - !!calculated_price?.is_calculated_price_tax_inclusive, - } + 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) { throw new MedusaError( diff --git a/packages/core/types/src/cart/workflows.ts b/packages/core/types/src/cart/workflows.ts index b6ee128593e84..7b00783fb9d55 100644 --- a/packages/core/types/src/cart/workflows.ts +++ b/packages/core/types/src/cart/workflows.ts @@ -104,6 +104,7 @@ export interface CartWorkflowDTO extends CartDTO { export interface ListShippingOptionsForCartWorkflowInputDTO { cart_id: string + is_return?: boolean sales_channel_id?: string currency_code: string shipping_address: { diff --git a/packages/medusa/src/api/store/shipping-options/route.ts b/packages/medusa/src/api/store/shipping-options/route.ts index 91a476e82547d..254fb8b28d64b 100644 --- a/packages/medusa/src/api/store/shipping-options/route.ts +++ b/packages/medusa/src/api/store/shipping-options/route.ts @@ -2,12 +2,15 @@ import { listShippingOptionsForCartWorkflow } from "@medusajs/core-flows" import { HttpTypes, ICartModuleService } from "@medusajs/framework/types" import { MedusaError, Modules } from "@medusajs/framework/utils" import { MedusaRequest, MedusaResponse } from "../../../types/routing" +import { StoreGetShippingOptionsType } from "./validators" export const GET = async ( req: MedusaRequest, res: MedusaResponse ) => { - const { cart_id } = req.filterableFields as { cart_id: string } + const { cart_id, is_return } = + req.filterableFields as StoreGetShippingOptionsType + if (!cart_id) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, @@ -34,6 +37,7 @@ export const GET = async ( cart_id: cart.id, sales_channel_id: cart.sales_channel_id, currency_code: cart.currency_code, + is_return: !!is_return, shipping_address: { city: cart.shipping_address?.city, country_code: cart.shipping_address?.country_code, diff --git a/packages/medusa/src/api/store/shipping-options/validators.ts b/packages/medusa/src/api/store/shipping-options/validators.ts index 464c81dadaf41..8080aaa09f751 100644 --- a/packages/medusa/src/api/store/shipping-options/validators.ts +++ b/packages/medusa/src/api/store/shipping-options/validators.ts @@ -10,6 +10,7 @@ export const StoreGetShippingOptions = createFindParams({ }).merge( z.object({ cart_id: z.string(), + is_return: z.boolean().optional(), $and: z.lazy(() => StoreGetShippingOptions.array()).optional(), $or: z.lazy(() => StoreGetShippingOptions.array()).optional(), }) diff --git a/packages/modules/fulfillment/src/utils/utils.ts b/packages/modules/fulfillment/src/utils/utils.ts index 4710125c8fafd..8e85efec690cc 100644 --- a/packages/modules/fulfillment/src/utils/utils.ts +++ b/packages/modules/fulfillment/src/utils/utils.ts @@ -1,9 +1,9 @@ import { + MedusaError, + RuleOperator, isObject, isString, - MedusaError, pickValueFromObject, - RuleOperator, } from "@medusajs/framework/utils" /** @@ -81,6 +81,7 @@ export function isContextValid( const predicate = (rule) => { const { attribute, operator, value } = rule const contextValue = pickValueFromObject(attribute, context) + return operatorsPredicate[operator]( contextValue, value as string & string[]