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