diff --git a/integration-tests/http/__tests__/cart/store/cart.spec.ts b/integration-tests/http/__tests__/cart/store/cart.spec.ts index 0a5d36472ba00..6005a7f6d5de1 100644 --- a/integration-tests/http/__tests__/cart/store/cart.spec.ts +++ b/integration-tests/http/__tests__/cart/store/cart.spec.ts @@ -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.", + }) + }) + }) }) }, }) diff --git a/integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts b/integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts index 19644d9e8de89..8907c3563ec1a 100644 --- a/integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts +++ b/integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts @@ -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", }), diff --git a/integration-tests/http/__tests__/shipping-option/store/shipping-option.spec.ts b/integration-tests/http/__tests__/shipping-option/store/shipping-option.spec.ts index 5c6e49a39417a..9261db2b2d224 100644 --- a/integration-tests/http/__tests__/shipping-option/store/shipping-option.spec.ts +++ b/integration-tests/http/__tests__/shipping-option/store/shipping-option.spec.ts @@ -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", - }) - - regionTwo = await regionService.createRegions({ - name: "Test region two", - countries: ["DK"], - currency_code: "dkk", - }) + region = ( + await api.post( + "/admin/regions", + { name: "US", currency_code: "usd", countries: ["US"] }, + adminHeaders + ) + ).data.region - 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", }) ) 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 78f66698d01bc..f9a3ebb560e4b 100644 --- a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts @@ -34,8 +34,11 @@ import { import { adminHeaders, createAdminUser, + generatePublishableKey, + generateStoreHeaders, } from "../../../../helpers/create-admin-user" import { seedStorefrontDefaults } from "../../../../helpers/seed-storefront-defaults" +import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer" jest.setTimeout(200000) @@ -56,9 +59,10 @@ medusaIntegrationTestRunner({ let stockLocationModule: IStockLocationService let inventoryModule: IInventoryService let fulfillmentModule: IFulfillmentModuleService - let remoteLink, remoteQuery - + let remoteLink, remoteQuery, storeHeaders + let salesChannel let defaultRegion + let customer, storeHeadersWithCustomer beforeAll(async () => { appContainer = getContainer() @@ -69,9 +73,9 @@ medusaIntegrationTestRunner({ productModule = appContainer.resolve(Modules.PRODUCT) pricingModule = appContainer.resolve(Modules.PRICING) paymentModule = appContainer.resolve(Modules.PAYMENT) + fulfillmentModule = appContainer.resolve(Modules.FULFILLMENT) inventoryModule = appContainer.resolve(Modules.INVENTORY) stockLocationModule = appContainer.resolve(Modules.STOCK_LOCATION) - fulfillmentModule = appContainer.resolve(Modules.FULFILLMENT) remoteLink = appContainer.resolve(ContainerRegistrationKeys.REMOTE_LINK) remoteQuery = appContainer.resolve( ContainerRegistrationKeys.REMOTE_QUERY @@ -79,11 +83,35 @@ medusaIntegrationTestRunner({ }) beforeEach(async () => { + const publishableKey = await generatePublishableKey(appContainer) + storeHeaders = generateStoreHeaders({ publishableKey }) await createAdminUser(dbConnection, adminHeaders, appContainer) + const result = await createAuthenticatedCustomer(api, storeHeaders, { + first_name: "tony", + last_name: "stark", + email: "tony@test-industries.com", + }) + + customer = result.customer + storeHeadersWithCustomer = { + headers: { + ...storeHeaders.headers, + authorization: `Bearer ${result.jwt}`, + }, + } + const { region } = await seedStorefrontDefaults(appContainer, "dkk") defaultRegion = region + + salesChannel = ( + await api.post( + "/admin/sales-channels", + { name: "test sales channel", description: "channel" }, + adminHeaders + ) + ).data.sales_channel }) describe("CreateCartWorkflow", () => { @@ -896,123 +924,6 @@ medusaIntegrationTestRunner({ }) describe("updateLineItemInCartWorkflow", () => { - it("should update item in cart", async () => { - 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 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, - }, - }, - ]) - - let cart = await cartModuleService.createCarts({ - currency_code: "usd", - sales_channel_id: salesChannel.id, - items: [ - { - variant_id: product.variants[0].id, - quantity: 1, - unit_price: 5000, - title: "Test item", - }, - ], - }) - - cart = await cartModuleService.retrieveCart(cart.id, { - select: ["id", "region_id", "currency_code"], - relations: ["items", "items.variant_id", "items.metadata"], - }) - - const item = cart.items?.[0]! - - const { errors } = await updateLineItemInCartWorkflow( - appContainer - ).run({ - input: { - cart, - item, - update: { - metadata: { - foo: "bar", - }, - quantity: 2, - }, - }, - throwOnError: false, - }) - - const updatedItem = await cartModuleService.retrieveLineItem(item.id) - - expect(updatedItem).toEqual( - expect.objectContaining({ - id: item.id, - unit_price: 3000, - quantity: 2, - title: "Test item", - }) - ) - }) - describe("compensation", () => { it("should revert line item update to original state", async () => { expect.assertions(2) @@ -1544,45 +1455,23 @@ medusaIntegrationTestRunner({ let shippingProfile let fulfillmentSet let priceSet + let region + let stockLocation beforeEach(async () => { - cart = await cartModuleService.createCarts({ - currency_code: "usd", - shipping_address: { - country_code: "us", - province: "ny", - }, - }) - - shippingProfile = await fulfillmentModule.createShippingProfiles({ - name: "Test", - type: "default", - }) - - fulfillmentSet = await fulfillmentModule.createFulfillmentSets({ - name: "Test", - type: "test-type", - service_zones: [ + region = ( + await api.post( + "/admin/regions", { - name: "Test", - geo_zones: [{ type: "country", country_code: "us" }], + name: "test-region", + currency_code: "usd", + countries: ["us"], }, - ], - }) - - priceSet = await pricingModule.createPriceSets({ - prices: [{ amount: 3000, currency_code: "usd" }], - }) - - await pricingModule.createPricePreferences({ - attribute: "currency_code", - value: "usd", - is_tax_inclusive: true, - }) - }) + adminHeaders + ) + ).data.region - it("should add shipping method to cart", async () => { - const stockLocation = ( + stockLocation = ( await api.post( `/admin/stock-locations`, { name: "test location" }, @@ -1590,16 +1479,41 @@ medusaIntegrationTestRunner({ ) ).data.stock_location - await remoteLink.create([ - { - [Modules.STOCK_LOCATION]: { - stock_location_id: stockLocation.id, + await api.post( + `/admin/stock-locations/${stockLocation.id}/sales-channels`, + { add: [salesChannel.id] }, + adminHeaders + ) + + 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", }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: fulfillmentSet.id, + adminHeaders + ) + ).data.stock_location.fulfillment_sets + + fulfillmentSet = ( + await api.post( + `/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`, + { + name: "Test", + geo_zones: [{ type: "country", country_code: "us" }], }, - }, - ]) + adminHeaders + ) + ).data.fulfillment_set await api.post( `/admin/stock-locations/${stockLocation.id}/fulfillment-providers`, @@ -1607,37 +1521,61 @@ medusaIntegrationTestRunner({ adminHeaders ) - 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", - }, + cart = ( + await api.post( + `/store/carts`, { - operator: RuleOperator.EQ, - attribute: "enabled_in_store", - value: "true", + currency_code: "usd", + region_id: region.id, + sales_channel_id: salesChannel.id, }, - ], - }) + storeHeaders + ) + ).data.cart - await remoteLink.create([ + await api.post( + "/admin/price-preferences", { - [Modules.FULFILLMENT]: { shipping_option_id: shippingOption.id }, - [Modules.PRICING]: { price_set_id: priceSet.id }, + attribute: "currency_code", + value: "usd", + is_tax_inclusive: true, }, - ]) + adminHeaders + ) + }) + + it("should add shipping method to cart", 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 await addShippingMethodToCartWorkflow(appContainer).run({ input: { @@ -1646,9 +1584,8 @@ medusaIntegrationTestRunner({ }, }) - cart = await cartModuleService.retrieveCart(cart.id, { - relations: ["shipping_methods"], - }) + cart = (await api.get(`/store/carts/${cart.id}`, storeHeaders)).data + .cart expect(cart).toEqual( expect.objectContaining({ @@ -1658,7 +1595,6 @@ medusaIntegrationTestRunner({ expect.objectContaining({ amount: 3000, is_tax_inclusive: true, - name: "Test shipping option", }), ], }) @@ -1666,40 +1602,37 @@ medusaIntegrationTestRunner({ }) it("should throw error when shipping option is not valid", async () => { - 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: [ + const shippingOption = ( + await api.post( + `/admin/shipping-options`, { - operator: RuleOperator.EQ, - attribute: "shipping_address.city", - value: "sf", + 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: "shipping_address.city", + value: "sf", + }, + ], + prices: [{ amount: 3000, currency_code: "usd" }], }, - ], - }) - - await remoteLink.create([ - { - [Modules.FULFILLMENT]: { shipping_option_id: shippingOption.id }, - [Modules.PRICING]: { price_set_id: priceSet.id }, - }, - ]) + adminHeaders + ) + ).data.shipping_option const { errors } = await addShippingMethodToCartWorkflow( appContainer ).run({ - input: { - options: [{ id: shippingOption.id }], - cart_id: cart.id, - }, + input: { options: [{ id: shippingOption.id }], cart_id: cart.id }, throwOnError: false, }) @@ -1737,97 +1670,62 @@ medusaIntegrationTestRunner({ }) it("should add shipping method with custom data", async () => { - const stockLocation = ( + const shippingOption = ( await api.post( - `/admin/stock-locations`, - { name: "test location" }, + `/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", + }, + rules: [ + { + operator: RuleOperator.EQ, + attribute: "is_return", + value: "false", + }, + { + operator: RuleOperator.EQ, + attribute: "enabled_in_store", + value: "true", + }, + ], + prices: [{ amount: 3000, currency_code: "usd" }], + }, adminHeaders ) - ).data.stock_location - - await remoteLink.create([ - { - [Modules.STOCK_LOCATION]: { - stock_location_id: stockLocation.id, - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: fulfillmentSet.id, - }, - }, - ]) - - await api.post( - `/admin/stock-locations/${stockLocation.id}/fulfillment-providers`, - { add: ["manual_test-provider"] }, - adminHeaders - ) + ).data.shipping_option - 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", + await addShippingMethodToCartWorkflow(appContainer).run({ + input: { + options: [{ id: shippingOption.id, data: { test: "test" } }], + cart_id: cart.id, }, - 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 }, - }, - ]) - - await addShippingMethodToCartWorkflow(appContainer).run({ - input: { - options: [{ id: shippingOption.id, data: { test: "test" } }], - cart_id: cart.id, - }, - }) - - cart = await cartModuleService.retrieveCart(cart.id, { - select: ["id"], - relations: ["shipping_methods"], - }) + cart = ( + await api.get( + `/store/carts/${cart.id}?fields=+shipping_methods.data`, + storeHeaders + ) + ).data.cart expect(cart).toEqual( expect.objectContaining({ id: cart.id, shipping_methods: [ - { - id: expect.any(String), - cart_id: cart.id, - description: null, + expect.objectContaining({ amount: 3000, - raw_amount: { - value: "3000", - precision: 20, - }, - metadata: null, is_tax_inclusive: true, - name: "Test shipping option", data: { test: "test" }, shipping_option_id: shippingOption.id, - deleted_at: null, - updated_at: expect.any(Date), - created_at: expect.any(Date), - }, + }), ], }) ) @@ -1835,123 +1733,146 @@ medusaIntegrationTestRunner({ }) describe("listShippingOptionsForCartWorkflow", () => { + let cart + let shippingProfile + let fulfillmentSet let region + let stockLocation beforeEach(async () => { - region = await regionModuleService.createRegions({ - name: "US", - currency_code: "usd", - }) - }) + region = ( + await api.post( + "/admin/regions", + { + name: "test-region", + currency_code: "usd", + countries: ["us"], + }, + adminHeaders + ) + ).data.region - it("should list shipping options for cart", async () => { - const salesChannel = await scModuleService.createSalesChannels({ - name: "Webshop", - }) + stockLocation = ( + await api.post( + `/admin/stock-locations`, + { name: "test location" }, + adminHeaders + ) + ).data.stock_location - const location = await stockLocationModule.createStockLocations({ - name: "Europe", - }) + await api.post( + `/admin/stock-locations/${stockLocation.id}/sales-channels`, + { add: [salesChannel.id] }, + adminHeaders + ) - let cart = await cartModuleService.createCarts({ - currency_code: "usd", - region_id: region.id, - sales_channel_id: salesChannel.id, - shipping_address: { - city: "CPH", - province: "Sjaelland", - country_code: "dk", - }, - }) + shippingProfile = ( + await api.post( + `/admin/shipping-profiles`, + { name: "Test", type: "default" }, + adminHeaders + ) + ).data.shipping_profile - const shippingProfile = - await fulfillmentModule.createShippingProfiles({ - name: "Test", - type: "default", - }) + 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 - const fulfillmentSet = await fulfillmentModule.createFulfillmentSets({ - name: "Test", - type: "test-type", - service_zones: [ + fulfillmentSet = ( + await api.post( + `/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`, { name: "Test", - geo_zones: [ - { - type: "country", - country_code: "dk", - }, - ], + geo_zones: [{ type: "country", country_code: "us" }], }, - ], - }) + adminHeaders + ) + ).data.fulfillment_set - 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", - }, - }) + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-providers`, + { add: ["manual_test-provider"] }, + adminHeaders + ) - const priceSet = await pricingModule.createPriceSets({ - prices: [ + cart = ( + await api.post( + `/store/carts`, { - amount: 3000, currency_code: "usd", - }, - ], - }) - - await remoteLink.create([ - { - [Modules.SALES_CHANNEL]: { + region_id: region.id, sales_channel_id: salesChannel.id, }, - [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, - }, - }, + storeHeaders + ) + ).data.cart + + await api.post( + "/admin/price-preferences", { - [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: fulfillmentSet.id, - }, + attribute: "currency_code", + value: "usd", + is_tax_inclusive: true, }, - { - [Modules.FULFILLMENT]: { - shipping_option_id: shippingOption.id, - }, - [Modules.PRICING]: { - price_set_id: priceSet.id, + adminHeaders + ) + }) + + it("should list shipping options for cart", 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 cartModuleService.retrieveCart(cart.id, { - select: ["id"], - relations: ["shipping_address"], - }) + cart = (await api.get(`/store/carts/${cart.id}`, storeHeaders)).data + .cart const { result } = await listShippingOptionsForCartWorkflow( appContainer - ).run({ - input: { - cart_id: cart.id, - }, - }) + ).run({ input: { cart_id: cart.id } }) expect(result).toEqual([ expect.objectContaining({ amount: 3000, - name: "Test shipping option", id: shippingOption.id, }), ]) @@ -1962,43 +1883,15 @@ medusaIntegrationTestRunner({ name: "Webshop", }) - const location = await stockLocationModule.createStockLocations({ - name: "Europe", - }) - let cart = await cartModuleService.createCarts({ currency_code: "usd", region_id: region.id, sales_channel_id: salesChannel.id, shipping_address: { - city: "CPH", - province: "Sjaelland", - country_code: "dk", + country_code: "us", }, }) - 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: "dk", - }, - ], - }, - ], - }) - const shippingOption = await fulfillmentModule.createShippingOptions([ { name: "Return shipping option", @@ -2057,12 +1950,12 @@ medusaIntegrationTestRunner({ sales_channel_id: salesChannel.id, }, [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, + stock_location_id: stockLocation.id, }, }, { [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, + stock_location_id: stockLocation.id, }, [Modules.FULFILLMENT]: { fulfillment_set_id: fulfillmentSet.id, @@ -2116,88 +2009,49 @@ medusaIntegrationTestRunner({ name: "Webshop", }) - const location = await stockLocationModule.createStockLocations({ - name: "Europe", - }) - - let cart = await cartModuleService.createCarts({ - currency_code: "usd", - region_id: region.id, - sales_channel_id: salesChannel.id, - shipping_address: { - city: "CPH", - province: "Sjaelland", - country_code: "dk", - }, - }) - - 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", - }, - ], - }, - ], - }) - - 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", - }, - }) - - const priceSet = await pricingModule.createPriceSets({ - prices: [ - { - amount: 3000, - currency_code: "usd", - }, - ], - }) + await api.post( + `/store/carts/${cart.id}`, + { sales_channel_id: salesChannel.id }, + storeHeaders + ) - await remoteLink.create([ - { - [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: fulfillmentSet.id, - }, - }, + await api.post( + `/admin/shipping-options`, { - [Modules.FULFILLMENT]: { - shipping_option_id: shippingOption.id, - }, - [Modules.PRICING]: { - price_set_id: priceSet.id, + 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 + ) - cart = await cartModuleService.retrieveCart(cart.id, { - select: ["id"], - relations: ["shipping_address"], - }) + cart = (await api.get(`/store/carts/${cart.id}`, storeHeaders)).data + .cart const { result } = await listShippingOptionsForCartWorkflow( appContainer @@ -2207,99 +2061,6 @@ medusaIntegrationTestRunner({ expect(result).toEqual([]) }) - - it("should throw when shipping options are missing prices", async () => { - const salesChannel = await scModuleService.createSalesChannels({ - name: "Webshop", - }) - - const location = await stockLocationModule.createStockLocations({ - name: "Europe", - }) - - let cart = await cartModuleService.createCarts({ - currency_code: "usd", - region_id: region.id, - sales_channel_id: salesChannel.id, - shipping_address: { - city: "CPH", - province: "Sjaelland", - country_code: "dk", - }, - }) - - 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: "dk", - }, - ], - }, - ], - }) - - 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", - }, - }) - - await remoteLink.create([ - { - [Modules.SALES_CHANNEL]: { - sales_channel_id: salesChannel.id, - }, - [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, - }, - }, - { - [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: fulfillmentSet.id, - }, - }, - ]) - - cart = await cartModuleService.retrieveCart(cart.id, { - select: ["id"], - relations: ["shipping_address"], - }) - - const { errors } = await listShippingOptionsForCartWorkflow( - appContainer - ).run({ - input: { cart_id: cart.id }, - throwOnError: false, - }) - - expect(errors).toEqual([ - expect.objectContaining({ - message: `Shipping options with IDs ${shippingOption.id} do not have a price`, - }), - ]) - }) }) describe("updateTaxLinesWorkflow", () => { diff --git a/integration-tests/modules/__tests__/cart/store/carts.spec.ts b/integration-tests/modules/__tests__/cart/store/carts.spec.ts index 42f3d21e228c7..b268a1ff80d47 100644 --- a/integration-tests/modules/__tests__/cart/store/carts.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/carts.spec.ts @@ -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 diff --git a/packages/core/core-flows/src/cart/steps/index.ts b/packages/core/core-flows/src/cart/steps/index.ts index 5f9cddd02f526..de29ab97c2f77 100644 --- a/packages/core/core-flows/src/cart/steps/index.ts +++ b/packages/core/core-flows/src/cart/steps/index.ts @@ -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" - diff --git a/packages/core/core-flows/src/cart/steps/refresh-cart-shipping-methods.ts b/packages/core/core-flows/src/cart/steps/refresh-cart-shipping-methods.ts deleted file mode 100644 index 5c3830838ab32..0000000000000 --- a/packages/core/core-flows/src/cart/steps/refresh-cart-shipping-methods.ts +++ /dev/null @@ -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( - Modules.FULFILLMENT - ) - - const cartModule = container.resolve(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(Modules.CART) - - await cartModule.restoreShippingMethods(shippingMethodsToRestore) - } - } -) diff --git a/packages/core/core-flows/src/cart/steps/remove-shipping-method-from-cart.ts b/packages/core/core-flows/src/cart/steps/remove-shipping-method-from-cart.ts index d9c739abd05cc..3706c8b02c0d1 100644 --- a/packages/core/core-flows/src/cart/steps/remove-shipping-method-from-cart.ts +++ b/packages/core/core-flows/src/cart/steps/remove-shipping-method-from-cart.ts @@ -16,6 +16,10 @@ export const removeShippingMethodFromCartStep = createStep( async (data: RemoveShippingMethodFromCartStepInput, { container }) => { const cartService = container.resolve(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 } diff --git a/packages/core/core-flows/src/cart/steps/update-shipping-methods.ts b/packages/core/core-flows/src/cart/steps/update-shipping-methods.ts new file mode 100644 index 0000000000000..b53b2f981a7d6 --- /dev/null +++ b/packages/core/core-flows/src/cart/steps/update-shipping-methods.ts @@ -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(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) + } +) diff --git a/packages/core/core-flows/src/cart/steps/validate-shipping-options-price.ts b/packages/core/core-flows/src/cart/steps/validate-shipping-options-price.ts new file mode 100644 index 0000000000000..51784e93cdb10 --- /dev/null +++ b/packages/core/core-flows/src/cart/steps/validate-shipping-options-price.ts @@ -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) + } +) diff --git a/packages/core/core-flows/src/cart/utils/fields.ts b/packages/core/core-flows/src/cart/utils/fields.ts index 9d58454953fe6..f3d6f7e69aa78 100644 --- a/packages/core/core-flows/src/cart/utils/fields.ts +++ b/packages/core/core-flows/src/cart/utils/fields.ts @@ -3,6 +3,7 @@ export const cartFieldsForRefreshSteps = [ "currency_code", "quantity", "subtotal", + "total", "item_subtotal", "shipping_subtotal", "region_id", 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 86a2cf16b0799..d097137586e85 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 @@ -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, 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 90a129cf5df48..7050c005adc00 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 @@ -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 || [])] - }) - - const cart = useRemoteQueryStep({ - entry_point: "cart", - fields: cartFieldsForRefreshSteps, - variables: { id: input.cart.id }, - list: false, - }).config({ name: "refetch–cart" }) - - parallelize( - refreshCartShippingMethodsStep({ cart }), - emitEventStep({ - eventName: CartWorkflowEvents.UPDATED, - data: { id: input.cart.id }, - }) - ) - - updateTaxLinesWorkflow.runAsStep({ - input: { - cart_id: input.cart.id, - }, + refreshCartItemsWorkflow.runAsStep({ + input: { cart_id: input.cart.id }, }) - updateCartPromotionsWorkflow.runAsStep({ - input: { - cart_id: input.cart.id, - }, + emitEventStep({ + eventName: CartWorkflowEvents.UPDATED, + data: { id: input.cart.id }, }) - - refreshPaymentCollectionForCartWorkflow.runAsStep({ - input: { - cart_id: input.cart.id, - }, - }) - - return new WorkflowResponse(items) } ) 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 d1069c0e3f809..c2a72e172e3fb 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,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" }) - - return transform({ customerQuery }, ({ customerQuery }) => { - const customer = customerQuery.data[0] + const queryVariables = transform( + { input, fulfillmentSetIds, cart }, + ({ input, fulfillmentSetIds, cart }) => ({ + id: input.option_ids, - if (!isPresent(customer)) { - return [] - } + context: { + is_return: input.is_return ?? false, + enabled_in_store: input.enabled_in_store ?? true, + }, - const { groups = [] } = customer + filters: { + fulfillment_set_id: fulfillmentSetIds, - return groups.map((group) => group.id) - }) - }) + 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, + }, + }, - 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 options = data.shippingOptions.map((shippingOption) => { - const { calculated_price, ...options } = shippingOption ?? {} - - if (options?.id && !isPresent(calculated_price?.calculated_amount)) { - optionsMissingPrices.push(options.id) - } + const shippingOptionsWithPrice = transform( + { shippingOptions }, + ({ shippingOptions }) => + shippingOptions.map((shippingOption) => { + const price = shippingOption.calculated_price - 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) } 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 93c4eadbe02a8..46e1194d86213 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 @@ -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: "refetch–cart" }) - refreshCartShippingMethodsStep({ cart: refetchedCart }) + refreshCartShippingMethodsWorkflow.runAsStep({ + input: { cart_id: cart.id }, + }) updateTaxLinesWorkflow.runAsStep({ input: { cart_id: cart.id }, diff --git a/packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts b/packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts new file mode 100644 index 0000000000000..9d8850686809c --- /dev/null +++ b/packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts @@ -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) + ) + }) + } +) 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 edfe4320338ec..528c4b01a4376 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,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) - - const cart = useRemoteQueryStep({ - entry_point: "cart", - fields: cartFieldsForRefreshSteps, - variables: { id: input.cart.id }, - list: false, - }).config({ name: "refetch–cart" }) + updateLineItemsStepWithSelector(lineItemUpdate) - 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) } ) diff --git a/packages/core/types/src/cart/mutations.ts b/packages/core/types/src/cart/mutations.ts index a8e13860c8ea5..3d51cd8cb6051 100644 --- a/packages/core/types/src/cart/mutations.ts +++ b/packages/core/types/src/cart/mutations.ts @@ -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. */ diff --git a/packages/core/types/src/cart/service.ts b/packages/core/types/src/cart/service.ts index cc2f31532618e..f9f42d41abc7d 100644 --- a/packages/core/types/src/cart/service.ts +++ b/packages/core/types/src/cart/service.ts @@ -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 + /** + * This method updates existing shipping methods. + * + * @param {UpdateShippingMethodDTO[]} data - A list of shipping methods to update + * @returns {Promise} The updated shipping methods. + * + * @example + * const shippingMethods = await cartModuleService.updateShippingMethods([ + * { + * id: "casm_123", + * amount: 2, + * }, + * ]) + */ + updateShippingMethods( + data: UpdateShippingMethodDTO[] + ): Promise + + /** + * This method updates an existing shipping method. + * + * @param {string} shippingMethodId - The shipping methods's ID. + * @param {Partial} 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} The updated shipping method. + * + * @example + * const lineItem = await cartModuleService.updateShippingMethods( + * "casm_123", + * { + * amount: 3000, + * } + * ) + */ + updateShippingMethods( + shippingMethodId: string, + data: Partial, + sharedContext?: Context + ): Promise + /** * This method retrieves a paginated list of line item adjustments based on optional filters and configuration. * diff --git a/packages/medusa/src/api/admin/shipping-options/validators.ts b/packages/medusa/src/api/admin/shipping-options/validators.ts index e63e6215abe95..61a8b59dc495e 100644 --- a/packages/medusa/src/api/admin/shipping-options/validators.ts +++ b/packages/medusa/src/api/admin/shipping-options/validators.ts @@ -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(), }) diff --git a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts index 72f73687f529a..109c23d606148 100644 --- a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts +++ b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts @@ -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,