diff --git a/.changeset/slimy-kings-eat.md b/.changeset/slimy-kings-eat.md new file mode 100644 index 0000000000..83fbc596dc --- /dev/null +++ b/.changeset/slimy-kings-eat.md @@ -0,0 +1,8 @@ +--- +"@medusajs/types": patch +"@medusajs/utils": patch +"medusa-test-utils": patch +--- + +Feat(fulfillment): service provider registration + fulfillment management + diff --git a/.eslintignore b/.eslintignore index 37ab11f81e..28af48ab6d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -31,6 +31,7 @@ packages/* !packages/workflow-engine-redis !packages/workflow-engine-inmemory !packages/fulfillment +!packages/fulfillment-manual **/models/* diff --git a/.eslintrc.js b/.eslintrc.js index f083ae8b04..0d018397ca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -103,6 +103,7 @@ module.exports = { "./packages/workflow-engine-redis/tsconfig.spec.json", "./packages/workflow-engine-inmemory/tsconfig.spec.json", "./packages/fulfillment/tsconfig.spec.json", + "./packages/fulfillment-manual/tsconfig.spec.json", ], }, rules: { diff --git a/packages/fulfillment-manual/.gitignore b/packages/fulfillment-manual/.gitignore new file mode 100644 index 0000000000..83cb36a41e --- /dev/null +++ b/packages/fulfillment-manual/.gitignore @@ -0,0 +1,4 @@ +dist +node_modules +.DS_store +yarn.lock diff --git a/packages/fulfillment-manual/CHANGELOG.md b/packages/fulfillment-manual/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/fulfillment-manual/README.md b/packages/fulfillment-manual/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/fulfillment-manual/jest.config.js b/packages/fulfillment-manual/jest.config.js new file mode 100644 index 0000000000..e564d67c70 --- /dev/null +++ b/packages/fulfillment-manual/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + globals: { + "ts-jest": { + tsconfig: "tsconfig.spec.json", + isolatedModules: false, + }, + }, + transform: { + "^.+\\.[jt]s?$": "ts-jest", + }, + testEnvironment: `node`, + moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`], +} diff --git a/packages/fulfillment-manual/package.json b/packages/fulfillment-manual/package.json new file mode 100644 index 0000000000..0bee32f08e --- /dev/null +++ b/packages/fulfillment-manual/package.json @@ -0,0 +1,40 @@ +{ + "name": "@medusajs/fulfillment-manual", + "version": "0.0.1", + "description": "Manual fulfillment for Medusa", + "main": "dist/index.js", + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/fulfillment-manual" + }, + "files": [ + "dist" + ], + "engines": { + "node": ">=16" + }, + "author": "Medusa", + "license": "MIT", + "scripts": { + "prepublishOnly": "cross-env NODE_ENV=production tsc --build", + "test": "jest --passWithNoTests src", + "build": "rimraf dist && tsc -p ./tsconfig.json", + "watch": "tsc --watch" + }, + "devDependencies": { + "cross-env": "^5.2.1", + "jest": "^25.5.4", + "rimraf": "^5.0.1", + "typescript": "^4.9.5" + }, + "dependencies": { + "@medusajs/utils": "^1.11.3", + "body-parser": "^1.19.0", + "express": "^4.17.1" + }, + "keywords": [ + "medusa-plugin", + "medusa-plugin-fulfillment" + ] +} diff --git a/packages/fulfillment-manual/src/index.ts b/packages/fulfillment-manual/src/index.ts new file mode 100644 index 0000000000..89339c0067 --- /dev/null +++ b/packages/fulfillment-manual/src/index.ts @@ -0,0 +1,10 @@ +import { ModuleProviderExports } from "@medusajs/types" +import { ManualFulfillmentService } from "./services/manual-fulfillment" + +const services = [ManualFulfillmentService] + +const providerExport: ModuleProviderExports = { + services, +} + +export default providerExport diff --git a/packages/fulfillment-manual/src/services/manual-fulfillment.ts b/packages/fulfillment-manual/src/services/manual-fulfillment.ts new file mode 100644 index 0000000000..aee49682a8 --- /dev/null +++ b/packages/fulfillment-manual/src/services/manual-fulfillment.ts @@ -0,0 +1,44 @@ +import { AbstractFulfillmentProviderService } from "@medusajs/utils" + +// TODO rework type and DTO's + +export class ManualFulfillmentService extends AbstractFulfillmentProviderService { + static identifier = "manual" + + constructor() { + super() + } + + async getFulfillmentOptions(): Promise[]> { + return [ + { + id: "manual-fulfillment", + }, + { + id: "manual-fulfillment-return", + is_return: true, + }, + ] + } + + async validateFulfillmentData( + optionData: Record, + data: Record, + context: Record + ): Promise { + return data + } + + async validateOption(data: Record): Promise { + return true + } + + async createFulfillment(): Promise> { + // No data is being sent anywhere + return {} + } + + async cancelFulfillment(fulfillment: Record): Promise { + return {} + } +} diff --git a/packages/fulfillment-manual/tsconfig.json b/packages/fulfillment-manual/tsconfig.json new file mode 100644 index 0000000000..65e5a4fd5b --- /dev/null +++ b/packages/fulfillment-manual/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "lib": [ + "es5", + "es6", + "es2019" + ], + "target": "es5", + "jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true, // to use ES5 specific tooling + "inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */ + }, + "include": ["src"], + "exclude": [ + "dist", + "build", + "src/**/__tests__", + "src/**/__mocks__", + "src/**/__fixtures__", + "node_modules", + ".eslintrc.js" + ] +} diff --git a/packages/fulfillment-manual/tsconfig.spec.json b/packages/fulfillment-manual/tsconfig.spec.json new file mode 100644 index 0000000000..9b62409191 --- /dev/null +++ b/packages/fulfillment-manual/tsconfig.spec.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/fulfillment/integration-tests/__fixtures__/fulfillment.ts b/packages/fulfillment/integration-tests/__fixtures__/fulfillment.ts new file mode 100644 index 0000000000..cf60b6bd52 --- /dev/null +++ b/packages/fulfillment/integration-tests/__fixtures__/fulfillment.ts @@ -0,0 +1,46 @@ +import { CreateFulfillmentDTO } from "@medusajs/types" + +export function generateCreateFulfillmentData( + data: Partial & { + provider_id: string + shipping_option_id: string + } +) { + return { + location_id: "test-location", + packed_at: null, + shipped_at: null, + delivered_at: null, + canceled_at: null, + data: null, + provider_id: data.provider_id, + shipping_option_id: data.shipping_option_id, + metadata: data.metadata ?? null, + delivery_address: data.delivery_address ?? { + address_1: "test-address", + address_2: "test-address", + city: "test-city", + postal_code: "test-postal-code", + country_code: "test-country-code", + province: "test-province", + phone: "test-phone", + full_name: "test-full-name", + }, + items: data.items ?? [ + { + title: "test-title", + sku: "test-sku", + quantity: 1, + barcode: "test-barcode", + }, + ], + labels: data.labels ?? [ + { + tracking_number: "test-tracking-number", + tracking_url: "test-tracking-url", + label_url: "test-label-url", + }, + ], + order: data.order ?? {}, + } +} diff --git a/packages/fulfillment/integration-tests/__fixtures__/index.ts b/packages/fulfillment/integration-tests/__fixtures__/index.ts index a62bc9bd44..6ff6616db4 100644 --- a/packages/fulfillment/integration-tests/__fixtures__/index.ts +++ b/packages/fulfillment/integration-tests/__fixtures__/index.ts @@ -1,39 +1,2 @@ -import { CreateShippingOptionDTO } from "@medusajs/types" - -export function generateCreateShippingOptionsData({ - name, - service_zone_id, - shipping_profile_id, - service_provider_id, - price_type, - rules, - type, - data, -}: Omit & { - price_type?: CreateShippingOptionDTO["price_type"] - name?: string - type?: CreateShippingOptionDTO["type"] -}): Required { - return { - service_zone_id: service_zone_id, - shipping_profile_id: shipping_profile_id, - service_provider_id: service_provider_id, - type: type ?? { - code: "test-type", - description: "test-description", - label: "test-label", - }, - data: data ?? { - amount: 1000, - }, - name: name ?? Math.random().toString(36).substring(7), - price_type: price_type ?? "flat", - rules: rules ?? [ - { - attribute: "weight", - operator: "eq", - value: "test", - }, - ], - } -} +export * from "./shipping-options" +export * from "./fulfillment" diff --git a/packages/fulfillment/integration-tests/__fixtures__/providers/default-provider.ts b/packages/fulfillment/integration-tests/__fixtures__/providers/default-provider.ts new file mode 100644 index 0000000000..bf179a8e5e --- /dev/null +++ b/packages/fulfillment/integration-tests/__fixtures__/providers/default-provider.ts @@ -0,0 +1,19 @@ +import { AbstractFulfillmentProviderService } from "@medusajs/utils/src" + +export class FulfillmentProviderServiceFixtures extends AbstractFulfillmentProviderService { + static identifier = "fixtures-fulfillment-provider" + + async createFulfillment(data, items, order, fulfillment): Promise { + return {} + } + + async cancelFulfillment(fulfillment): Promise { + return {} + } + + async getFulfillmentOptions(): Promise { + return {} + } +} + +export const services = [FulfillmentProviderServiceFixtures] diff --git a/packages/fulfillment/integration-tests/__fixtures__/providers/index.ts b/packages/fulfillment/integration-tests/__fixtures__/providers/index.ts new file mode 100644 index 0000000000..e19230b5b7 --- /dev/null +++ b/packages/fulfillment/integration-tests/__fixtures__/providers/index.ts @@ -0,0 +1 @@ +export * from "./default-provider" diff --git a/packages/fulfillment/integration-tests/__fixtures__/shipping-options.ts b/packages/fulfillment/integration-tests/__fixtures__/shipping-options.ts new file mode 100644 index 0000000000..9a77d6032e --- /dev/null +++ b/packages/fulfillment/integration-tests/__fixtures__/shipping-options.ts @@ -0,0 +1,39 @@ +import { CreateShippingOptionDTO } from "@medusajs/types" + +export function generateCreateShippingOptionsData({ + name, + service_zone_id, + shipping_profile_id, + fulfillment_provider_id, + price_type, + rules, + type, + data, +}: Omit & { + price_type?: CreateShippingOptionDTO["price_type"] + name?: string + type?: CreateShippingOptionDTO["type"] +}): Required { + return { + service_zone_id: service_zone_id, + shipping_profile_id: shipping_profile_id, + fulfillment_provider_id: fulfillment_provider_id, + type: type ?? { + code: "test-type", + description: "test-description", + label: "test-label", + }, + data: data ?? { + amount: 1000, + }, + name: name ?? Math.random().toString(36).substring(7), + price_type: price_type ?? "flat", + rules: rules ?? [ + { + attribute: "weight", + operator: "eq", + value: "test", + }, + ], + } +} diff --git a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service.spec.ts b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service.spec.ts deleted file mode 100644 index 6cccf1c5ae..0000000000 --- a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service.spec.ts +++ /dev/null @@ -1,2502 +0,0 @@ -import {Modules} from "@medusajs/modules-sdk" -import { - CreateFulfillmentSetDTO, - CreateGeoZoneDTO, - CreateServiceZoneDTO, - CreateShippingOptionDTO, - CreateShippingProfileDTO, - GeoZoneDTO, - IFulfillmentModuleService, - ServiceZoneDTO, - UpdateFulfillmentSetDTO, - UpdateGeoZoneDTO, - UpdateServiceZoneDTO, -} from "@medusajs/types" -import {GeoZoneType} from "@medusajs/utils" -import {moduleIntegrationTestRunner, SuiteOptions} from "medusa-test-utils" -import {generateCreateShippingOptionsData} from "../__fixtures__" - -jest.setTimeout(100000) - -// TODO: Temporary until the providers are sorted out -const createProvider = async (MikroOrmWrapper, providerId: string) => { - const [{ id }] = await MikroOrmWrapper.forkManager().execute( - `insert into service_provider (id) values ('${providerId}') returning id` - ) - - return id -} - -moduleIntegrationTestRunner({ - moduleName: Modules.FULFILLMENT, - testSuite: ({ - MikroOrmWrapper, - service, - }: SuiteOptions) => { - describe("Fulfillment Module Service", () => { - describe("read", () => { - describe("fulfillment set", () => { - it("should list fulfillment sets with a filter", async function () { - const createdSet1 = await service.create({ - name: "test", - type: "test-type", - }) - const createdSet2 = await service.create({ - name: "test2", - type: "test-type", - service_zones: [ - { - name: "test", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - { - name: "test2", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - { - name: "_test", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - ], - }) - - let listedSets = await service.list( - { - type: createdSet1.type, - }, - { - relations: ["service_zones"], - } - ) - - const listedSets2 = await service.list( - { - type: createdSet1.type, - }, - { - relations: ["service_zones"], - } - ) - - expect(listedSets).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: createdSet1.id }), - expect.objectContaining({ id: createdSet2.id }), - ]) - ) - - // Respecting order id by default - expect(listedSets[1].service_zones).toEqual([ - expect.objectContaining({ name: "test" }), - expect.objectContaining({ name: "test2" }), - expect.objectContaining({ name: "_test" }), - ]) - - expect(listedSets2).toEqual(listedSets2) - - listedSets = await service.list({ - name: createdSet2.name, - }) - - expect(listedSets).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: createdSet2.id }), - ]) - ) - expect(listedSets).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: createdSet1.id }), - ]) - ) - - listedSets = await service.list({ - service_zones: { name: "test" }, - }) - - expect(listedSets).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: createdSet2.id }), - ]) - ) - expect(listedSets).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: createdSet1.id }), - ]) - ) - - listedSets = await service.list({ - service_zones: { geo_zones: { country_code: "fr" } }, - }) - - expect(listedSets).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: createdSet2.id }), - ]) - ) - expect(listedSets).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: createdSet1.id }), - ]) - ) - }) - }) - - describe("service zones", () => { - it("should list service zones with a filter", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - - const createdZone1 = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - const createdZone2 = await service.createServiceZones({ - name: "test2", - fulfillment_set_id: fulfillmentSet.id, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }) - - let listedZones = await service.listServiceZones({ - name: createdZone2.name, - }) - - expect(listedZones).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: createdZone2.id }), - ]) - ) - expect(listedZones).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: createdZone1.id }), - ]) - ) - - listedZones = await service.listServiceZones({ - geo_zones: { country_code: "fr" }, - }) - - expect(listedZones).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: createdZone2.id }), - ]) - ) - expect(listedZones).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: createdZone1.id }), - ]) - ) - }) - }) - - describe("geo zones", () => { - it("should list geo zones with a filter", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - - const createdZone1 = await service.createGeoZones({ - service_zone_id: serviceZone.id, - type: GeoZoneType.COUNTRY, - country_code: "fr", - }) - const createdZone2 = await service.createGeoZones({ - service_zone_id: serviceZone.id, - type: GeoZoneType.COUNTRY, - country_code: "us", - }) - - let listedZones = await service.listGeoZones({ - type: createdZone1.type, - }) - - expect(listedZones).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: createdZone1.id }), - expect.objectContaining({ id: createdZone2.id }), - ]) - ) - - listedZones = await service.listGeoZones({ - country_code: createdZone2.country_code, - }) - - expect(listedZones).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: createdZone2.id }), - ]) - ) - expect(listedZones).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: createdZone1.id }), - ]) - ) - }) - }) - - describe("shipping options", () => { - it("should list shipping options with a filter", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - service_zones: [ - { - name: "test", - }, - ], - }) - - const shippingProfile = await service.createShippingProfiles({ - name: "test", - type: "default", - }) - - const providerId = await createProvider( - MikroOrmWrapper, - "sp_jdafwfleiwuonl" - ) - - const [shippingOption1] = await service.createShippingOptions([ - generateCreateShippingOptionsData({ - service_zone_id: fulfillmentSet.service_zones[0].id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - rules: [ - { - attribute: "test-attribute", - operator: "in", - value: ["test"], - }, - ], - }), - generateCreateShippingOptionsData({ - service_zone_id: fulfillmentSet.service_zones[0].id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - rules: [ - { - attribute: "test-attribute", - operator: "eq", - value: "test", - }, - { - attribute: "test-attribute2.options", - operator: "in", - value: ["test", "test2"], - }, - ], - }), - ]) - - const listedOptions = await service.listShippingOptions({ - name: shippingOption1.name, - }) - - expect(listedOptions).toHaveLength(1) - expect(listedOptions[0].id).toEqual(shippingOption1.id) - }) - - it("should list shipping options with a context", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - service_zones: [ - { - name: "test", - }, - ], - }) - - const shippingProfile = await service.createShippingProfiles({ - name: "test", - type: "default", - }) - - const providerId = await createProvider( - MikroOrmWrapper, - "sp_jdafwfleiwuonl" - ) - - const [shippingOption1, , shippingOption3] = - await service.createShippingOptions([ - generateCreateShippingOptionsData({ - service_zone_id: fulfillmentSet.service_zones[0].id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - rules: [ - { - attribute: "test-attribute", - operator: "in", - value: ["test"], - }, - ], - }), - generateCreateShippingOptionsData({ - service_zone_id: fulfillmentSet.service_zones[0].id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - rules: [ - { - attribute: "test-attribute", - operator: "in", - value: ["test-test"], - }, - ], - }), - generateCreateShippingOptionsData({ - service_zone_id: fulfillmentSet.service_zones[0].id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - rules: [ - { - attribute: "test-attribute", - operator: "eq", - value: "test", - }, - { - attribute: "test-attribute2.options", - operator: "in", - value: ["test", "test2"], - }, - ], - }), - ]) - - let listedOptions = await service.listShippingOptions({ - context: { - "test-attribute": "test", - "test-attribute2": { - options: "test2", - }, - }, - }) - - expect(listedOptions).toHaveLength(2) - expect(listedOptions).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: shippingOption1.id }), - expect.objectContaining({ id: shippingOption3.id }), - ]) - ) - - listedOptions = await service.listShippingOptions({ - fulfillment_set_id: { $ne: fulfillmentSet.id }, - context: { - "test-attribute": "test", - "test-attribute2": { - options: "test2", - }, - }, - }) - - expect(listedOptions).toHaveLength(0) - - listedOptions = await service.listShippingOptions({ - fulfillment_set_type: "non-existing-type", - context: { - "test-attribute": "test", - "test-attribute2": { - options: "test2", - }, - }, - }) - - expect(listedOptions).toHaveLength(0) - }) - }) - }) - - describe("mutations", () => { - describe("on create", () => { - it("should create a new fulfillment set", async function () { - const data: CreateFulfillmentSetDTO = { - name: "test", - type: "test-type", - } - - const fulfillmentSet = await service.create(data) - - expect(fulfillmentSet).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: data.name, - type: data.type, - }) - ) - }) - - it("should create a collection of fulfillment sets", async function () { - const data = [ - { - name: "test", - type: "test-type", - }, - { - name: "test2", - type: "test-type2", - }, - ] - - const fulfillmentSets = await service.create(data) - - expect(fulfillmentSets).toHaveLength(2) - - let i = 0 - for (const data_ of data) { - expect(fulfillmentSets[i]).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: data_.name, - type: data_.type, - }) - ) - ++i - } - }) - - it("should create a new fulfillment set with new service zones", async function () { - const data = { - name: "test", - type: "test-type", - service_zones: [ - { - name: "test", - }, - ], - } - - const fulfillmentSet = await service.create(data) - - expect(fulfillmentSet).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: data.name, - type: data.type, - service_zones: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - name: data.service_zones[0].name, - }), - ]), - }) - ) - }) - - it("should create a collection of fulfillment sets with new service zones", async function () { - const data = [ - { - name: "test", - type: "test-type", - service_zones: [ - { - name: "test", - }, - ], - }, - { - name: "test2", - type: "test-type2", - service_zones: [ - { - name: "test2", - }, - ], - }, - { - name: "test3", - type: "test-type3", - service_zones: [ - { - name: "test3", - }, - ], - }, - ] - - const fulfillmentSets = await service.create(data) - - expect(fulfillmentSets).toHaveLength(3) - - let i = 0 - for (const data_ of data) { - expect(fulfillmentSets[i]).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: data_.name, - type: data_.type, - service_zones: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - name: data_.service_zones[0].name, - }), - ]), - }) - ) - ++i - } - }) - - it("should create a new fulfillment set with new service zones and new geo zones", async function () { - const data: CreateFulfillmentSetDTO = { - name: "test", - type: "test-type", - service_zones: [ - { - name: "test", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - ], - } - - const fulfillmentSet = await service.create(data) - - expect(fulfillmentSet).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: data.name, - type: data.type, - service_zones: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - name: (data.service_zones![0] as any).name, - geo_zones: expect.arrayContaining([ - expect.objectContaining({ - type: (data.service_zones![0] as any).geo_zones[0].type, - country_code: (data.service_zones![0] as any) - .geo_zones[0].country_code, - }), - ]), - }), - ]), - }) - ) - }) - - it("should create a collection of fulfillment sets with new service zones and new geo zones", async function () { - const data: CreateFulfillmentSetDTO[] = [ - { - name: "test", - type: "test-type", - service_zones: [ - { - name: "test", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - ], - }, - { - name: "test2", - type: "test-type2", - service_zones: [ - { - name: "test2", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - ], - }, - { - name: "test3", - type: "test-type3", - service_zones: [ - { - name: "test3", - geo_zones: [ - { - type: GeoZoneType.CITY, - country_code: "fr", - city: "lyon", - }, - ], - }, - ], - }, - ] - - const fulfillmentSets = await service.create(data) - - expect(fulfillmentSets).toHaveLength(3) - - let i = 0 - for (const data_ of data) { - expect(fulfillmentSets[i]).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: data_.name, - type: data_.type, - service_zones: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - name: (data_.service_zones![0] as any).name, - geo_zones: expect.arrayContaining([ - expect.objectContaining({ - type: (data_.service_zones![0] as any).geo_zones[0] - .type, - country_code: (data_.service_zones![0] as any) - .geo_zones[0].country_code, - }), - ]), - }), - ]), - }) - ) - ++i - } - }) - - it(`should fail on duplicated fulfillment set name`, async function () { - const data: CreateFulfillmentSetDTO = { - name: "test", - type: "test-type", - } - - await service.create(data) - const err = await service.create(data).catch((e) => e) - - expect(err).toBeDefined() - expect(err.constraint).toBe("IDX_fulfillment_set_name_unique") - }) - }) - - describe("on create service zones", () => { - it("should create a new service zone", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - - const data: CreateServiceZoneDTO = { - name: "test", - fulfillment_set_id: fulfillmentSet.id, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - } - - const serviceZone = await service.createServiceZones(data) - - expect(serviceZone).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: data.name, - geo_zones: expect.arrayContaining([ - expect.objectContaining({ - type: (data.geo_zones![0] as GeoZoneDTO).type, - country_code: (data.geo_zones![0] as GeoZoneDTO) - .country_code, - }), - ]), - }) - ) - }) - - it("should create a collection of service zones", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - - const data: CreateServiceZoneDTO[] = [ - { - name: "test", - fulfillment_set_id: fulfillmentSet.id, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - { - name: "test2", - fulfillment_set_id: fulfillmentSet.id, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - { - name: "test3", - fulfillment_set_id: fulfillmentSet.id, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "uk", - }, - ], - }, - ] - - const serviceZones = await service.createServiceZones(data) - - expect(serviceZones).toHaveLength(3) - - let i = 0 - for (const data_ of data) { - expect(serviceZones[i]).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: data_.name, - geo_zones: expect.arrayContaining([ - expect.objectContaining({ - type: (data_.geo_zones![0] as GeoZoneDTO).type, - country_code: (data_.geo_zones![0] as GeoZoneDTO) - .country_code, - }), - ]), - }) - ) - ++i - } - }) - - it("should fail on duplicated service zone name", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - - const data: CreateServiceZoneDTO = { - name: "test", - fulfillment_set_id: fulfillmentSet.id, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - } - - await service.createServiceZones(data) - const err = await service.createServiceZones(data).catch((e) => e) - - expect(err).toBeDefined() - expect(err.constraint).toBe("IDX_service_zone_name_unique") - }) - }) - - describe("on create geo zones", () => { - it("should create a new geo zone", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - - const data: CreateGeoZoneDTO = { - service_zone_id: serviceZone.id, - type: GeoZoneType.COUNTRY, - country_code: "fr", - } - - const geoZone = await service.createGeoZones(data) - - expect(geoZone).toEqual( - expect.objectContaining({ - id: expect.any(String), - type: data.type, - country_code: data.country_code, - }) - ) - }) - - it("should create a collection of geo zones", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - - const data: CreateGeoZoneDTO[] = [ - { - service_zone_id: serviceZone.id, - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - { - service_zone_id: serviceZone.id, - type: GeoZoneType.COUNTRY, - country_code: "us", - }, - ] - - const geoZones = await service.createGeoZones(data) - - expect(geoZones).toHaveLength(2) - - let i = 0 - for (const data_ of data) { - expect(geoZones[i]).toEqual( - expect.objectContaining({ - id: expect.any(String), - type: data_.type, - country_code: data_.country_code, - }) - ) - ++i - } - }) - }) - - describe("on create shipping profiles", () => { - it("should create a new shipping profile", async function () { - const createData: CreateShippingProfileDTO = { - name: "test-default-profile", - type: "default", - } - - const createdShippingProfile = await service.createShippingProfiles( - createData - ) - - expect(createdShippingProfile).toEqual( - expect.objectContaining({ - name: createData.name, - type: createData.type, - }) - ) - }) - - it("should create multiple new shipping profiles", async function () { - const createData: CreateShippingProfileDTO[] = [ - { - name: "test-profile-1", - type: "default", - }, - { - name: "test-profile-2", - type: "custom", - }, - ] - - const createdShippingProfiles = - await service.createShippingProfiles(createData) - - expect(createdShippingProfiles).toHaveLength(2) - - let i = 0 - for (const data_ of createData) { - expect(createdShippingProfiles[i]).toEqual( - expect.objectContaining({ - name: data_.name, - type: data_.type, - }) - ) - ++i - } - }) - - it("should fail on duplicated shipping profile name", async function () { - const createData: CreateShippingProfileDTO = { - name: "test-default-profile", - type: "default", - } - - await service.createShippingProfiles(createData) - - const err = await service - .createShippingProfiles(createData) - .catch((e) => e) - - expect(err).toBeDefined() - expect(err.constraint).toBe("IDX_shipping_profile_name_unique") - }) - }) - - describe("on create shipping options", () => { - it("should create a new shipping option", async function () { - const shippingProfile = await service.createShippingProfiles({ - name: "test", - type: "default", - }) - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - - const providerId = await createProvider( - MikroOrmWrapper, - "sp_jdafwfleiwuonl" - ) - - const createData: CreateShippingOptionDTO = generateCreateShippingOptionsData({ - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - }) - - const createdShippingOption = await service.createShippingOptions( - createData - ) - - expect(createdShippingOption).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: createData.name, - price_type: createData.price_type, - service_zone_id: createData.service_zone_id, - shipping_profile_id: createData.shipping_profile_id, - service_provider_id: createData.service_provider_id, - shipping_option_type_id: expect.any(String), - type: expect.objectContaining({ - id: expect.any(String), - code: createData.type.code, - description: createData.type.description, - label: createData.type.label, - }), - data: createData.data, - rules: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - attribute: createData.rules![0].attribute, - operator: createData.rules![0].operator, - value: createData.rules![0].value, - }), - ]), - }) - ) - }) - - it("should create multiple new shipping options", async function () { - const shippingProfile = await service.createShippingProfiles({ - name: "test", - type: "default", - }) - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - - const providerId = await createProvider( - MikroOrmWrapper, - "sp_jdafwfleiwuonl" - ) - - const createData: CreateShippingOptionDTO[] = [ - generateCreateShippingOptionsData({ - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - }), - generateCreateShippingOptionsData({ - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - }) - ] - - const createdShippingOptions = await service.createShippingOptions( - createData - ) - - expect(createdShippingOptions).toHaveLength(2) - - let i = 0 - for (const data_ of createData) { - expect(createdShippingOptions[i]).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: data_.name, - price_type: data_.price_type, - service_zone_id: data_.service_zone_id, - shipping_profile_id: data_.shipping_profile_id, - service_provider_id: data_.service_provider_id, - shipping_option_type_id: expect.any(String), - type: expect.objectContaining({ - id: expect.any(String), - code: data_.type.code, - description: data_.type.description, - label: data_.type.label, - }), - data: data_.data, - rules: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - attribute: data_.rules![0].attribute, - operator: data_.rules![0].operator, - value: data_.rules![0].value, - }), - ]), - }) - ) - ++i - } - }) - - it("should fail to create a new shipping option with invalid rules", async function () { - const shippingProfile = await service.createShippingProfiles({ - name: "test", - type: "default", - }) - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - - const providerId = await createProvider( - MikroOrmWrapper, - "sp_jdafwfleiwuonl" - ) - - const createData: CreateShippingOptionDTO = generateCreateShippingOptionsData({ - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - rules: [ - { - attribute: "test-attribute", - operator: "invalid" as any, - value: "test-value", - }, - ], - }) - - const err = await service - .createShippingOptions(createData) - .catch((e) => e) - - expect(err).toBeDefined() - expect(err.message).toBe( - "Rule operator invalid is not supported. Must be one of in, eq, ne, gt, gte, lt, lte, nin" - ) - }) - }) - - describe("on create shipping option rules", () => { - it("should create a new rule", async () => { - const shippingProfile = await service.createShippingProfiles({ - name: "test", - type: "default", - }) - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - - // service provider - const [{ id: providerId }] = - await MikroOrmWrapper.forkManager().execute( - "insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id" - ) - - const shippingOption = await service.createShippingOptions( - generateCreateShippingOptionsData({ - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - }) - ) - - const ruleData = { - attribute: "test-attribute", - operator: "eq", - value: "test-value", - shipping_option_id: shippingOption.id, - } - - const rule = await service.createShippingOptionRules(ruleData) - - expect(rule).toEqual( - expect.objectContaining({ - id: expect.any(String), - attribute: ruleData.attribute, - operator: ruleData.operator, - value: ruleData.value, - shipping_option_id: ruleData.shipping_option_id, - }) - ) - - const rules = await service.listShippingOptionRules() - expect(rules).toHaveLength(2) - expect(rules).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: rule.id, - attribute: ruleData.attribute, - operator: ruleData.operator, - value: ruleData.value, - shipping_option_id: shippingOption.id, - }), - expect.objectContaining({ - id: shippingOption.rules[0].id, - attribute: shippingOption.rules[0].attribute, - operator: shippingOption.rules[0].operator, - value: shippingOption.rules[0].value, - shipping_option_id: shippingOption.id, - }), - ]) - ) - }) - }) - - describe("on update", () => { - it("should update an existing fulfillment set", async function () { - const createData: CreateFulfillmentSetDTO = { - name: "test", - type: "test-type", - } - - const createdFulfillmentSet = await service.create(createData) - - const updateData = { - id: createdFulfillmentSet.id, - name: "updated-test", - type: "updated-test-type", - } - - const updatedFulfillmentSets = await service.update(updateData) - - expect(updatedFulfillmentSets).toEqual( - expect.objectContaining({ - id: createdFulfillmentSet.id, - name: updateData.name, - type: updateData.type, - }) - ) - }) - - it("should update a collection of fulfillment sets", async function () { - const createData = [ - { - name: "test", - type: "test-type", - }, - { - name: "test2", - type: "test-type2", - }, - ] - - const createdFulfillmentSets = await service.create(createData) - - const updateData = createdFulfillmentSets.map( - (fulfillmentSet, index) => ({ - id: fulfillmentSet.id, - name: `updated-test${index + 1}`, - type: `updated-test-type${index + 1}`, - }) - ) - - const updatedFulfillmentSets = await service.update(updateData) - const fullfillmentSets = await service.list({ - id: updateData.map((ud) => ud.id), - }) - - expect(updatedFulfillmentSets).toHaveLength(2) - - for (const data_ of updateData) { - const currentFullfillmentSet = fullfillmentSets.find( - (fs) => fs.id === data_.id - ) - - expect(currentFullfillmentSet).toEqual( - expect.objectContaining({ - id: data_.id, - name: data_.name, - type: data_.type, - }) - ) - } - }) - - it("should update an existing fulfillment set and replace old service zones by a new one", async function () { - const createData: CreateFulfillmentSetDTO = { - name: "test", - type: "test-type", - service_zones: [ - { - name: "service-zone-test", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - ], - } - - const createdFulfillmentSet = await service.create(createData) - - const createServiceZoneData: CreateServiceZoneDTO = { - fulfillment_set_id: createdFulfillmentSet.id, - name: "service-zone-test2", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "us", - }, - ], - } - - const updateData: UpdateFulfillmentSetDTO = { - id: createdFulfillmentSet.id, - name: "updated-test", - type: "updated-test-type", - service_zones: [createServiceZoneData], - } - - const updatedFulfillmentSet = await service.update(updateData) - - expect(updatedFulfillmentSet).toEqual( - expect.objectContaining({ - id: updateData.id, - name: updateData.name, - type: updateData.type, - service_zones: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - name: (updateData.service_zones![0] as ServiceZoneDTO).name, - geo_zones: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - type: (updateData.service_zones![0] as ServiceZoneDTO) - .geo_zones[0].type, - country_code: ( - updateData.service_zones![0] as ServiceZoneDTO - ).geo_zones[0].country_code, - }), - ]), - }), - ]), - }) - ) - - const serviceZones = await service.listServiceZones() - - expect(serviceZones).toHaveLength(1) - expect(serviceZones[0]).toEqual( - expect.objectContaining({ - id: updatedFulfillmentSet.service_zones[0].id, - }) - ) - }) - - it("should update an existing fulfillment set and add a new service zone", async function () { - const createData: CreateFulfillmentSetDTO = { - name: "test", - type: "test-type", - service_zones: [ - { - name: "service-zone-test", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - ], - } - - const createdFulfillmentSet = await service.create(createData) - - const createServiceZoneData: CreateServiceZoneDTO = { - fulfillment_set_id: createdFulfillmentSet.id, - name: "service-zone-test2", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "us", - }, - ], - } - - const updateData: UpdateFulfillmentSetDTO = { - id: createdFulfillmentSet.id, - name: "updated-test", - type: "updated-test-type", - service_zones: [ - { id: createdFulfillmentSet.service_zones[0].id }, - createServiceZoneData, - ], - } - - const updatedFulfillmentSet = await service.update(updateData) - - expect(updatedFulfillmentSet).toEqual( - expect.objectContaining({ - id: updateData.id, - name: updateData.name, - type: updateData.type, - service_zones: expect.arrayContaining([ - expect.objectContaining({ - id: createdFulfillmentSet.service_zones[0].id, - }), - expect.objectContaining({ - id: expect.any(String), - name: (updateData.service_zones![1] as ServiceZoneDTO).name, - geo_zones: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - type: (updateData.service_zones![1] as ServiceZoneDTO) - .geo_zones[0].type, - country_code: ( - updateData.service_zones![1] as ServiceZoneDTO - ).geo_zones[0].country_code, - }), - ]), - }), - ]), - }) - ) - }) - - it("should fail on duplicated fulfillment set name", async function () { - const createData = [ - { - name: "test", - type: "test-type", - }, - { - name: "test2", - type: "test-type2", - }, - ] - - const createdFulfillmentSets = await service.create(createData) - - const updateData = { - id: createdFulfillmentSets[1].id, - name: "test", // This is the name of the first fulfillment set - type: "updated-test-type2", - } - - const err = await service.update(updateData).catch((e) => e) - - expect(err).toBeDefined() - expect(err.constraint).toBe("IDX_fulfillment_set_name_unique") - }) - - it("should update a collection of fulfillment sets and replace old service zones by new ones", async function () { - const createData: CreateFulfillmentSetDTO[] = [ - { - name: "test1", - type: "test-type1", - service_zones: [ - { - name: "service-zone-test1", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - ], - }, - { - name: "test2", - type: "test-type2", - service_zones: [ - { - name: "service-zone-test2", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "us", - }, - ], - }, - ], - }, - ] - - const createdFulfillmentSets = await service.create(createData) - - const updateData: UpdateFulfillmentSetDTO[] = - createdFulfillmentSets.map((fulfillmentSet, index) => ({ - id: fulfillmentSet.id, - name: `updated-test${index + 1}`, - type: `updated-test-type${index + 1}`, - service_zones: [ - { - name: `new-service-zone-test${index + 1}`, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "test", - }, - ], - }, - ], - })) - - const updatedFulfillmentSets = await service.update(updateData) - - expect(updatedFulfillmentSets).toHaveLength(2) - - for (const data_ of updateData) { - const expectedFulfillmentSet = updatedFulfillmentSets.find( - (f) => f.id === data_.id - ) - expect(expectedFulfillmentSet).toEqual( - expect.objectContaining({ - id: data_.id, - name: data_.name, - type: data_.type, - service_zones: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - name: (data_.service_zones![0] as ServiceZoneDTO).name, - geo_zones: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - type: (data_.service_zones![0] as ServiceZoneDTO) - .geo_zones[0].type, - country_code: ( - data_.service_zones![0] as ServiceZoneDTO - ).geo_zones[0].country_code, - }), - ]), - }), - ]), - }) - ) - } - - const serviceZones = await service.listServiceZones() - - expect(serviceZones).toHaveLength(2) - expect(serviceZones).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - name: (updateData[0].service_zones![0] as ServiceZoneDTO) - .name, - }), - expect.objectContaining({ - name: (updateData[1].service_zones![0] as ServiceZoneDTO) - .name, - }), - ]) - ) - }) - - it("should update a collection of fulfillment sets and add new service zones", async function () { - const createData: CreateFulfillmentSetDTO[] = [ - { - name: "test1", - type: "test-type1", - service_zones: [ - { - name: "service-zone-test1", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - ], - }, - { - name: "test2", - type: "test-type2", - service_zones: [ - { - name: "service-zone-test2", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "us", - }, - ], - }, - ], - }, - ] - - const createdFulfillmentSets = await service.create(createData) - - const updateData: UpdateFulfillmentSetDTO[] = - createdFulfillmentSets.map((fulfillmentSet, index) => ({ - id: fulfillmentSet.id, - name: `updated-test${index + 1}`, - type: `updated-test-type${index + 1}`, - service_zones: [ - ...fulfillmentSet.service_zones, - { - name: `added-service-zone-test${index + 1}`, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "test", - }, - ], - }, - ], - })) - - const updatedFulfillmentSets = await service.update(updateData) - - expect(updatedFulfillmentSets).toHaveLength(2) - - for (const data_ of updateData) { - const expectedFulfillmentSet = updatedFulfillmentSets.find( - (f) => f.id === data_.id - ) - expect(expectedFulfillmentSet).toEqual( - expect.objectContaining({ - id: data_.id, - name: data_.name, - type: data_.type, - service_zones: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - }), - expect.objectContaining({ - id: expect.any(String), - name: (data_.service_zones![1] as ServiceZoneDTO).name, - geo_zones: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - type: (data_.service_zones![1] as ServiceZoneDTO) - .geo_zones[0].type, - country_code: ( - data_.service_zones![1] as ServiceZoneDTO - ).geo_zones[0].country_code, - }), - ]), - }), - ]), - }) - ) - } - - const serviceZones = await service.listServiceZones() - - expect(serviceZones).toHaveLength(4) - expect(serviceZones).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - name: createdFulfillmentSets[0].service_zones![0].name, - }), - expect.objectContaining({ - name: createdFulfillmentSets[1].service_zones![0].name, - }), - expect.objectContaining({ - name: (updateData[0].service_zones![1] as ServiceZoneDTO) - .name, - }), - expect.objectContaining({ - name: (updateData[1].service_zones![1] as ServiceZoneDTO) - .name, - }), - ]) - ) - }) - }) - - describe("on update service zones", () => { - it("should update an existing service zone", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - - const createData: CreateServiceZoneDTO = { - name: "service-zone-test", - fulfillment_set_id: fulfillmentSet.id, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - } - - const createdServiceZone = await service.createServiceZones( - createData - ) - - const updateData = { - id: createdServiceZone.id, - name: "updated-service-zone-test", - geo_zones: [ - { - id: createdServiceZone.geo_zones[0].id, - type: GeoZoneType.COUNTRY, - country_code: "us", - }, - ], - } - - const updatedServiceZone = await service.updateServiceZones( - updateData - ) - - expect(updatedServiceZone).toEqual( - expect.objectContaining({ - id: updateData.id, - name: updateData.name, - geo_zones: expect.arrayContaining([ - expect.objectContaining({ - id: updateData.geo_zones[0].id, - type: updateData.geo_zones[0].type, - country_code: updateData.geo_zones[0].country_code, - }), - ]), - }) - ) - }) - - it("should update a collection of service zones", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - - const createData: CreateServiceZoneDTO[] = [ - { - name: "service-zone-test", - fulfillment_set_id: fulfillmentSet.id, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - { - name: "service-zone-test2", - fulfillment_set_id: fulfillmentSet.id, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "us", - }, - ], - }, - ] - - const createdServiceZones = await service.createServiceZones( - createData - ) - - const updateData: UpdateServiceZoneDTO[] = createdServiceZones.map( - (serviceZone, index) => ({ - id: serviceZone.id, - name: `updated-service-zone-test${index + 1}`, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: index % 2 === 0 ? "us" : "fr", - }, - ], - }) - ) - - const updatedServiceZones = await service.updateServiceZones( - updateData - ) - - expect(updatedServiceZones).toHaveLength(2) - - for (const data_ of updateData) { - const expectedServiceZone = updatedServiceZones.find( - (serviceZone) => serviceZone.id === data_.id - ) - expect(expectedServiceZone).toEqual( - expect.objectContaining({ - id: data_.id, - name: data_.name, - geo_zones: expect.arrayContaining([ - expect.objectContaining({ - type: (data_.geo_zones![0] as GeoZoneDTO).type, - country_code: (data_.geo_zones![0] as GeoZoneDTO) - .country_code, - }), - ]), - }) - ) - } - }) - - it("should fail on duplicated service zone name", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - - const createData: CreateServiceZoneDTO[] = [ - { - name: "service-zone-test", - fulfillment_set_id: fulfillmentSet.id, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - { - name: "service-zone-test2", - fulfillment_set_id: fulfillmentSet.id, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "us", - }, - ], - }, - ] - - const createdServiceZones = await service.createServiceZones( - createData - ) - - const updateData: UpdateServiceZoneDTO = { - id: createdServiceZones[1].id, - name: "service-zone-test", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "us", - }, - ], - } - - const err = await service - .updateServiceZones(updateData) - .catch((e) => e) - - expect(err).toBeDefined() - expect(err.constraint).toBe("IDX_service_zone_name_unique") - }) - }) - - describe("on update geo zones", () => { - it("should update an existing geo zone", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - - const createData: CreateGeoZoneDTO = { - service_zone_id: serviceZone.id, - type: GeoZoneType.COUNTRY, - country_code: "fr", - } - - const createdGeoZone = await service.createGeoZones(createData) - - const updateData: UpdateGeoZoneDTO = { - id: createdGeoZone.id, - type: GeoZoneType.COUNTRY, - country_code: "us", - } - - const updatedGeoZone = await service.updateGeoZones(updateData) - - expect(updatedGeoZone).toEqual( - expect.objectContaining({ - id: updateData.id, - type: updateData.type, - country_code: updateData.country_code, - }) - ) - }) - - it("should update a collection of geo zones", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - - const createData: CreateGeoZoneDTO[] = [ - { - service_zone_id: serviceZone.id, - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - { - service_zone_id: serviceZone.id, - type: GeoZoneType.COUNTRY, - country_code: "us", - }, - ] - - const createdGeoZones = await service.createGeoZones(createData) - - const updateData: UpdateGeoZoneDTO[] = createdGeoZones.map( - (geoZone, index) => ({ - id: geoZone.id, - type: GeoZoneType.COUNTRY, - country_code: index % 2 === 0 ? "us" : "fr", - }) - ) - - const updatedGeoZones = await service.updateGeoZones(updateData) - - expect(updatedGeoZones).toHaveLength(2) - - for (const data_ of updateData) { - const expectedGeoZone = updatedGeoZones.find( - (geoZone) => geoZone.id === data_.id - ) - expect(expectedGeoZone).toEqual( - expect.objectContaining({ - id: data_.id, - type: data_.type, - country_code: data_.country_code, - }) - ) - } - }) - }) - - describe("on update shipping options", () => { - it("should update a shipping option", async () => { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - const shippingProfile = await service.createShippingProfiles({ - name: "test", - type: "default", - }) - - const providerId = await createProvider( - MikroOrmWrapper, - "sp_jdafwfleiwuonl" - ) - - const shippingOptionData = generateCreateShippingOptionsData({ - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - }) - - const shippingOption = await service.createShippingOptions( - shippingOptionData - ) - - const updateData = { - id: shippingOption.id, - name: "updated-test", - price_type: "calculated", - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - type: { - code: "updated-test", - description: "updated-test", - label: "updated-test", - }, - data: { - amount: 2000, - }, - rules: [ - { - attribute: "new-test", - operator: "eq", - value: "new-test", - }, - ], - } - - const updatedShippingOption = await service.updateShippingOptions( - updateData - ) - - expect(updatedShippingOption).toEqual( - expect.objectContaining({ - id: updateData.id, - name: updateData.name, - price_type: updateData.price_type, - service_zone_id: updateData.service_zone_id, - shipping_profile_id: updateData.shipping_profile_id, - service_provider_id: updateData.service_provider_id, - shipping_option_type_id: expect.any(String), - type: expect.objectContaining({ - id: expect.any(String), - code: updateData.type.code, - description: updateData.type.description, - label: updateData.type.label, - }), - data: updateData.data, - rules: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - attribute: updateData.rules[0].attribute, - operator: updateData.rules[0].operator, - value: updateData.rules[0].value, - }), - ]), - }) - ) - - const rules = await service.listShippingOptionRules() - expect(rules).toHaveLength(1) - expect(rules[0]).toEqual( - expect.objectContaining({ - id: updatedShippingOption.rules[0].id, - }) - ) - - const types = await service.listShippingOptionTypes() - expect(types).toHaveLength(1) - expect(types[0]).toEqual( - expect.objectContaining({ - code: updateData.type.code, - description: updateData.type.description, - label: updateData.type.label, - }) - ) - }) - - it("should update a shipping option without updating the rules or the type", async () => { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - const shippingProfile = await service.createShippingProfiles({ - name: "test", - type: "default", - }) - - const providerId = await createProvider( - MikroOrmWrapper, - "sp_jdafwfleiwuonl" - ) - - const shippingOptionData = generateCreateShippingOptionsData({ - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - }) - - const shippingOption = await service.createShippingOptions( - shippingOptionData - ) - - const updateData = { - id: shippingOption.id, - name: "updated-test", - price_type: "calculated", - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - data: { - amount: 2000, - }, - } - - await service.updateShippingOptions(updateData) - - const updatedShippingOption = await service.retrieveShippingOption( - shippingOption.id, - { - relations: ["rules", "type"], - } - ) - - expect(updatedShippingOption).toEqual( - expect.objectContaining({ - id: updateData.id, - name: updateData.name, - price_type: updateData.price_type, - service_zone_id: updateData.service_zone_id, - shipping_profile_id: updateData.shipping_profile_id, - service_provider_id: updateData.service_provider_id, - shipping_option_type_id: expect.any(String), - type: expect.objectContaining({ - id: expect.any(String), - code: shippingOptionData.type.code, - description: shippingOptionData.type.description, - label: shippingOptionData.type.label, - }), - data: updateData.data, - rules: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - attribute: shippingOptionData.rules[0].attribute, - operator: shippingOptionData.rules[0].operator, - value: shippingOptionData.rules[0].value, - }), - ]), - }) - ) - - const rules = await service.listShippingOptionRules() - expect(rules).toHaveLength(1) - expect(rules[0]).toEqual( - expect.objectContaining({ - id: updatedShippingOption.rules[0].id, - }) - ) - - const types = await service.listShippingOptionTypes() - expect(types).toHaveLength(1) - expect(types[0]).toEqual( - expect.objectContaining({ - code: shippingOptionData.type.code, - description: shippingOptionData.type.description, - label: shippingOptionData.type.label, - }) - ) - }) - - it("should update a collection of shipping options", async () => { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - const shippingProfile = await service.createShippingProfiles({ - name: "test", - type: "default", - }) - - const providerId = await createProvider( - MikroOrmWrapper, - "sp_jdafwfleiwuonl" - ) - - const shippingOptionData = [ - generateCreateShippingOptionsData({ - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - }), - generateCreateShippingOptionsData({ - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - }) - ] - - const shippingOptions = await service.createShippingOptions( - shippingOptionData - ) - - const updateData = [ - { - id: shippingOptions[0].id, - name: "updated-test", - price_type: "calculated", - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - type: { - code: "updated-test", - description: "updated-test", - label: "updated-test", - }, - data: { - amount: 2000, - }, - rules: [ - { - attribute: "new-test", - operator: "eq", - value: "new-test", - }, - ], - }, - { - id: shippingOptions[1].id, - name: "updated-test", - price_type: "calculated", - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - type: { - code: "updated-test", - description: "updated-test", - label: "updated-test", - }, - data: { - amount: 2000, - }, - rules: [ - { - attribute: "new-test", - operator: "eq", - value: "new-test", - }, - ], - }, - ] - - const updatedShippingOption = await service.updateShippingOptions( - updateData - ) - - for (const data_ of updateData) { - const expectedShippingOption = updatedShippingOption.find( - (shippingOption) => shippingOption.id === data_.id - ) - expect(expectedShippingOption).toEqual( - expect.objectContaining({ - id: data_.id, - name: data_.name, - price_type: data_.price_type, - service_zone_id: data_.service_zone_id, - shipping_profile_id: data_.shipping_profile_id, - service_provider_id: data_.service_provider_id, - shipping_option_type_id: expect.any(String), - type: expect.objectContaining({ - id: expect.any(String), - code: data_.type.code, - description: data_.type.description, - label: data_.type.label, - }), - data: data_.data, - rules: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - attribute: data_.rules[0].attribute, - operator: data_.rules[0].operator, - value: data_.rules[0].value, - }), - ]), - }) - ) - } - - const rules = await service.listShippingOptionRules() - expect(rules).toHaveLength(2) - expect(rules).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: updatedShippingOption[0].rules[0].id, - }), - expect.objectContaining({ - id: updatedShippingOption[1].rules[0].id, - }), - ]) - ) - - const types = await service.listShippingOptionTypes() - expect(types).toHaveLength(2) - expect(types).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - code: updateData[0].type.code, - description: updateData[0].type.description, - label: updateData[0].type.label, - }), - expect.objectContaining({ - code: updateData[1].type.code, - description: updateData[1].type.description, - label: updateData[1].type.label, - }), - ]) - ) - }) - - it("should fail to update a non-existent shipping option", async () => { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - const shippingProfile = await service.createShippingProfiles({ - name: "test", - type: "default", - }) - - const [serviceProvider] = - await MikroOrmWrapper.forkManager().execute( - "insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id" - ) - - const shippingOptionData = { - id: "sp_jdafwfleiwuonl", - name: "test", - price_type: "flat", - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: serviceProvider.id, - type: { - code: "test", - description: "test", - label: "test", - }, - data: { - amount: 1000, - }, - rules: [ - { - attribute: "test", - operator: "eq", - value: "test", - }, - ], - } - - const err = await service - .updateShippingOptions(shippingOptionData) - .catch((e) => e) - - expect(err).toBeDefined() - expect(err.message).toBe( - `The following shipping options do not exist: ${shippingOptionData.id}` - ) - }) - - it("should fail to update a shipping option when adding non existing rules", async () => { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - const shippingProfile = await service.createShippingProfiles({ - name: "test", - type: "default", - }) - - const providerId = await createProvider( - MikroOrmWrapper, - "sp_jdafwfleiwuonl" - ) - - const shippingOptionData = generateCreateShippingOptionsData({ - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - }) - - const shippingOption = await service.createShippingOptions( - shippingOptionData - ) - - const updateData = [ - { - id: shippingOption.id, - rules: [ - { - id: "sp_jdafwfleiwuonl", - }, - ], - }, - ] - - const err = await service - .updateShippingOptions(updateData) - .catch((e) => e) - - expect(err).toBeDefined() - expect(err.message).toBe( - `The following rules does not exists: ${updateData[0].rules[0].id} on shipping option ${shippingOption.id}` - ) - }) - - it("should fail to update a shipping option when adding invalid rules", async () => { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - const shippingProfile = await service.createShippingProfiles({ - name: "test", - type: "default", - }) - - const providerId = await createProvider( - MikroOrmWrapper, - "sp_jdafwfleiwuonl" - ) - - const shippingOptionData = generateCreateShippingOptionsData({ - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - }) - - const shippingOption = await service.createShippingOptions( - shippingOptionData - ) - - const updateData = [ - { - id: shippingOption.id, - rules: [ - { - attribute: "test", - operator: "invalid", - value: "test", - }, - ], - }, - ] - - const err = await service - .updateShippingOptions(updateData) - .catch((e) => e) - - expect(err).toBeDefined() - expect(err.message).toBe( - `Rule operator invalid is not supported. Must be one of in, eq, ne, gt, gte, lt, lte, nin` - ) - }) - }) - - describe("on update shipping option rules", () => { - it("should update a shipping option rule", async () => { - const shippingProfile = await service.createShippingProfiles({ - name: "test", - type: "default", - }) - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - const serviceZone = await service.createServiceZones({ - name: "test", - fulfillment_set_id: fulfillmentSet.id, - }) - const providerId = await createProvider( - MikroOrmWrapper, - "sp_jdafwfleiwuonl" - ) - - const shippingOption = await service.createShippingOptions( - generateCreateShippingOptionsData({ - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - }) - ) - - const updateData = { - id: shippingOption.rules[0].id, - attribute: "updated-test", - operator: "eq", - value: "updated-test", - } - - const updatedRule = await service.updateShippingOptionRules( - updateData - ) - - expect(updatedRule).toEqual( - expect.objectContaining({ - id: updateData.id, - attribute: updateData.attribute, - operator: updateData.operator, - value: updateData.value, - }) - ) - }) - - it("should fail to update a non-existent shipping option rule", async () => { - const updateData = { - id: "sp_jdafwfleiwuonl", - attribute: "updated-test", - operator: "eq", - value: "updated-test", - } - - const err = await service - .updateShippingOptionRules(updateData) - .catch((e) => e) - - expect(err).toBeDefined() - expect(err.message).toBe( - `ShippingOptionRule with id "${updateData.id}" not found` - ) - }) - }) - }) - }) - }, -}) diff --git a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/fulfillment-set.spec.ts b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/fulfillment-set.spec.ts new file mode 100644 index 0000000000..bf82435b62 --- /dev/null +++ b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/fulfillment-set.spec.ts @@ -0,0 +1,865 @@ +import {Modules} from "@medusajs/modules-sdk" +import { + CreateFulfillmentSetDTO, + CreateServiceZoneDTO, + IFulfillmentModuleService, + ServiceZoneDTO, + UpdateFulfillmentSetDTO, +} from "@medusajs/types" +import {GeoZoneType} from "@medusajs/utils" +import {moduleIntegrationTestRunner, SuiteOptions} from "medusa-test-utils" + +jest.setTimeout(100000) + +moduleIntegrationTestRunner({ + moduleName: Modules.FULFILLMENT, + testSuite: ({ service }: SuiteOptions) => { + describe("Fulfillment Module Service", () => { + describe("read", () => { + it("should list fulfillment sets with a filter", async function () { + const createdSet1 = await service.create({ + name: "test", + type: "test-type", + }) + const createdSet2 = await service.create({ + name: "test2", + type: "test-type", + service_zones: [ + { + name: "test", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + { + name: "test2", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + { + name: "_test", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + ], + }) + + let listedSets = await service.list( + { + type: createdSet1.type, + }, + { + relations: ["service_zones"], + } + ) + + const listedSets2 = await service.list( + { + type: createdSet1.type, + }, + { + relations: ["service_zones"], + } + ) + + expect(listedSets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdSet1.id }), + expect.objectContaining({ id: createdSet2.id }), + ]) + ) + + // Respecting order id by default + expect(listedSets[1].service_zones).toEqual([ + expect.objectContaining({ name: "test" }), + expect.objectContaining({ name: "test2" }), + expect.objectContaining({ name: "_test" }), + ]) + + expect(listedSets2).toEqual(listedSets2) + + listedSets = await service.list({ + name: createdSet2.name, + }) + + expect(listedSets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdSet2.id }), + ]) + ) + expect(listedSets).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdSet1.id }), + ]) + ) + + listedSets = await service.list({ + service_zones: { name: "test" }, + }) + + expect(listedSets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdSet2.id }), + ]) + ) + expect(listedSets).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdSet1.id }), + ]) + ) + + listedSets = await service.list({ + service_zones: { geo_zones: { country_code: "fr" } }, + }) + + expect(listedSets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdSet2.id }), + ]) + ) + expect(listedSets).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdSet1.id }), + ]) + ) + }) + }) + + describe("mutations", () => { + describe("on create", () => { + it("should create a new fulfillment set", async function () { + const data: CreateFulfillmentSetDTO = { + name: "test", + type: "test-type", + } + + const fulfillmentSet = await service.create(data) + + expect(fulfillmentSet).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: data.name, + type: data.type, + }) + ) + }) + + it("should create a collection of fulfillment sets", async function () { + const data = [ + { + name: "test", + type: "test-type", + }, + { + name: "test2", + type: "test-type2", + }, + ] + + const fulfillmentSets = await service.create(data) + + expect(fulfillmentSets).toHaveLength(2) + + let i = 0 + for (const data_ of data) { + expect(fulfillmentSets[i]).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: data_.name, + type: data_.type, + }) + ) + ++i + } + }) + + it("should create a new fulfillment set with new service zones", async function () { + const data = { + name: "test", + type: "test-type", + service_zones: [ + { + name: "test", + }, + ], + } + + const fulfillmentSet = await service.create(data) + + expect(fulfillmentSet).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: data.name, + type: data.type, + service_zones: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + name: data.service_zones[0].name, + }), + ]), + }) + ) + }) + + it("should create a collection of fulfillment sets with new service zones", async function () { + const data = [ + { + name: "test", + type: "test-type", + service_zones: [ + { + name: "test", + }, + ], + }, + { + name: "test2", + type: "test-type2", + service_zones: [ + { + name: "test2", + }, + ], + }, + { + name: "test3", + type: "test-type3", + service_zones: [ + { + name: "test3", + }, + ], + }, + ] + + const fulfillmentSets = await service.create(data) + + expect(fulfillmentSets).toHaveLength(3) + + let i = 0 + for (const data_ of data) { + expect(fulfillmentSets[i]).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: data_.name, + type: data_.type, + service_zones: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + name: data_.service_zones[0].name, + }), + ]), + }) + ) + ++i + } + }) + + it("should create a new fulfillment set with new service zones and new geo zones", async function () { + const data: CreateFulfillmentSetDTO = { + name: "test", + type: "test-type", + service_zones: [ + { + name: "test", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + ], + } + + const fulfillmentSet = await service.create(data) + + expect(fulfillmentSet).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: data.name, + type: data.type, + service_zones: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + name: (data.service_zones![0] as any).name, + geo_zones: expect.arrayContaining([ + expect.objectContaining({ + type: (data.service_zones![0] as any).geo_zones[0].type, + country_code: (data.service_zones![0] as any) + .geo_zones[0].country_code, + }), + ]), + }), + ]), + }) + ) + }) + + it("should create a collection of fulfillment sets with new service zones and new geo zones", async function () { + const data: CreateFulfillmentSetDTO[] = [ + { + name: "test", + type: "test-type", + service_zones: [ + { + name: "test", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + ], + }, + { + name: "test2", + type: "test-type2", + service_zones: [ + { + name: "test2", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + ], + }, + { + name: "test3", + type: "test-type3", + service_zones: [ + { + name: "test3", + geo_zones: [ + { + type: GeoZoneType.CITY, + country_code: "fr", + city: "lyon", + }, + ], + }, + ], + }, + ] + + const fulfillmentSets = await service.create(data) + + expect(fulfillmentSets).toHaveLength(3) + + let i = 0 + for (const data_ of data) { + expect(fulfillmentSets[i]).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: data_.name, + type: data_.type, + service_zones: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + name: (data_.service_zones![0] as any).name, + geo_zones: expect.arrayContaining([ + expect.objectContaining({ + type: (data_.service_zones![0] as any).geo_zones[0] + .type, + country_code: (data_.service_zones![0] as any) + .geo_zones[0].country_code, + }), + ]), + }), + ]), + }) + ) + ++i + } + }) + + it(`should fail on duplicated fulfillment set name`, async function () { + const data: CreateFulfillmentSetDTO = { + name: "test", + type: "test-type", + } + + await service.create(data) + const err = await service.create(data).catch((e) => e) + + expect(err).toBeDefined() + expect(err.constraint).toBe("IDX_fulfillment_set_name_unique") + }) + }) + + describe("on update", () => { + it("should update an existing fulfillment set", async function () { + const createData: CreateFulfillmentSetDTO = { + name: "test", + type: "test-type", + } + + const createdFulfillmentSet = await service.create(createData) + + const updateData = { + id: createdFulfillmentSet.id, + name: "updated-test", + type: "updated-test-type", + } + + const updatedFulfillmentSets = await service.update(updateData) + + expect(updatedFulfillmentSets).toEqual( + expect.objectContaining({ + id: createdFulfillmentSet.id, + name: updateData.name, + type: updateData.type, + }) + ) + }) + + it("should update a collection of fulfillment sets", async function () { + const createData = [ + { + name: "test", + type: "test-type", + }, + { + name: "test2", + type: "test-type2", + }, + ] + + const createdFulfillmentSets = await service.create(createData) + + const updateData = createdFulfillmentSets.map( + (fulfillmentSet, index) => ({ + id: fulfillmentSet.id, + name: `updated-test${index + 1}`, + type: `updated-test-type${index + 1}`, + }) + ) + + const updatedFulfillmentSets = await service.update(updateData) + const fullfillmentSets = await service.list({ + id: updateData.map((ud) => ud.id), + }) + + expect(updatedFulfillmentSets).toHaveLength(2) + + for (const data_ of updateData) { + const currentFullfillmentSet = fullfillmentSets.find( + (fs) => fs.id === data_.id + ) + + expect(currentFullfillmentSet).toEqual( + expect.objectContaining({ + id: data_.id, + name: data_.name, + type: data_.type, + }) + ) + } + }) + + it("should update an existing fulfillment set and replace old service zones by a new one", async function () { + const createData: CreateFulfillmentSetDTO = { + name: "test", + type: "test-type", + service_zones: [ + { + name: "service-zone-test", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + ], + } + + const createdFulfillmentSet = await service.create(createData) + + const createServiceZoneData: CreateServiceZoneDTO = { + fulfillment_set_id: createdFulfillmentSet.id, + name: "service-zone-test2", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "us", + }, + ], + } + + const updateData: UpdateFulfillmentSetDTO = { + id: createdFulfillmentSet.id, + name: "updated-test", + type: "updated-test-type", + service_zones: [createServiceZoneData], + } + + const updatedFulfillmentSet = await service.update(updateData) + + expect(updatedFulfillmentSet).toEqual( + expect.objectContaining({ + id: updateData.id, + name: updateData.name, + type: updateData.type, + service_zones: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + name: (updateData.service_zones![0] as ServiceZoneDTO).name, + geo_zones: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + type: (updateData.service_zones![0] as ServiceZoneDTO) + .geo_zones[0].type, + country_code: ( + updateData.service_zones![0] as ServiceZoneDTO + ).geo_zones[0].country_code, + }), + ]), + }), + ]), + }) + ) + + const serviceZones = await service.listServiceZones() + + expect(serviceZones).toHaveLength(1) + expect(serviceZones[0]).toEqual( + expect.objectContaining({ + id: updatedFulfillmentSet.service_zones[0].id, + }) + ) + }) + + it("should update an existing fulfillment set and add a new service zone", async function () { + const createData: CreateFulfillmentSetDTO = { + name: "test", + type: "test-type", + service_zones: [ + { + name: "service-zone-test", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + ], + } + + const createdFulfillmentSet = await service.create(createData) + + const createServiceZoneData: CreateServiceZoneDTO = { + fulfillment_set_id: createdFulfillmentSet.id, + name: "service-zone-test2", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "us", + }, + ], + } + + const updateData: UpdateFulfillmentSetDTO = { + id: createdFulfillmentSet.id, + name: "updated-test", + type: "updated-test-type", + service_zones: [ + { id: createdFulfillmentSet.service_zones[0].id }, + createServiceZoneData, + ], + } + + const updatedFulfillmentSet = await service.update(updateData) + + expect(updatedFulfillmentSet).toEqual( + expect.objectContaining({ + id: updateData.id, + name: updateData.name, + type: updateData.type, + service_zones: expect.arrayContaining([ + expect.objectContaining({ + id: createdFulfillmentSet.service_zones[0].id, + }), + expect.objectContaining({ + id: expect.any(String), + name: (updateData.service_zones![1] as ServiceZoneDTO).name, + geo_zones: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + type: (updateData.service_zones![1] as ServiceZoneDTO) + .geo_zones[0].type, + country_code: ( + updateData.service_zones![1] as ServiceZoneDTO + ).geo_zones[0].country_code, + }), + ]), + }), + ]), + }) + ) + }) + + it("should fail on duplicated fulfillment set name", async function () { + const createData = [ + { + name: "test", + type: "test-type", + }, + { + name: "test2", + type: "test-type2", + }, + ] + + const createdFulfillmentSets = await service.create(createData) + + const updateData = { + id: createdFulfillmentSets[1].id, + name: "test", // This is the name of the first fulfillment set + type: "updated-test-type2", + } + + const err = await service.update(updateData).catch((e) => e) + + expect(err).toBeDefined() + expect(err.constraint).toBe("IDX_fulfillment_set_name_unique") + }) + + it("should update a collection of fulfillment sets and replace old service zones by new ones", async function () { + const createData: CreateFulfillmentSetDTO[] = [ + { + name: "test1", + type: "test-type1", + service_zones: [ + { + name: "service-zone-test1", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + ], + }, + { + name: "test2", + type: "test-type2", + service_zones: [ + { + name: "service-zone-test2", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "us", + }, + ], + }, + ], + }, + ] + + const createdFulfillmentSets = await service.create(createData) + + const updateData: UpdateFulfillmentSetDTO[] = + createdFulfillmentSets.map((fulfillmentSet, index) => ({ + id: fulfillmentSet.id, + name: `updated-test${index + 1}`, + type: `updated-test-type${index + 1}`, + service_zones: [ + { + name: `new-service-zone-test${index + 1}`, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "test", + }, + ], + }, + ], + })) + + const updatedFulfillmentSets = await service.update(updateData) + + expect(updatedFulfillmentSets).toHaveLength(2) + + for (const data_ of updateData) { + const expectedFulfillmentSet = updatedFulfillmentSets.find( + (f) => f.id === data_.id + ) + expect(expectedFulfillmentSet).toEqual( + expect.objectContaining({ + id: data_.id, + name: data_.name, + type: data_.type, + service_zones: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + name: (data_.service_zones![0] as ServiceZoneDTO).name, + geo_zones: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + type: (data_.service_zones![0] as ServiceZoneDTO) + .geo_zones[0].type, + country_code: ( + data_.service_zones![0] as ServiceZoneDTO + ).geo_zones[0].country_code, + }), + ]), + }), + ]), + }) + ) + } + + const serviceZones = await service.listServiceZones() + + expect(serviceZones).toHaveLength(2) + expect(serviceZones).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: (updateData[0].service_zones![0] as ServiceZoneDTO) + .name, + }), + expect.objectContaining({ + name: (updateData[1].service_zones![0] as ServiceZoneDTO) + .name, + }), + ]) + ) + }) + + it("should update a collection of fulfillment sets and add new service zones", async function () { + const createData: CreateFulfillmentSetDTO[] = [ + { + name: "test1", + type: "test-type1", + service_zones: [ + { + name: "service-zone-test1", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + ], + }, + { + name: "test2", + type: "test-type2", + service_zones: [ + { + name: "service-zone-test2", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "us", + }, + ], + }, + ], + }, + ] + + const createdFulfillmentSets = await service.create(createData) + + const updateData: UpdateFulfillmentSetDTO[] = + createdFulfillmentSets.map((fulfillmentSet, index) => ({ + id: fulfillmentSet.id, + name: `updated-test${index + 1}`, + type: `updated-test-type${index + 1}`, + service_zones: [ + ...fulfillmentSet.service_zones, + { + name: `added-service-zone-test${index + 1}`, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "test", + }, + ], + }, + ], + })) + + const updatedFulfillmentSets = await service.update(updateData) + + expect(updatedFulfillmentSets).toHaveLength(2) + + for (const data_ of updateData) { + const expectedFulfillmentSet = updatedFulfillmentSets.find( + (f) => f.id === data_.id + ) + expect(expectedFulfillmentSet).toEqual( + expect.objectContaining({ + id: data_.id, + name: data_.name, + type: data_.type, + service_zones: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + }), + expect.objectContaining({ + id: expect.any(String), + name: (data_.service_zones![1] as ServiceZoneDTO).name, + geo_zones: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + type: (data_.service_zones![1] as ServiceZoneDTO) + .geo_zones[0].type, + country_code: ( + data_.service_zones![1] as ServiceZoneDTO + ).geo_zones[0].country_code, + }), + ]), + }), + ]), + }) + ) + } + + const serviceZones = await service.listServiceZones() + + expect(serviceZones).toHaveLength(4) + expect(serviceZones).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: createdFulfillmentSets[0].service_zones![0].name, + }), + expect.objectContaining({ + name: createdFulfillmentSets[1].service_zones![0].name, + }), + expect.objectContaining({ + name: (updateData[0].service_zones![1] as ServiceZoneDTO) + .name, + }), + expect.objectContaining({ + name: (updateData[1].service_zones![1] as ServiceZoneDTO) + .name, + }), + ]) + ) + }) + }) + }) + }) + }, +}) diff --git a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/fulfillment.spec.ts b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/fulfillment.spec.ts new file mode 100644 index 0000000000..8f1927e4e0 --- /dev/null +++ b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/fulfillment.spec.ts @@ -0,0 +1,287 @@ +import { resolve } from "path" +import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" +import { IFulfillmentModuleService } from "@medusajs/types" +import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" +import { + generateCreateFulfillmentData, + generateCreateShippingOptionsData, +} from "../../__fixtures__" +import { initModules } from "medusa-test-utils/dist" +import { FulfillmentProviderService } from "@services" +import { FulfillmentProviderServiceFixtures } from "../../__fixtures__/providers" + +jest.setTimeout(100000) + +const moduleOptions = { + providers: [ + { + resolve: resolve( + process.cwd() + + "/integration-tests/__fixtures__/providers/default-provider" + ), + options: { + config: { + "test-provider": {}, + }, + }, + }, + ], +} + +const providerId = "fixtures-fulfillment-provider_test-provider" + +moduleIntegrationTestRunner({ + moduleName: Modules.FULFILLMENT, + moduleOptions: moduleOptions, + testSuite: ({ + MikroOrmWrapper, + service, + }: SuiteOptions) => { + describe("Fulfillment Module Service", () => { + it("should load and save all the providers on bootstrap", async () => { + const databaseConfig = { + schema: "public", + clientUrl: MikroOrmWrapper.clientUrl, + } + + const providersConfig = {} + for (let i = 0; i < 10; i++) { + providersConfig[`provider-${i}`] = {} + } + + const moduleOptions = { + databaseConfig, + modulesConfig: { + [Modules.FULFILLMENT]: { + definition: ModulesDefinition[Modules.FULFILLMENT], + options: { + databaseConfig, + providers: [ + { + resolve: resolve( + process.cwd() + + "/integration-tests/__fixtures__/providers/default-provider" + ), + options: { + config: providersConfig, + }, + }, + ], + }, + }, + }, + } + + const { shutdown } = await initModules(moduleOptions) + + const fulfillmentProviderrs = + await MikroOrmWrapper.forkManager().execute( + `SELECT * FROM fulfillment_provider` + ) + + expect(fulfillmentProviderrs).toHaveLength( + Object.keys(providersConfig).length + 1 // +1 for the default provider + ) + + for (const [name] of Object.entries(providersConfig)) { + const provider = fulfillmentProviderrs.find((p) => { + return ( + p.id === + FulfillmentProviderService.getRegistrationIdentifier( + FulfillmentProviderServiceFixtures, + name + ) + ) + }) + expect(provider).toBeDefined() + } + + await shutdown() + }) + }) + + describe("Fulfillment Module Service", () => { + describe("read", () => { + it("should list fulfillment", async () => { + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + + const shippingOption = await service.createShippingOptions( + generateCreateShippingOptionsData({ + fulfillment_provider_id: providerId, + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + }) + ) + + const fulfillment = await service.createFulfillment( + generateCreateFulfillmentData({ + provider_id: providerId, + shipping_option_id: shippingOption.id, + }) + ) + + const result = await service.listFulfillments({ + shipping_option_id: shippingOption.id, + }) + + expect(result.length).toEqual(1) + expect(result[0].id).toEqual(fulfillment.id) + }) + + it("should retrieve the fulfillment options", async () => { + const fulfillmentOptions = await service.retrieveFulfillmentOptions( + providerId + ) + + expect(fulfillmentOptions).toEqual({}) + }) + }) + + describe("mutations", () => { + describe("on create", () => { + it("should create a fulfillment", async () => { + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + + const shippingOption = await service.createShippingOptions( + generateCreateShippingOptionsData({ + fulfillment_provider_id: providerId, + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + }) + ) + + const fulfillment = await service.createFulfillment( + generateCreateFulfillmentData({ + provider_id: providerId, + shipping_option_id: shippingOption.id, + }) + ) + + expect(fulfillment).toEqual( + expect.objectContaining({ + id: expect.any(String), + packed_at: null, + shipped_at: null, + delivered_at: null, + canceled_at: null, + data: null, + provider_id: providerId, + shipping_option_id: shippingOption.id, + metadata: null, + delivery_address: expect.objectContaining({ + id: expect.any(String), + }), + items: [ + expect.objectContaining({ + id: expect.any(String), + }), + ], + labels: [ + expect.objectContaining({ + id: expect.any(String), + }), + ], + }) + ) + }) + }) + + describe("on cancel", () => { + let fulfillment + + beforeEach(async () => { + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + + const shippingOption = await service.createShippingOptions( + generateCreateShippingOptionsData({ + fulfillment_provider_id: providerId, + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + }) + ) + + fulfillment = await service.createFulfillment( + generateCreateFulfillmentData({ + provider_id: providerId, + shipping_option_id: shippingOption.id, + }) + ) + }) + + it("should cancel a fulfillment successfully", async () => { + const result = await service.cancelFulfillment(fulfillment.id) + // should be idempotent + const idempotentResult = await service.cancelFulfillment( + fulfillment.id + ) + + expect(result.canceled_at).not.toBeNull() + expect(idempotentResult.canceled_at).not.toBeNull() + expect(idempotentResult.canceled_at).toEqual(result.canceled_at) + }) + + it("should fail to cancel a fulfillment that is already shipped", async () => { + await service.updateFulfillment(fulfillment.id, { + shipped_at: new Date(), + }) + + const err = await service + .cancelFulfillment(fulfillment.id) + .catch((e) => e) + + expect(err.message).toEqual( + `Fulfillment with id ${fulfillment.id} already shipped` + ) + }) + + it("should fail to cancel a fulfillment that is already delivered", async () => { + await service.updateFulfillment(fulfillment.id, { + delivered_at: new Date(), + }) + + const err = await service + .cancelFulfillment(fulfillment.id) + .catch((e) => e) + + expect(err.message).toEqual( + `Fulfillment with id ${fulfillment.id} already delivered` + ) + }) + }) + }) + }) + }, +}) diff --git a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/geo-zone.spec.ts b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/geo-zone.spec.ts new file mode 100644 index 0000000000..84e6c2c103 --- /dev/null +++ b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/geo-zone.spec.ts @@ -0,0 +1,228 @@ +import {Modules} from "@medusajs/modules-sdk" +import { + CreateGeoZoneDTO, + IFulfillmentModuleService, + UpdateGeoZoneDTO, +} from "@medusajs/types" +import {GeoZoneType} from "@medusajs/utils" +import {moduleIntegrationTestRunner, SuiteOptions} from "medusa-test-utils" + +jest.setTimeout(100000) + +moduleIntegrationTestRunner({ + moduleName: Modules.FULFILLMENT, + testSuite: ({ service }: SuiteOptions) => { + describe("Fulfillment Module Service", () => { + describe("read", () => { + it("should list geo zones with a filter", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + + const createdZone1 = await service.createGeoZones({ + service_zone_id: serviceZone.id, + type: GeoZoneType.COUNTRY, + country_code: "fr", + }) + const createdZone2 = await service.createGeoZones({ + service_zone_id: serviceZone.id, + type: GeoZoneType.COUNTRY, + country_code: "us", + }) + + let listedZones = await service.listGeoZones({ + type: createdZone1.type, + }) + + expect(listedZones).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdZone1.id }), + expect.objectContaining({ id: createdZone2.id }), + ]) + ) + + listedZones = await service.listGeoZones({ + country_code: createdZone2.country_code, + }) + + expect(listedZones).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdZone2.id }), + ]) + ) + expect(listedZones).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdZone1.id }), + ]) + ) + }) + }) + + describe("mutations", () => { + describe("on create", () => { + it("should create a new geo zone", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + + const data: CreateGeoZoneDTO = { + service_zone_id: serviceZone.id, + type: GeoZoneType.COUNTRY, + country_code: "fr", + } + + const geoZone = await service.createGeoZones(data) + + expect(geoZone).toEqual( + expect.objectContaining({ + id: expect.any(String), + type: data.type, + country_code: data.country_code, + }) + ) + }) + + it("should create a collection of geo zones", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + + const data: CreateGeoZoneDTO[] = [ + { + service_zone_id: serviceZone.id, + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + { + service_zone_id: serviceZone.id, + type: GeoZoneType.COUNTRY, + country_code: "us", + }, + ] + + const geoZones = await service.createGeoZones(data) + + expect(geoZones).toHaveLength(2) + + let i = 0 + for (const data_ of data) { + expect(geoZones[i]).toEqual( + expect.objectContaining({ + id: expect.any(String), + type: data_.type, + country_code: data_.country_code, + }) + ) + ++i + } + }) + }) + + describe("on update", () => { + it("should update an existing geo zone", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + + const createData: CreateGeoZoneDTO = { + service_zone_id: serviceZone.id, + type: GeoZoneType.COUNTRY, + country_code: "fr", + } + + const createdGeoZone = await service.createGeoZones(createData) + + const updateData: UpdateGeoZoneDTO = { + id: createdGeoZone.id, + type: GeoZoneType.COUNTRY, + country_code: "us", + } + + const updatedGeoZone = await service.updateGeoZones(updateData) + + expect(updatedGeoZone).toEqual( + expect.objectContaining({ + id: updateData.id, + type: updateData.type, + country_code: updateData.country_code, + }) + ) + }) + + it("should update a collection of geo zones", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + + const createData: CreateGeoZoneDTO[] = [ + { + service_zone_id: serviceZone.id, + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + { + service_zone_id: serviceZone.id, + type: GeoZoneType.COUNTRY, + country_code: "us", + }, + ] + + const createdGeoZones = await service.createGeoZones(createData) + + const updateData: UpdateGeoZoneDTO[] = createdGeoZones.map( + (geoZone, index) => ({ + id: geoZone.id, + type: GeoZoneType.COUNTRY, + country_code: index % 2 === 0 ? "us" : "fr", + }) + ) + + const updatedGeoZones = await service.updateGeoZones(updateData) + + expect(updatedGeoZones).toHaveLength(2) + + for (const data_ of updateData) { + const expectedGeoZone = updatedGeoZones.find( + (geoZone) => geoZone.id === data_.id + ) + expect(expectedGeoZone).toEqual( + expect.objectContaining({ + id: data_.id, + type: data_.type, + country_code: data_.country_code, + }) + ) + } + }) + }) + }) + }) + }, +}) diff --git a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts new file mode 100644 index 0000000000..df3e2dcde4 --- /dev/null +++ b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts @@ -0,0 +1,374 @@ +import { Modules } from "@medusajs/modules-sdk" +import { + CreateServiceZoneDTO, + GeoZoneDTO, + IFulfillmentModuleService, + UpdateServiceZoneDTO, +} from "@medusajs/types" +import { GeoZoneType } from "@medusajs/utils" +import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" + +jest.setTimeout(100000) + +moduleIntegrationTestRunner({ + moduleName: Modules.FULFILLMENT, + testSuite: ({ service }: SuiteOptions) => { + describe("Fulfillment Module Service", () => { + describe("read", () => { + it("should list service zones with a filter", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + + const createdZone1 = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + const createdZone2 = await service.createServiceZones({ + name: "test2", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }) + + let listedZones = await service.listServiceZones({ + name: createdZone2.name, + }) + + expect(listedZones).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdZone2.id }), + ]) + ) + expect(listedZones).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdZone1.id }), + ]) + ) + + listedZones = await service.listServiceZones({ + geo_zones: { country_code: "fr" }, + }) + + expect(listedZones).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdZone2.id }), + ]) + ) + expect(listedZones).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdZone1.id }), + ]) + ) + }) + }) + + describe("mutations", () => { + describe("on create", () => { + it("should create a new service zone", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + + const data: CreateServiceZoneDTO = { + name: "test", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + } + + const serviceZone = await service.createServiceZones(data) + + expect(serviceZone).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: data.name, + geo_zones: expect.arrayContaining([ + expect.objectContaining({ + type: (data.geo_zones![0] as GeoZoneDTO).type, + country_code: (data.geo_zones![0] as GeoZoneDTO) + .country_code, + }), + ]), + }) + ) + }) + + it("should create a collection of service zones", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + + const data: CreateServiceZoneDTO[] = [ + { + name: "test", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + { + name: "test2", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + { + name: "test3", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "uk", + }, + ], + }, + ] + + const serviceZones = await service.createServiceZones(data) + + expect(serviceZones).toHaveLength(3) + + let i = 0 + for (const data_ of data) { + expect(serviceZones[i]).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: data_.name, + geo_zones: expect.arrayContaining([ + expect.objectContaining({ + type: (data_.geo_zones![0] as GeoZoneDTO).type, + country_code: (data_.geo_zones![0] as GeoZoneDTO) + .country_code, + }), + ]), + }) + ) + ++i + } + }) + + it("should fail on duplicated service zone name", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + + const data: CreateServiceZoneDTO = { + name: "test", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + } + + await service.createServiceZones(data) + const err = await service.createServiceZones(data).catch((e) => e) + + expect(err).toBeDefined() + expect(err.constraint).toBe("IDX_service_zone_name_unique") + }) + }) + + describe("on update", () => { + it("should update an existing service zone", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + + const createData: CreateServiceZoneDTO = { + name: "service-zone-test", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + } + + const createdServiceZone = await service.createServiceZones( + createData + ) + + const updateData = { + id: createdServiceZone.id, + name: "updated-service-zone-test", + geo_zones: [ + { + id: createdServiceZone.geo_zones[0].id, + type: GeoZoneType.COUNTRY, + country_code: "us", + }, + ], + } + + const updatedServiceZone = await service.updateServiceZones( + updateData + ) + + expect(updatedServiceZone).toEqual( + expect.objectContaining({ + id: updateData.id, + name: updateData.name, + geo_zones: expect.arrayContaining([ + expect.objectContaining({ + id: updateData.geo_zones[0].id, + type: updateData.geo_zones[0].type, + country_code: updateData.geo_zones[0].country_code, + }), + ]), + }) + ) + }) + + it("should update a collection of service zones", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + + const createData: CreateServiceZoneDTO[] = [ + { + name: "service-zone-test", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + { + name: "service-zone-test2", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "us", + }, + ], + }, + ] + + const createdServiceZones = await service.createServiceZones( + createData + ) + + const updateData: UpdateServiceZoneDTO[] = createdServiceZones.map( + (serviceZone, index) => ({ + id: serviceZone.id, + name: `updated-service-zone-test${index + 1}`, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: index % 2 === 0 ? "us" : "fr", + }, + ], + }) + ) + + const updatedServiceZones = await service.updateServiceZones( + updateData + ) + + expect(updatedServiceZones).toHaveLength(2) + + for (const data_ of updateData) { + const expectedServiceZone = updatedServiceZones.find( + (serviceZone) => serviceZone.id === data_.id + ) + expect(expectedServiceZone).toEqual( + expect.objectContaining({ + id: data_.id, + name: data_.name, + geo_zones: expect.arrayContaining([ + expect.objectContaining({ + type: (data_.geo_zones![0] as GeoZoneDTO).type, + country_code: (data_.geo_zones![0] as GeoZoneDTO) + .country_code, + }), + ]), + }) + ) + } + }) + + it("should fail on duplicated service zone name", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + + const createData: CreateServiceZoneDTO[] = [ + { + name: "service-zone-test", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + { + name: "service-zone-test2", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "us", + }, + ], + }, + ] + + const createdServiceZones = await service.createServiceZones( + createData + ) + + const updateData: UpdateServiceZoneDTO = { + id: createdServiceZones[1].id, + name: "service-zone-test", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "us", + }, + ], + } + + const err = await service + .updateServiceZones(updateData) + .catch((e) => e) + + expect(err).toBeDefined() + expect(err.constraint).toBe("IDX_service_zone_name_unique") + }) + }) + }) + }) + }, +}) diff --git a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts new file mode 100644 index 0000000000..e4a730266e --- /dev/null +++ b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts @@ -0,0 +1,982 @@ +import { Modules } from "@medusajs/modules-sdk" +import { + CreateShippingOptionDTO, + IFulfillmentModuleService, +} from "@medusajs/types" +import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" +import { generateCreateShippingOptionsData } from "../../__fixtures__" +import { resolve } from "path" +import { FulfillmentProviderService } from "@services" +import { FulfillmentProviderServiceFixtures } from "../../__fixtures__/providers" + +jest.setTimeout(100000) + +const moduleOptions = { + providers: [ + { + resolve: resolve( + process.cwd() + + "/integration-tests/__fixtures__/providers/default-provider" + ), + options: { + config: { + "test-provider": {}, + }, + }, + }, + ], +} + +const providerId = FulfillmentProviderService.getRegistrationIdentifier( + FulfillmentProviderServiceFixtures, + "test-provider" +) + +moduleIntegrationTestRunner({ + moduleName: Modules.FULFILLMENT, + moduleOptions, + testSuite: ({ + MikroOrmWrapper, + service, + }: SuiteOptions) => { + describe("Fulfillment Module Service", () => { + describe("read", () => { + it("should list shipping options with a filter", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + service_zones: [ + { + name: "test", + }, + ], + }) + + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + + const [shippingOption1] = await service.createShippingOptions([ + generateCreateShippingOptionsData({ + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + rules: [ + { + attribute: "test-attribute", + operator: "in", + value: ["test"], + }, + ], + }), + generateCreateShippingOptionsData({ + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + rules: [ + { + attribute: "test-attribute", + operator: "eq", + value: "test", + }, + { + attribute: "test-attribute2.options", + operator: "in", + value: ["test", "test2"], + }, + ], + }), + ]) + + const listedOptions = await service.listShippingOptions({ + name: shippingOption1.name, + }) + + expect(listedOptions).toHaveLength(1) + expect(listedOptions[0].id).toEqual(shippingOption1.id) + }) + + it("should list shipping options with a context", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + service_zones: [ + { + name: "test", + }, + ], + }) + + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + + const [shippingOption1, , shippingOption3] = + await service.createShippingOptions([ + generateCreateShippingOptionsData({ + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + rules: [ + { + attribute: "test-attribute", + operator: "in", + value: ["test"], + }, + ], + }), + generateCreateShippingOptionsData({ + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + rules: [ + { + attribute: "test-attribute", + operator: "in", + value: ["test-test"], + }, + ], + }), + generateCreateShippingOptionsData({ + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + rules: [ + { + attribute: "test-attribute", + operator: "eq", + value: "test", + }, + { + attribute: "test-attribute2.options", + operator: "in", + value: ["test", "test2"], + }, + ], + }), + ]) + + let listedOptions = await service.listShippingOptions({ + context: { + "test-attribute": "test", + "test-attribute2": { + options: "test2", + }, + }, + }) + + expect(listedOptions).toHaveLength(2) + expect(listedOptions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: shippingOption1.id }), + expect.objectContaining({ id: shippingOption3.id }), + ]) + ) + + listedOptions = await service.listShippingOptions({ + fulfillment_set_id: { $ne: fulfillmentSet.id }, + context: { + "test-attribute": "test", + "test-attribute2": { + options: "test2", + }, + }, + }) + + expect(listedOptions).toHaveLength(0) + + listedOptions = await service.listShippingOptions({ + fulfillment_set_type: "non-existing-type", + context: { + "test-attribute": "test", + "test-attribute2": { + options: "test2", + }, + }, + }) + + expect(listedOptions).toHaveLength(0) + }) + }) + + describe("mutations", () => { + describe("on create", () => { + it("should create a new shipping option", async function () { + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + + const createData: CreateShippingOptionDTO = + generateCreateShippingOptionsData({ + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + }) + + const createdShippingOption = await service.createShippingOptions( + createData + ) + + expect(createdShippingOption).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: createData.name, + price_type: createData.price_type, + service_zone_id: createData.service_zone_id, + shipping_profile_id: createData.shipping_profile_id, + fulfillment_provider_id: createData.fulfillment_provider_id, + shipping_option_type_id: expect.any(String), + type: expect.objectContaining({ + id: expect.any(String), + code: createData.type.code, + description: createData.type.description, + label: createData.type.label, + }), + data: createData.data, + rules: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + attribute: createData.rules![0].attribute, + operator: createData.rules![0].operator, + value: createData.rules![0].value, + }), + ]), + }) + ) + }) + + it("should create multiple new shipping options", async function () { + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + + const createData: CreateShippingOptionDTO[] = [ + generateCreateShippingOptionsData({ + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + }), + generateCreateShippingOptionsData({ + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + }), + ] + + const createdShippingOptions = await service.createShippingOptions( + createData + ) + + expect(createdShippingOptions).toHaveLength(2) + + let i = 0 + for (const data_ of createData) { + expect(createdShippingOptions[i]).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: data_.name, + price_type: data_.price_type, + service_zone_id: data_.service_zone_id, + shipping_profile_id: data_.shipping_profile_id, + fulfillment_provider_id: data_.fulfillment_provider_id, + shipping_option_type_id: expect.any(String), + type: expect.objectContaining({ + id: expect.any(String), + code: data_.type.code, + description: data_.type.description, + label: data_.type.label, + }), + data: data_.data, + rules: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + attribute: data_.rules![0].attribute, + operator: data_.rules![0].operator, + value: data_.rules![0].value, + }), + ]), + }) + ) + ++i + } + }) + + it("should fail to create a new shipping option with invalid rules", async function () { + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + + const createData: CreateShippingOptionDTO = + generateCreateShippingOptionsData({ + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + rules: [ + { + attribute: "test-attribute", + operator: "invalid" as any, + value: "test-value", + }, + ], + }) + + const err = await service + .createShippingOptions(createData) + .catch((e) => e) + + expect(err).toBeDefined() + expect(err.message).toBe( + "Rule operator invalid is not supported. Must be one of in, eq, ne, gt, gte, lt, lte, nin" + ) + }) + }) + + describe("on update", () => { + it("should update a shipping option", async () => { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + + const shippingOptionData = generateCreateShippingOptionsData({ + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + }) + + const shippingOption = await service.createShippingOptions( + shippingOptionData + ) + + const updateData = { + id: shippingOption.id, + name: "updated-test", + price_type: "calculated", + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + type: { + code: "updated-test", + description: "updated-test", + label: "updated-test", + }, + data: { + amount: 2000, + }, + rules: [ + { + attribute: "new-test", + operator: "eq", + value: "new-test", + }, + ], + } + + const updatedShippingOption = await service.updateShippingOptions( + updateData + ) + + expect(updatedShippingOption).toEqual( + expect.objectContaining({ + id: updateData.id, + name: updateData.name, + price_type: updateData.price_type, + service_zone_id: updateData.service_zone_id, + shipping_profile_id: updateData.shipping_profile_id, + fulfillment_provider_id: updateData.fulfillment_provider_id, + shipping_option_type_id: expect.any(String), + type: expect.objectContaining({ + id: expect.any(String), + code: updateData.type.code, + description: updateData.type.description, + label: updateData.type.label, + }), + data: updateData.data, + rules: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + attribute: updateData.rules[0].attribute, + operator: updateData.rules[0].operator, + value: updateData.rules[0].value, + }), + ]), + }) + ) + + const rules = await service.listShippingOptionRules() + expect(rules).toHaveLength(1) + expect(rules[0]).toEqual( + expect.objectContaining({ + id: updatedShippingOption.rules[0].id, + }) + ) + + const types = await service.listShippingOptionTypes() + expect(types).toHaveLength(1) + expect(types[0]).toEqual( + expect.objectContaining({ + code: updateData.type.code, + description: updateData.type.description, + label: updateData.type.label, + }) + ) + }) + + it("should update a shipping option without updating the rules or the type", async () => { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + + const shippingOptionData = generateCreateShippingOptionsData({ + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + }) + + const shippingOption = await service.createShippingOptions( + shippingOptionData + ) + + const updateData = { + id: shippingOption.id, + name: "updated-test", + price_type: "calculated", + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + data: { + amount: 2000, + }, + } + + await service.updateShippingOptions(updateData) + + const updatedShippingOption = await service.retrieveShippingOption( + shippingOption.id, + { + relations: ["rules", "type"], + } + ) + + expect(updatedShippingOption).toEqual( + expect.objectContaining({ + id: updateData.id, + name: updateData.name, + price_type: updateData.price_type, + service_zone_id: updateData.service_zone_id, + shipping_profile_id: updateData.shipping_profile_id, + fulfillment_provider_id: updateData.fulfillment_provider_id, + shipping_option_type_id: expect.any(String), + type: expect.objectContaining({ + id: expect.any(String), + code: shippingOptionData.type.code, + description: shippingOptionData.type.description, + label: shippingOptionData.type.label, + }), + data: updateData.data, + rules: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + attribute: shippingOptionData.rules[0].attribute, + operator: shippingOptionData.rules[0].operator, + value: shippingOptionData.rules[0].value, + }), + ]), + }) + ) + + const rules = await service.listShippingOptionRules() + expect(rules).toHaveLength(1) + expect(rules[0]).toEqual( + expect.objectContaining({ + id: updatedShippingOption.rules[0].id, + }) + ) + + const types = await service.listShippingOptionTypes() + expect(types).toHaveLength(1) + expect(types[0]).toEqual( + expect.objectContaining({ + code: shippingOptionData.type.code, + description: shippingOptionData.type.description, + label: shippingOptionData.type.label, + }) + ) + }) + + it("should update a collection of shipping options", async () => { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + + const shippingOptionData = [ + generateCreateShippingOptionsData({ + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + }), + generateCreateShippingOptionsData({ + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + }), + ] + + const shippingOptions = await service.createShippingOptions( + shippingOptionData + ) + + const updateData = [ + { + id: shippingOptions[0].id, + name: "updated-test", + price_type: "calculated", + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + type: { + code: "updated-test", + description: "updated-test", + label: "updated-test", + }, + data: { + amount: 2000, + }, + rules: [ + { + attribute: "new-test", + operator: "eq", + value: "new-test", + }, + ], + }, + { + id: shippingOptions[1].id, + name: "updated-test", + price_type: "calculated", + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + type: { + code: "updated-test", + description: "updated-test", + label: "updated-test", + }, + data: { + amount: 2000, + }, + rules: [ + { + attribute: "new-test", + operator: "eq", + value: "new-test", + }, + ], + }, + ] + + const updatedShippingOption = await service.updateShippingOptions( + updateData + ) + + for (const data_ of updateData) { + const expectedShippingOption = updatedShippingOption.find( + (shippingOption) => shippingOption.id === data_.id + ) + expect(expectedShippingOption).toEqual( + expect.objectContaining({ + id: data_.id, + name: data_.name, + price_type: data_.price_type, + service_zone_id: data_.service_zone_id, + shipping_profile_id: data_.shipping_profile_id, + fulfillment_provider_id: data_.fulfillment_provider_id, + shipping_option_type_id: expect.any(String), + type: expect.objectContaining({ + id: expect.any(String), + code: data_.type.code, + description: data_.type.description, + label: data_.type.label, + }), + data: data_.data, + rules: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + attribute: data_.rules[0].attribute, + operator: data_.rules[0].operator, + value: data_.rules[0].value, + }), + ]), + }) + ) + } + + const rules = await service.listShippingOptionRules() + expect(rules).toHaveLength(2) + expect(rules).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: updatedShippingOption[0].rules[0].id, + }), + expect.objectContaining({ + id: updatedShippingOption[1].rules[0].id, + }), + ]) + ) + + const types = await service.listShippingOptionTypes() + expect(types).toHaveLength(2) + expect(types).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: updateData[0].type.code, + description: updateData[0].type.description, + label: updateData[0].type.label, + }), + expect.objectContaining({ + code: updateData[1].type.code, + description: updateData[1].type.description, + label: updateData[1].type.label, + }), + ]) + ) + }) + + it("should fail to update a non-existent shipping option", async () => { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + + const [fulfillmentProvider] = + await MikroOrmWrapper.forkManager().execute( + "insert into fulfillment_provider (id) values ('sp_jdafwfleiwuonl') returning id" + ) + + const shippingOptionData = { + id: "sp_jdafwfleiwuonl", + name: "test", + price_type: "flat", + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: fulfillmentProvider.id, + type: { + code: "test", + description: "test", + label: "test", + }, + data: { + amount: 1000, + }, + rules: [ + { + attribute: "test", + operator: "eq", + value: "test", + }, + ], + } + + const err = await service + .updateShippingOptions(shippingOptionData) + .catch((e) => e) + + expect(err).toBeDefined() + expect(err.message).toBe( + `The following shipping options do not exist: ${shippingOptionData.id}` + ) + }) + + it("should fail to update a shipping option when adding non existing rules", async () => { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + + const shippingOptionData = generateCreateShippingOptionsData({ + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + }) + + const shippingOption = await service.createShippingOptions( + shippingOptionData + ) + + const updateData = [ + { + id: shippingOption.id, + rules: [ + { + id: "sp_jdafwfleiwuonl", + }, + ], + }, + ] + + const err = await service + .updateShippingOptions(updateData) + .catch((e) => e) + + expect(err).toBeDefined() + expect(err.message).toBe( + `The following rules does not exists: ${updateData[0].rules[0].id} on shipping option ${shippingOption.id}` + ) + }) + + it("should fail to update a shipping option when adding invalid rules", async () => { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + + const shippingOptionData = generateCreateShippingOptionsData({ + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + }) + + const shippingOption = await service.createShippingOptions( + shippingOptionData + ) + + const updateData = [ + { + id: shippingOption.id, + rules: [ + { + attribute: "test", + operator: "invalid", + value: "test", + }, + ], + }, + ] + + const err = await service + .updateShippingOptions(updateData) + .catch((e) => e) + + expect(err).toBeDefined() + expect(err.message).toBe( + `Rule operator invalid is not supported. Must be one of in, eq, ne, gt, gte, lt, lte, nin` + ) + }) + }) + + describe("on create shipping option rules", () => { + it("should create a new rule", async () => { + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + + // service provider + const [{ id: providerId }] = + await MikroOrmWrapper.forkManager().execute( + "insert into fulfillment_provider (id) values ('sp_jdafwfleiwuonl') returning id" + ) + + const shippingOption = await service.createShippingOptions( + generateCreateShippingOptionsData({ + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + }) + ) + + const ruleData = { + attribute: "test-attribute", + operator: "eq", + value: "test-value", + shipping_option_id: shippingOption.id, + } + + const rule = await service.createShippingOptionRules(ruleData) + + expect(rule).toEqual( + expect.objectContaining({ + id: expect.any(String), + attribute: ruleData.attribute, + operator: ruleData.operator, + value: ruleData.value, + shipping_option_id: ruleData.shipping_option_id, + }) + ) + + const rules = await service.listShippingOptionRules() + expect(rules).toHaveLength(2) + expect(rules).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: rule.id, + attribute: ruleData.attribute, + operator: ruleData.operator, + value: ruleData.value, + shipping_option_id: shippingOption.id, + }), + expect.objectContaining({ + id: shippingOption.rules[0].id, + attribute: shippingOption.rules[0].attribute, + operator: shippingOption.rules[0].operator, + value: shippingOption.rules[0].value, + shipping_option_id: shippingOption.id, + }), + ]) + ) + }) + }) + + describe("on update shipping option rules", () => { + it("should update a shipping option rule", async () => { + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + + const shippingOption = await service.createShippingOptions( + generateCreateShippingOptionsData({ + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + fulfillment_provider_id: providerId, + }) + ) + + const updateData = { + id: shippingOption.rules[0].id, + attribute: "updated-test", + operator: "eq", + value: "updated-test", + } + + const updatedRule = await service.updateShippingOptionRules( + updateData + ) + + expect(updatedRule).toEqual( + expect.objectContaining({ + id: updateData.id, + attribute: updateData.attribute, + operator: updateData.operator, + value: updateData.value, + }) + ) + }) + + it("should fail to update a non-existent shipping option rule", async () => { + const updateData = { + id: "sp_jdafwfleiwuonl", + attribute: "updated-test", + operator: "eq", + value: "updated-test", + } + + const err = await service + .updateShippingOptionRules(updateData) + .catch((e) => e) + + expect(err).toBeDefined() + expect(err.message).toBe( + `ShippingOptionRule with id "${updateData.id}" not found` + ) + }) + }) + }) + }) + }, +}) diff --git a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-profile.spec.ts b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-profile.spec.ts new file mode 100644 index 0000000000..9c407b2936 --- /dev/null +++ b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-profile.spec.ts @@ -0,0 +1,82 @@ +import { Modules } from "@medusajs/modules-sdk" +import { + CreateShippingProfileDTO, + IFulfillmentModuleService, +} from "@medusajs/types" +import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" + +jest.setTimeout(100000) + +moduleIntegrationTestRunner({ + moduleName: Modules.FULFILLMENT, + testSuite: ({ service }: SuiteOptions) => { + describe("Fulfillment Module Service", () => { + describe("mutations", () => { + describe("on create", () => { + it("should create a new shipping profile", async function () { + const createData: CreateShippingProfileDTO = { + name: "test-default-profile", + type: "default", + } + + const createdShippingProfile = await service.createShippingProfiles( + createData + ) + + expect(createdShippingProfile).toEqual( + expect.objectContaining({ + name: createData.name, + type: createData.type, + }) + ) + }) + + it("should create multiple new shipping profiles", async function () { + const createData: CreateShippingProfileDTO[] = [ + { + name: "test-profile-1", + type: "default", + }, + { + name: "test-profile-2", + type: "custom", + }, + ] + + const createdShippingProfiles = + await service.createShippingProfiles(createData) + + expect(createdShippingProfiles).toHaveLength(2) + + let i = 0 + for (const data_ of createData) { + expect(createdShippingProfiles[i]).toEqual( + expect.objectContaining({ + name: data_.name, + type: data_.type, + }) + ) + ++i + } + }) + + it("should fail on duplicated shipping profile name", async function () { + const createData: CreateShippingProfileDTO = { + name: "test-default-profile", + type: "default", + } + + await service.createShippingProfiles(createData) + + const err = await service + .createShippingProfiles(createData) + .catch((e) => e) + + expect(err).toBeDefined() + expect(err.constraint).toBe("IDX_shipping_profile_name_unique") + }) + }) + }) + }) + }, +}) diff --git a/packages/fulfillment/jest.config.js b/packages/fulfillment/jest.config.js index d634e47925..16e27fda6c 100644 --- a/packages/fulfillment/jest.config.js +++ b/packages/fulfillment/jest.config.js @@ -10,7 +10,7 @@ module.exports = { "^.+\\.[jt]s?$": [ "ts-jest", { - tsConfig: "tsconfig.spec.json", + tsconfig: "tsconfig.spec.json", isolatedModules: true, }, ], diff --git a/packages/fulfillment/package.json b/packages/fulfillment/package.json index e38c00c728..b40114ddcf 100644 --- a/packages/fulfillment/package.json +++ b/packages/fulfillment/package.json @@ -37,6 +37,7 @@ "orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear" }, "devDependencies": { + "@medusajs/fulfillment-manual": "workspace:*", "@mikro-orm/cli": "5.9.7", "cross-env": "^5.2.1", "jest": "^29.6.3", diff --git a/packages/fulfillment/src/loaders/providers.ts b/packages/fulfillment/src/loaders/providers.ts new file mode 100644 index 0000000000..d15746d129 --- /dev/null +++ b/packages/fulfillment/src/loaders/providers.ts @@ -0,0 +1,87 @@ +import { moduleProviderLoader } from "@medusajs/modules-sdk" +import { LoaderOptions, ModuleProvider, ModulesSdkTypes } from "@medusajs/types" +import { asFunction, asValue, Lifetime } from "awilix" +import { FulfillmentIdentifiersRegistrationName } from "@types" +import { lowerCaseFirst } from "@medusajs/utils" +import { FulfillmentProviderService } from "@services" +import { ContainerRegistrationKeys } from "@medusajs/utils/src" + +const registrationFn = async (klass, container, pluginOptions) => { + Object.entries(pluginOptions.config || []).map(([name, config]) => { + const key = FulfillmentProviderService.getRegistrationIdentifier( + klass, + name + ) + + container.register({ + ["fp_" + key]: asFunction((cradle) => new klass(cradle, config), { + lifetime: klass.LIFE_TIME || Lifetime.SINGLETON, + }), + }) + + container.registerAdd(FulfillmentIdentifiersRegistrationName, asValue(key)) + }) +} + +export default async ({ + container, + options, +}: LoaderOptions< + ( + | ModulesSdkTypes.ModuleServiceInitializeOptions + | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions + ) & { providers: ModuleProvider[] } +>): Promise => { + container.registerAdd( + FulfillmentIdentifiersRegistrationName, + asValue(undefined) + ) + + // Local providers + // TODO + + await moduleProviderLoader({ + container, + providers: options?.providers || [], + registerServiceFn: registrationFn, + }) + + await syncDatabaseProviders({ + container, + }) +} + +async function syncDatabaseProviders({ container }) { + const providerServiceRegistrationKey = lowerCaseFirst( + FulfillmentProviderService.name + ) + + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + try { + const providerIdentifiers: string[] = ( + container.resolve(FulfillmentIdentifiersRegistrationName) ?? [] + ).filter(Boolean) + + const providerService: ModulesSdkTypes.InternalModuleService = + container.resolve(providerServiceRegistrationKey) + + const providers = await providerService.list({ + id: providerIdentifiers, + }) + + const loadedProvidersMap = new Map(providers.map((p) => [p.id, p])) + + const providersToCreate: any[] = [] + for (const identifier of providerIdentifiers) { + if (loadedProvidersMap.has(identifier)) { + continue + } + + providersToCreate.push({ id: identifier }) + } + + await providerService.create(providersToCreate) + } catch (error) { + logger.error(`Error syncing providers: ${error.message}`) + } +} diff --git a/packages/fulfillment/src/migrations/.snapshot-medusa-fulfillment.json b/packages/fulfillment/src/migrations/.snapshot-medusa-fulfillment.json index da2599d319..dc71772361 100644 --- a/packages/fulfillment/src/migrations/.snapshot-medusa-fulfillment.json +++ b/packages/fulfillment/src/migrations/.snapshot-medusa-fulfillment.json @@ -15,15 +15,6 @@ "nullable": false, "mappedType": "text" }, - "fulfillment_id": { - "name": "fulfillment_id", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "text" - }, "company": { "name": "company", "type": "text", @@ -159,16 +150,6 @@ "name": "fulfillment_address", "schema": "public", "indexes": [ - { - "keyName": "IDX_fulfillment_address_fulfillment_id", - "columnNames": [ - "fulfillment_id" - ], - "composite": false, - "primary": false, - "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_address_fulfillment_id\" ON \"fulfillment_address\" (fulfillment_id) WHERE deleted_at IS NULL" - }, { "keyName": "IDX_fulfillment_address_deleted_at", "columnNames": [ @@ -192,6 +173,85 @@ "checks": [], "foreignKeys": {} }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "fulfillment_provider", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_fulfillment_provider_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_provider_deleted_at\" ON \"fulfillment_provider\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "fulfillment_provider_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, { "columns": { "id": { @@ -299,85 +359,6 @@ "checks": [], "foreignKeys": {} }, - { - "columns": { - "id": { - "name": "id", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "text" - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "json" - }, - "created_at": { - "name": "created_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 6, - "default": "now()", - "mappedType": "datetime" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 6, - "default": "now()", - "mappedType": "datetime" - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "length": 6, - "mappedType": "datetime" - } - }, - "name": "service_provider", - "schema": "public", - "indexes": [ - { - "keyName": "IDX_service_provider_deleted_at", - "columnNames": [ - "deleted_at" - ], - "composite": false, - "primary": false, - "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_service_provider_deleted_at\" ON \"service_provider\" (deleted_at) WHERE deleted_at IS NOT NULL" - }, - { - "keyName": "service_provider_pkey", - "columnNames": [ - "id" - ], - "composite": false, - "primary": true, - "unique": true - } - ], - "checks": [], - "foreignKeys": {} - }, { "columns": { "id": { @@ -957,8 +938,8 @@ "nullable": true, "mappedType": "text" }, - "service_provider_id": { - "name": "service_provider_id", + "fulfillment_provider_id": { + "name": "fulfillment_provider_id", "type": "text", "unsigned": false, "autoincrement": false, @@ -1059,14 +1040,14 @@ "expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_option_shipping_profile_id\" ON \"shipping_option\" (shipping_profile_id) WHERE deleted_at IS NULL" }, { - "keyName": "IDX_shipping_option_service_provider_id", + "keyName": "IDX_shipping_option_fulfillment_provider_id", "columnNames": [ - "service_provider_id" + "fulfillment_provider_id" ], "composite": false, "primary": false, "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_option_service_provider_id\" ON \"shipping_option\" (service_provider_id) WHERE deleted_at IS NULL" + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_option_fulfillment_provider_id\" ON \"shipping_option\" (fulfillment_provider_id) WHERE deleted_at IS NULL" }, { "keyName": "IDX_shipping_option_shipping_option_type_id", @@ -1125,16 +1106,16 @@ "deleteRule": "set null", "updateRule": "cascade" }, - "shipping_option_service_provider_id_foreign": { - "constraintName": "shipping_option_service_provider_id_foreign", + "shipping_option_fulfillment_provider_id_foreign": { + "constraintName": "shipping_option_fulfillment_provider_id_foreign", "columnNames": [ - "service_provider_id" + "fulfillment_provider_id" ], "localTableName": "public.shipping_option", "referencedColumnNames": [ "id" ], - "referencedTableName": "public.service_provider", + "referencedTableName": "public.fulfillment_provider", "deleteRule": "set null", "updateRule": "cascade" }, @@ -1397,15 +1378,6 @@ "nullable": false, "mappedType": "text" }, - "items_id": { - "name": "items_id", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "text" - }, "created_at": { "name": "created_at", "type": "timestamptz", @@ -1442,6 +1414,15 @@ "name": "fulfillment", "schema": "public", "indexes": [ + { + "columnNames": [ + "delivery_address_id" + ], + "composite": false, + "keyName": "fulfillment_delivery_address_id_unique", + "primary": false, + "unique": true + }, { "keyName": "IDX_fulfillment_location_id", "columnNames": [ @@ -1494,6 +1475,18 @@ ], "checks": [], "foreignKeys": { + "fulfillment_provider_id_foreign": { + "constraintName": "fulfillment_provider_id_foreign", + "columnNames": [ + "provider_id" + ], + "localTableName": "public.fulfillment", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.fulfillment_provider", + "updateRule": "cascade" + }, "fulfillment_shipping_option_id_foreign": { "constraintName": "fulfillment_shipping_option_id_foreign", "columnNames": [ @@ -1507,18 +1500,6 @@ "deleteRule": "set null", "updateRule": "cascade" }, - "fulfillment_provider_id_foreign": { - "constraintName": "fulfillment_provider_id_foreign", - "columnNames": [ - "provider_id" - ], - "localTableName": "public.fulfillment", - "referencedColumnNames": [ - "id" - ], - "referencedTableName": "public.service_provider", - "updateRule": "cascade" - }, "fulfillment_delivery_address_id_foreign": { "constraintName": "fulfillment_delivery_address_id_foreign", "columnNames": [ @@ -1530,18 +1511,6 @@ ], "referencedTableName": "public.fulfillment_address", "updateRule": "cascade" - }, - "fulfillment_items_id_foreign": { - "constraintName": "fulfillment_items_id_foreign", - "columnNames": [ - "items_id" - ], - "localTableName": "public.fulfillment", - "referencedColumnNames": [ - "id" - ], - "referencedTableName": "public.fulfillment_item", - "updateRule": "cascade" } } }, @@ -1670,6 +1639,7 @@ "id" ], "referencedTableName": "public.fulfillment", + "deleteRule": "cascade", "updateRule": "cascade" } } @@ -1721,6 +1691,15 @@ "nullable": false, "mappedType": "decimal" }, + "raw_quantity": { + "name": "raw_quantity", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "json" + }, "line_item_id": { "name": "line_item_id", "type": "text", @@ -1846,6 +1825,7 @@ "id" ], "referencedTableName": "public.fulfillment", + "deleteRule": "cascade", "updateRule": "cascade" } } diff --git a/packages/fulfillment/src/migrations/Migration20240221164918_InitialSetupMigration.ts b/packages/fulfillment/src/migrations/Migration20240305095931_InitialSetupMigration.ts similarity index 80% rename from packages/fulfillment/src/migrations/Migration20240221164918_InitialSetupMigration.ts rename to packages/fulfillment/src/migrations/Migration20240305095931_InitialSetupMigration.ts index de4028689c..2889ff6be0 100644 --- a/packages/fulfillment/src/migrations/Migration20240221164918_InitialSetupMigration.ts +++ b/packages/fulfillment/src/migrations/Migration20240305095931_InitialSetupMigration.ts @@ -1,19 +1,18 @@ import { Migration } from '@mikro-orm/migrations'; -export class Migration20240221164918_InitialSetupMigration extends Migration { +export class Migration20240305095931_InitialSetupMigration extends Migration { async up(): Promise { - this.addSql('create table if not exists "fulfillment_address" ("id" text not null, "fulfillment_id" text null, "company" text null, "first_name" text null, "last_name" text null, "address_1" text null, "address_2" text null, "city" text null, "country_code" text null, "province" text null, "postal_code" text null, "phone" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_address_pkey" primary key ("id"));'); - this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_address_fulfillment_id" ON "fulfillment_address" (fulfillment_id) WHERE deleted_at IS NULL;'); + this.addSql('create table if not exists "fulfillment_address" ("id" text not null, "company" text null, "first_name" text null, "last_name" text null, "address_1" text null, "address_2" text null, "city" text null, "country_code" text null, "province" text null, "postal_code" text null, "phone" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_address_pkey" primary key ("id"));'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_address_deleted_at" ON "fulfillment_address" (deleted_at) WHERE deleted_at IS NOT NULL;'); + this.addSql('create table if not exists "fulfillment_provider" ("id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_provider_pkey" primary key ("id"));'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_provider_deleted_at" ON "fulfillment_provider" (deleted_at) WHERE deleted_at IS NOT NULL;'); + this.addSql('create table if not exists "fulfillment_set" ("id" text not null, "name" text not null, "type" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_set_pkey" primary key ("id"));'); this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_fulfillment_set_name_unique" ON "fulfillment_set" (name) WHERE deleted_at IS NULL;'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_set_deleted_at" ON "fulfillment_set" (deleted_at) WHERE deleted_at IS NOT NULL;'); - this.addSql('create table if not exists "service_provider" ("id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "service_provider_pkey" primary key ("id"));'); - this.addSql('CREATE INDEX IF NOT EXISTS "IDX_service_provider_deleted_at" ON "service_provider" (deleted_at) WHERE deleted_at IS NOT NULL;'); - this.addSql('create table if not exists "service_zone" ("id" text not null, "name" text not null, "metadata" jsonb null, "fulfillment_set_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "service_zone_pkey" primary key ("id"));'); this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_service_zone_name_unique" ON "service_zone" (name) WHERE deleted_at IS NULL;'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_service_zone_fulfillment_set_id" ON "service_zone" (fulfillment_set_id) WHERE deleted_at IS NULL;'); @@ -33,11 +32,11 @@ export class Migration20240221164918_InitialSetupMigration extends Migration { this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_shipping_profile_name_unique" ON "shipping_profile" (name) WHERE deleted_at IS NULL;'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_profile_deleted_at" ON "shipping_profile" (deleted_at) WHERE deleted_at IS NOT NULL;'); - this.addSql('create table if not exists "shipping_option" ("id" text not null, "name" text not null, "price_type" text check ("price_type" in (\'calculated\', \'flat\')) not null default \'calculated\', "service_zone_id" text not null, "shipping_profile_id" text null, "service_provider_id" text null, "data" jsonb null, "metadata" jsonb null, "shipping_option_type_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_pkey" primary key ("id"));'); + this.addSql('create table if not exists "shipping_option" ("id" text not null, "name" text not null, "price_type" text check ("price_type" in (\'calculated\', \'flat\')) not null default \'calculated\', "service_zone_id" text not null, "shipping_profile_id" text null, "fulfillment_provider_id" text null, "data" jsonb null, "metadata" jsonb null, "shipping_option_type_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_pkey" primary key ("id"));'); this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_shipping_option_type_id_unique" unique ("shipping_option_type_id");'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_service_zone_id" ON "shipping_option" (service_zone_id) WHERE deleted_at IS NULL;'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_shipping_profile_id" ON "shipping_option" (shipping_profile_id) WHERE deleted_at IS NULL;'); - this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_service_provider_id" ON "shipping_option" (service_provider_id) WHERE deleted_at IS NULL;'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_fulfillment_provider_id" ON "shipping_option" (fulfillment_provider_id) WHERE deleted_at IS NULL;'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_shipping_option_type_id" ON "shipping_option" (shipping_option_type_id) WHERE deleted_at IS NULL;'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_deleted_at" ON "shipping_option" (deleted_at) WHERE deleted_at IS NOT NULL;'); @@ -45,7 +44,8 @@ export class Migration20240221164918_InitialSetupMigration extends Migration { this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_rule_shipping_option_id" ON "shipping_option_rule" (shipping_option_id) WHERE deleted_at IS NULL;'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_rule_deleted_at" ON "shipping_option_rule" (deleted_at) WHERE deleted_at IS NOT NULL;'); - this.addSql('create table if not exists "fulfillment" ("id" text not null, "location_id" text not null, "packed_at" timestamptz null, "shipped_at" timestamptz null, "delivered_at" timestamptz null, "canceled_at" timestamptz null, "data" jsonb null, "provider_id" text not null, "shipping_option_id" text null, "metadata" jsonb null, "delivery_address_id" text not null, "items_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_pkey" primary key ("id"));'); + this.addSql('create table if not exists "fulfillment" ("id" text not null, "location_id" text not null, "packed_at" timestamptz null, "shipped_at" timestamptz null, "delivered_at" timestamptz null, "canceled_at" timestamptz null, "data" jsonb null, "provider_id" text not null, "shipping_option_id" text null, "metadata" jsonb null, "delivery_address_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_pkey" primary key ("id"));'); + this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_delivery_address_id_unique" unique ("delivery_address_id");'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_location_id" ON "fulfillment" (location_id) WHERE deleted_at IS NULL;'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_provider_id" ON "fulfillment" (provider_id) WHERE deleted_at IS NULL;'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_shipping_option_id" ON "fulfillment" (shipping_option_id) WHERE deleted_at IS NULL;'); @@ -55,7 +55,7 @@ export class Migration20240221164918_InitialSetupMigration extends Migration { this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_label_fulfillment_id" ON "fulfillment_label" (fulfillment_id) WHERE deleted_at IS NULL;'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_label_deleted_at" ON "fulfillment_label" (deleted_at) WHERE deleted_at IS NOT NULL;'); - this.addSql('create table if not exists "fulfillment_item" ("id" text not null, "title" text not null, "sku" text not null, "barcode" text not null, "quantity" numeric not null, "line_item_id" text null, "inventory_item_id" text null, "fulfillment_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_item_pkey" primary key ("id"));'); + this.addSql('create table if not exists "fulfillment_item" ("id" text not null, "title" text not null, "sku" text not null, "barcode" text not null, "quantity" numeric not null, "raw_quantity" jsonb not null, "line_item_id" text null, "inventory_item_id" text null, "fulfillment_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_item_pkey" primary key ("id"));'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_item_line_item_id" ON "fulfillment_item" (line_item_id) WHERE deleted_at IS NULL;'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_item_inventory_item_id" ON "fulfillment_item" (inventory_item_id) WHERE deleted_at IS NULL;'); this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_item_fulfillment_id" ON "fulfillment_item" (fulfillment_id) WHERE deleted_at IS NULL;'); @@ -67,19 +67,18 @@ export class Migration20240221164918_InitialSetupMigration extends Migration { this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_service_zone_id_foreign" foreign key ("service_zone_id") references "service_zone" ("id") on update cascade;'); this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_shipping_profile_id_foreign" foreign key ("shipping_profile_id") references "shipping_profile" ("id") on update cascade on delete set null;'); - this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_service_provider_id_foreign" foreign key ("service_provider_id") references "service_provider" ("id") on update cascade on delete set null;'); + this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_fulfillment_provider_id_foreign" foreign key ("fulfillment_provider_id") references "fulfillment_provider" ("id") on update cascade on delete set null;'); this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_shipping_option_type_id_foreign" foreign key ("shipping_option_type_id") references "shipping_option_type" ("id") on update cascade on delete cascade;'); this.addSql('alter table if exists "shipping_option_rule" add constraint "shipping_option_rule_shipping_option_id_foreign" foreign key ("shipping_option_id") references "shipping_option" ("id") on update cascade;'); + this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_provider_id_foreign" foreign key ("provider_id") references "fulfillment_provider" ("id") on update cascade;'); this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_shipping_option_id_foreign" foreign key ("shipping_option_id") references "shipping_option" ("id") on update cascade on delete set null;'); - this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_provider_id_foreign" foreign key ("provider_id") references "service_provider" ("id") on update cascade;'); this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_delivery_address_id_foreign" foreign key ("delivery_address_id") references "fulfillment_address" ("id") on update cascade;'); - this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_items_id_foreign" foreign key ("items_id") references "fulfillment_item" ("id") on update cascade;'); - this.addSql('alter table if exists "fulfillment_label" add constraint "fulfillment_label_fulfillment_id_foreign" foreign key ("fulfillment_id") references "fulfillment" ("id") on update cascade;'); + this.addSql('alter table if exists "fulfillment_label" add constraint "fulfillment_label_fulfillment_id_foreign" foreign key ("fulfillment_id") references "fulfillment" ("id") on update cascade on delete cascade;'); - this.addSql('alter table if exists "fulfillment_item" add constraint "fulfillment_item_fulfillment_id_foreign" foreign key ("fulfillment_id") references "fulfillment" ("id") on update cascade;'); + this.addSql('alter table if exists "fulfillment_item" add constraint "fulfillment_item_fulfillment_id_foreign" foreign key ("fulfillment_id") references "fulfillment" ("id") on update cascade on delete cascade;'); } } diff --git a/packages/fulfillment/src/models/address.ts b/packages/fulfillment/src/models/address.ts index b70907bdd4..ed9ba7e2c2 100644 --- a/packages/fulfillment/src/models/address.ts +++ b/packages/fulfillment/src/models/address.ts @@ -14,12 +14,6 @@ import { type OptionalAddressProps = DAL.SoftDeletableEntityDateColumns -const FulfillmentIdIndex = createPsqlIndexStatementHelper({ - tableName: "fulfillment_address", - columns: "fulfillment_id", - where: "deleted_at IS NULL", -}) - const FulfillmentDeletedAtIndex = createPsqlIndexStatementHelper({ tableName: "fulfillment_address", columns: "deleted_at", @@ -33,10 +27,6 @@ export default class Address { @PrimaryKey({ columnType: "text" }) id!: string - @Property({ columnType: "text", nullable: true }) - @FulfillmentIdIndex.MikroORMIndex() - fulfillment_id: string | null = null - @Property({ columnType: "text", nullable: true }) company: string | null = null diff --git a/packages/fulfillment/src/models/fulfillment-item.ts b/packages/fulfillment/src/models/fulfillment-item.ts index cde928de80..f7b4e1079e 100644 --- a/packages/fulfillment/src/models/fulfillment-item.ts +++ b/packages/fulfillment/src/models/fulfillment-item.ts @@ -1,10 +1,12 @@ import { + BigNumber, createPsqlIndexStatementHelper, DALUtils, generateEntityId, + MikroOrmBigNumberProperty, } from "@medusajs/utils" -import { DAL } from "@medusajs/types" +import { BigNumberRawValue, DAL } from "@medusajs/types" import { BeforeCreate, Entity, @@ -60,8 +62,11 @@ export default class FulfillmentItem { @Property({ columnType: "text" }) barcode: string - @Property({ columnType: "numeric", serializer: Number }) - quantity: number // TODO: probably allow big numbers here + @MikroOrmBigNumberProperty() + quantity: BigNumber | number + + @Property({ columnType: "jsonb" }) + raw_quantity: BigNumberRawValue @Property({ columnType: "text", nullable: true }) @LineItemIdIndex.MikroORMIndex() @@ -71,11 +76,16 @@ export default class FulfillmentItem { @InventoryItemIdIndex.MikroORMIndex() inventory_item_id: string | null = null - @Property({ columnType: "text" }) + @ManyToOne(() => Fulfillment, { + columnType: "text", + mapToPk: true, + fieldName: "fulfillment_id", + onDelete: "cascade", + }) @FulfillmentIdIndex.MikroORMIndex() fulfillment_id: string - @ManyToOne(() => Fulfillment) + @ManyToOne(() => Fulfillment, { persist: false }) fulfillment: Fulfillment @Property({ @@ -100,10 +110,12 @@ export default class FulfillmentItem { @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "fulit") + this.fulfillment_id ??= this.fulfillment.id } @OnInit() onInit() { this.id = generateEntityId(this.id, "fulit") + this.fulfillment_id ??= this.fulfillment.id } } diff --git a/packages/fulfillment/src/models/fulfillment-label.ts b/packages/fulfillment/src/models/fulfillment-label.ts index 8640e5117f..d18ebbb7ba 100644 --- a/packages/fulfillment/src/models/fulfillment-label.ts +++ b/packages/fulfillment/src/models/fulfillment-label.ts @@ -48,11 +48,16 @@ export default class FulfillmentLabel { @Property({ columnType: "text" }) label_url: string - @Property({ columnType: "text" }) + @ManyToOne(() => Fulfillment, { + columnType: "text", + mapToPk: true, + fieldName: "fulfillment_id", + onDelete: "cascade", + }) @FulfillmentIdIndex.MikroORMIndex() fulfillment_id: string - @ManyToOne(() => Fulfillment) + @ManyToOne(() => Fulfillment, { persist: false }) fulfillment: Fulfillment @Property({ @@ -77,10 +82,12 @@ export default class FulfillmentLabel { @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "fulla") + this.fulfillment_id ??= this.fulfillment.id } @OnInit() onInit() { this.id = generateEntityId(this.id, "fulla") + this.fulfillment_id ??= this.fulfillment.id } } diff --git a/packages/fulfillment/src/models/service-provider.ts b/packages/fulfillment/src/models/fulfillment-provider.ts similarity index 83% rename from packages/fulfillment/src/models/service-provider.ts rename to packages/fulfillment/src/models/fulfillment-provider.ts index 77784a969f..380d349cf5 100644 --- a/packages/fulfillment/src/models/service-provider.ts +++ b/packages/fulfillment/src/models/fulfillment-provider.ts @@ -18,18 +18,18 @@ import { } from "@mikro-orm/core" import ShippingOption from "./shipping-option" -type ServiceProviderOptionalProps = DAL.SoftDeletableEntityDateColumns +type FulfillmentProviderOptionalProps = DAL.SoftDeletableEntityDateColumns const DeletedAtIndex = createPsqlIndexStatementHelper({ - tableName: "service_provider", + tableName: "fulfillment_provider", columns: "deleted_at", where: "deleted_at IS NOT NULL", }) @Entity() @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) -export default class ServiceProvider { - [OptionalProps]?: ServiceProviderOptionalProps +export default class FulfillmentProvider { + [OptionalProps]?: FulfillmentProviderOptionalProps @PrimaryKey({ columnType: "text" }) id: string @@ -39,7 +39,7 @@ export default class ServiceProvider { @OneToMany( () => ShippingOption, - (shippingOption) => shippingOption.service_provider + (shippingOption) => shippingOption.fulfillment_provider ) shipping_options = new Collection(this) diff --git a/packages/fulfillment/src/models/fulfillment.ts b/packages/fulfillment/src/models/fulfillment.ts index 77b4e9cbbb..3416853bd8 100644 --- a/packages/fulfillment/src/models/fulfillment.ts +++ b/packages/fulfillment/src/models/fulfillment.ts @@ -12,6 +12,7 @@ import { Filter, ManyToOne, OneToMany, + OneToOne, OnInit, OptionalProps, PrimaryKey, @@ -20,7 +21,7 @@ import { import Address from "./address" import FulfillmentItem from "./fulfillment-item" import FulfillmentLabel from "./fulfillment-label" -import ServiceProvider from "./service-provider" +import FulfillmentProvider from "./fulfillment-provider" import ShippingOption from "./shipping-option" type FulfillmentOptionalProps = DAL.SoftDeletableEntityDateColumns @@ -88,28 +89,37 @@ export default class Fulfillment { @Property({ columnType: "jsonb", nullable: true }) data: Record | null = null - @Property({ columnType: "text" }) + @ManyToOne(() => FulfillmentProvider, { + columnType: "text", + fieldName: "provider_id", + mapToPk: true, + }) @FulfillmentProviderIdIndex.MikroORMIndex() provider_id: string - @Property({ columnType: "text", nullable: true }) + @ManyToOne(() => ShippingOption, { + columnType: "text", + fieldName: "shipping_option_id", + nullable: true, + mapToPk: true, + }) @FulfillmentShippingOptionIdIndex.MikroORMIndex() shipping_option_id: string | null = null @Property({ columnType: "jsonb", nullable: true }) metadata: Record | null = null - @ManyToOne(() => ShippingOption, { nullable: true }) + @ManyToOne(() => ShippingOption, { persist: false }) shipping_option: ShippingOption | null - @ManyToOne(() => ServiceProvider) - provider: ServiceProvider + @ManyToOne(() => FulfillmentProvider, { persist: false }) + provider: FulfillmentProvider - @ManyToOne(() => Address) - delivery_address: Address + @OneToOne() + delivery_address!: Address - @ManyToOne(() => FulfillmentItem) - items: FulfillmentItem + @OneToMany(() => FulfillmentItem, (item) => item.fulfillment) + items = new Collection(this) @OneToMany(() => FulfillmentLabel, (label) => label.fulfillment) labels = new Collection(this) @@ -136,10 +146,12 @@ export default class Fulfillment { @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "ful") + this.provider_id ??= this.provider.id } @OnInit() onInit() { this.id = generateEntityId(this.id, "ful") + this.provider_id ??= this.provider.id } } diff --git a/packages/fulfillment/src/models/index.ts b/packages/fulfillment/src/models/index.ts index 9408a1bfad..016f580a32 100644 --- a/packages/fulfillment/src/models/index.ts +++ b/packages/fulfillment/src/models/index.ts @@ -5,7 +5,7 @@ export { default as GeoZone } from "./geo-zone" export { default as ServiceZone } from "./service-zone" export { default as FulfillmentItem } from "./fulfillment-item" export { default as FulfillmentLabel } from "./fulfillment-label" -export { default as ServiceProvider } from "./service-provider" +export { default as FulfillmentProvider } from "./fulfillment-provider" export { default as ShippingOption } from "./shipping-option" export { default as ShippingOptionType } from "./shipping-option-type" export { default as ShippingOptionRule } from "./shipping-option-rule" diff --git a/packages/fulfillment/src/models/shipping-option.ts b/packages/fulfillment/src/models/shipping-option.ts index bf98ddb87a..f1687d1e12 100644 --- a/packages/fulfillment/src/models/shipping-option.ts +++ b/packages/fulfillment/src/models/shipping-option.ts @@ -22,7 +22,7 @@ import { Property, } from "@mikro-orm/core" import Fulfillment from "./fulfillment" -import ServiceProvider from "./service-provider" +import FulfillmentProvider from "./fulfillment-provider" import ServiceZone from "./service-zone" import ShippingOptionRule from "./shipping-option-rule" import ShippingOptionType from "./shipping-option-type" @@ -48,9 +48,9 @@ const ShippingProfileIdIndex = createPsqlIndexStatementHelper({ where: "deleted_at IS NULL", }) -const ServiceProviderIdIndex = createPsqlIndexStatementHelper({ +const FulfillmentProviderIdIndex = createPsqlIndexStatementHelper({ tableName: "shipping_option", - columns: "service_provider_id", + columns: "fulfillment_provider_id", where: "deleted_at IS NULL", }) @@ -94,14 +94,14 @@ export default class ShippingOption { @ShippingProfileIdIndex.MikroORMIndex() shipping_profile_id: string | null - @ManyToOne(() => ServiceProvider, { + @ManyToOne(() => FulfillmentProvider, { type: "text", - fieldName: "service_provider_id", + fieldName: "fulfillment_provider_id", mapToPk: true, nullable: true, }) - @ServiceProviderIdIndex.MikroORMIndex() - service_provider_id: string + @FulfillmentProviderIdIndex.MikroORMIndex() + fulfillment_provider_id: string @Property({ columnType: "text", persist: false }) @ShippingOptionTypeIdIndex.MikroORMIndex() @@ -121,10 +121,10 @@ export default class ShippingOption { }) shipping_profile: ShippingProfile | null - @ManyToOne(() => ServiceProvider, { + @ManyToOne(() => FulfillmentProvider, { persist: false, }) - service_provider: ServiceProvider | null + fulfillment_provider: FulfillmentProvider | null @OneToOne(() => ShippingOptionType, (so) => so.shipping_option, { owner: true, diff --git a/packages/fulfillment/src/module-definition.ts b/packages/fulfillment/src/module-definition.ts index bc17d7d091..9bbb2cdea9 100644 --- a/packages/fulfillment/src/module-definition.ts +++ b/packages/fulfillment/src/module-definition.ts @@ -6,6 +6,7 @@ import * as Models from "@models" import * as ModuleModels from "@models" import { ModulesSdkUtils } from "@medusajs/utils" import * as ModuleRepositories from "@repositories" +import loadProviders from "./loaders/providers" const migrationScriptOptions = { moduleName: Modules.FULFILLMENT, @@ -34,7 +35,7 @@ const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({ }) const service = FulfillmentModuleService -const loaders = [containerLoader, connectionLoader] +const loaders = [containerLoader, connectionLoader, loadProviders] export const moduleDefinition: ModuleExports = { service, diff --git a/packages/fulfillment/src/services/fulfillment-module-service.ts b/packages/fulfillment/src/services/fulfillment-module-service.ts index e2d1239ad9..c84a0bb1a1 100644 --- a/packages/fulfillment/src/services/fulfillment-module-service.ts +++ b/packages/fulfillment/src/services/fulfillment-module-service.ts @@ -1,9 +1,11 @@ import { Context, DAL, + FilterableFulfillmentSetProps, FilterableShippingOptionProps, FilterQuery, FindConfig, + FulfillmentDTO, FulfillmentTypes, IFulfillmentModuleService, InternalModuleDeclaration, @@ -25,6 +27,7 @@ import { import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { + Fulfillment, FulfillmentSet, GeoZone, ServiceZone, @@ -34,6 +37,7 @@ import { ShippingProfile, } from "@models" import { isContextValid, validateRules } from "@utils" +import FulfillmentProviderService from "./fulfillment-provider" const generateMethodForModels = [ ServiceZone, @@ -42,6 +46,7 @@ const generateMethodForModels = [ ShippingProfile, ShippingOptionRule, ShippingOptionType, + // Not adding Fulfillment to not auto generate the methods under the hood and only provide the methods we want to expose8 ] type InjectedDependencies = { @@ -53,6 +58,8 @@ type InjectedDependencies = { shippingOptionService: ModulesSdkTypes.InternalModuleService shippingOptionRuleService: ModulesSdkTypes.InternalModuleService shippingOptionTypeService: ModulesSdkTypes.InternalModuleService + fulfillmentProviderService: FulfillmentProviderService + fulfillmentService: ModulesSdkTypes.InternalModuleService } export default class FulfillmentModuleService< @@ -62,7 +69,8 @@ export default class FulfillmentModuleService< TShippingProfileEntity extends ShippingProfile = ShippingProfile, TShippingOptionEntity extends ShippingOption = ShippingOption, TShippingOptionRuleEntity extends ShippingOptionRule = ShippingOptionRule, - TSippingOptionTypeEntity extends ShippingOptionType = ShippingOptionType + TSippingOptionTypeEntity extends ShippingOptionType = ShippingOptionType, + TFulfillmentEntity extends Fulfillment = Fulfillment > extends ModulesSdkUtils.abstractModuleServiceFactory< InjectedDependencies, @@ -87,6 +95,8 @@ export default class FulfillmentModuleService< protected readonly shippingOptionService_: ModulesSdkTypes.InternalModuleService protected readonly shippingOptionRuleService_: ModulesSdkTypes.InternalModuleService protected readonly shippingOptionTypeService_: ModulesSdkTypes.InternalModuleService + protected readonly fulfillmentProviderService_: FulfillmentProviderService + protected readonly fulfillmentService_: ModulesSdkTypes.InternalModuleService constructor( { @@ -98,6 +108,8 @@ export default class FulfillmentModuleService< shippingOptionService, shippingOptionRuleService, shippingOptionTypeService, + fulfillmentProviderService, + fulfillmentService, }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { @@ -111,6 +123,8 @@ export default class FulfillmentModuleService< this.shippingOptionService_ = shippingOptionService this.shippingOptionRuleService_ = shippingOptionRuleService this.shippingOptionTypeService_ = shippingOptionTypeService + this.fulfillmentProviderService_ = fulfillmentProviderService + this.fulfillmentService_ = fulfillmentService } __joinerConfig(): ModuleJoinerConfig { @@ -129,7 +143,7 @@ export default class FulfillmentModuleService< "rules", "type", "shipping_profile", - "service_provider", + "fulfillment_provider", ...(normalizedConfig.relations ?? []), ] // The assumption is that there won't be an infinite amount of shipping options. So if a context filtering needs to be applied we can retrieve them all. @@ -173,7 +187,7 @@ export default class FulfillmentModuleService< async listShippingOptions( filters: FilterableShippingOptionProps = {}, config: FindConfig = {}, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const { filters: normalizedFilters, @@ -211,6 +225,76 @@ export default class FulfillmentModuleService< }) } + @InjectManager("baseRepository_") + async retrieveFulfillment( + id: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const fulfillment = await this.fulfillmentService_.retrieve( + id, + config, + sharedContext + ) + + return await this.baseRepository_.serialize( + fulfillment, + { + populate: true, + } + ) + } + + async retrieveFulfillmentOptions( + providerId: string + ): Promise[]> { + return await this.fulfillmentProviderService_.getFulfillmentOptions( + providerId + ) + } + + @InjectManager("baseRepository_") + async listFulfillments( + filters: FulfillmentTypes.FilterableFulfillmentProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const fulfillments = await this.fulfillmentService_.list( + filters, + config, + sharedContext + ) + + return await this.baseRepository_.serialize< + FulfillmentTypes.FulfillmentDTO[] + >(fulfillments, { + populate: true, + }) + } + + @InjectManager("baseRepository_") + async listAndCountFulfillments( + filters?: FilterableFulfillmentSetProps, + config?: FindConfig, + @MedusaContext() sharedContext: Context = {} + ): Promise<[FulfillmentDTO[], number]> { + const [fulfillments, count] = await this.fulfillmentService_.listAndCount( + filters, + config, + sharedContext + ) + + return [ + await this.baseRepository_.serialize( + fulfillments, + { + populate: true, + } + ), + count, + ] + } + create( data: FulfillmentTypes.CreateFulfillmentSetDTO[], sharedContext?: Context @@ -504,6 +588,55 @@ export default class FulfillmentModuleService< : createdShippingOptionRules[0] } + @InjectManager("baseRepository_") + async createFulfillment( + data: FulfillmentTypes.CreateFulfillmentDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const { order, ...fulfillmentDataToCreate } = data + + const fulfillment = await this.fulfillmentService_.create( + fulfillmentDataToCreate, + sharedContext + ) + + const { + items, + data: fulfillmentData, + provider_id, + ...fulfillmentRest + } = fulfillment + + let fulfillmentThirdPartyData!: any + try { + fulfillmentThirdPartyData = + await this.fulfillmentProviderService_.createFulfillment( + provider_id, + fulfillmentData || {}, + items.map((i) => i), + order, + fulfillmentRest + ) + await this.fulfillmentService_.update( + { + id: fulfillment.id, + data: fulfillmentThirdPartyData ?? {}, + }, + sharedContext + ) + } catch (error) { + await this.fulfillmentService_.delete(fulfillment.id, sharedContext) + throw error + } + + return await this.baseRepository_.serialize( + fulfillment, + { + populate: true, + } + ) + } + update( data: FulfillmentTypes.UpdateFulfillmentSetDTO[], sharedContext?: Context @@ -1074,6 +1207,88 @@ export default class FulfillmentModuleService< : updatedShippingOptionRules[0] } + @InjectTransactionManager("baseRepository_") + async updateFulfillment( + id: string, + data: FulfillmentTypes.UpdateFulfillmentDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const fulfillment = await this.fulfillmentService_.update( + { id, ...data }, + sharedContext + ) + + const serialized = + await this.baseRepository_.serialize( + fulfillment, + { + populate: true, + } + ) + + return Array.isArray(serialized) ? serialized[0] : serialized + } + + @InjectManager("baseRepository_") + async cancelFulfillment( + id: string, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const canceledAt = new Date() + + let fulfillment = await this.fulfillmentService_.retrieve( + id, + {}, + sharedContext + ) + + FulfillmentModuleService.canCancelFulfillmentOrThrow(fulfillment) + + // Make this action idempotent + if (!fulfillment.canceled_at) { + try { + await this.fulfillmentProviderService_.cancelFulfillment( + fulfillment.provider_id, + fulfillment.data ?? {} + ) + } catch (error) { + throw error + } + + fulfillment = await this.fulfillmentService_.update( + { + id, + canceled_at: canceledAt, + }, + sharedContext + ) + } + + const result = await this.baseRepository_.serialize(fulfillment, { + populate: true, + }) + + return Array.isArray(result) ? result[0] : result + } + + protected static canCancelFulfillmentOrThrow(fulfillment: Fulfillment) { + if (fulfillment.shipped_at) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Fulfillment with id ${fulfillment.id} already shipped` + ) + } + + if (fulfillment.delivered_at) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Fulfillment with id ${fulfillment.id} already delivered` + ) + } + + return true + } + protected static validateMissingShippingOptions_( shippingOptions: ShippingOption[], shippingOptionsData: FulfillmentTypes.UpdateShippingOptionDTO[] diff --git a/packages/fulfillment/src/services/fulfillment-provider.ts b/packages/fulfillment/src/services/fulfillment-provider.ts new file mode 100644 index 0000000000..5b7e18521f --- /dev/null +++ b/packages/fulfillment/src/services/fulfillment-provider.ts @@ -0,0 +1,104 @@ +import { + Constructor, + DAL, + FulfillmentTypes, + IFulfillmentProvider, +} from "@medusajs/types" +import { ModulesSdkUtils, promiseAll } from "@medusajs/utils" +import { MedusaError } from "medusa-core-utils" +import { FulfillmentProvider } from "@models" + +type InjectedDependencies = { + fulfillmentProviderRepository: DAL.RepositoryService + [key: `fp_${string}`]: FulfillmentTypes.IFulfillmentProvider +} + +// TODO rework DTO's + +export default class FulfillmentProviderService extends ModulesSdkUtils.internalModuleServiceFactory( + FulfillmentProvider +) { + protected readonly fulfillmentProviderRepository_: DAL.RepositoryService + + constructor(container: InjectedDependencies) { + super(container) + this.fulfillmentProviderRepository_ = + container.fulfillmentProviderRepository + } + + static getRegistrationIdentifier( + providerClass: Constructor, + optionName?: string + ) { + return `${(providerClass as any).identifier}_${optionName}` + } + + protected retrieveProviderRegistration( + providerId: string + ): FulfillmentTypes.IFulfillmentProvider { + try { + return this.__container__[`fp_${providerId}`] + } catch (err) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Could not find a fulfillment provider with id: ${providerId}` + ) + } + } + + async listFulfillmentOptions(providerIds: string[]): Promise { + return await promiseAll( + providerIds.map(async (p) => { + const provider = this.retrieveProviderRegistration(p) + return { + provider_id: p, + options: (await provider.getFulfillmentOptions()) as Record< + string, + unknown + >[], + } + }) + ) + } + + async getFulfillmentOptions( + providerId: string + ): Promise[]> { + const provider = this.retrieveProviderRegistration(providerId) + return await provider.getFulfillmentOptions() + } + + async validateFulfillmentData( + providerId: string, + optionData: Record, + data: Record, + context: Record + ) { + const provider = this.retrieveProviderRegistration(providerId) + return await provider.validateFulfillmentData(optionData, data, context) + } + + async validateOption(data: any) { + const provider = this.retrieveProviderRegistration(data.provider_id) + return await provider.validateOption(data) + } + + async createFulfillment( + providerId: string, + data: object, + items: object[], + order: object, + fulfillment: Record + ): Promise> { + const provider = this.retrieveProviderRegistration(providerId) + return await provider.createFulfillment(data, items, order, fulfillment) + } + + async cancelFulfillment( + providerId: string, + fulfillment: Record + ): Promise { + const provider = this.retrieveProviderRegistration(providerId) + return await provider.cancelFulfillment(fulfillment) + } +} diff --git a/packages/fulfillment/src/services/index.ts b/packages/fulfillment/src/services/index.ts index 856e2bbbf6..7b793219c4 100644 --- a/packages/fulfillment/src/services/index.ts +++ b/packages/fulfillment/src/services/index.ts @@ -1 +1,2 @@ export { default as FulfillmentModuleService } from "./fulfillment-module-service" +export { default as FulfillmentProviderService } from "./fulfillment-provider" diff --git a/packages/fulfillment/src/types/index.ts b/packages/fulfillment/src/types/index.ts index fdac085753..bae7407b28 100644 --- a/packages/fulfillment/src/types/index.ts +++ b/packages/fulfillment/src/types/index.ts @@ -4,3 +4,6 @@ export type InitializeModuleInjectableDependencies = { logger?: Logger eventBusService?: IEventBusModuleService } + +export const FulfillmentIdentifiersRegistrationName = + "fulfillment_providers_identifier" diff --git a/packages/medusa-test-utils/src/module-test-runner.ts b/packages/medusa-test-utils/src/module-test-runner.ts index bc8730a955..ebe036aa4b 100644 --- a/packages/medusa-test-utils/src/module-test-runner.ts +++ b/packages/medusa-test-utils/src/module-test-runner.ts @@ -1,6 +1,10 @@ import { ContainerRegistrationKeys, ModulesSdkUtils } from "@medusajs/utils" import { initModules, InitModulesOptions } from "./init-modules" -import { MedusaAppOutput, ModulesDefinition } from "@medusajs/modules-sdk" +import { + MedusaAppOutput, + MedusaModuleConfig, + ModulesDefinition +} from "@medusajs/modules-sdk" import { getDatabaseURL, getMikroOrmWrapper, TestDatabase } from "./database" import { MockEventBusService } from "." @@ -18,6 +22,7 @@ export interface SuiteOptions { export function moduleIntegrationTestRunner({ moduleName, moduleModels, + moduleOptions = {}, joinerConfig = [], schema = "public", debug = false, @@ -26,6 +31,7 @@ export function moduleIntegrationTestRunner({ }: { moduleName: string moduleModels?: any[] + moduleOptions?: Record joinerConfig?: any[] schema?: string dbName?: string @@ -62,11 +68,12 @@ export function moduleIntegrationTestRunner({ database: dbConfig, }, database: dbConfig, + ...moduleOptions, }, }, } - const moduleOptions: InitModulesOptions = { + const moduleOptions_: InitModulesOptions = { injectedDependencies: { [ContainerRegistrationKeys.PG_CONNECTION]: connection, eventBusService: new MockEventBusService(), @@ -106,7 +113,7 @@ export function moduleIntegrationTestRunner({ const beforeEach_ = async () => { try { await MikroOrmWrapper.setupDatabase() - const output = await initModules(moduleOptions) + const output = await initModules(moduleOptions_) shutdown = output.shutdown medusaApp = output.medusaApp moduleService = output.medusaApp.modules[moduleName] diff --git a/packages/types/src/fulfillment/common/fulfillment.ts b/packages/types/src/fulfillment/common/fulfillment.ts index 7e8966b899..cd1bfd95e9 100644 --- a/packages/types/src/fulfillment/common/fulfillment.ts +++ b/packages/types/src/fulfillment/common/fulfillment.ts @@ -3,6 +3,7 @@ import { ServiceProviderDTO } from "./service-provider" import { FulfillmentAddressDTO } from "./address" import { FulfillmentItemDTO } from "./fulfillment-item" import { FulfillmentLabelDTO } from "./fulfillment-label" +import { BaseFilterable, OperatorMap } from "../../dal" export interface FulfillmentDTO { id: string @@ -24,3 +25,17 @@ export interface FulfillmentDTO { updated_at: Date deleted_at: Date | null } + +export interface FilterableFulfillmentProps + extends BaseFilterable { + id?: string | string[] | OperatorMap + location_id?: string | string[] | OperatorMap + packed_at?: Date | OperatorMap + shipped_at?: Date | OperatorMap + delivered_at?: Date | OperatorMap + canceled_at?: Date | OperatorMap + provider_id?: string | string[] | OperatorMap + shipping_option_id?: string | null + created_at?: Date | OperatorMap + updated_at?: Date | OperatorMap +} diff --git a/packages/types/src/fulfillment/index.ts b/packages/types/src/fulfillment/index.ts index 0c73656566..a8cf1df979 100644 --- a/packages/types/src/fulfillment/index.ts +++ b/packages/types/src/fulfillment/index.ts @@ -1,3 +1,4 @@ export * from "./common" export * from "./mutations" export * from "./service" +export * from "./provider" diff --git a/packages/types/src/fulfillment/mutations/fulfillment-address.ts b/packages/types/src/fulfillment/mutations/fulfillment-address.ts new file mode 100644 index 0000000000..b76a7ad5e1 --- /dev/null +++ b/packages/types/src/fulfillment/mutations/fulfillment-address.ts @@ -0,0 +1,14 @@ +export interface CreateFulfillmentAddressDTO { + fulfillment_id: string + company?: string | null + first_name?: string | null + last_name?: string | null + address_1?: string | null + address_2?: string | null + city?: string | null + country_code?: string | null + province?: string | null + postal_code?: string | null + phone?: string | null + metadata?: Record | null +} diff --git a/packages/types/src/fulfillment/mutations/fulfillment-item.ts b/packages/types/src/fulfillment/mutations/fulfillment-item.ts new file mode 100644 index 0000000000..8c863d40ab --- /dev/null +++ b/packages/types/src/fulfillment/mutations/fulfillment-item.ts @@ -0,0 +1,9 @@ +export interface CreateFulfillmentItemDTO { + fulfillment_id: string + title: string + sku: string + quantity: number + barcode: string + line_item_id?: string | null + inventory_item_id?: string | null +} diff --git a/packages/types/src/fulfillment/mutations/fulfillment-label.ts b/packages/types/src/fulfillment/mutations/fulfillment-label.ts new file mode 100644 index 0000000000..a1581c22f4 --- /dev/null +++ b/packages/types/src/fulfillment/mutations/fulfillment-label.ts @@ -0,0 +1,6 @@ +export interface CreateFulfillmentLabelDTO { + tracking_number: string + tracking_url: string + label_url: string + fulfillment_id: string +} diff --git a/packages/types/src/fulfillment/mutations/fulfillment.ts b/packages/types/src/fulfillment/mutations/fulfillment.ts new file mode 100644 index 0000000000..e0703101f0 --- /dev/null +++ b/packages/types/src/fulfillment/mutations/fulfillment.ts @@ -0,0 +1,30 @@ +import { CreateFulfillmentAddressDTO } from "./fulfillment-address" +import { CreateFulfillmentItemDTO } from "./fulfillment-item" +import { CreateFulfillmentLabelDTO } from "./fulfillment-label" + +export interface CreateFulfillmentOrderDTO {} + +export interface CreateFulfillmentDTO { + location_id: string + packed_at?: Date | null + shipped_at?: Date | null + delivered_at?: Date | null + canceled_at?: Date | null + data?: Record | null + provider_id: string + shipping_option_id?: string | null + metadata?: Record | null + delivery_address: Omit + items: Omit[] + labels: Omit[] + order: CreateFulfillmentOrderDTO +} + +export interface UpdateFulfillmentDTO { + location_id?: string + packed_at?: Date | null + shipped_at?: Date | null + delivered_at?: Date | null + data?: Record | null + metadata?: Record | null +} diff --git a/packages/types/src/fulfillment/mutations/index.ts b/packages/types/src/fulfillment/mutations/index.ts index 2f36038547..4e4be343f5 100644 --- a/packages/types/src/fulfillment/mutations/index.ts +++ b/packages/types/src/fulfillment/mutations/index.ts @@ -5,3 +5,7 @@ export * from "./geo-zone" export * from "./service-zone" export * from "./shipping-option" export * from "./fulfillment-set" +export * from "./fulfillment" +export * from "./fulfillment-address" +export * from "./fulfillment-label" +export * from "./fulfillment-item" diff --git a/packages/types/src/fulfillment/provider.ts b/packages/types/src/fulfillment/provider.ts new file mode 100644 index 0000000000..63fdb31a39 --- /dev/null +++ b/packages/types/src/fulfillment/provider.ts @@ -0,0 +1,96 @@ +export interface IFulfillmentProvider { + /** + * @ignore + * + * Return a unique identifier to retrieve the fulfillment plugin provider + */ + getIdentifier(): string + /** + * @ignore + * + * Return the available fulfillment options for the given data. + */ + getFulfillmentOptions(): Promise[]> + /** + * @ignore + * + * Validate the given fulfillment data. + */ + validateFulfillmentData( + optionData: Record, + data: Record, + context: Record + ): Promise + /** + * @ignore + * + * Validate the given option. + */ + validateOption(data: Record): Promise + /** + * @ignore + * + * Check if the provider can calculate the fulfillment price. + */ + canCalculate(data: Record): Promise + /** + * @ignore + * + * Calculate the price for the given fulfillment option. + */ + calculatePrice( + optionData: Record, + data: Record, + context: Record + ): Promise + /** + * @ignore + * + * Create a fulfillment for the given data. + */ + createFulfillment( + data: object, + items: object[], + order: object, + fulfillment: Record + ): Promise> + /** + * @ignore + * + * Cancel the given fulfillment. + */ + cancelFulfillment(fulfillment: Record): Promise + /** + * @ignore + * + * Get the documents for the given fulfillment data. + */ + getFulfillmentDocuments(data: Record): Promise + /** + * @ignore + * + * Create a return for the given data. + */ + createReturnFulfillment(fromData: Record): Promise + /** + * @ignore + * + * Get the documents for the given return data. + */ + retrieveDocuments( + fulfillmentData: Record, + documentType: string + ): Promise + /** + * @ignore + * + * Get the documents for the given return data. + */ + getReturnDocuments(data: Record): Promise + /** + * @ignore + * + * Get the documents for the given shipment data. + */ + getShipmentDocuments(data: Record): Promise +} diff --git a/packages/types/src/fulfillment/service.ts b/packages/types/src/fulfillment/service.ts index af8b3b864b..7f511c1113 100644 --- a/packages/types/src/fulfillment/service.ts +++ b/packages/types/src/fulfillment/service.ts @@ -7,6 +7,7 @@ import { FilterableShippingOptionRuleProps, FilterableShippingOptionTypeProps, FilterableShippingProfileProps, + FulfillmentDTO, FulfillmentSetDTO, GeoZoneDTO, ServiceZoneDTO, @@ -24,6 +25,7 @@ import { CreateServiceZoneDTO, CreateShippingOptionDTO, CreateShippingOptionRuleDTO, + UpdateFulfillmentDTO, UpdateFulfillmentSetDTO, UpdateGeoZoneDTO, UpdateServiceZoneDTO, @@ -31,6 +33,7 @@ import { UpdateShippingOptionRuleDTO, } from "./mutations" import { CreateShippingProfileDTO } from "./mutations/shipping-profile" +import { CreateFulfillmentDTO } from "./mutations/fulfillment" export interface IFulfillmentModuleService extends IModuleService { /** @@ -202,6 +205,17 @@ export interface IFulfillmentModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * Update a fulfillment + * @param data + * @param sharedContext + */ + updateFulfillment( + id: string, + data: UpdateFulfillmentDTO, + sharedContext?: Context + ): Promise + /** * Delete a fulfillment set * @param ids @@ -337,6 +351,18 @@ export interface IFulfillmentModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * Retrieve a fulfillment + * @param id + * @param config + * @param sharedContext + */ + retrieveFulfillment( + id: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + /** * List fulfillment sets * @param filters @@ -421,6 +447,18 @@ export interface IFulfillmentModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * List fulfillments + * @param filters + * @param config + * @param sharedContext + */ + listFulfillments( + filters?: FilterableFulfillmentSetProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + /** * List and count fulfillment sets * @param filters @@ -505,6 +543,18 @@ export interface IFulfillmentModuleService extends IModuleService { sharedContext?: Context ): Promise<[ShippingOptionTypeDTO[], number]> + /** + * List and count fulfillments + * @param filters + * @param config + * @param sharedContext + */ + listAndCountFulfillments( + filters?: FilterableFulfillmentSetProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[FulfillmentDTO[], number]> + /** * Soft delete fulfillment sets * @param fulfillmentIds @@ -572,4 +622,31 @@ export interface IFulfillmentModuleService extends IModuleService { ): Promise | void> // TODO define needed soft delete/delete/restore methods + + /** + * Retrieve the available fulfillment options for the given data. + */ + retrieveFulfillmentOptions( + providerId: string + ): Promise[]> + + /** + * Create a new fulfillment including into the third party provider + * @param data + * @param sharedContext + */ + createFulfillment( + data: CreateFulfillmentDTO, + sharedContext?: Context + ): Promise + + /** + * Cancel the given fulfillment including into the third party provider + * @param id + * @param sharedContext + */ + cancelFulfillment( + id: string, + sharedContext?: Context + ): Promise } diff --git a/packages/utils/src/dal/mikro-orm/utils.ts b/packages/utils/src/dal/mikro-orm/utils.ts index a80ff2286c..33fb2bbc09 100644 --- a/packages/utils/src/dal/mikro-orm/utils.ts +++ b/packages/utils/src/dal/mikro-orm/utils.ts @@ -148,11 +148,30 @@ export const mikroOrmSerializer = async ( options?: any ): Promise => { options ??= {} + + const data_ = Array.isArray(data) ? data : [data] + + const forSerialization: unknown[] = [] + const notForSerialization: unknown[] = [] + + data_.forEach((object) => { + if (object.__meta) { + return forSerialization.push(object) + } + + return notForSerialization.push(object) + }) + const { serialize } = await import("@mikro-orm/core") - const result = serialize(data, { + let result: any = serialize(forSerialization, { forceObject: true, populate: true, ...options, - }) - return result as unknown as Promise + }) as TOutput[] + + if (notForSerialization.length) { + result = result.concat(notForSerialization) + } + + return Array.isArray(data) ? result : result[0] } diff --git a/packages/utils/src/fulfillment/index.ts b/packages/utils/src/fulfillment/index.ts index 969389b91e..d4fc9ea972 100644 --- a/packages/utils/src/fulfillment/index.ts +++ b/packages/utils/src/fulfillment/index.ts @@ -1,2 +1,3 @@ export * from "./geo-zone" export * from "./shipping-options" +export * from "./provider" diff --git a/packages/utils/src/fulfillment/provider.ts b/packages/utils/src/fulfillment/provider.ts new file mode 100644 index 0000000000..fa66903c6f --- /dev/null +++ b/packages/utils/src/fulfillment/provider.ts @@ -0,0 +1,65 @@ +import { IFulfillmentProvider } from "@medusajs/types" + +export class AbstractFulfillmentProviderService + implements IFulfillmentProvider +{ + static identifier: string + + static _isFulfillmentService = true + + static isFulfillmentService(obj) { + return obj?.constructor?._isFulfillmentService + } + + getIdentifier() { + return (this.constructor as any).identifier + } + + async getFulfillmentOptions(): Promise[]> { + throw Error("getFulfillmentOptions must be overridden by the child class") + } + + async validateFulfillmentData(optionData, data, context): Promise { + throw Error("validateFulfillmentData must be overridden by the child class") + } + + async validateOption(data): Promise { + throw Error("validateOption must be overridden by the child class") + } + + async canCalculate(data) { + throw Error("canCalculate must be overridden by the child class") + } + + async calculatePrice(optionData, data, cart) { + throw Error("calculatePrice must be overridden by the child class") + } + + async createFulfillment(data, items, order, fulfillment): Promise { + throw Error("createFulfillment must be overridden by the child class") + } + + async cancelFulfillment(fulfillment): Promise { + throw Error("cancelFulfillment must be overridden by the child class") + } + + async getFulfillmentDocuments(data) { + return [] + } + + async createReturnFulfillment(fromData): Promise { + throw Error("createReturn must be overridden by the child class") + } + + async getReturnDocuments(data) { + return [] + } + + async getShipmentDocuments(data) { + return [] + } + + async retrieveDocuments(fulfillmentData, documentType) { + throw Error("retrieveDocuments must be overridden by the child class") + } +} diff --git a/yarn.lock b/yarn.lock index 780dfbf567..9649071ecf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8234,10 +8234,25 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/fulfillment-manual@workspace:*, @medusajs/fulfillment-manual@workspace:packages/fulfillment-manual": + version: 0.0.0-use.local + resolution: "@medusajs/fulfillment-manual@workspace:packages/fulfillment-manual" + dependencies: + "@medusajs/utils": ^1.11.3 + body-parser: ^1.19.0 + cross-env: ^5.2.1 + express: ^4.17.1 + jest: ^25.5.4 + rimraf: ^5.0.1 + typescript: ^4.9.5 + languageName: unknown + linkType: soft + "@medusajs/fulfillment@workspace:packages/fulfillment": version: 0.0.0-use.local resolution: "@medusajs/fulfillment@workspace:packages/fulfillment" dependencies: + "@medusajs/fulfillment-manual": "workspace:*" "@medusajs/modules-sdk": ^1.12.4 "@medusajs/types": ^1.11.8 "@medusajs/utils": ^1.11.1