diff --git a/.changeset/nine-nails-end.md b/.changeset/nine-nails-end.md new file mode 100644 index 0000000000..36ea7dcd0f --- /dev/null +++ b/.changeset/nine-nails-end.md @@ -0,0 +1,5 @@ +--- +"@medusajs/types": patch +--- + +feat(fulfillment): Module service implementation first iteration diff --git a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service.spec.ts b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service.spec.ts new file mode 100644 index 0000000000..eb85670c2e --- /dev/null +++ b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service.spec.ts @@ -0,0 +1,1373 @@ +import { Modules } from "@medusajs/modules-sdk" +import { initModules } from "medusa-test-utils/dist" +import { + CreateFulfillmentSetDTO, + CreateGeoZoneDTO, + CreateServiceZoneDTO, + GeoZoneDTO, + IFulfillmentModuleService, + ServiceZoneDTO, + UpdateFulfillmentSetDTO, + UpdateGeoZoneDTO, +} from "@medusajs/types" +import { getInitModuleConfig, MikroOrmWrapper } from "../utils" +import { GeoZoneType } from "@medusajs/utils" + +describe("fulfillment module service", function () { + let service: IFulfillmentModuleService + let shutdownFunc: () => Promise + + beforeAll(async () => { + const initModulesConfig = getInitModuleConfig() + + const { medusaApp, shutdown } = await initModules(initModulesConfig) + + service = medusaApp.modules[Modules.FULFILLMENT] + + shutdownFunc = shutdown + }) + + beforeEach(async () => { + await MikroOrmWrapper.setupDatabase() + }) + + afterEach(async () => { + await MikroOrmWrapper.clearDatabase() + }) + + afterAll(async () => { + await shutdownFunc() + }) + + 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", + }, + ], + }, + ], + }) + + let listedSets = await service.list({ type: createdSet1.type }) + + expect(listedSets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: createdSet1.id }), + expect.objectContaining({ id: createdSet2.id }), + ]) + ) + + 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("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 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) + + expect(updatedFulfillmentSets).toHaveLength(2) + + let i = 0 + for (const data_ of updateData) { + expect(updatedFulfillmentSets[i]).toEqual( + expect.objectContaining({ + id: createdFulfillmentSets[i].id, + name: data_.name, + type: data_.type, + }) + ) + ++i + } + }) + + 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) + + let i = 0 + for (const data_ of updateData) { + expect(updatedFulfillmentSets[i]).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, + }), + ]), + }), + ]), + }) + ) + ++i + } + + const serviceZones = await service.listServiceZones() + + expect(serviceZones).toHaveLength(2) + expect(serviceZones).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: updateData[0].service_zones![0].name, + }), + expect.objectContaining({ + name: updateData[1].service_zones![0].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) + + let i = 0 + for (const data_ of updateData) { + expect(updatedFulfillmentSets[i]).toEqual( + expect.objectContaining({ + id: data_.id, + name: data_.name, + type: data_.type, + service_zones: expect.arrayContaining([ + expect.objectContaining({ + id: createdFulfillmentSets[i].service_zones[0].id, + }), + 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, + }), + ]), + }), + ]), + }) + ) + ++i + } + + 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].name, + }), + expect.objectContaining({ + name: updateData[1].service_zones![1].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 = 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) + + let i = 0 + for (const data_ of updateData) { + expect(updatedServiceZones[i]).toEqual( + expect.objectContaining({ + id: data_.id, + name: data_.name, + geo_zones: expect.arrayContaining([ + expect.objectContaining({ + type: data_.geo_zones[0].type, + country_code: data_.geo_zones[0].country_code, + }), + ]), + }) + ) + ++i + } + }) + + 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 = { + 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) + + let i = 0 + for (const data_ of updateData) { + expect(updatedGeoZones[i]).toEqual( + expect.objectContaining({ + id: data_.id, + type: data_.type, + country_code: data_.country_code, + }) + ) + ++i + } + }) + }) + }) +}) diff --git a/packages/fulfillment/integration-tests/__tests__/index.ts b/packages/fulfillment/integration-tests/__tests__/index.ts deleted file mode 100644 index 333c84c1dd..0000000000 --- a/packages/fulfillment/integration-tests/__tests__/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe("noop", function () { - it("should run", function () { - expect(true).toBe(true) - }) -}) diff --git a/packages/fulfillment/integration-tests/utils/database.ts b/packages/fulfillment/integration-tests/utils/database.ts index 118464d4d7..d1ba6a830c 100644 --- a/packages/fulfillment/integration-tests/utils/database.ts +++ b/packages/fulfillment/integration-tests/utils/database.ts @@ -2,17 +2,12 @@ import { TestDatabaseUtils } from "medusa-test-utils" import * as Models from "@models" -const pathToMigrations = "../../src/migrations" const mikroOrmEntities = Models as unknown as any[] export const MikroOrmWrapper = TestDatabaseUtils.getMikroOrmWrapper( mikroOrmEntities, - pathToMigrations -) - -export const MikroOrmConfig = TestDatabaseUtils.getMikroOrmConfig( - mikroOrmEntities, - pathToMigrations + null, + process.env.MEDUSA_FULFILLMENT_DB_SCHEMA ) export const DB_URL = TestDatabaseUtils.getDatabaseURL() diff --git a/packages/fulfillment/integration-tests/utils/index.ts b/packages/fulfillment/integration-tests/utils/index.ts index 6b917ed30e..ba28fb5523 100644 --- a/packages/fulfillment/integration-tests/utils/index.ts +++ b/packages/fulfillment/integration-tests/utils/index.ts @@ -1 +1,2 @@ export * from "./database" +export * from "./get-init-module-config" diff --git a/packages/fulfillment/src/migrations/.snapshot-medusa-fulfillment.json b/packages/fulfillment/src/migrations/.snapshot-medusa-fulfillment.json new file mode 100644 index 0000000000..f2b6dde656 --- /dev/null +++ b/packages/fulfillment/src/migrations/.snapshot-medusa-fulfillment.json @@ -0,0 +1,1822 @@ +{ + "namespaces": [ + "public" + ], + "name": "public", + "tables": [ + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "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", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "first_name": { + "name": "first_name", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "last_name": { + "name": "last_name", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "address_1": { + "name": "address_1", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "address_2": { + "name": "address_2", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "city": { + "name": "city", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "country_code": { + "name": "country_code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "province": { + "name": "province", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "phone": { + "name": "phone", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "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_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": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_address_deleted_at\" ON \"fulfillment_address\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "fulfillment_address_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "name": { + "name": "name", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "type": { + "name": "type", + "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_set", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_fulfillment_set_name_unique", + "columnNames": [ + "name" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_fulfillment_set_name_unique\" ON \"fulfillment_set\" (name) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_fulfillment_set_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_set_deleted_at\" ON \"fulfillment_set\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "fulfillment_set_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "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": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "name": { + "name": "name", + "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" + }, + "fulfillment_set_id": { + "name": "fulfillment_set_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "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_zone", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_service_zone_name_unique", + "columnNames": [ + "name" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_service_zone_name_unique\" ON \"service_zone\" (name) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_service_zone_fulfillment_set_id", + "columnNames": [ + "fulfillment_set_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_service_zone_fulfillment_set_id\" ON \"service_zone\" (fulfillment_set_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_service_zone_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_service_zone_deleted_at\" ON \"service_zone\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "service_zone_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "service_zone_fulfillment_set_id_foreign": { + "constraintName": "service_zone_fulfillment_set_id_foreign", + "columnNames": [ + "fulfillment_set_id" + ], + "localTableName": "public.service_zone", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.fulfillment_set", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "type": { + "name": "type", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "'country'", + "enumItems": [ + "country", + "province", + "city", + "zip" + ], + "mappedType": "enum" + }, + "country_code": { + "name": "country_code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "province_code": { + "name": "province_code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "city": { + "name": "city", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "service_zone_id": { + "name": "service_zone_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "postal_expression": { + "name": "postal_expression", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "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": "geo_zone", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_geo_zone_country_code", + "columnNames": [ + "country_code" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_geo_zone_country_code\" ON \"geo_zone\" (country_code) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_geo_zone_province_code", + "columnNames": [ + "province_code" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_geo_zone_province_code\" ON \"geo_zone\" (province_code) WHERE deleted_at IS NULL AND province_code IS NOT NULL" + }, + { + "keyName": "IDX_geo_zone_city", + "columnNames": [ + "city" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_geo_zone_city\" ON \"geo_zone\" (city) WHERE deleted_at IS NULL AND city IS NOT NULL" + }, + { + "keyName": "IDX_geo_zone_service_zone_id", + "columnNames": [ + "service_zone_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_geo_zone_service_zone_id\" ON \"geo_zone\" (service_zone_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_geo_zone_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_geo_zone_deleted_at\" ON \"geo_zone\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "geo_zone_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "geo_zone_service_zone_id_foreign": { + "constraintName": "geo_zone_service_zone_id_foreign", + "columnNames": [ + "service_zone_id" + ], + "localTableName": "public.geo_zone", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.service_zone", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "label": { + "name": "label", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "description": { + "name": "description", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "code": { + "name": "code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "shipping_option_id": { + "name": "shipping_option_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "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": "shipping_option_type", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_shipping_option_type_shipping_option_id", + "columnNames": [ + "shipping_option_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_option_type_shipping_option_id\" ON \"shipping_option_type\" (shipping_option_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_shipping_option_type_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_option_type_deleted_at\" ON \"shipping_option_type\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "shipping_option_type_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "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": "shipping_profile", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_shipping_profile_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_profile_deleted_at\" ON \"shipping_profile\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "shipping_profile_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "name": { + "name": "name", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "price_type": { + "name": "price_type", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "'calculated'", + "enumItems": [ + "calculated", + "flat" + ], + "mappedType": "enum" + }, + "service_zone_id": { + "name": "service_zone_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "shipping_profile_id": { + "name": "shipping_profile_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "service_provider_id": { + "name": "service_provider_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "shipping_option_type_id": { + "name": "shipping_option_type_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "data": { + "name": "data", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "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": "shipping_option", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "shipping_option_type_id" + ], + "composite": false, + "keyName": "shipping_option_shipping_option_type_id_unique", + "primary": false, + "unique": true + }, + { + "keyName": "IDX_shipping_option_service_zone_id", + "columnNames": [ + "service_zone_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_option_service_zone_id\" ON \"shipping_option\" (service_zone_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_shipping_option_shipping_profile_id", + "columnNames": [ + "shipping_profile_id" + ], + "composite": false, + "primary": false, + "unique": false, + "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", + "columnNames": [ + "service_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" + }, + { + "keyName": "IDX_shipping_option_shipping_option_type_id", + "columnNames": [ + "shipping_option_type_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_option_shipping_option_type_id\" ON \"shipping_option\" (shipping_option_type_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_shipping_option_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_option_deleted_at\" ON \"shipping_option\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "shipping_option_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "shipping_option_service_zone_id_foreign": { + "constraintName": "shipping_option_service_zone_id_foreign", + "columnNames": [ + "service_zone_id" + ], + "localTableName": "public.shipping_option", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.service_zone", + "updateRule": "cascade" + }, + "shipping_option_shipping_profile_id_foreign": { + "constraintName": "shipping_option_shipping_profile_id_foreign", + "columnNames": [ + "shipping_profile_id" + ], + "localTableName": "public.shipping_option", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.shipping_profile", + "updateRule": "cascade" + }, + "shipping_option_service_provider_id_foreign": { + "constraintName": "shipping_option_service_provider_id_foreign", + "columnNames": [ + "service_provider_id" + ], + "localTableName": "public.shipping_option", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.service_provider", + "updateRule": "cascade" + }, + "shipping_option_shipping_option_type_id_foreign": { + "constraintName": "shipping_option_shipping_option_type_id_foreign", + "columnNames": [ + "shipping_option_type_id" + ], + "localTableName": "public.shipping_option", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.shipping_option_type", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "attribute": { + "name": "attribute", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "operator": { + "name": "operator", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "value": { + "name": "value", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "shipping_option_id": { + "name": "shipping_option_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "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": "shipping_option_rule", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_shipping_option_rule_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_option_rule_deleted_at\" ON \"shipping_option_rule\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "shipping_option_rule_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "shipping_option_rule_shipping_option_id_foreign": { + "constraintName": "shipping_option_rule_shipping_option_id_foreign", + "columnNames": [ + "shipping_option_id" + ], + "localTableName": "public.shipping_option_rule", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.shipping_option", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "location_id": { + "name": "location_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "packed_at": { + "name": "packed_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "shipped_at": { + "name": "shipped_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "data": { + "name": "data", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "shipping_option_id": { + "name": "shipping_option_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "delivery_address_id": { + "name": "delivery_address_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "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", + "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", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_fulfillment_location_id", + "columnNames": [ + "location_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_location_id\" ON \"fulfillment\" (location_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_fulfillment_provider_id", + "columnNames": [ + "provider_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_provider_id\" ON \"fulfillment\" (provider_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_fulfillment_shipping_option_id", + "columnNames": [ + "shipping_option_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_shipping_option_id\" ON \"fulfillment\" (shipping_option_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_fulfillment_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_deleted_at\" ON \"fulfillment\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "fulfillment_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "fulfillment_shipping_option_id_foreign": { + "constraintName": "fulfillment_shipping_option_id_foreign", + "columnNames": [ + "shipping_option_id" + ], + "localTableName": "public.fulfillment", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.shipping_option", + "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": [ + "delivery_address_id" + ], + "localTableName": "public.fulfillment", + "referencedColumnNames": [ + "id" + ], + "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" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "tracking_number": { + "name": "tracking_number", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "tracking_url": { + "name": "tracking_url", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "label_url": { + "name": "label_url", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "fulfillment_id": { + "name": "fulfillment_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "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_label", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_fulfillment_label_fulfillment_id", + "columnNames": [ + "fulfillment_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_label_fulfillment_id\" ON \"fulfillment_label\" (fulfillment_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_fulfillment_label_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_label_deleted_at\" ON \"fulfillment_label\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "fulfillment_label_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "fulfillment_label_fulfillment_id_foreign": { + "constraintName": "fulfillment_label_fulfillment_id_foreign", + "columnNames": [ + "fulfillment_id" + ], + "localTableName": "public.fulfillment_label", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.fulfillment", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "title": { + "name": "title", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "sku": { + "name": "sku", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "barcode": { + "name": "barcode", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "decimal" + }, + "line_item_id": { + "name": "line_item_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "inventory_item_id": { + "name": "inventory_item_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "fulfillment_id": { + "name": "fulfillment_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "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_item", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_fulfillment_item_line_item_id", + "columnNames": [ + "line_item_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_item_fulfillment_id\" ON \"fulfillment_item\" (line_item_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_fulfillment_item_inventory_item_id", + "columnNames": [ + "inventory_item_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_item_fulfillment_id\" ON \"fulfillment_item\" (inventory_item_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_fulfillment_item_fulfillment_id", + "columnNames": [ + "fulfillment_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_item_fulfillment_id\" ON \"fulfillment_item\" (fulfillment_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_fulfillment_item_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_item_deleted_at\" ON \"fulfillment_item\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "fulfillment_item_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "fulfillment_item_fulfillment_id_foreign": { + "constraintName": "fulfillment_item_fulfillment_id_foreign", + "columnNames": [ + "fulfillment_id" + ], + "localTableName": "public.fulfillment_item", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.fulfillment", + "updateRule": "cascade" + } + } + } + ] +} diff --git a/packages/fulfillment/src/migrations/Migration20240214103108.ts b/packages/fulfillment/src/migrations/Migration20240214103108.ts new file mode 100644 index 0000000000..1a7fa50ce5 --- /dev/null +++ b/packages/fulfillment/src/migrations/Migration20240214103108.ts @@ -0,0 +1,84 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20240214103108 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 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_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;'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_service_zone_deleted_at" ON "service_zone" (deleted_at) WHERE deleted_at IS NOT NULL;'); + + this.addSql('create table if not exists "geo_zone" ("id" text not null, "type" text check ("type" in (\'country\', \'province\', \'city\', \'zip\')) not null default \'country\', "country_code" text not null, "province_code" text null, "city" text null, "service_zone_id" text not null, "postal_expression" jsonb null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "geo_zone_pkey" primary key ("id"));'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_geo_zone_country_code" ON "geo_zone" (country_code) WHERE deleted_at IS NULL;'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_geo_zone_province_code" ON "geo_zone" (province_code) WHERE deleted_at IS NULL AND province_code IS NOT NULL;'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_geo_zone_city" ON "geo_zone" (city) WHERE deleted_at IS NULL AND city IS NOT NULL;'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_geo_zone_service_zone_id" ON "geo_zone" (service_zone_id) WHERE deleted_at IS NULL;'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_geo_zone_deleted_at" ON "geo_zone" (deleted_at) WHERE deleted_at IS NOT NULL;'); + + this.addSql('create table if not exists "shipping_option_type" ("id" text not null, "label" text not null, "description" text null, "code" text not null, "shipping_option_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_type_pkey" primary key ("id"));'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_type_shipping_option_id" ON "shipping_option_type" (shipping_option_id) WHERE deleted_at IS NULL;'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_type_deleted_at" ON "shipping_option_type" (deleted_at) WHERE deleted_at IS NOT NULL;'); + + this.addSql('create table if not exists "shipping_profile" ("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 "shipping_profile_pkey" primary key ("id"));'); + 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 not null, "service_provider_id" text not null, "shipping_option_type_id" text null, "data" jsonb null, "metadata" jsonb 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_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;'); + + this.addSql('create table if not exists "shipping_option_rule" ("id" text not null, "attribute" text not null, "operator" text not null, "value" jsonb null, "shipping_option_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_rule_pkey" primary key ("id"));'); + 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 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;'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_deleted_at" ON "fulfillment" (deleted_at) WHERE deleted_at IS NOT NULL;'); + + this.addSql('create table if not exists "fulfillment_label" ("id" text not null, "tracking_number" text not null, "tracking_url" text not null, "label_url" text not 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_label_pkey" primary key ("id"));'); + 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 INDEX IF NOT EXISTS "IDX_fulfillment_item_fulfillment_id" ON "fulfillment_item" (line_item_id) WHERE deleted_at IS NULL;'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_item_fulfillment_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;'); + this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_item_deleted_at" ON "fulfillment_item" (deleted_at) WHERE deleted_at IS NOT NULL;'); + + this.addSql('alter table if exists "service_zone" add constraint "service_zone_fulfillment_set_id_foreign" foreign key ("fulfillment_set_id") references "fulfillment_set" ("id") on update cascade;'); + + this.addSql('alter table if exists "geo_zone" add constraint "geo_zone_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_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;'); + 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;'); + 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;'); + + 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_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_item" add constraint "fulfillment_item_fulfillment_id_foreign" foreign key ("fulfillment_id") references "fulfillment" ("id") on update cascade;'); + } + +} diff --git a/packages/fulfillment/src/models/fullfilment-set.ts b/packages/fulfillment/src/models/fullfilment-set.ts index 54eaaf5f3c..38c056ea11 100644 --- a/packages/fulfillment/src/models/fullfilment-set.ts +++ b/packages/fulfillment/src/models/fullfilment-set.ts @@ -6,11 +6,12 @@ import { import { BeforeCreate, + Cascade, Collection, Entity, Filter, Index, - ManyToMany, + OneToMany, OnInit, OptionalProps, PrimaryKey, @@ -29,6 +30,15 @@ const deletedAtIndexStatement = createPsqlIndexStatementHelper({ where: "deleted_at IS NOT NULL", }) +const nameIndexName = "IDX_fulfillment_set_name_unique" +const nameIndexStatement = createPsqlIndexStatementHelper({ + name: nameIndexName, + tableName: "fulfillment_set", + columns: "name", + unique: true, + where: "deleted_at IS NULL", +}) + @Entity() @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class FulfillmentSet { @@ -38,6 +48,10 @@ export default class FulfillmentSet { id: string @Property({ columnType: "text" }) + @Index({ + name: nameIndexName, + expression: nameIndexStatement, + }) name: string @Property({ columnType: "text" }) @@ -46,11 +60,9 @@ export default class FulfillmentSet { @Property({ columnType: "jsonb", nullable: true }) metadata: Record | null = null - @ManyToMany(() => ServiceZone, "fulfillment_sets", { - owner: true, - pivotTable: "fulfillment_set_service_zones", - joinColumn: "fulfillment_set_id", - inverseJoinColumn: "service_zone_id", + @OneToMany(() => ServiceZone, "fulfillment_set", { + cascade: [Cascade.PERSIST, "soft-remove"] as any, + orphanRemoval: true, }) service_zones = new Collection(this) diff --git a/packages/fulfillment/src/models/geo-zone.ts b/packages/fulfillment/src/models/geo-zone.ts index f7f6074a41..3104a4c4fb 100644 --- a/packages/fulfillment/src/models/geo-zone.ts +++ b/packages/fulfillment/src/models/geo-zone.ts @@ -7,12 +7,11 @@ import { import { BeforeCreate, - Collection, Entity, Enum, Filter, Index, - ManyToMany, + ManyToOne, OnInit, OptionalProps, PrimaryKey, @@ -55,6 +54,14 @@ const cityIndexStatement = createPsqlIndexStatementHelper({ where: "deleted_at IS NULL AND city IS NOT NULL", }) +const serviceZoneIdIndexName = "IDX_geo_zone_service_zone_id" +const serviceZoneIdStatement = createPsqlIndexStatementHelper({ + name: serviceZoneIdIndexName, + tableName: "geo_zone", + columns: "service_zone_id", + where: "deleted_at IS NULL", +}) + @Entity() @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class GeoZone { @@ -87,6 +94,13 @@ export default class GeoZone { @Property({ columnType: "text", nullable: true }) city: string | null = null + @Property({ columnType: "text" }) + @Index({ + name: serviceZoneIdIndexName, + expression: serviceZoneIdStatement, + }) + service_zone_id: string + // TODO: Do we have an example or idea of what would be stored in this field? like lat/long for example? @Property({ columnType: "jsonb", nullable: true }) postal_expression: Record | null = null @@ -94,8 +108,10 @@ export default class GeoZone { @Property({ columnType: "jsonb", nullable: true }) metadata: Record | null = null - @ManyToMany(() => ServiceZone, (serviceZone) => serviceZone.geo_zones) - service_zones = new Collection(this) + @ManyToOne(() => ServiceZone, { + persist: false, + }) + service_zone: ServiceZone @Property({ onCreate: () => new Date(), @@ -122,10 +138,12 @@ export default class GeoZone { @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, " fgz") + this.service_zone_id ??= this.service_zone?.id } @OnInit() onInit() { this.id = generateEntityId(this.id, "fgz") + this.service_zone_id ??= this.service_zone?.id } } diff --git a/packages/fulfillment/src/models/service-zone.ts b/packages/fulfillment/src/models/service-zone.ts index 415570e53f..5ca0fe6a25 100644 --- a/packages/fulfillment/src/models/service-zone.ts +++ b/packages/fulfillment/src/models/service-zone.ts @@ -6,11 +6,12 @@ import { import { BeforeCreate, + Cascade, Collection, Entity, Filter, Index, - ManyToMany, + ManyToOne, OneToMany, OnInit, OptionalProps, @@ -32,6 +33,23 @@ const deletedAtIndexStatement = createPsqlIndexStatementHelper({ where: "deleted_at IS NOT NULL", }) +const nameIndexName = "IDX_service_zone_name_unique" +const nameIndexStatement = createPsqlIndexStatementHelper({ + name: nameIndexName, + tableName: "service_zone", + columns: "name", + unique: true, + where: "deleted_at IS NULL", +}) + +const fulfillmentSetIdIndexName = "IDX_service_zone_fulfillment_set_id" +const fulfillmentSetIdIndexStatement = createPsqlIndexStatementHelper({ + name: fulfillmentSetIdIndexName, + tableName: "service_zone", + columns: "fulfillment_set_id", + where: "deleted_at IS NULL", +}) + @Entity() @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class ServiceZone { @@ -41,22 +59,28 @@ export default class ServiceZone { id: string @Property({ columnType: "text" }) + @Index({ + name: nameIndexName, + expression: nameIndexStatement, + }) name: string @Property({ columnType: "jsonb", nullable: true }) metadata: Record | null = null - @ManyToMany( - () => FulfillmentSet, - (fulfillmentSet) => fulfillmentSet.service_zones - ) - fulfillment_sets = new Collection(this) + @Property({ columnType: "text" }) + @Index({ + name: fulfillmentSetIdIndexName, + expression: fulfillmentSetIdIndexStatement, + }) + fulfillment_set_id: string - @ManyToMany(() => GeoZone, "service_zones", { - owner: true, - pivotTable: "service_zone_geo_zones", - joinColumn: "service_zone_id", - inverseJoinColumn: "geo_zone_id", + @ManyToOne(() => FulfillmentSet, { persist: false }) + fulfillment_set: FulfillmentSet + + @OneToMany(() => GeoZone, "service_zone", { + cascade: [Cascade.PERSIST, "soft-remove"] as any, + orphanRemoval: true, }) geo_zones = new Collection(this) @@ -91,10 +115,12 @@ export default class ServiceZone { @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "serzo") + this.fulfillment_set_id ??= this.fulfillment_set?.id } @OnInit() onInit() { this.id = generateEntityId(this.id, "serzo") + this.fulfillment_set_id ??= this.fulfillment_set?.id } } diff --git a/packages/fulfillment/src/repositories/fulfillment-set.ts b/packages/fulfillment/src/repositories/fulfillment-set.ts new file mode 100644 index 0000000000..97b718201f --- /dev/null +++ b/packages/fulfillment/src/repositories/fulfillment-set.ts @@ -0,0 +1,59 @@ +/* +import { Context, FulfillmentTypes } from "@medusajs/types" +import { DALUtils, promiseAll } from "@medusajs/utils" +import { FulfillmentSet, ServiceZone } from "@models" +import { SqlEntityManager } from "@mikro-orm/postgresql" + +interface CreateFulfillmentSetDTO + extends FulfillmentTypes.CreateFulfillmentSetDTO { + service_zones: { id: string; name: string }[] +} + +export class FulfillmentSetRepository extends DALUtils.mikroOrmBaseRepositoryFactory( + FulfillmentSet +) { + async update( + data: { + entity: FulfillmentSet + update: FulfillmentTypes.FulfillmentSetDTO + }[], + context?: Context + ): Promise { + const manager = this.getActiveManager(context) + + // init all service zones collections + await promiseAll( + data.map(async ({ entity }) => { + return await entity.service_zones.init() + }) + ) + + const flfillmentSetsToUpdate = data.map(({ entity, update }) => { + const { service_zones, ...restToUpdate } = update + + const currentServiceZones = entity.service_zones.getItems() + const serviceZonesToDetach = currentServiceZones.filter( + (serviceZone) => + !update.service_zones.find( + (newServiceZone) => newServiceZone.id === serviceZone.id + ) + ) + const serviceZonesToAttach = update.service_zones.filter( + (newServiceZone) => + !currentServiceZones.find( + (serviceZone) => serviceZone.id === newServiceZone.id + ) + ) + + entity.service_zones.remove(serviceZonesToDetach) + entity.service_zones.add(serviceZonesToAttach as unknown as ServiceZone[]) + + return manager.assign(entity, restToUpdate) + }) + + manager.persist(flfillmentSetsToUpdate) + + return flfillmentSetsToUpdate + } +} +*/ diff --git a/packages/fulfillment/src/services/fulfillment-module-service.ts b/packages/fulfillment/src/services/fulfillment-module-service.ts index 908de3fd3e..2dbd8de4ef 100644 --- a/packages/fulfillment/src/services/fulfillment-module-service.ts +++ b/packages/fulfillment/src/services/fulfillment-module-service.ts @@ -8,20 +8,32 @@ import { ModulesSdkTypes, UpdateFulfillmentSetDTO, } from "@medusajs/types" -import { InjectTransactionManager, ModulesSdkUtils } from "@medusajs/utils" +import { + InjectManager, + InjectTransactionManager, + MedusaContext, + MedusaError, + ModulesSdkUtils, + promiseAll, + getSetDifference +} from "@medusajs/utils" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" -import { FulfillmentSet, ServiceZone, ShippingOption } from "@models" +import { FulfillmentSet, GeoZone, ServiceZone, ShippingOption } from "@models" -const generateMethodForModels = [ServiceZone, ShippingOption] +const generateMethodForModels = [ServiceZone, ShippingOption, GeoZone] type InjectedDependencies = { baseRepository: DAL.RepositoryService - fulfillmentService: ModulesSdkTypes.InternalModuleService + fulfillmentSetService: ModulesSdkTypes.InternalModuleService + serviceZoneService: ModulesSdkTypes.InternalModuleService + geoZoneService: ModulesSdkTypes.InternalModuleService } export default class FulfillmentModuleService< - TEntity extends FulfillmentSet = FulfillmentSet + TEntity extends FulfillmentSet = FulfillmentSet, + TServiceZoneEntity extends ServiceZone = ServiceZone, + TGeoZoneEntity extends GeoZone = GeoZone > extends ModulesSdkUtils.abstractModuleServiceFactory< InjectedDependencies, @@ -30,21 +42,31 @@ export default class FulfillmentModuleService< FulfillmentSet: { dto: FulfillmentTypes.FulfillmentSetDTO } ServiceZone: { dto: FulfillmentTypes.ServiceZoneDTO } ShippingOption: { dto: FulfillmentTypes.ShippingOptionDTO } + GeoZone: { dto: FulfillmentTypes.GeoZoneDTO } } >(FulfillmentSet, generateMethodForModels, entityNameToLinkableKeysMap) implements IFulfillmentModuleService { protected baseRepository_: DAL.RepositoryService - protected readonly fulfillmentService_: ModulesSdkTypes.InternalModuleService + protected readonly fulfillmentSetService_: ModulesSdkTypes.InternalModuleService + protected readonly serviceZoneService_: ModulesSdkTypes.InternalModuleService + protected readonly geoZoneService_: ModulesSdkTypes.InternalModuleService constructor( - { baseRepository, fulfillmentService }: InjectedDependencies, + { + baseRepository, + fulfillmentSetService, + serviceZoneService, + geoZoneService, + }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { // @ts-ignore super(...arguments) this.baseRepository_ = baseRepository - this.fulfillmentService_ = fulfillmentService + this.fulfillmentSetService_ = fulfillmentSetService + this.serviceZoneService_ = serviceZoneService + this.geoZoneService_ = geoZoneService } __joinerConfig(): ModuleJoinerConfig { @@ -60,16 +82,41 @@ export default class FulfillmentModuleService< sharedContext?: Context ): Promise - @InjectTransactionManager("baseRepository_") + @InjectManager("baseRepository_") async create( data: | FulfillmentTypes.CreateFulfillmentSetDTO | FulfillmentTypes.CreateFulfillmentSetDTO[], - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise< FulfillmentTypes.FulfillmentSetDTO | FulfillmentTypes.FulfillmentSetDTO[] > { - return [] + const createdFulfillmentSets = await this.create_(data, sharedContext) + + return await this.baseRepository_.serialize< + FulfillmentTypes.FulfillmentSetDTO | FulfillmentTypes.FulfillmentSetDTO[] + >(createdFulfillmentSets, { + populate: true, + }) + } + + @InjectTransactionManager("baseRepository_") + protected async create_( + data: + | FulfillmentTypes.CreateFulfillmentSetDTO + | FulfillmentTypes.CreateFulfillmentSetDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + + const createdFulfillmentSets = await this.fulfillmentSetService_.create( + data_, + sharedContext + ) + + return Array.isArray(data) + ? createdFulfillmentSets + : createdFulfillmentSets[0] } createServiceZones( @@ -81,16 +128,46 @@ export default class FulfillmentModuleService< sharedContext?: Context ): Promise - @InjectTransactionManager("baseRepository_") + @InjectManager("baseRepository_") async createServiceZones( data: | FulfillmentTypes.CreateServiceZoneDTO[] | FulfillmentTypes.CreateServiceZoneDTO, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise< FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[] > { - return [] + const createdServiceZones = await this.createServiceZones_( + data, + sharedContext + ) + + return await this.baseRepository_.serialize< + FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[] + >(createdServiceZones, { + populate: true, + }) + } + + @InjectTransactionManager("baseRepository_") + protected async createServiceZones_( + data: + | FulfillmentTypes.CreateServiceZoneDTO[] + | FulfillmentTypes.CreateServiceZoneDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise { + let data_ = Array.isArray(data) ? data : [data] + + if (!data_.length) { + return [] + } + + const createdServiceZones = await this.serviceZoneService_.create( + data_, + sharedContext + ) + + return Array.isArray(data) ? createdServiceZones : createdServiceZones[0] } createShippingOptions( @@ -107,13 +184,42 @@ export default class FulfillmentModuleService< data: | FulfillmentTypes.CreateShippingOptionDTO[] | FulfillmentTypes.CreateShippingOptionDTO, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise< FulfillmentTypes.ShippingOptionDTO | FulfillmentTypes.ShippingOptionDTO[] > { return [] } + createGeoZones( + data: FulfillmentTypes.CreateGeoZoneDTO[], + sharedContext?: Context + ): Promise + createGeoZones( + data: FulfillmentTypes.CreateGeoZoneDTO, + sharedContext?: Context + ): Promise + + @InjectManager("baseRepository_") + async createGeoZones( + data: + | FulfillmentTypes.CreateGeoZoneDTO + | FulfillmentTypes.CreateGeoZoneDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const createdGeoZones = await this.geoZoneService_.create( + data, + sharedContext + ) + + return await this.baseRepository_.serialize( + createdGeoZones, + { + populate: true, + } + ) + } + update( data: FulfillmentTypes.UpdateFulfillmentSetDTO[], sharedContext?: Context @@ -123,14 +229,170 @@ export default class FulfillmentModuleService< sharedContext?: Context ): Promise - @InjectTransactionManager("baseRepository_") + @InjectManager("baseRepository_") async update( data: UpdateFulfillmentSetDTO[] | UpdateFulfillmentSetDTO, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise< FulfillmentTypes.FulfillmentSetDTO[] | FulfillmentTypes.FulfillmentSetDTO > { - return [] + const updatedFulfillmentSets = await this.update_(data, sharedContext) + + return await this.baseRepository_.serialize< + FulfillmentTypes.FulfillmentSetDTO | FulfillmentTypes.FulfillmentSetDTO[] + >(updatedFulfillmentSets, { + populate: true, + }) + } + + @InjectTransactionManager("baseRepository_") + protected async update_( + data: UpdateFulfillmentSetDTO[] | UpdateFulfillmentSetDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + + if (!data_.length) { + return [] + } + + const fulfillmentSetIds = data_.map((f) => f.id) + if (!fulfillmentSetIds.length) { + return [] + } + + const fulfillmentSets = await this.fulfillmentSetService_.list( + { + id: fulfillmentSetIds, + }, + { + relations: ["service_zones", "service_zones.geo_zones"], + }, + sharedContext + ) + + const fulfillmentSetSet = new Set(fulfillmentSets.map((f) => f.id)) + const expectedFulfillmentSetSet = new Set(data_.map((f) => f.id)) + const missingFulfillmentSetIds = getSetDifference( + expectedFulfillmentSetSet, + fulfillmentSetSet + ) + + if (missingFulfillmentSetIds.size) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `The following fulfillment sets does not exists: ${Array.from( + missingFulfillmentSetIds + ).join(", ")}` + ) + } + + const fulfillmentSetMap = new Map( + fulfillmentSets.map((f) => [f.id, f]) + ) + + // find service zones to delete + const serviceZoneIdsToDelete: string[] = [] + const geoZoneIdsToDelete: string[] = [] + data_.forEach((fulfillmentSet) => { + if (fulfillmentSet.service_zones) { + /** + * Detect and delete service zones that are not in the updated + */ + + const existingFulfillmentSet = fulfillmentSetMap.get(fulfillmentSet.id)! + const existingServiceZones = existingFulfillmentSet.service_zones + const updatedServiceZones = fulfillmentSet.service_zones + const toDeleteServiceZoneIds = getSetDifference( + new Set(existingServiceZones.map((s) => s.id)), + new Set( + updatedServiceZones + .map((s) => "id" in s && s.id) + .filter((id): id is string => !!id) + ) + ) + if (toDeleteServiceZoneIds.size) { + serviceZoneIdsToDelete.push(...Array.from(toDeleteServiceZoneIds)) + geoZoneIdsToDelete.push( + ...existingServiceZones + .filter((s) => toDeleteServiceZoneIds.has(s.id)) + .flatMap((s) => s.geo_zones.map((g) => g.id)) + ) + } + + /** + * Detect and re assign service zones to the fulfillment set that are still present + */ + + const serviceZonesMap = new Map( + existingFulfillmentSet.service_zones.map((serviceZone) => [ + serviceZone.id, + serviceZone, + ]) + ) + const serviceZonesSet = new Set( + existingServiceZones + .map((s) => "id" in s && s.id) + .filter((id): id is string => !!id) + ) + const expectedServiceZoneSet = new Set( + fulfillmentSet.service_zones + .map((s) => "id" in s && s.id) + .filter((id): id is string => !!id) + ) + const missingServiceZoneIds = getSetDifference( + expectedServiceZoneSet, + serviceZonesSet + ) + + if (missingServiceZoneIds.size) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `The following service zones does not exists: ${Array.from( + missingServiceZoneIds + ).join(", ")}` + ) + } + + // re assign service zones to the fulfillment set + if (fulfillmentSet.service_zones) { + fulfillmentSet.service_zones = fulfillmentSet.service_zones.map( + (serviceZone) => { + if (!("id" in serviceZone)) { + return serviceZone + } + return serviceZonesMap.get(serviceZone.id)! + } + ) + } + } + }) + + if (serviceZoneIdsToDelete.length) { + await promiseAll([ + this.geoZoneService_.delete( + { + id: geoZoneIdsToDelete, + }, + sharedContext + ), + this.serviceZoneService_.delete( + { + id: serviceZoneIdsToDelete, + }, + sharedContext + ), + ]) + } + + const updatedFulfillmentSets = await this.fulfillmentSetService_.update( + data_, + sharedContext + ) + + return Array.isArray(data) + ? updatedFulfillmentSets + : updatedFulfillmentSets[0] } updateServiceZones( @@ -142,16 +404,146 @@ export default class FulfillmentModuleService< sharedContext?: Context ): Promise - @InjectTransactionManager("baseRepository_") + @InjectManager("baseRepository_") async updateServiceZones( data: | FulfillmentTypes.UpdateServiceZoneDTO[] | FulfillmentTypes.UpdateServiceZoneDTO, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise< FulfillmentTypes.ServiceZoneDTO[] | FulfillmentTypes.ServiceZoneDTO > { - return [] + const updatedServiceZones = await this.updateServiceZones_( + data, + sharedContext + ) + + return await this.baseRepository_.serialize< + FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[] + >(updatedServiceZones, { + populate: true, + }) + } + + @InjectTransactionManager("baseRepository_") + protected async updateServiceZones_( + data: + | FulfillmentTypes.UpdateServiceZoneDTO[] + | FulfillmentTypes.UpdateServiceZoneDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + + if (!data_.length) { + return [] + } + + const serviceZoneIds = data_.map((s) => s.id) + if (!serviceZoneIds.length) { + return [] + } + + const serviceZones = await this.serviceZoneService_.list( + { + id: serviceZoneIds, + }, + { + relations: ["geo_zones"], + }, + sharedContext + ) + + const serviceZoneSet = new Set(serviceZones.map((s) => s.id)) + const expectedServiceZoneSet = new Set(data_.map((s) => s.id)) + const missingServiceZoneIds = getSetDifference( + expectedServiceZoneSet, + serviceZoneSet + ) + + if (missingServiceZoneIds.size) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `The following service zones does not exists: ${Array.from( + missingServiceZoneIds + ).join(", ")}` + ) + } + + const serviceZoneMap = new Map( + serviceZones.map((s) => [s.id, s]) + ) + + const serviceZoneIdsToDelete: string[] = [] + const geoZoneIdsToDelete: string[] = [] + + data_.forEach((serviceZone) => { + if (serviceZone.geo_zones) { + const existingServiceZone = serviceZoneMap.get(serviceZone.id)! + const existingGeoZones = existingServiceZone.geo_zones + const updatedGeoZones = serviceZone.geo_zones + const toDeleteGeoZoneIds = getSetDifference( + new Set(existingGeoZones.map((g) => g.id)), + new Set( + updatedGeoZones + .map((g) => "id" in g && g.id) + .filter((id): id is string => !!id) + ) + ) + if (toDeleteGeoZoneIds.size) { + geoZoneIdsToDelete.push(...Array.from(toDeleteGeoZoneIds)) + } + + const geoZonesMap = new Map( + existingServiceZone.geo_zones.map((geoZone) => [geoZone.id, geoZone]) + ) + const geoZonesSet = new Set( + existingGeoZones + .map((g) => "id" in g && g.id) + .filter((id): id is string => !!id) + ) + const expectedGeoZoneSet = new Set( + serviceZone.geo_zones + .map((g) => "id" in g && g.id) + .filter((id): id is string => !!id) + ) + const missingGeoZoneIds = getSetDifference( + expectedGeoZoneSet, + geoZonesSet + ) + + if (missingGeoZoneIds.size) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `The following geo zones does not exists: ${Array.from( + missingGeoZoneIds + ).join(", ")}` + ) + } + + serviceZone.geo_zones = serviceZone.geo_zones.map((geoZone) => { + if (!("id" in geoZone)) { + return geoZone + } + return geoZonesMap.get(geoZone.id)! + }) + } + }) + + if (geoZoneIdsToDelete.length) { + await this.geoZoneService_.delete( + { + id: geoZoneIdsToDelete, + }, + sharedContext + ) + } + + const updatedServiceZones = await this.serviceZoneService_.update( + data_, + sharedContext + ) + + return Array.isArray(data) ? updatedServiceZones : updatedServiceZones[0] } updateShippingOptions( @@ -168,10 +560,40 @@ export default class FulfillmentModuleService< data: | FulfillmentTypes.UpdateShippingOptionDTO[] | FulfillmentTypes.UpdateShippingOptionDTO, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise< FulfillmentTypes.ShippingOptionDTO[] | FulfillmentTypes.ShippingOptionDTO > { return [] } + + updateGeoZones( + data: FulfillmentTypes.UpdateGeoZoneDTO[], + sharedContext?: Context + ): Promise + updateGeoZones( + data: FulfillmentTypes.UpdateGeoZoneDTO, + sharedContext?: Context + ): Promise + + @InjectManager("baseRepository_") + async updateGeoZones( + data: + | FulfillmentTypes.UpdateGeoZoneDTO + | FulfillmentTypes.UpdateGeoZoneDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const updatedGeoZones = await this.geoZoneService_.update( + data, + sharedContext + ) + + const serialized = await this.baseRepository_.serialize< + FulfillmentTypes.GeoZoneDTO[] + >(updatedGeoZones, { + populate: true, + }) + + return Array.isArray(data) ? serialized : serialized[0] + } } diff --git a/packages/types/src/fulfillment/mutations/fulfillment-set.ts b/packages/types/src/fulfillment/mutations/fulfillment-set.ts index 4c74f55481..9d70778676 100644 --- a/packages/types/src/fulfillment/mutations/fulfillment-set.ts +++ b/packages/types/src/fulfillment/mutations/fulfillment-set.ts @@ -3,10 +3,12 @@ import { CreateServiceZoneDTO } from "./service-zone" export interface CreateFulfillmentSetDTO { name: string type: string - service_zones: Omit[] + service_zones?: Omit[] } -export interface UpdateFulfillmentSetDTO - extends Partial { +export interface UpdateFulfillmentSetDTO { id: string + name?: string + type?: string + service_zones?: (Omit | { id: string })[] } diff --git a/packages/types/src/fulfillment/mutations/geo-zone.ts b/packages/types/src/fulfillment/mutations/geo-zone.ts index 9ffe572068..087e9f6ead 100644 --- a/packages/types/src/fulfillment/mutations/geo-zone.ts +++ b/packages/types/src/fulfillment/mutations/geo-zone.ts @@ -9,24 +9,20 @@ interface CreateGeoZoneBaseDTO { interface CreateCountryGeoZoneDTO extends CreateGeoZoneBaseDTO { type: "country" - country_code: string } interface CreateProvinceGeoZoneDTO extends CreateGeoZoneBaseDTO { type: "province" - country_code: string province_code: string } interface CreateCityGeoZoneDTO extends CreateGeoZoneBaseDTO { type: "city" - country_code: string city: string } interface CreateZipGeoZoneDTO extends CreateGeoZoneBaseDTO { type: "zip" - country_code: string postal_expression: Record } @@ -42,24 +38,20 @@ interface UpdateGeoZoneBaseDTO extends Partial { interface UpdateCountryGeoZoneDTO extends UpdateGeoZoneBaseDTO { type: "country" - country_code: string } interface UpdateProvinceGeoZoneDTO extends UpdateGeoZoneBaseDTO { type: "province" - country_code: string province_code: string } interface UpdateCityGeoZoneDTO extends UpdateGeoZoneBaseDTO { type: "city" - country_code: string city: string } interface UpdateZipGeoZoneDTO extends UpdateGeoZoneBaseDTO { type: "zip" - country_code: string postal_expression: Record } diff --git a/packages/types/src/fulfillment/mutations/service-zone.ts b/packages/types/src/fulfillment/mutations/service-zone.ts index 0c436e6cfd..8d678acc07 100644 --- a/packages/types/src/fulfillment/mutations/service-zone.ts +++ b/packages/types/src/fulfillment/mutations/service-zone.ts @@ -1,14 +1,13 @@ -import { CreateGeoZoneDTO, UpdateGeoZoneDTO } from "./geo-zone" +import { CreateGeoZoneDTO } from "./geo-zone" export interface CreateServiceZoneDTO { - fulfillment_set_id: string name: string - geo_zones: ( - | Omit - | Omit - )[] + fulfillment_set_id: string + geo_zones?: Omit[] } -export interface UpdateServiceZoneDTO extends Partial { +export interface UpdateServiceZoneDTO { id: string + name?: string + geo_zones?: (Omit | { id: string })[] } diff --git a/packages/types/src/fulfillment/service.ts b/packages/types/src/fulfillment/service.ts index 034f65b9cc..ef47d0651d 100644 --- a/packages/types/src/fulfillment/service.ts +++ b/packages/types/src/fulfillment/service.ts @@ -1,9 +1,11 @@ import { IModuleService } from "../modules-sdk" import { FilterableFulfillmentSetProps, + FilterableGeoZoneProps, FilterableServiceZoneProps, FilterableShippingOptionProps, FulfillmentSetDTO, + GeoZoneDTO, ServiceZoneDTO, ShippingOptionDTO, } from "./common" @@ -12,9 +14,11 @@ import { Context } from "../shared-context" import { RestoreReturn, SoftDeleteReturn } from "../dal" import { CreateFulfillmentSetDTO, + CreateGeoZoneDTO, CreateServiceZoneDTO, CreateShippingOptionDTO, UpdateFulfillmentSetDTO, + UpdateGeoZoneDTO, UpdateServiceZoneDTO, UpdateShippingOptionDTO, } from "./mutations" @@ -62,6 +66,20 @@ export interface IFulfillmentModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * Create a new geo zone + * @param data + * @param sharedContext + */ + createGeoZones( + data: CreateGeoZoneDTO[], + sharedContext?: Context + ): Promise + createGeoZones( + data: CreateGeoZoneDTO, + sharedContext?: Context + ): Promise + /** * Update a fulfillment set * @param data @@ -104,6 +122,20 @@ export interface IFulfillmentModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * Update a geo zone + * @param data + * @param sharedContext + */ + updateGeoZones( + data: UpdateGeoZoneDTO[], + sharedContext?: Context + ): Promise + updateGeoZones( + data: UpdateGeoZoneDTO, + sharedContext?: Context + ): Promise + /** * Delete a fulfillment set * @param ids @@ -128,6 +160,14 @@ export interface IFulfillmentModuleService extends IModuleService { deleteShippingOptions(ids: string[], sharedContext?: Context): Promise deleteShippingOptions(id: string, sharedContext?: Context): Promise + /** + * Delete a geo zone + * @param ids + * @param sharedContext + */ + deleteGeoZones(ids: string[], sharedContext?: Context): Promise + deleteGeoZones(id: string, sharedContext?: Context): Promise + /** * Retrieve a fulfillment set * @param id @@ -164,6 +204,18 @@ export interface IFulfillmentModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * Retrieve a geo zone + * @param id + * @param config + * @param sharedContext + */ + retrieveGeoZone( + id: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + /** * List fulfillment sets * @param filters @@ -200,6 +252,18 @@ export interface IFulfillmentModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * List geo zones + * @param filters + * @param config + * @param sharedContext + */ + listGeoZones( + filters?: FilterableGeoZoneProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + /** * List and count fulfillment sets * @param filters @@ -236,6 +300,18 @@ export interface IFulfillmentModuleService extends IModuleService { sharedContext?: Context ): Promise<[ShippingOptionDTO[], number]> + /** + * List and count geo zones + * @param filters + * @param config + * @param sharedContext + */ + listAndCountGeoZones( + filters?: FilterableGeoZoneProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[GeoZoneDTO[], number]> + /** * Soft delete fulfillment sets * @param fulfillmentIds @@ -272,6 +348,18 @@ export interface IFulfillmentModuleService extends IModuleService { sharedContext?: Context ): Promise | void> + /** + * Soft delete geo zones + * @param geoZoneIds + * @param config + * @param sharedContext + */ + softDeleteGeoZones( + geoZoneIds: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + restore( fulfillmentIds: string[], config?: RestoreReturn, diff --git a/packages/utils/src/common/get-set-difference.ts b/packages/utils/src/common/get-set-difference.ts new file mode 100644 index 0000000000..ed3826bd4e --- /dev/null +++ b/packages/utils/src/common/get-set-difference.ts @@ -0,0 +1,19 @@ +/** + * Get the difference between two sets. The difference is the elements that are in the original set but not in the compare set. + * @param orignalSet + * @param compareSet + */ +export function getSetDifference( + orignalSet: Set, + compareSet: Set +): Set { + const difference = new Set() + + orignalSet.forEach((element) => { + if (!compareSet.has(element)) { + difference.add(element) + } + }) + + return difference +} diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index e654f2eb46..230ce46dcc 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -1,5 +1,6 @@ export * from "./alter-columns-helper" export * from "./array-difference" +export * from "./get-set-difference" export * from "./build-query" export * from "./camel-to-snake-case" export * from "./container" @@ -46,4 +47,3 @@ export * from "./to-pascal-case" export * from "./transaction" export * from "./upper-case-first" export * from "./wrap-handler" -