diff --git a/integration-tests/api/__tests__/admin/shipping-options.js b/integration-tests/api/__tests__/admin/shipping-options.js deleted file mode 100644 index 390723fd04..0000000000 --- a/integration-tests/api/__tests__/admin/shipping-options.js +++ /dev/null @@ -1,595 +0,0 @@ -const path = require("path") -const { ShippingProfile } = require("@medusajs/medusa") - -const setupServer = require("../../../environment-helpers/setup-server") -const startServerWithEnvironment = - require("../../../environment-helpers/start-server-with-environment").default -const { useApi } = require("../../../environment-helpers/use-api") -const { initDb, useDb } = require("../../../environment-helpers/use-db") -const adminSeeder = require("../../../helpers/admin-seeder") -const shippingOptionSeeder = require("../../../helpers/shipping-option-seeder") -const { - simpleShippingOptionFactory, - simpleRegionFactory, -} = require("../../../factories") - -const adminReqConfig = { - headers: { - "x-medusa-access-token": "test_token", - }, -} - -jest.setTimeout(30000) - -describe("/admin/shipping-options", () => { - let medusaProcess - let dbConnection - - beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")) - dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd }) - }) - - afterAll(async () => { - const db = useDb() - await db.shutdown() - medusaProcess.kill() - }) - - describe("POST /admin/shipping-options/:id", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - await shippingOptionSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("should update a shipping option with no existing requirements", async () => { - const api = useApi() - - const payload = { - name: "Test option", - amount: 100, - requirements: [ - { - type: "min_subtotal", - amount: 1, - }, - { - type: "max_subtotal", - amount: 2, - }, - ], - } - - const res = await api.post( - `/admin/shipping-options/test-out`, - payload, - adminReqConfig - ) - - const requirements = res.data.shipping_option.requirements - - expect(res.status).toEqual(200) - expect(requirements.length).toEqual(2) - expect(requirements[0]).toEqual( - expect.objectContaining({ - type: "min_subtotal", - shipping_option_id: "test-out", - amount: 1, - }) - ) - expect(requirements[1]).toEqual( - expect.objectContaining({ - type: "max_subtotal", - shipping_option_id: "test-out", - amount: 2, - }) - ) - }) - - it("should update a shipping option with price_type", async () => { - const api = useApi() - - const payload = { - price_type: "calculated", - requirements: [], - } - - const res = await api - .post(`/admin/shipping-options/test-out`, payload, adminReqConfig) - .catch(console.log) - - expect(res.status).toEqual(200) - expect(res.data.shipping_option).toEqual( - expect.objectContaining({ - price_type: "calculated", - }) - ) - }) - - it("should fail to add a a requirement with an id if it does not exists", async () => { - const api = useApi() - - const payload = { - name: "Test option", - amount: 100, - requirements: [ - { - id: "not_allowed", - type: "min_subtotal", - amount: 1, - }, - { - id: "really_not_allowed", - type: "max_subtotal", - amount: 2, - }, - ], - } - - const res = await api - .post(`/admin/shipping-options/test-out`, payload, adminReqConfig) - .catch((err) => { - return err.response - }) - - expect(res.status).toEqual(400) - expect(res.data.message).toEqual( - "Shipping option requirement with id not_allowed does not exist" - ) - }) - - it("should successfully updates a set of existing requirements", async () => { - const api = useApi() - - const payload = { - requirements: [ - { - id: "option-req", - type: "min_subtotal", - amount: 15, - }, - { - id: "option-req-2", - type: "max_subtotal", - amount: 20, - }, - ], - amount: 200, - } - - const res = await api - .post( - `/admin/shipping-options/test-option-req`, - payload, - adminReqConfig - ) - .catch((err) => { - console.log(err.response.data.message) - }) - - expect(res.status).toEqual(200) - }) - - it("should successfully updates a set of existing requirements by updating one and deleting the other", async () => { - const api = useApi() - - const payload = { - requirements: [ - { - id: "option-req", - type: "min_subtotal", - amount: 15, - }, - ], - } - - const res = await api - .post( - `/admin/shipping-options/test-option-req`, - payload, - adminReqConfig - ) - .catch((err) => { - console.log(err.response.data.message) - }) - - expect(res.status).toEqual(200) - }) - - it("should successfully updates a set of requirements because max. subtotal >= min. subtotal", async () => { - const api = useApi() - - const payload = { - requirements: [ - { - id: "option-req", - type: "min_subtotal", - amount: 150, - }, - { - id: "option-req-2", - type: "max_subtotal", - amount: 200, - }, - ], - } - - const res = await api - .post( - `/admin/shipping-options/test-option-req`, - payload, - adminReqConfig - ) - .catch((err) => { - console.log(err.response.data.message) - }) - - expect(res.status).toEqual(200) - expect(res.data.shipping_option.requirements[0].amount).toEqual(150) - expect(res.data.shipping_option.requirements[1].amount).toEqual(200) - }) - - it("should fail to updates a set of requirements because max. subtotal <= min. subtotal", async () => { - const api = useApi() - - const payload = { - requirements: [ - { - id: "option-req", - type: "min_subtotal", - amount: 1500, - }, - { - id: "option-req-2", - type: "max_subtotal", - amount: 200, - }, - ], - } - - const res = await api - .post( - `/admin/shipping-options/test-option-req`, - payload, - adminReqConfig - ) - .catch((err) => { - return err.response - }) - - expect(res.status).toEqual(400) - expect(res.data.message).toEqual( - "Max. subtotal must be greater than Min. subtotal" - ) - }) - }) - - describe("POST /admin/shipping-options", () => { - let payload - - beforeEach(async () => { - await adminSeeder(dbConnection) - await shippingOptionSeeder(dbConnection) - - const api = useApi() - await api.post( - `/admin/regions/region`, - { - fulfillment_providers: ["test-ful"], - }, - adminReqConfig - ) - - const manager = dbConnection.manager - const defaultProfile = await manager.findOne(ShippingProfile, { - where: { - type: ShippingProfile.default, - }, - }) - - payload = { - name: "Test option", - amount: 100, - price_type: "flat_rate", - region_id: "region", - provider_id: "test-ful", - data: {}, - profile_id: defaultProfile.id, - } - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("should create a shipping option with requirements", async () => { - const api = useApi() - payload.requirements = [ - { - type: "max_subtotal", - amount: 2, - }, - { - type: "min_subtotal", - amount: 1, - }, - ] - - const res = await api.post( - `/admin/shipping-options`, - payload, - adminReqConfig - ) - - expect(res.status).toEqual(200) - expect(res.data.shipping_option.requirements.length).toEqual(2) - }) - - it("should create a shipping option with no requirements", async () => { - const api = useApi() - const res = await api.post( - `/admin/shipping-options`, - payload, - adminReqConfig - ) - - expect(res.status).toEqual(200) - expect(res.data.shipping_option.requirements.length).toEqual(0) - }) - - it("should fail on same requirement types", async () => { - const api = useApi() - payload.requirements = [ - { - type: "max_subtotal", - amount: 2, - }, - { - type: "max_subtotal", - amount: 1, - }, - ] - - try { - await api.post(`/admin/shipping-options`, payload, adminReqConfig) - } catch (error) { - expect(error.response.data.message).toEqual( - "Only one requirement of each type is allowed" - ) - } - }) - - it("should fail when min_subtotal > max_subtotal", async () => { - const api = useApi() - payload.requirements = [ - { - type: "max_subtotal", - amount: 2, - }, - { - type: "min_subtotal", - amount: 4, - }, - ] - - try { - await api.post(`/admin/shipping-options`, payload, adminReqConfig) - } catch (error) { - expect(error.response.data.message).toEqual( - "Max. subtotal must be greater than Min. subtotal" - ) - } - }) - }) - describe("GET /admin/shipping-options", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - await shippingOptionSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("should list shipping options", async () => { - const api = useApi() - const res = await api.get(`/admin/shipping-options`, adminReqConfig) - - expect(res.status).toEqual(200) - }) - - it("should list admin only shipping options", async () => { - const api = useApi() - const res = await api.get( - `/admin/shipping-options?admin_only=true`, - adminReqConfig - ) - - expect(res.status).toEqual(200) - expect(res.data.shipping_options).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "test-option-req-admin-only", - admin_only: true, - }), - ]) - ) - }) - - it("should list return shipping options", async () => { - const api = useApi() - const res = await api.get( - `/admin/shipping-options?is_return=true`, - adminReqConfig - ) - - expect(res.status).toEqual(200) - expect(res.data.shipping_options).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "test-option-req-return", - is_return: true, - }), - ]) - ) - }) - - it("should list shipping options without return and admin options", async () => { - const api = useApi() - const res = await api.get( - `/admin/shipping-options?is_return=false&admin_only=true`, - adminReqConfig - ) - - expect(res.status).toEqual(200) - expect(res.data.shipping_options).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "test-option-req-return", - is_return: true, - }), - expect.objectContaining({ - id: "test-option-req-admin-only", - admin_only: true, - }), - ]) - ) - }) - }) -}) - -describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/shipping-options", () => { - let medusaProcess - let dbConnection - - beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")) - const [process, connection] = await startServerWithEnvironment({ - cwd, - env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true }, - }) - dbConnection = connection - medusaProcess = process - }) - - afterAll(async () => { - const db = useDb() - await db.shutdown() - - medusaProcess.kill() - }) - - describe("POST /admin/shipping-options", () => { - const shippingOptionIncludesTaxId = "shipping-option-1-includes-tax" - let region - - beforeEach(async () => { - try { - await adminSeeder(dbConnection) - region = await simpleRegionFactory(dbConnection, { - id: "region", - countries: ["fr"], - }) - await simpleShippingOptionFactory(dbConnection, { - id: shippingOptionIncludesTaxId, - region_id: region.id, - }) - } catch (err) { - console.log(err) - throw err - } - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("should creates a shipping option that includes tax", async () => { - const api = useApi() - - const defaultProfile = await dbConnection.manager.findOne( - ShippingProfile, - { - where: { - type: ShippingProfile.default, - }, - } - ) - - const payload = { - name: "Test option", - amount: 100, - price_type: "flat_rate", - region_id: region.id, - provider_id: "test-ful", - data: {}, - profile_id: defaultProfile.id, - includes_tax: true, - } - - const response = await api - .post("/admin/shipping-options", payload, adminReqConfig) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.shipping_option).toEqual( - expect.objectContaining({ - id: expect.any(String), - includes_tax: true, - }) - ) - }) - - it("should update a shipping option that include_tax", async () => { - const api = useApi() - - let response = await api - .get( - `/admin/shipping-options/${shippingOptionIncludesTaxId}`, - adminReqConfig - ) - .catch((err) => { - console.log(err) - }) - - expect(response.data.shipping_option.includes_tax).toBe(false) - - const payload = { - requirements: [ - { - type: "min_subtotal", - amount: 1, - }, - { - type: "max_subtotal", - amount: 2, - }, - ], - includes_tax: true, - } - - response = await api - .post( - `/admin/shipping-options/${shippingOptionIncludesTaxId}`, - payload, - adminReqConfig - ) - .catch((err) => { - console.log(err) - }) - - expect(response.data.shipping_option.includes_tax).toBe(true) - }) - }) -}) diff --git a/integration-tests/api/__tests__/admin/shipping-profile.spec.ts b/integration-tests/api/__tests__/admin/shipping-profile.spec.ts deleted file mode 100644 index 358cc67438..0000000000 --- a/integration-tests/api/__tests__/admin/shipping-profile.spec.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { medusaIntegrationTestRunner } from "medusa-test-utils" -import { breaking } from "../../../helpers/breaking" -import { - adminHeaders, - createAdminUser, -} from "../../../helpers/create-admin-user" - -let { - simpleProductFactory, - simpleShippingOptionFactory, - simpleShippingProfileFactory, -} = {} - -jest.setTimeout(30000) - -medusaIntegrationTestRunner({ - env: { - // MEDUSA_FF_MEDUSA_V2: true, - }, - testSuite: ({ dbConnection, getContainer, api }) => { - let appContainer - - beforeAll(() => { - ;({ - simpleProductFactory, - simpleShippingOptionFactory, - simpleShippingProfileFactory, - } = require("../../../factories")) - }) - - beforeEach(async () => { - appContainer = getContainer() - - await createAdminUser(dbConnection, adminHeaders, appContainer) - }) - - describe("Admin - Shipping Profiles", () => { - // TODO: Missing update tests - it("should test the entire lifecycle of a shipping profile", async () => { - const payload = { - name: "test-profile-2023", - type: "custom", - } - - const { - data: { shipping_profile }, - status, - } = await api.post("/admin/shipping-profiles", payload, adminHeaders) - - expect(status).toEqual(200) - expect(shipping_profile).toEqual( - expect.objectContaining({ - id: expect.any(String), - created_at: expect.any(String), - updated_at: expect.any(String), - ...payload, - }) - ) - - const { - data: { shipping_profiles }, - } = await api.get("/admin/shipping-profiles", adminHeaders) - - // In V1, response should contain default and gift_card profiles too - expect(shipping_profiles.length).toEqual( - breaking( - () => 3, - () => 1 - ) - ) - - const { - data: { shipping_profile: retrievedProfile }, - } = await api.get( - `/admin/shipping-profiles/${shipping_profile.id}`, - adminHeaders - ) - - expect(status).toEqual(200) - expect(retrievedProfile).toEqual( - expect.objectContaining({ - id: shipping_profile.id, - updated_at: expect.any(String), - created_at: expect.any(String), - }) - ) - - const { data } = await api.delete( - `/admin/shipping-profiles/${shipping_profile.id}`, - adminHeaders - ) - - expect(data).toEqual({ - id: retrievedProfile.id, - object: "shipping_profile", - deleted: true, - }) - - await api - .get(`/admin/shipping-profiles/${shipping_profile.id}`, adminHeaders) - .catch((err) => { - expect(err.response.status).toEqual(404) - }) - }) - }) - - describe("POST /admin/shipping-profiles", () => { - // TODO: There is no invalid types in V2 yet, so this will fail - it("fails to create a shipping profile with invalid type", async () => { - expect.assertions(2) - - const payload = { - name: "test-profile-2023", - type: "invalid", - } - - await api - .post("/admin/shipping-profiles", payload, adminHeaders) - .catch((err) => { - expect(err.response.status).toEqual(400) - expect(err.response.data.message).toEqual( - "type must be one of 'default', 'custom', 'gift_card'" - ) - }) - }) - - it("updates a shipping profile", async () => { - // TODO: Update is not added yet - const testProducts = await Promise.all( - [...Array(5).keys()].map(async () => { - return await simpleProductFactory(dbConnection) - }) - ) - - const testShippingOptions = await Promise.all( - [...Array(5).keys()].map(async () => { - return await simpleShippingOptionFactory(dbConnection) - }) - ) - - const payload = { - name: "test-profile-2023", - type: "custom", - metadata: { - my_key: "my_value", - }, - } - - const { - data: { shipping_profile: created }, - } = await api.post("/admin/shipping-profiles", payload, adminHeaders) - - const updatePayload = { - name: "test-profile-2023-updated", - products: testProducts.map((p) => p.id), - shipping_options: testShippingOptions.map((o) => o.id), - metadata: { - my_key: "", - my_new_key: "my_new_value", - }, - } - - const { - data: { shipping_profile }, - status, - } = await api.post( - `/admin/shipping-profiles/${created.id}`, - updatePayload, - adminHeaders - ) - - expect(status).toEqual(200) - expect(shipping_profile).toEqual( - expect.objectContaining({ - name: "test-profile-2023-updated", - created_at: expect.any(String), - updated_at: expect.any(String), - metadata: { - my_new_key: "my_new_value", - }, - deleted_at: null, - type: "custom", - }) - ) - - const { - data: { products }, - } = await api.get(`/admin/products`, adminHeaders) - - expect(products.length).toEqual(5) - expect(products).toEqual( - expect.arrayContaining( - testProducts.map((p) => { - return expect.objectContaining({ - id: p.id, - profile_id: shipping_profile.id, - }) - }) - ) - ) - - const { - data: { shipping_options }, - } = await api.get(`/admin/shipping-options`, adminHeaders) - - const numberOfShippingOptionsWithProfile = shipping_options.filter( - (so) => so.profile_id === shipping_profile.id - ).length - - expect(numberOfShippingOptionsWithProfile).toEqual(5) - expect(shipping_options).toEqual( - expect.arrayContaining( - testShippingOptions.map((o) => { - return expect.objectContaining({ - id: o.id, - profile_id: shipping_profile.id, - }) - }) - ) - ) - }) - }) - - describe("DELETE /admin/shipping-profiles", () => { - it("deletes a shipping profile", async () => { - expect.assertions(2) - - const profile = await simpleShippingProfileFactory(dbConnection) - - const { status } = await api.delete( - `/admin/shipping-profiles/${profile.id}`, - adminHeaders - ) - - expect(status).toEqual(200) - await api - .get(`/admin/shipping-profiles/${profile.id}`, adminHeaders) - .catch((err) => { - expect(err.response.status).toEqual(404) - }) - }) - }) - }, -}) diff --git a/integration-tests/modules/__tests__/shipping-options/admin/shipping-options.spec.ts b/integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts similarity index 63% rename from integration-tests/modules/__tests__/shipping-options/admin/shipping-options.spec.ts rename to integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts index 3e188eb37b..5b374bf285 100644 --- a/integration-tests/modules/__tests__/shipping-options/admin/shipping-options.spec.ts +++ b/integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts @@ -1,25 +1,16 @@ -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { - IFulfillmentModuleService, - IRegionModuleService, -} from "@medusajs/types" import { RuleOperator } from "@medusajs/utils" import { medusaIntegrationTestRunner } from "medusa-test-utils" -import { createAdminUser } from "../../../../helpers/create-admin-user" +import { + adminHeaders, + createAdminUser, +} from "../../../../helpers/create-admin-user" jest.setTimeout(50000) -const env = { MEDUSA_FF_MEDUSA_V2: true } -const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } } - +// BREAKING: Shipping setup has significantly changed from v1, exact migration needs more investigation medusaIntegrationTestRunner({ - env, testSuite: ({ dbConnection, getContainer, api }) => { describe("Admin: Shipping Option API", () => { - let appContainer - let fulfillmentModule: IFulfillmentModuleService - let regionService: IRegionModuleService - let shippingProfile let fulfillmentSet let region @@ -30,38 +21,64 @@ medusaIntegrationTestRunner({ value: "old value", } - beforeAll(async () => { - appContainer = getContainer() - fulfillmentModule = appContainer.resolve( - ModuleRegistrationName.FULFILLMENT - ) - regionService = appContainer.resolve(ModuleRegistrationName.REGION) - }) - beforeEach(async () => { + const appContainer = getContainer() await createAdminUser(dbConnection, adminHeaders, appContainer) - shippingProfile = await fulfillmentModule.createShippingProfiles({ - name: "Test", - type: "default", - }) + shippingProfile = ( + await api.post( + `/admin/shipping-profiles`, + { + name: "Test", + type: "default", + }, + adminHeaders + ) + ).data.shipping_profile - fulfillmentSet = await fulfillmentModule.create({ - name: "Test", - type: "test-type", - service_zones: [ + let location = ( + await api.post( + `/admin/stock-locations`, + { + name: "Test location", + }, + adminHeaders + ) + ).data.stock_location + + location = ( + await api.post( + `/admin/stock-locations/${location.id}/fulfillment-sets?fields=*fulfillment_sets`, + { + name: "Test", + type: "test-type", + }, + adminHeaders + ) + ).data.stock_location + + fulfillmentSet = ( + await api.post( + `/admin/fulfillment-sets/${location.fulfillment_sets[0].id}/service-zones`, { name: "Test", geo_zones: [{ type: "country", country_code: "us" }], }, - ], - }) + adminHeaders + ) + ).data.fulfillment_set - region = await regionService.create({ - name: "Test region", - countries: ["FR"], - currency_code: "eur", - }) + region = ( + await api.post( + `/admin/regions`, + { + name: "Test region", + countries: ["FR"], + currency_code: "eur", + }, + adminHeaders + ) + ).data.region }) describe("POST /admin/shipping-options", () => { @@ -363,6 +380,154 @@ medusaIntegrationTestRunner({ expect(shippingOptions.data.shipping_options).toHaveLength(0) }) }) + + describe("POST /admin/shipping-options/:id/rules/batch", () => { + it.skip("should throw error when required params are missing", async () => { + const shippingOptionPayload = { + 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: [ + { + currency_code: "usd", + amount: 1000, + }, + { + region_id: region.id, + amount: 1000, + }, + ], + rules: [shippingOptionRule], + } + + const shippingOption = ( + await api.post( + `/admin/shipping-options`, + shippingOptionPayload, + adminHeaders + ) + ).data.shipping_option + + const { response } = await api + .post( + `/admin/shipping-options/${shippingOption.id}/rules/batch`, + { + create: [{ operator: RuleOperator.EQ, value: "new_value" }], + }, + adminHeaders + ) + .catch((e) => e) + + expect(response.status).toEqual(400) + expect(response.data).toEqual({ + type: "invalid_data", + message: + "attribute must be a string, attribute should not be empty", + }) + }) + + it("should throw error when shipping option does not exist", async () => { + const { response } = await api + .post( + `/admin/shipping-options/does-not-exist/rules/batch`, + { + create: [ + { attribute: "new_attr", operator: "eq", value: "new value" }, + ], + }, + adminHeaders + ) + .catch((e) => e) + + expect(response.status).toEqual(404) + expect(response.data).toEqual({ + type: "not_found", + message: + "You tried to set relationship shipping_option_id: does-not-exist, but such entity does not exist", + }) + }) + + it.skip("should add rules to a shipping option successfully", async () => { + const shippingOptionPayload = { + 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: [ + { + currency_code: "usd", + amount: 1000, + }, + { + region_id: region.id, + amount: 1000, + }, + ], + rules: [shippingOptionRule], + } + + const shippingOption = ( + await api.post( + `/admin/shipping-options`, + shippingOptionPayload, + adminHeaders + ) + ).data.shipping_option + + const response = await api.post( + `/admin/shipping-options/${shippingOption.id}/rules/batch`, + { + create: [ + { operator: "eq", attribute: "new_attr", value: "new value" }, + ], + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + + const updatedShippingOption = ( + await api.get( + `/admin/shipping-options/${shippingOption.id}`, + adminHeaders + ) + ).data.shipping_option + expect(updatedShippingOption).toEqual( + expect.objectContaining({ + id: shippingOption.id, + rules: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + operator: "eq", + attribute: "old_attr", + value: "old value", + shipping_option_id: shippingOption.id, + }), + expect.objectContaining({ + id: expect.any(String), + operator: "eq", + attribute: "new_attr", + value: "new value", + shipping_option_id: shippingOption.id, + }), + ]), + }) + ) + }) + }) }) }, }) diff --git a/integration-tests/http/__tests__/shipping-profile/admin/shipping-profile.spec.ts b/integration-tests/http/__tests__/shipping-profile/admin/shipping-profile.spec.ts new file mode 100644 index 0000000000..f73d8d75ec --- /dev/null +++ b/integration-tests/http/__tests__/shipping-profile/admin/shipping-profile.spec.ts @@ -0,0 +1,114 @@ +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { + adminHeaders, + createAdminUser, +} from "../../../../helpers/create-admin-user" + +jest.setTimeout(30000) + +medusaIntegrationTestRunner({ + testSuite: ({ dbConnection, getContainer, api }) => { + beforeEach(async () => { + const appContainer = getContainer() + await createAdminUser(dbConnection, adminHeaders, appContainer) + }) + + describe("Admin - Shipping Profiles", () => { + it("should test the entire lifecycle of a shipping profile", async () => { + const payload = { + name: "test-profile-2023", + type: "custom", + } + + // Create + const { + data: { shipping_profile }, + status, + } = await api.post("/admin/shipping-profiles", payload, adminHeaders) + + expect(status).toEqual(200) + expect(shipping_profile).toEqual( + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + ...payload, + }) + ) + + // List + const { + data: { shipping_profiles }, + } = await api.get("/admin/shipping-profiles", adminHeaders) + + expect(shipping_profiles.length).toEqual(1) + + // Retrieve + const { + data: { shipping_profile: retrievedProfile }, + } = await api.get( + `/admin/shipping-profiles/${shipping_profile.id}`, + adminHeaders + ) + + expect(status).toEqual(200) + expect(retrievedProfile).toEqual( + expect.objectContaining({ + id: shipping_profile.id, + updated_at: expect.any(String), + created_at: expect.any(String), + }) + ) + + // update + const { + data: { shipping_profile: updatedProfile }, + } = await api.post( + `/admin/shipping-profiles/${shipping_profile.id}`, + { + name: "test-profile-updated", + type: "express", + metadata: { + my_new_key: "my_new_value", + }, + }, + adminHeaders + ) + + expect(status).toEqual(200) + expect(updatedProfile).toEqual( + expect.objectContaining({ + id: shipping_profile.id, + name: "test-profile-updated", + type: "express", + metadata: { + my_new_key: "my_new_value", + }, + updated_at: expect.any(String), + created_at: expect.any(String), + }) + ) + + // Delete + const { data } = await api.delete( + `/admin/shipping-profiles/${shipping_profile.id}`, + adminHeaders + ) + + expect(data).toEqual({ + id: retrievedProfile.id, + object: "shipping_profile", + deleted: true, + }) + + await api + .get(`/admin/shipping-profiles/${shipping_profile.id}`, adminHeaders) + .catch((err) => { + expect(err.response.status).toEqual(404) + }) + }) + }) + + // TODO: Associate products with shipping profiles + }, +}) diff --git a/integration-tests/http/medusa-config.js b/integration-tests/http/medusa-config.js index 7ef71198f8..c719ef6ffd 100644 --- a/integration-tests/http/medusa-config.js +++ b/integration-tests/http/medusa-config.js @@ -1,4 +1,4 @@ -const { defineConfig } = require("@medusajs/utils") +const { defineConfig, Modules } = require("@medusajs/utils") const DB_HOST = process.env.DB_HOST const DB_USERNAME = process.env.DB_USERNAME const DB_PASSWORD = process.env.DB_PASSWORD @@ -7,6 +7,15 @@ const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}` process.env.DATABASE_URL = DB_URL process.env.LOG_LEVEL = "error" +const customFulfillmentProvider = { + resolve: "@medusajs/fulfillment-manual", + options: { + config: { + "test-provider": {}, + }, + }, +} + module.exports = defineConfig({ admin: { disable: true, @@ -16,4 +25,12 @@ module.exports = defineConfig({ jwtSecret: "test", }, }, + modules: { + [Modules.FULFILLMENT]: { + /** @type {import('@medusajs/fulfillment').FulfillmentModuleOptions} */ + options: { + providers: [customFulfillmentProvider], + }, + }, + }, }) diff --git a/integration-tests/modules/__tests__/shipping-options/admin/shipping-option-rules.spec.ts b/integration-tests/modules/__tests__/shipping-options/admin/shipping-option-rules.spec.ts deleted file mode 100644 index 8ec9246293..0000000000 --- a/integration-tests/modules/__tests__/shipping-options/admin/shipping-option-rules.spec.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IFulfillmentModuleService } from "@medusajs/types" -import { RuleOperator } from "@medusajs/utils" -import { medusaIntegrationTestRunner } from "medusa-test-utils" -import { createAdminUser } from "../../../../helpers/create-admin-user" - -jest.setTimeout(50000) - -const env = { MEDUSA_FF_MEDUSA_V2: true } -const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } } - -medusaIntegrationTestRunner({ - env, - testSuite: ({ dbConnection, getContainer, api }) => { - describe("Admin: Shipping Option Rules API", () => { - let appContainer - let shippingOption - let fulfillmentModule: IFulfillmentModuleService - const shippingOptionRule = { - operator: RuleOperator.EQ, - attribute: "old_attr", - value: "old value", - } - - beforeAll(async () => { - appContainer = getContainer() - fulfillmentModule = appContainer.resolve( - ModuleRegistrationName.FULFILLMENT - ) - }) - - beforeEach(async () => { - await createAdminUser(dbConnection, adminHeaders, appContainer) - - const shippingProfile = await fulfillmentModule.createShippingProfiles({ - name: "Test", - type: "default", - }) - - const fulfillmentSet = await fulfillmentModule.create({ - name: "Test", - type: "test-type", - service_zones: [ - { - name: "Test", - geo_zones: [{ type: "country", country_code: "us" }], - }, - ], - }) - - 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: [shippingOptionRule], - }) - }) - - describe("POST /admin/shipping-options/:id/rules/batch", () => { - it("should throw error when required params are missing", async () => { - const { response } = await api - .post( - `/admin/shipping-options/${shippingOption.id}/rules/batch`, - { - create: [{ operator: RuleOperator.EQ, value: "new_value" }], - }, - adminHeaders - ) - .catch((e) => e) - - expect(response.status).toEqual(400) - expect(response.data).toEqual({ - type: "invalid_data", - message: - "attribute must be a string, attribute should not be empty", - }) - }) - - it.only("should throw error when shipping option does not exist", async () => { - const { response } = await api - .post( - `/admin/shipping-options/does-not-exist/rules/batch`, - { - create: [ - { attribute: "new_attr", operator: "eq", value: "new value" }, - ], - }, - adminHeaders - ) - .catch((e) => e) - - expect(response.status).toEqual(404) - expect(response.data).toEqual({ - type: "not_found", - message: - "You tried to set relationship shipping_option_id: does-not-exist, but such entity does not exist", - }) - }) - - it("should add rules to a shipping option successfully", async () => { - const response = await api.post( - `/admin/shipping-options/${shippingOption.id}/rules/batch`, - { - create: [ - { operator: "eq", attribute: "new_attr", value: "new value" }, - ], - }, - adminHeaders - ) - - expect(response.status).toEqual(200) - - const updatedShippingOption = ( - await api.get( - `/admin/shipping-options/${shippingOption.id}`, - adminHeaders - ) - ).data.shipping_option - expect(updatedShippingOption).toEqual( - expect.objectContaining({ - id: shippingOption.id, - rules: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - operator: "eq", - attribute: "old_attr", - value: "old value", - shipping_option_id: shippingOption.id, - }), - expect.objectContaining({ - id: expect.any(String), - operator: "eq", - attribute: "new_attr", - value: "new value", - shipping_option_id: shippingOption.id, - }), - ]), - }) - ) - }) - }) - - describe("POST /admin/shipping-options/:id/rules/batch", () => { - it("should throw error when required params are missing", async () => { - const { response } = await api - .post( - `/admin/shipping-options/${shippingOption.id}/rules/batch`, - {}, - adminHeaders - ) - .catch((e) => e) - - expect(response.status).toEqual(400) - expect(response.data).toEqual({ - type: "invalid_data", - message: - "each value in rule_ids must be a string, rule_ids should not be empty", - }) - }) - - it("should throw error when shipping option does not exist", async () => { - const { response } = await api - .post( - `/admin/shipping-options/does-not-exist/rules/batch`, - { delete: ["test"] }, - adminHeaders - ) - .catch((e) => e) - - expect(response.status).toEqual(404) - expect(response.data).toEqual({ - type: "not_found", - message: "ShippingOption with id: does-not-exist was not found", - }) - }) - - it("should add rules to a shipping option successfully", async () => { - const response = await api.post( - `/admin/shipping-options/${shippingOption.id}/rules/batch`, - { - delete: [shippingOption.rules[0].id], - }, - adminHeaders - ) - - expect(response.status).toEqual(200) - - const updatedShippingOption = ( - await api.get( - `/admin/shipping-options/${shippingOption.id}`, - adminHeaders - ) - ).data.shipping_option - expect(updatedShippingOption).toEqual( - expect.objectContaining({ - id: shippingOption.id, - rules: [], - }) - ) - }) - }) - }) - }, -}) diff --git a/packages/core/core-flows/src/fulfillment/steps/index.ts b/packages/core/core-flows/src/fulfillment/steps/index.ts index 29161d709a..a5f3485c52 100644 --- a/packages/core/core-flows/src/fulfillment/steps/index.ts +++ b/packages/core/core-flows/src/fulfillment/steps/index.ts @@ -12,5 +12,7 @@ export * from "./delete-shipping-option-rules" export * from "./delete-shipping-options" export * from "./set-shipping-options-prices" export * from "./update-fulfillment" +export * from "./update-shipping-profiles" export * from "./upsert-shipping-options" export * from "./validate-shipment" + diff --git a/packages/core/core-flows/src/fulfillment/steps/update-shipping-profiles.ts b/packages/core/core-flows/src/fulfillment/steps/update-shipping-profiles.ts new file mode 100644 index 0000000000..16631007c9 --- /dev/null +++ b/packages/core/core-flows/src/fulfillment/steps/update-shipping-profiles.ts @@ -0,0 +1,50 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + FilterableShippingProfileProps, + IFulfillmentModuleService, + UpdateShippingProfileDTO, +} from "@medusajs/types" +import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type StepInput = { + update: UpdateShippingProfileDTO + selector: FilterableShippingProfileProps +} + +export const updateShippingProfilesStepId = "update-shipping-profiles" +export const updateShippingProfilesStep = createStep( + updateShippingProfilesStepId, + async (input: StepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + + const { selects, relations } = getSelectsAndRelationsFromObjectArray([ + input.update, + ]) + + const prevData = await service.listShippingProfiles(input.selector, { + select: selects, + relations, + }) + + const profiles = await service.updateShippingProfiles( + input.selector, + input.update + ) + + return new StepResponse(profiles, prevData) + }, + async (prevData, { container }) => { + if (!prevData?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + + await service.upsertShippingProfiles(prevData) + } +) diff --git a/packages/core/core-flows/src/fulfillment/workflows/index.ts b/packages/core/core-flows/src/fulfillment/workflows/index.ts index 7148ccd64c..9340c1aca3 100644 --- a/packages/core/core-flows/src/fulfillment/workflows/index.ts +++ b/packages/core/core-flows/src/fulfillment/workflows/index.ts @@ -12,3 +12,5 @@ export * from "./delete-shipping-options" export * from "./update-fulfillment" export * from "./update-service-zones" export * from "./update-shipping-options" +export * from "./update-shipping-profiles" + diff --git a/packages/core/core-flows/src/fulfillment/workflows/update-shipping-profiles.ts b/packages/core/core-flows/src/fulfillment/workflows/update-shipping-profiles.ts new file mode 100644 index 0000000000..da61fe2523 --- /dev/null +++ b/packages/core/core-flows/src/fulfillment/workflows/update-shipping-profiles.ts @@ -0,0 +1,16 @@ +import { FulfillmentWorkflow } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { updateShippingProfilesStep } from "../steps/update-shipping-profiles" + +export const updateShippingProfilesWorkflowId = + "update-shipping-profiles-workflow" +export const updateShippingProfilesWorkflow = createWorkflow( + updateShippingProfilesWorkflowId, + ( + input: WorkflowData + ): WorkflowData => { + const shippingProfiles = updateShippingProfilesStep(input) + + return shippingProfiles + } +) diff --git a/packages/core/types/src/fulfillment/mutations/shipping-profile.ts b/packages/core/types/src/fulfillment/mutations/shipping-profile.ts index 06813e3be9..fc47d8e831 100644 --- a/packages/core/types/src/fulfillment/mutations/shipping-profile.ts +++ b/packages/core/types/src/fulfillment/mutations/shipping-profile.ts @@ -15,11 +15,32 @@ export interface CreateShippingProfileDTO { /** * Holds custom data in key-value pairs. */ - metadata?: Record + metadata?: Record | null } /** * The attributes to update in the shipping profile. */ -export interface UpdateShippingProfileDTO - extends Partial {} +export interface UpdateShippingProfileDTO { + /** + * The name of the shipping profile. + */ + name?: string + + /** + * The type of the shipping profile. + */ + type?: string + + /** + * Holds custom data in key-value pairs. + */ + metadata?: Record | null +} + +/** + * The attributes to update in the shipping profile. + */ +export interface UpsertShippingProfileDTO extends UpdateShippingProfileDTO { + id?: string +} diff --git a/packages/core/types/src/fulfillment/service.ts b/packages/core/types/src/fulfillment/service.ts index 69f450f0c8..b9015cc815 100644 --- a/packages/core/types/src/fulfillment/service.ts +++ b/packages/core/types/src/fulfillment/service.ts @@ -40,7 +40,7 @@ import { UpsertShippingOptionDTO, } from "./mutations" import { CreateFulfillmentDTO } from "./mutations/fulfillment" -import { CreateShippingProfileDTO } from "./mutations/shipping-profile" +import { CreateShippingProfileDTO, UpsertShippingProfileDTO } from "./mutations/shipping-profile" /** * The main service interface for the Fulfillment Module. @@ -1649,7 +1649,8 @@ export interface IFulfillmentModuleService extends IModuleService { /** * This method updates existing shipping profiles. * - * @param {CreateShippingProfileDTO[]} data - The shipping profiles to be created. + * @param {UpdateShippingProfileDTO} data - The shipping profiles update data. + * @param {FilterableShippingProfileProps} selector - The selector of shipping profiles to update * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The updated shipping profiles. * @@ -1667,14 +1668,16 @@ export interface IFulfillmentModuleService extends IModuleService { * ]) */ updateShippingProfiles( - data: UpdateShippingProfileDTO[], + selector: FilterableShippingProfileProps, + data: UpdateShippingProfileDTO, sharedContext?: Context ): Promise /** * This method updates an existing shipping profiles. * - * @param {CreateShippingProfileDTO} data - The shipping profile to be created. + * @param {string} id - The shipping profile to be updated. + * @param {UpdateShippingProfileDTO} data - The shipping profile to be created. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The updated shipping profiles. * @@ -1686,6 +1689,7 @@ export interface IFulfillmentModuleService extends IModuleService { * }) */ updateShippingProfiles( + id: string, data: UpdateShippingProfileDTO, sharedContext?: Context ): Promise @@ -1719,6 +1723,51 @@ export interface IFulfillmentModuleService extends IModuleService { */ deleteShippingProfiles(id: string, sharedContext?: Context): Promise + /** + * This method updates existing shipping profiles, or creates new ones if they don't exist. + * + * @param {UpdateShippingProfileDTO[]} data - The attributes to update or create for each profile. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated and created profiles. + * + * @example + * const productTags = await productModuleService.upsertShippingProfiles([ + * { + * id: "id_1234", + * metadata: { + * test: true, + * }, + * }, + * { + * name: "Digital", + * }, + * ]) + */ + upsertShippingProfiles( + data: UpsertShippingProfileDTO[], + sharedContext?: Context + ): Promise + + /** + * This method updates an existing shipping profile, or creates a new one if it doesn't exist. + * + * @param {UpdateShippingProfileDTO} data - The attributes to update or create for the profile. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated or created profile. + * + * @example + * const productTag = await productModuleService.upsertShippingProfiles({ + * id: "id_1234", + * metadata: { + * test: true, + * }, + * }) + */ + upsertShippingProfiles( + data: UpsertShippingProfileDTO, + sharedContext?: Context + ): Promise + /** * This method soft deletes shipping profiles by their IDs. * diff --git a/packages/medusa/src/api/admin/shipping-profiles/[id]/route.ts b/packages/medusa/src/api/admin/shipping-profiles/[id]/route.ts index 06d0a883fa..6f55445305 100644 --- a/packages/medusa/src/api/admin/shipping-profiles/[id]/route.ts +++ b/packages/medusa/src/api/admin/shipping-profiles/[id]/route.ts @@ -1,16 +1,22 @@ +import { + deleteShippingProfileWorkflow, + updateShippingProfilesWorkflow, +} from "@medusajs/core-flows" import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { AdminShippingProfileDeleteResponse, AdminShippingProfileResponse, IFulfillmentModuleService, } from "@medusajs/types" -import { deleteShippingProfileWorkflow } from "@medusajs/core-flows" import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../../types/routing" -import { AdminGetShippingProfileParamsType } from "../validators" import { refetchShippingProfile } from "../helpers" +import { + AdminGetShippingProfileParamsType, + AdminUpdateShippingProfileType, +} from "../validators" export const GET = async ( req: AuthenticatedMedusaRequest, @@ -48,3 +54,24 @@ export const DELETE = async ( deleted: true, }) } + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + + await updateShippingProfilesWorkflow(req.scope).run({ + input: { selector: { id }, update: req.body }, + }) + + const shippingProfile = await refetchShippingProfile( + req.params.id, + req.scope, + req.remoteQueryConfig.fields + ) + + res.status(200).json({ + shipping_profile: shippingProfile, + }) +} diff --git a/packages/medusa/src/api/admin/shipping-profiles/middlewares.ts b/packages/medusa/src/api/admin/shipping-profiles/middlewares.ts index 1707527e3b..302470cf21 100644 --- a/packages/medusa/src/api/admin/shipping-profiles/middlewares.ts +++ b/packages/medusa/src/api/admin/shipping-profiles/middlewares.ts @@ -9,6 +9,7 @@ import { AdminCreateShippingProfile, AdminGetShippingProfileParams, AdminGetShippingProfilesParams, + AdminUpdateShippingProfile, } from "./validators" export const adminShippingProfilesMiddlewares: MiddlewareRoute[] = [ @@ -33,6 +34,17 @@ export const adminShippingProfilesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/admin/shipping-profiles/:id", + middlewares: [ + validateAndTransformBody(AdminUpdateShippingProfile), + validateAndTransformQuery( + AdminGetShippingProfileParams, + retrieveTransformQueryConfig + ), + ], + }, { method: ["GET"], matcher: "/admin/shipping-profiles/:id", diff --git a/packages/medusa/src/api/admin/shipping-profiles/validators.ts b/packages/medusa/src/api/admin/shipping-profiles/validators.ts index 20418a729d..4377b085d1 100644 --- a/packages/medusa/src/api/admin/shipping-profiles/validators.ts +++ b/packages/medusa/src/api/admin/shipping-profiles/validators.ts @@ -39,3 +39,14 @@ export const AdminCreateShippingProfile = z metadata: z.record(z.string(), z.unknown()).optional(), }) .strict() + +export type AdminUpdateShippingProfileType = z.infer< + typeof AdminUpdateShippingProfile +> +export const AdminUpdateShippingProfile = z + .object({ + name: z.string().optional(), + type: z.string().optional(), + metadata: z.record(z.string(), z.unknown()).optional().nullable(), + }) + .strict() diff --git a/packages/modules/fulfillment/src/services/fulfillment-module-service.ts b/packages/modules/fulfillment/src/services/fulfillment-module-service.ts index d79caee46f..1b4ea6db4a 100644 --- a/packages/modules/fulfillment/src/services/fulfillment-module-service.ts +++ b/packages/modules/fulfillment/src/services/fulfillment-module-service.ts @@ -14,18 +14,18 @@ import { UpdateServiceZoneDTO, } from "@medusajs/types" import { - arrayDifference, - deepEqualObj, EmitEvents, - getSetDifference, InjectManager, InjectTransactionManager, - isDefined, - isPresent, - isString, MedusaContext, MedusaError, ModulesSdkUtils, + arrayDifference, + deepEqualObj, + getSetDifference, + isDefined, + isPresent, + isString, promiseAll, } from "@medusajs/utils" import { @@ -49,8 +49,8 @@ import { } from "@utils" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { UpdateShippingOptionsInput } from "../types/service" -import FulfillmentProviderService from "./fulfillment-provider" import { buildCreatedShippingOptionEvents } from "../utils/events" +import FulfillmentProviderService from "./fulfillment-provider" const generateMethodForModels = [ ServiceZone, @@ -1541,25 +1541,106 @@ export default class FulfillmentModuleService< } updateShippingProfiles( - data: FulfillmentTypes.UpdateShippingProfileDTO[], + selector: FulfillmentTypes.FilterableShippingProfileProps, + data: FulfillmentTypes.UpdateShippingProfileDTO, sharedContext?: Context ): Promise updateShippingProfiles( + id: string, data: FulfillmentTypes.UpdateShippingProfileDTO, sharedContext?: Context ): Promise @InjectTransactionManager("baseRepository_") async updateShippingProfiles( - data: - | FulfillmentTypes.UpdateShippingProfileDTO - | FulfillmentTypes.UpdateShippingProfileDTO[], + idOrSelector: string | FulfillmentTypes.FilterableShippingProfileProps, + data: FulfillmentTypes.UpdateShippingProfileDTO, @MedusaContext() sharedContext: Context = {} ): Promise< FulfillmentTypes.ShippingProfileDTO | FulfillmentTypes.ShippingProfileDTO[] > { - // TODO: should we implement that or can we get rid of the profiles concept entirely and link to the so instead? - return [] + let normalizedInput: ({ + id: string + } & FulfillmentTypes.UpdateShippingProfileDTO)[] = [] + if (isString(idOrSelector)) { + await this.shippingProfileService_.retrieve( + idOrSelector, + {}, + sharedContext + ) + normalizedInput = [{ id: idOrSelector, ...data }] + } else { + const profiles = await this.shippingProfileService_.list( + idOrSelector, + {}, + sharedContext + ) + + normalizedInput = profiles.map((profile) => ({ + id: profile.id, + ...data, + })) + } + + const profiles = await this.shippingProfileService_.update( + normalizedInput, + sharedContext + ) + + const updatedProfiles = await this.baseRepository_.serialize< + FulfillmentTypes.ShippingProfileDTO[] + >(profiles) + + return isString(idOrSelector) ? updatedProfiles[0] : updatedProfiles + } + + async upsertShippingProfiles( + data: FulfillmentTypes.UpsertShippingProfileDTO[], + sharedContext?: Context + ): Promise + async upsertShippingProfiles( + data: FulfillmentTypes.UpsertShippingProfileDTO, + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async upsertShippingProfiles( + data: + | FulfillmentTypes.UpsertShippingProfileDTO[] + | FulfillmentTypes.UpsertShippingProfileDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise< + FulfillmentTypes.ShippingProfileDTO[] | FulfillmentTypes.ShippingProfileDTO + > { + const input = Array.isArray(data) ? data : [data] + const forUpdate = input.filter((prof) => !!prof.id) + const forCreate = input.filter( + (prof): prof is FulfillmentTypes.CreateShippingProfileDTO => !prof.id + ) + + let created: ShippingProfile[] = [] + let updated: ShippingProfile[] = [] + + if (forCreate.length) { + created = await this.shippingProfileService_.create( + forCreate, + sharedContext + ) + } + if (forUpdate.length) { + updated = await this.shippingProfileService_.update( + forUpdate, + sharedContext + ) + } + + const result = [...created, ...updated] + const allProfiles = await this.baseRepository_.serialize< + | FulfillmentTypes.ShippingProfileDTO[] + | FulfillmentTypes.ShippingProfileDTO + >(result) + + return Array.isArray(data) ? allProfiles : allProfiles[0] } updateGeoZones(