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:
Adrien de Peretti
2024-03-05 12:11:14 +01:00
committed by GitHub
parent f9ef37a2f2
commit 62a7bcc30c
55 changed files with 4066 additions and 2749 deletions

View File

@@ -0,0 +1,8 @@
---
"@medusajs/types": patch
"@medusajs/utils": patch
"medusa-test-utils": patch
---
Feat(fulfillment): service provider registration + fulfillment management

View File

@@ -31,6 +31,7 @@ packages/*
!packages/workflow-engine-redis
!packages/workflow-engine-inmemory
!packages/fulfillment
!packages/fulfillment-manual
**/models/*

View File

@@ -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: {

View File

@@ -0,0 +1,4 @@
dist
node_modules
.DS_store
yarn.lock

View File

View File

View 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`],
}

View 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"
]
}

View 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

View File

@@ -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 {}
}
}

View 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"
]
}

View File

@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -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 ?? {},
}
}

View File

@@ -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"

View File

@@ -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]

View File

@@ -0,0 +1 @@
export * from "./default-provider"

View File

@@ -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",
},
],
}
}

View File

@@ -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,
}),
])
)
})
})
})
})
},
})

View File

@@ -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`
)
})
})
})
})
},
})

View File

@@ -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,
})
)
}
})
})
})
})
},
})

View File

@@ -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")
})
})
})
})
},
})

View File

@@ -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`
)
})
})
})
})
},
})

View File

@@ -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")
})
})
})
})
},
})

View File

@@ -10,7 +10,7 @@ module.exports = {
"^.+\\.[jt]s?$": [
"ts-jest",
{
tsConfig: "tsconfig.spec.json",
tsconfig: "tsconfig.spec.json",
isolatedModules: true,
},
],

View File

@@ -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",

View 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}`)
}
}

View File

@@ -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"
}
}

View File

@@ -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;');
}
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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"

View File

@@ -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,

View File

@@ -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,

View File

@@ -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[]

View 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)
}
}

View File

@@ -1 +1,2 @@
export { default as FulfillmentModuleService } from "./fulfillment-module-service"
export { default as FulfillmentProviderService } from "./fulfillment-provider"

View File

@@ -4,3 +4,6 @@ export type InitializeModuleInjectableDependencies = {
logger?: Logger
eventBusService?: IEventBusModuleService
}
export const FulfillmentIdentifiersRegistrationName =
"fulfillment_providers_identifier"

View File

@@ -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]

View File

@@ -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[]>
}

View File

@@ -1,3 +1,4 @@
export * from "./common"
export * from "./mutations"
export * from "./service"
export * from "./provider"

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1,6 @@
export interface CreateFulfillmentLabelDTO {
tracking_number: string
tracking_url: string
label_url: string
fulfillment_id: string
}

View 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
}

View File

@@ -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"

View 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>
}

View File

@@ -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>
}

View File

@@ -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]
}

View File

@@ -1,2 +1,3 @@
export * from "./geo-zone"
export * from "./shipping-options"
export * from "./provider"

View 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")
}
}

View File

@@ -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