chore(): Reorganize modules (#7210)

**What**
Move all modules to the modules directory
This commit is contained in:
Adrien de Peretti
2024-05-02 15:33:34 +00:00
committed by GitHub
parent 7a351eef09
commit 4eae25e1ef
870 changed files with 91 additions and 62 deletions
+6
View File
@@ -0,0 +1,6 @@
/dist
node_modules
.DS_store
.env*
.env
*.sql
+23
View File
@@ -0,0 +1,23 @@
# @medusajs/fulfillment
## 0.1.2
### Patch Changes
- [#7175](https://github.com/medusajs/medusa/pull/7175) [`e26cda4b6a`](https://github.com/medusajs/medusa/commit/e26cda4b6afb7fb25f0b0a7a7ce20b7f914d35db) Thanks [@adrien2p](https://github.com/adrien2p)! - feat(medusa, core-flows, types): Allow to update the rules from a shipping options
- Updated dependencies [[`0c0b425de7`](https://github.com/medusajs/medusa/commit/0c0b425de7b154b80b712ab17b16215cf62d1e83), [`8d356217bd`](https://github.com/medusajs/medusa/commit/8d356217bd31c97a196e861ee243822a4d924df7), [`1eeb1e9de3`](https://github.com/medusajs/medusa/commit/1eeb1e9de3e0b571735437b00968ee96e4aabad5), [`20e8df914e`](https://github.com/medusajs/medusa/commit/20e8df914ec5fdf8d562d4fa84f72c58c7056195), [`27f4f0d724`](https://github.com/medusajs/medusa/commit/27f4f0d7243367c2dfc6012bf1f6b7400a77ec7b), [`e0b02a1012`](https://github.com/medusajs/medusa/commit/e0b02a1012981c29830d7779f59ebe805bbfd137), [`e944a627f0`](https://github.com/medusajs/medusa/commit/e944a627f074fb39a56f4bc7b3d6d315736ebf7c), [`1a48fe0282`](https://github.com/medusajs/medusa/commit/1a48fe0282a8bc1f8548a4736255e457d173da09), [`86f499de2f`](https://github.com/medusajs/medusa/commit/86f499de2f31356ab36ad5e93f27345443b3e5f6), [`09a2220569`](https://github.com/medusajs/medusa/commit/09a22205693da62fbf8fd450535d5024cb9c01d1), [`78f603e4f1`](https://github.com/medusajs/medusa/commit/78f603e4f18c9d16f4b58a2189c959026453d8b2), [`cc557c8752`](https://github.com/medusajs/medusa/commit/cc557c8752fd0554f5a1b58522d9a88dc43a8509), [`dd35a4dbff`](https://github.com/medusajs/medusa/commit/dd35a4dbff10c86ea3c5f7f817c18b6e60d599e3), [`58c68f6715`](https://github.com/medusajs/medusa/commit/58c68f67156e993255fbc25d91db15ae23bc95c0), [`1bcb13f892`](https://github.com/medusajs/medusa/commit/1bcb13f892bc61db21b3fc6bdbce85f747aeec4c), [`82a176e30e`](https://github.com/medusajs/medusa/commit/82a176e30e47a7d11caaf31c3023bd8db588b465), [`11517f0faf`](https://github.com/medusajs/medusa/commit/11517f0fafdf00af256240448b58d149d8b6f600), [`62b9dcc6c1`](https://github.com/medusajs/medusa/commit/62b9dcc6c1ce46aadb7944215006c12da3c9f619), [`5d9aea053c`](https://github.com/medusajs/medusa/commit/5d9aea053ce6e04f242f86fb9053c13dec515d5b), [`e26cda4b6a`](https://github.com/medusajs/medusa/commit/e26cda4b6afb7fb25f0b0a7a7ce20b7f914d35db), [`bc06ad2db4`](https://github.com/medusajs/medusa/commit/bc06ad2db48c999023ab823fefc1375196976e9b), [`18f3aacee6`](https://github.com/medusajs/medusa/commit/18f3aacee6752854d377faa806f4cc67bc71456b), [`232322d035`](https://github.com/medusajs/medusa/commit/232322d03515f81e56867ff8c765b8409399ee68), [`38c971f111`](https://github.com/medusajs/medusa/commit/38c971f111af69f176e7e9892eb59f5bae831fa7), [`45c49e89f2`](https://github.com/medusajs/medusa/commit/45c49e89f28123ef622fc1c07253bae94fd74875), [`528ef4ca90`](https://github.com/medusajs/medusa/commit/528ef4ca90bb2cf6173dccc9fd6a9f9932ff9b76), [`65794f4bb5`](https://github.com/medusajs/medusa/commit/65794f4bb56e4fd3f0ccb7656a948f856f05324e), [`93ef94cad3`](https://github.com/medusajs/medusa/commit/93ef94cad3ddc5b6973b4e48e422b0aa0e6ddbbe), [`4cf71af07d`](https://github.com/medusajs/medusa/commit/4cf71af07d1807c83df3889c1774f82cbd1b9a6f), [`4b57c5d286`](https://github.com/medusajs/medusa/commit/4b57c5d286f9dc6e2098c67e9fecb0d93175b5a1), [`c78915c7c5`](https://github.com/medusajs/medusa/commit/c78915c7c5e91a99c1b1bae932656c8d86b17daf), [`18f3aacee6`](https://github.com/medusajs/medusa/commit/18f3aacee6752854d377faa806f4cc67bc71456b), [`667c8609cc`](https://github.com/medusajs/medusa/commit/667c8609ccf3850f5df8cf784723a95bd0d6d2a6), [`f175cac4af`](https://github.com/medusajs/medusa/commit/f175cac4af63b71066a8398ecf9beaa6f28b20cc), [`0a9b9b073d`](https://github.com/medusajs/medusa/commit/0a9b9b073dd2d3f4aa5e5cb1c16e2221a7200e0d), [`a6562d2a41`](https://github.com/medusajs/medusa/commit/a6562d2a41453cbe7aa43be352c4924e3e4c79d5), [`00e6b21bb5`](https://github.com/medusajs/medusa/commit/00e6b21bb50dbc886bc37ad052a1c40ce865294e), [`8fd1488938`](https://github.com/medusajs/medusa/commit/8fd148893850eb66c5eae00c4ca9391a80ea2eb9), [`1c6ba4468e`](https://github.com/medusajs/medusa/commit/1c6ba4468eab1440931c88929affd5b4c593f377)]:
- @medusajs/types@1.11.16
- @medusajs/modules-sdk@1.12.11
- @medusajs/utils@1.11.9
## 0.1.1
### Patch Changes
- [#6700](https://github.com/medusajs/medusa/pull/6700) [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Version all modules to allow for initial testing
- Updated dependencies [[`1fd0457c15`](https://github.com/medusajs/medusa/commit/1fd0457c153b2ef7657c052878d8e5364e1b324a), [`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`d4b921f3db`](https://github.com/medusajs/medusa/commit/d4b921f3dbe0a38f1565a8de759996c70798d58e), [`ac86362e81`](https://github.com/medusajs/medusa/commit/ac86362e81d8523cb8e3dfad026fc94658513018), [`e4acde1aa2`](https://github.com/medusajs/medusa/commit/e4acde1aa2eb57f07e6692fe8b61f728948b9a96), [`1a661adf3e`](https://github.com/medusajs/medusa/commit/1a661adf3ef4991aa6e237dd894b6a5c47cd4aca), [`56cbf88115`](https://github.com/medusajs/medusa/commit/56cbf88115994adea7037c3f2814f0c96af3cfc0), [`36a61658f9`](https://github.com/medusajs/medusa/commit/36a61658f969a7b19c84a1e621ad1464927cafb1), [`04a532e5ef`](https://github.com/medusajs/medusa/commit/04a532e5efabbf75b1e4155520b1da175b686ffc), [`c319edb8e0`](https://github.com/medusajs/medusa/commit/c319edb8e0ecd13d086652147667916e5abab2d8), [`0b9fcb6324`](https://github.com/medusajs/medusa/commit/0b9fcb6324eee9f2556c7e6317775fae93b12a47), [`586df9da25`](https://github.com/medusajs/medusa/commit/586df9da250e492442769f5bac2f8b3de1d46f05), [`b3d826497b`](https://github.com/medusajs/medusa/commit/b3d826497b3dae5e1b26b7924706c24fd5e87ca5), [`a86c87fe14`](https://github.com/medusajs/medusa/commit/a86c87fe1442afce9285e39255914e01012b4449), [`640eccd5dd`](https://github.com/medusajs/medusa/commit/640eccd5ddbb163e0f987ce6c772f1129c2e2632), [`8ea37d03c9`](https://github.com/medusajs/medusa/commit/8ea37d03c914a5004a3e42770668b2d1f7f8f564), [`339a946f38`](https://github.com/medusajs/medusa/commit/339a946f389033c21e05338f9dbf07d88e140533), [`ac829fc67f`](https://github.com/medusajs/medusa/commit/ac829fc67f7495b08f28e55923c59f0fd6320311), [`d9d5afc3cf`](https://github.com/medusajs/medusa/commit/d9d5afc3cfc29221d0e65bff7b78474a8fb8f31f), [`c3c4f49fc2`](https://github.com/medusajs/medusa/commit/c3c4f49fc2126f950e69e291ca939ca88a15afd3), [`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`0d46abf0ff`](https://github.com/medusajs/medusa/commit/0d46abf0ffa4c5e03bf7d2a9cdf1db828a76bea8), [`fafde4f54d`](https://github.com/medusajs/medusa/commit/fafde4f54d3ef75a7d382e6cbf94e38b3deae99b), [`8dad2b51a2`](https://github.com/medusajs/medusa/commit/8dad2b51a26c4c3c14a6c95f70424c8bef2ad63e), [`0c705d7bd4`](https://github.com/medusajs/medusa/commit/0c705d7bd41a768c48017ae95b3c8414d96c6acb), [`a6d7070dd6`](https://github.com/medusajs/medusa/commit/a6d7070dd669c21ea19d70434d42c2f8167dc309), [`1d91b7429b`](https://github.com/medusajs/medusa/commit/1d91b7429beebd6f09d5027f7f7e1fe74ce3a8ff), [`168f02f138`](https://github.com/medusajs/medusa/commit/168f02f138ad101e1013f2c8c3f8dc19de12accf), [`1ed5f918c3`](https://github.com/medusajs/medusa/commit/1ed5f918c31794a70aca4a4e4cd83cf456593baa), [`c20eb15cd9`](https://github.com/medusajs/medusa/commit/c20eb15cd9b1bd90c8d01f68eca6f0f181cd902d), [`e5945479e0`](https://github.com/medusajs/medusa/commit/e5945479e091d9560ae3e7240306a31031ef4584), [`f5c2256286`](https://github.com/medusajs/medusa/commit/f5c22562867f412040f8bc6c55ab5de3a3735e62), [`000eb61e33`](https://github.com/medusajs/medusa/commit/000eb61e33e0302db95ee6ad1656ea9b430ed471), [`d550be3685`](https://github.com/medusajs/medusa/commit/d550be3685423218d47a20c57a5e06758f4a961a), [`62a7bcc30c`](https://github.com/medusajs/medusa/commit/62a7bcc30cbc7b234b2b51d7858439951a84edeb), [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8), [`6500f18b9b`](https://github.com/medusajs/medusa/commit/6500f18b9b80c5c9c473489e7e740d55dca74303), [`ce39b9b66e`](https://github.com/medusajs/medusa/commit/ce39b9b66e8c277ec0691ea6d0a950003be09cc1), [`a6a4b3f01a`](https://github.com/medusajs/medusa/commit/a6a4b3f01a6d2bd97b1580c59134279a1b033a5d), [`4d51f095b3`](https://github.com/medusajs/medusa/commit/4d51f095b3f98f468cefb760512563f7b77bb9cf), [`4625bd1241`](https://github.com/medusajs/medusa/commit/4625bd12416275b09c22cde4a09cb0f68df5d7c1), [`56b0b45304`](https://github.com/medusajs/medusa/commit/56b0b4530401a6ec5aa155874d371e45bb388fe2), [`cc1b66842c`](https://github.com/medusajs/medusa/commit/cc1b66842cbb37c6eab84e2d8b74844c214f38d7), [`24fb102a56`](https://github.com/medusajs/medusa/commit/24fb102a564b1253d1f8b039bb1e435cc5312fbb), [`e85463b2a7`](https://github.com/medusajs/medusa/commit/e85463b2a717751de2e21c39a4c745449b31affe)]:
- @medusajs/types@1.11.14
- @medusajs/utils@1.11.7
- @medusajs/modules-sdk@1.12.9
+1
View File
@@ -0,0 +1 @@
# Fulfillment Module
@@ -0,0 +1,24 @@
import { EventBusTypes } from "@medusajs/types"
export function buildExpectedEventMessageShape(options: {
eventName: string
action: string
object: string
eventGroupId?: string
data: any
options?: Record<string, unknown>
}): EventBusTypes.Message {
return {
eventName: options.eventName,
body: {
metadata: {
action: options.action,
eventGroupId: options.eventGroupId,
service: "fulfillment",
object: options.object,
},
data: options.data,
},
options: options.options,
}
}
@@ -0,0 +1,48 @@
import { CreateFulfillmentDTO } from "@medusajs/types"
export function generateCreateFulfillmentData(
data: Partial<CreateFulfillmentDTO> & {
provider_id: string
shipping_option_id: string
}
) {
const randomString = Math.random().toString(36).substring(7)
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_" + randomString,
address_2: "test-address_" + randomString,
city: "test-city_" + randomString,
postal_code: "test-postal-code_" + randomString,
country_code: "test-country-code_" + randomString,
province: "test-province_" + randomString,
phone: "test-phone_" + randomString,
full_name: "test-full-name_" + randomString,
},
items: data.items ?? [
{
title: "test-title_" + randomString,
sku: "test-sku_" + randomString,
quantity: 1,
barcode: "test-barcode_" + randomString,
},
],
labels: data.labels ?? [
{
tracking_number: "test-tracking-number_" + randomString,
tracking_url: "test-tracking-url_" + randomString,
label_url: "test-label-url_" + randomString,
},
],
order: data.order ?? {},
}
}
@@ -0,0 +1,53 @@
import { generateCreateShippingOptionsData } from "./shipping-options"
import { generateCreateFulfillmentData } from "./fulfillment"
import { IFulfillmentModuleService } from "@medusajs/types"
export * from "./shipping-options"
export * from "./fulfillment"
export * from "./events"
export async function createFullDataStructure(
service: IFulfillmentModuleService,
{
providerId,
}: {
providerId: string
}
) {
const randomString = Math.random().toString(36).substring(7)
const shippingProfile = await service.createShippingProfiles({
// generate random string
name: "test_" + randomString,
type: "default",
})
const fulfillmentSet = await service.create({
name: "test_" + randomString,
type: "test-type",
})
const serviceZone = await service.createServiceZones({
name: "test_" + randomString,
fulfillment_set_id: fulfillmentSet.id,
geo_zones: [
{
type: "country",
country_code: "US_" + randomString,
},
],
})
const shippingOption = await service.createShippingOptions(
generateCreateShippingOptionsData({
provider_id: providerId,
service_zone_id: serviceZone.id,
shipping_profile_id: shippingProfile.id,
})
)
await service.createFulfillment(
generateCreateFulfillmentData({
provider_id: providerId,
shipping_option_id: shippingOption.id,
})
)
}
@@ -0,0 +1,19 @@
import { AbstractFulfillmentProviderService } from "@medusajs/utils/src"
export class FulfillmentProviderServiceFixtures extends AbstractFulfillmentProviderService {
static identifier = "fixtures-fulfillment-provider"
async createFulfillment(data, items, order, fulfillment): Promise<any> {
return {}
}
async cancelFulfillment(fulfillment): Promise<any> {
return {}
}
async getFulfillmentOptions(): Promise<any> {
return {}
}
}
export const services = [FulfillmentProviderServiceFixtures]
@@ -0,0 +1 @@
export * from "./default-provider"
@@ -0,0 +1,41 @@
import { CreateShippingOptionDTO } from "@medusajs/types"
export function generateCreateShippingOptionsData({
name,
service_zone_id,
shipping_profile_id,
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> {
const randomString = Math.random().toString(36).substring(7)
return {
service_zone_id: service_zone_id,
shipping_profile_id: shipping_profile_id,
provider_id: provider_id,
type: type ?? {
code: "test-type_" + randomString,
description: "test-description_" + randomString,
label: "test-label_" + randomString,
},
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",
},
],
}
}
@@ -0,0 +1,218 @@
import { resolve } from "path"
import { Modules } from "@medusajs/modules-sdk"
import { IFulfillmentModuleService } from "@medusajs/types"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
import {
generateCreateFulfillmentData,
generateCreateShippingOptionsData,
} from "../../__fixtures__"
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: ({ service }: SuiteOptions<IFulfillmentModuleService>) => {
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({
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({
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({
provider_id: providerId,
service_zone_id: serviceZone.id,
shipping_profile_id: shippingProfile.id,
})
)
fulfillment = await service.createFulfillment(
generateCreateFulfillmentData({
provider_id: providerId,
shipping_option_id: shippingOption.id,
})
)
})
it("should cancel a fulfillment successfully", async () => {
const result = await service.cancelFulfillment(fulfillment.id)
// should be idempotent
const idempotentResult = await service.cancelFulfillment(
fulfillment.id
)
expect(result.canceled_at).not.toBeNull()
expect(idempotentResult.canceled_at).not.toBeNull()
expect(idempotentResult.canceled_at).toEqual(result.canceled_at)
})
it("should fail to cancel a fulfillment that is already shipped", async () => {
await service.updateFulfillment(fulfillment.id, {
shipped_at: new Date(),
})
const err = await service
.cancelFulfillment(fulfillment.id)
.catch((e) => e)
expect(err.message).toEqual(
`Fulfillment with id ${fulfillment.id} already shipped`
)
})
it("should fail to cancel a fulfillment that is already delivered", async () => {
await service.updateFulfillment(fulfillment.id, {
delivered_at: new Date(),
})
const err = await service
.cancelFulfillment(fulfillment.id)
.catch((e) => e)
expect(err.message).toEqual(
`Fulfillment with id ${fulfillment.id} already delivered`
)
})
})
})
})
},
})
@@ -0,0 +1,282 @@
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
}
})
it("should fail to create new geo zones that are not valid", async function () {
const fulfillmentSet = await service.create({
name: "test",
type: "test-type",
})
const serviceZone = await service.createServiceZones({
name: "test",
fulfillment_set_id: fulfillmentSet.id,
})
let data: CreateGeoZoneDTO = {
service_zone_id: serviceZone.id,
type: GeoZoneType.PROVINCE,
country_code: "fr",
} as any
let err = await service.createGeoZones(data).catch((e) => e)
expect(err.message).toBe(
"Missing required property province_code for geo zone type province"
)
data = {
service_zone_id: serviceZone.id,
type: GeoZoneType.CITY,
country_code: "fr",
province_code: "test",
} as any
err = await service.createGeoZones(data).catch((e) => e)
expect(err.message).toBe(
"Missing required property city for geo zone type city"
)
data = {
service_zone_id: serviceZone.id,
type: GeoZoneType.ZIP,
postal_expression: "test",
} as any
err = await service.createGeoZones(data).catch((e) => e)
expect(err.message).toBe(
"Missing required property country_code for geo zone type zip"
)
data = {
service_zone_id: serviceZone.id,
type: "unknown",
postal_expression: "test",
} as any
err = await service.createGeoZones(data).catch((e) => e)
expect(err.message).toBe(`Invalid geo zone type: unknown`)
})
})
describe("on update", () => {
it("should update an existing geo zone", async function () {
const fulfillmentSet = await service.create({
name: "test",
type: "test-type",
})
const serviceZone = await service.createServiceZones({
name: "test",
fulfillment_set_id: fulfillmentSet.id,
})
const createData: CreateGeoZoneDTO = {
service_zone_id: serviceZone.id,
type: GeoZoneType.COUNTRY,
country_code: "fr",
}
const createdGeoZone = await service.createGeoZones(createData)
const updateData: UpdateGeoZoneDTO = {
id: createdGeoZone.id,
type: GeoZoneType.COUNTRY,
country_code: "us",
}
const updatedGeoZone = await service.updateGeoZones(updateData)
expect(updatedGeoZone).toEqual(
expect.objectContaining({
id: updateData.id,
type: updateData.type,
country_code: updateData.country_code,
})
)
})
it("should update a collection of geo zones", async function () {
const fulfillmentSet = await service.create({
name: "test",
type: "test-type",
})
const serviceZone = await service.createServiceZones({
name: "test",
fulfillment_set_id: fulfillmentSet.id,
})
const createData: CreateGeoZoneDTO[] = [
{
service_zone_id: serviceZone.id,
type: GeoZoneType.COUNTRY,
country_code: "fr",
},
{
service_zone_id: serviceZone.id,
type: GeoZoneType.COUNTRY,
country_code: "us",
},
]
const createdGeoZones = await service.createGeoZones(createData)
const updateData: UpdateGeoZoneDTO[] = createdGeoZones.map(
(geoZone, index) => ({
id: geoZone.id,
type: GeoZoneType.COUNTRY,
country_code: index % 2 === 0 ? "us" : "fr",
})
)
const updatedGeoZones = await service.updateGeoZones(updateData)
expect(updatedGeoZones).toHaveLength(2)
for (const data_ of updateData) {
const expectedGeoZone = updatedGeoZones.find(
(geoZone) => geoZone.id === data_.id
)
expect(expectedGeoZone).toEqual(
expect.objectContaining({
id: data_.id,
type: data_.type,
country_code: data_.country_code,
})
)
}
})
})
})
})
},
})
@@ -0,0 +1,264 @@
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
import { FulfillmentSetDTO, IFulfillmentModuleService } from "@medusajs/types"
import {
initModules,
moduleIntegrationTestRunner,
SuiteOptions,
} from "medusa-test-utils/dist"
import { resolve } from "path"
import { createFullDataStructure } from "../../__fixtures__"
import { FulfillmentProviderService } from "@services"
import { FulfillmentProviderServiceFixtures } from "../../__fixtures__/providers"
let moduleOptions = {
providers: [
{
resolve: resolve(
process.cwd() +
"/integration-tests/__fixtures__/providers/default-provider"
),
options: {
config: {
"test-provider": {},
},
},
},
],
}
let providerId = "fixtures-fulfillment-provider_test-provider"
async function list(
service: IFulfillmentModuleService,
...args: Parameters<IFulfillmentModuleService["list"]>
) {
const [filters = {}, config = {}] = args
const finalConfig = {
relations: [
"service_zones.geo_zones",
"service_zones.shipping_options.shipping_profile",
"service_zones.shipping_options.provider",
"service_zones.shipping_options.type",
"service_zones.shipping_options.rules",
"service_zones.shipping_options.fulfillments.labels",
"service_zones.shipping_options.fulfillments.items",
"service_zones.shipping_options.fulfillments.delivery_address",
],
...config,
}
return await service.list(filters, finalConfig)
}
function expectSoftDeleted(
fulfillmentSets: FulfillmentSetDTO[],
{ softDeleted = false } = {}
) {
expect(fulfillmentSets).toHaveLength(1)
let fulfillmentSet = fulfillmentSets[0]
expect(!!fulfillmentSet.deleted_at).toEqual(softDeleted)
expect(fulfillmentSet.service_zones).toHaveLength(1)
let serviceZone = fulfillmentSet.service_zones[0]
expect(!!serviceZone.deleted_at).toEqual(softDeleted)
expect(serviceZone.geo_zones).toHaveLength(1)
expect(serviceZone.shipping_options).toHaveLength(1)
let geoZone = serviceZone.geo_zones[0]
expect(!!geoZone.deleted_at).toEqual(softDeleted)
let shippingOption = serviceZone.shipping_options[0]
expect(!!shippingOption.deleted_at).toEqual(softDeleted)
expect(!!shippingOption.shipping_profile.deleted_at).toEqual(false)
expect(!!shippingOption.type.deleted_at).toEqual(softDeleted)
expect(shippingOption.fulfillments).toHaveLength(1)
expect(shippingOption.rules).toHaveLength(1)
let rule = shippingOption.rules[0]
expect(!!rule.deleted_at).toEqual(softDeleted)
/**
* We do not expect the fulfillment to be soft deleted when soft deleting parents entities
*/
let fulfillment = shippingOption.fulfillments[0]
expect(!!fulfillment.deleted_at).toEqual(false)
expect(fulfillment.labels).toHaveLength(1)
expect(fulfillment.items).toHaveLength(1)
let label = fulfillment.labels[0]
expect(!!label.deleted_at).toEqual(false)
let item = fulfillment.items[0]
expect(!!item.deleted_at).toEqual(false)
let deliveryAddress = fulfillment.delivery_address
expect(!!deliveryAddress.deleted_at).toEqual(false)
}
moduleIntegrationTestRunner({
moduleName: Modules.FULFILLMENT,
moduleOptions,
testSuite: ({
MikroOrmWrapper,
service,
}: SuiteOptions<IFulfillmentModuleService>) =>
describe("Fulfillment Module Service", () => {
it("should load and save all the providers on bootstrap with the correct is_enabled value", async () => {
const databaseConfig = {
schema: "public",
clientUrl: MikroOrmWrapper.clientUrl,
}
const providersConfig = {}
for (let i = 0; i < 10; i++) {
providersConfig[`provider-${i}`] = {}
}
let 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,
},
},
],
},
},
},
}
let { shutdown } = await initModules(moduleOptions)
let fulfillmentProviders = await MikroOrmWrapper.forkManager().execute(
`SELECT * FROM fulfillment_provider`
)
expect(fulfillmentProviders).toHaveLength(
Object.keys(providersConfig).length + 1 // +1 for the default provider
)
for (const [name] of Object.entries(providersConfig)) {
const provider = fulfillmentProviders.find((p) => {
return (
p.id ===
FulfillmentProviderService.getRegistrationIdentifier(
FulfillmentProviderServiceFixtures,
name
)
)
})
expect(provider).toBeDefined()
expect(provider.is_enabled).toBeTruthy()
}
await shutdown()
const providersConfig2 = {}
for (let i = 10; i < 20; i++) {
providersConfig2[`provider-${i}`] = {}
}
moduleOptions = {
databaseConfig,
modulesConfig: {
[Modules.FULFILLMENT]: {
definition: ModulesDefinition[Modules.FULFILLMENT],
options: {
databaseConfig,
providers: [
{
resolve: resolve(
process.cwd() +
"/integration-tests/__fixtures__/providers/default-provider"
),
options: {
config: providersConfig2,
},
},
],
},
},
},
}
const medusaApp = await initModules(moduleOptions)
shutdown = medusaApp.shutdown
fulfillmentProviders = await MikroOrmWrapper.forkManager().execute(
`SELECT * FROM fulfillment_provider`
)
expect(fulfillmentProviders).toHaveLength(
Object.keys(providersConfig2).length +
Object.keys(providersConfig).length +
1 // +1 for the default provider
)
const allProviders = Object.assign(
{},
providersConfig,
providersConfig2
)
for (const [name] of Object.entries(allProviders)) {
const provider = fulfillmentProviders.find((p) => {
return (
p.id ===
FulfillmentProviderService.getRegistrationIdentifier(
FulfillmentProviderServiceFixtures,
name
)
)
})
expect(provider).toBeDefined()
const isEnabled = !!providersConfig2[name]
expect(provider.is_enabled).toEqual(isEnabled)
}
await shutdown().catch(() => void 0)
})
it("should soft delete and restore the data respecting the configured cascade", async () => {
await createFullDataStructure(service, { providerId })
let fulfillmentSets = await list(service)
expectSoftDeleted(fulfillmentSets)
/**
* Soft delete the fulfillment set
*/
await service.softDelete(fulfillmentSets[0].id)
const deletedFulfillmentSets = await list(
service,
{},
{
withDeleted: true,
}
)
expectSoftDeleted(deletedFulfillmentSets, { softDeleted: true })
/**
* Restore the fulfillment set
*/
await service.restore(fulfillmentSets[0].id)
const restoredFulfillmentSets = await list(service)
expectSoftDeleted(restoredFulfillmentSets)
})
}),
})
@@ -0,0 +1,445 @@
import { Modules } from "@medusajs/modules-sdk"
import {
CreateServiceZoneDTO,
GeoZoneDTO,
IFulfillmentModuleService,
UpdateServiceZoneDTO,
} from "@medusajs/types"
import { GeoZoneType } from "@medusajs/utils"
import { SuiteOptions, moduleIntegrationTestRunner } 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.message).toContain("exists")
})
it("should fail on creating a service zone and new geo zones that are not valid", async function () {
const fulfillmentSet = await service.create({
name: "test",
type: "test-type",
})
let data: CreateServiceZoneDTO = {
name: "test",
fulfillment_set_id: fulfillmentSet.id,
geo_zones: [
{
type: GeoZoneType.PROVINCE,
country_code: "fr",
} as any,
],
}
let err = await service.createServiceZones(data).catch((e) => e)
expect(err.message).toBe(
"Missing required property province_code for geo zone type province"
)
data = {
name: "test",
fulfillment_set_id: fulfillmentSet.id,
geo_zones: [
{
type: GeoZoneType.CITY,
country_code: "fr",
province_code: "test",
} as any,
],
}
err = await service.createServiceZones(data).catch((e) => e)
expect(err.message).toBe(
"Missing required property city for geo zone type city"
)
data = {
name: "test",
fulfillment_set_id: fulfillmentSet.id,
geo_zones: [
{
type: GeoZoneType.ZIP,
postal_expression: "test",
} as any,
],
}
err = await service.createServiceZones(data).catch((e) => e)
expect(err.message).toBe(
"Missing required property country_code for geo zone type zip"
)
data = {
name: "test",
fulfillment_set_id: fulfillmentSet.id,
geo_zones: [
{
type: "unknown",
postal_expression: "test",
} as any,
],
}
err = await service.createServiceZones(data).catch((e) => e)
expect(err.message).toBe(`Invalid geo zone type: unknown`)
})
})
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 = {
name: "updated-service-zone-test",
geo_zones: [
{
id: createdServiceZone.geo_zones[0].id,
type: GeoZoneType.COUNTRY,
country_code: "us",
},
],
}
const updatedServiceZone = await service.updateServiceZones(
createdServiceZone.id,
updateData
)
expect(updatedServiceZone).toEqual(
expect.objectContaining({
id: createdServiceZone.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 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 = {
name: "service-zone-test",
geo_zones: [
{
type: GeoZoneType.COUNTRY,
country_code: "us",
},
],
}
const err = await service
.updateServiceZones(createdServiceZones[1].id, updateData)
.catch((e) => e)
expect(err).toBeDefined()
expect(err.message).toContain("exists")
})
})
describe("on upsert", () => {
it("should upsert 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.upsertServiceZones(
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,
}),
]),
})
)
}
})
})
})
})
},
})
@@ -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.message).toContain("exists")
})
})
})
})
},
})
@@ -0,0 +1,22 @@
module.exports = {
moduleNameMapper: {
"^@models": "<rootDir>/src/models",
"^@services": "<rootDir>/src/services",
"^@repositories": "<rootDir>/src/repositories",
"^@types": "<rootDir>/src/types",
"^@utils": "<rootDir>/src/utils",
},
transform: {
"^.+\\.[jt]s?$": [
"ts-jest",
{
tsconfig: "tsconfig.spec.json",
isolatedModules: true,
},
],
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `ts`],
modulePathIgnorePatterns: ["dist/"],
/*setupFilesAfterEnv: ["<rootDir>/integration-tests/setup.js"],*/
}
@@ -0,0 +1,12 @@
import * as entities from "./src/models"
import { TSMigrationGenerator } from "@medusajs/utils"
module.exports = {
entities: Object.values(entities),
schema: "public",
clientUrl: "postgres://postgres@localhost/medusa-fulfillment",
type: "postgresql",
migrations: {
generator: TSMigrationGenerator,
},
}
+62
View File
@@ -0,0 +1,62 @@
{
"name": "@medusajs/fulfillment",
"version": "0.1.2",
"description": "Medusa Fulfillment module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">=16"
},
"bin": {
"medusa-fulfillment-seed": "dist/scripts/bin/run-seed.js"
},
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/fulfillment"
},
"publishConfig": {
"access": "public"
},
"author": "Medusa",
"license": "MIT",
"scripts": {
"watch": "tsc --build --watch",
"watch:test": "tsc --build tsconfig.spec.json --watch",
"prepublishOnly": "cross-env NODE_ENV=production tsc --build && tsc-alias -p tsconfig.json",
"build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json",
"test": "jest --runInBand --bail --forceExit --passWithNoTests -- src",
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.spec.ts",
"migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate",
"migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial -n InitialSetupMigration",
"migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create",
"migration:up": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:up",
"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",
"medusa-test-utils": "^1.1.44",
"rimraf": "^3.0.2",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.6",
"typescript": "^5.1.6"
},
"dependencies": {
"@medusajs/modules-sdk": "^1.12.11",
"@medusajs/types": "^1.11.16",
"@medusajs/utils": "^1.11.9",
"@mikro-orm/core": "5.9.7",
"@mikro-orm/migrations": "5.9.7",
"@mikro-orm/postgresql": "5.9.7",
"awilix": "^8.0.0",
"dotenv": "^16.4.5",
"knex": "2.4.2"
}
}
+14
View File
@@ -0,0 +1,14 @@
import { moduleDefinition } from "./module-definition"
import { initializeFactory, Modules } from "@medusajs/modules-sdk"
export * from "./types"
export * from "./models"
export * from "./services"
export const initialize = initializeFactory({
moduleName: Modules.FULFILLMENT,
moduleDefinition,
})
export const runMigrations = moduleDefinition.runMigrations
export const revertMigration = moduleDefinition.revertMigration
export default moduleDefinition
@@ -0,0 +1,94 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { MapToConfig } from "@medusajs/utils"
import {
Fulfillment,
FulfillmentProvider,
FulfillmentSet,
GeoZone,
ServiceZone,
ShippingOption,
ShippingOptionRule,
ShippingProfile,
} from "@models"
export const LinkableKeys: Record<string, string> = {
fulfillment_id: Fulfillment.name,
fulfillment_set_id: FulfillmentSet.name,
shipping_option_id: ShippingOption.name,
shipping_option_rule_id: ShippingOptionRule.name,
}
const entityLinkableKeysMap: MapToConfig = {}
Object.entries(LinkableKeys).forEach(([key, value]) => {
entityLinkableKeysMap[value] ??= []
entityLinkableKeysMap[value].push({
mapTo: key,
valueFrom: key.split("_").pop()!,
})
})
export const entityNameToLinkableKeysMap: MapToConfig = entityLinkableKeysMap
export const joinerConfig: ModuleJoinerConfig = {
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
linkableKeys: LinkableKeys,
alias: [
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: FulfillmentSet.name,
},
},
{
name: ["shipping_option", "shipping_options"],
args: {
entity: ShippingOption.name,
methodSuffix: "ShippingOptions",
},
},
{
name: ["shipping_profile", "shipping_profiles"],
args: {
entity: ShippingProfile.name,
methodSuffix: "ShippingProfiles",
},
},
{
name: ["fulfillment", "fulfillments"],
args: {
entity: Fulfillment.name,
methodSuffix: "Fulfillments",
},
},
{
name: ["fulfillment_provider", "fulfillment_providers"],
args: {
entity: FulfillmentProvider.name,
methodSuffix: "FulfillmentProviders",
},
},
{
name: ["service_zone", "service_zones"],
args: {
entity: ServiceZone.name,
methodSuffix: "ServiceZones",
},
},
{
name: ["geo_zone", "geo_zones"],
args: {
entity: GeoZone.name,
methodSuffix: "GeoZones",
},
},
{
name: ["shipping_option_rule", "shipping_option_rules"],
args: {
entity: ShippingOptionRule.name,
methodSuffix: "ShippingOptionRules",
},
},
],
} as ModuleJoinerConfig
@@ -0,0 +1,113 @@
import { moduleProviderLoader } from "@medusajs/modules-sdk"
import { LoaderOptions, ModuleProvider, ModulesSdkTypes } from "@medusajs/types"
import {
ContainerRegistrationKeys,
lowerCaseFirst,
promiseAll,
} from "@medusajs/utils"
import { FulfillmentProviderService } from "@services"
import { FulfillmentIdentifiersRegistrationName } from "@types"
import { Lifetime, asFunction, asValue } from "awilix"
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) ?? console
try {
const providerIdentifiers: string[] = (
container.resolve(FulfillmentIdentifiersRegistrationName) ?? []
).filter(Boolean)
const providerService: ModulesSdkTypes.InternalModuleService<any> =
container.resolve(providerServiceRegistrationKey)
const providers = await providerService.list({})
const loadedProvidersMap = new Map(providers.map((p) => [p.id, p]))
const providersToCreate = providerIdentifiers.filter(
(id) => !loadedProvidersMap.has(id)
)
const providersToEnabled = providerIdentifiers.filter((id) =>
loadedProvidersMap.has(id)
)
const providersToDisable = providers.filter(
(p) => !providerIdentifiers.includes(p.id)
)
const promises: Promise<any>[] = []
if (providersToCreate.length) {
promises.push(
providerService.create(providersToCreate.map((id) => ({ id })))
)
}
if (providersToEnabled.length) {
promises.push(
providerService.update(
providersToEnabled.map((id) => ({ id, is_enabled: true }))
)
)
}
if (providersToDisable.length) {
promises.push(
providerService.update(
providersToDisable.map((p) => ({ id: p.id, is_enabled: false }))
)
)
}
await promiseAll(promises)
} catch (error) {
logger.error(`Error syncing the fulfillment providers: ${error.message}`)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,603 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20240311145700_InitialSetupMigration extends Migration {
async up(): Promise<void> {
const shippingOptionTable = await this.execute(
`SELECT * FROM information_schema.tables where table_name = 'shipping_option' and table_schema = 'public';`
)
if (!shippingOptionTable.length) {
await migrateUpModuleMigration.call(this)
} else {
await migrateUpBackwardCompatibility.call(this)
}
}
}
/**
* This migration is for the initial setup of the fulfillment module in the case of
* an already existing database. It will check if the `shipping_option` table exists and in that case
* assume that all other associated tables exists and will migrate them all.
*
* This migration does not take into account the data migration part which
* should be handled separately. Also, after the data migration
* the tables should be cleaned up from the old columns.
**/
async function migrateUpBackwardCompatibility(
this: Migration20240311145700_InitialSetupMigration
) {
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(`
alter table fulfillment_provider
alter column id type text using id::text;
alter table fulfillment_provider
add is_enabled boolean default true 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_zone" ("id" text not null, "name" text not null, "metadata" jsonb null, "fulfillment_set_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "service_zone_pkey" primary key ("id"));'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_service_zone_name_unique" ON "service_zone" (name) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_service_zone_fulfillment_set_id" ON "service_zone" (fulfillment_set_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_service_zone_deleted_at" ON "service_zone" (deleted_at) WHERE deleted_at IS NOT NULL;'
)
this.addSql(
'create table if not exists "geo_zone" ("id" text not null, "type" text check ("type" in (\'country\', \'province\', \'city\', \'zip\')) not null default \'country\', "country_code" text not null, "province_code" text null, "city" text null, "service_zone_id" text not null, "postal_expression" jsonb null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "geo_zone_pkey" primary key ("id"));'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_geo_zone_country_code" ON "geo_zone" (country_code) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_geo_zone_province_code" ON "geo_zone" (province_code) WHERE deleted_at IS NULL AND province_code IS NOT NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_geo_zone_city" ON "geo_zone" (city) WHERE deleted_at IS NULL AND city IS NOT NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_geo_zone_service_zone_id" ON "geo_zone" (service_zone_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_geo_zone_deleted_at" ON "geo_zone" (deleted_at) WHERE deleted_at IS NOT NULL;'
)
this.addSql(
'create table if not exists "shipping_option_type" ("id" text not null, "label" text not null, "description" text null, "code" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_type_pkey" primary key ("id"));'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_shipping_option_type_deleted_at" ON "shipping_option_type" (deleted_at) WHERE deleted_at IS NOT NULL;'
)
this.addSql(`
alter table shipping_profile
alter column id type text using id::text;
alter table shipping_profile
alter column name type text using name::text;
alter table shipping_profile
alter column type type text using type::text;
create unique index if not exists "IDX_shipping_profile_name_unique"
on shipping_profile (name)
where (deleted_at IS NULL);
create index if not exists "IDX_shipping_profile_deleted_at"
on shipping_profile (deleted_at)
where (deleted_at IS NOT NULL);
`)
this.addSql(`
alter table shipping_option
alter column id type text using id::text;
alter table shipping_option
alter column name type text using name::text;
alter table shipping_option
add service_zone_id text not null;
alter table shipping_option
RENAME profile_id TO shipping_profile_id;
alter table shipping_option
alter column region_id drop not null;
alter table shipping_option
alter column price_type type text using price_type::text;
alter table shipping_option
alter column price_type set default 'calculated'::text;
alter table shipping_option
alter column data drop not null;
alter table shipping_option
add shipping_option_type_id text not null;
drop index if exists "IDX_5c58105f1752fca0f4ce69f466";
drop index if exists "IDX_c951439af4c98bf2bd7fb8726c";
drop index if exists "IDX_a0e206bfaed3cb63c186091734";
create index if not exists "IDX_shipping_option_service_zone_id"
on shipping_option (service_zone_id)
where (deleted_at IS NULL);
create index if not exists "IDX_shipping_option_shipping_profile_id"
on shipping_option (shipping_profile_id)
where (deleted_at IS NULL);
create index if not exists "IDX_shipping_option_provider_id"
on shipping_option (provider_id)
where (deleted_at IS NULL);
create index if not exists "IDX_shipping_option_shipping_option_type_id"
on shipping_option (shipping_option_type_id)
where (deleted_at IS NULL);
create index if not exists "IDX_shipping_option_deleted_at"
on shipping_option (deleted_at)
where (deleted_at IS NOT NULL);
alter table shipping_option
add constraint shipping_option_shipping_option_type_id_unique
unique (shipping_option_type_id);
alter table shipping_option
drop constraint "FK_5c58105f1752fca0f4ce69f4663";
alter table shipping_option
drop constraint "FK_c951439af4c98bf2bd7fb8726cd";
alter table shipping_option
drop constraint "FK_a0e206bfaed3cb63c1860917347";
alter table shipping_option
add constraint shipping_option_service_zone_id_foreign
foreign key (service_zone_id) references service_zone
on update cascade on delete cascade;
alter table shipping_option
add constraint shipping_option_shipping_profile_id_foreign
foreign key (shipping_profile_id) references shipping_profile
on update cascade on delete set null;
alter table shipping_option
add constraint shipping_option_provider_id_foreign
foreign key (provider_id) references fulfillment_provider
on update cascade on delete set null;
alter table shipping_option
add constraint shipping_option_shipping_option_type_id_foreign
foreign key (shipping_option_type_id) references shipping_option_type
on update cascade on delete cascade;
alter table shipping_option
drop constraint "CHK_7a367f5901ae0a5b0df75aee38";
alter table shipping_option
add constraint shipping_option_price_type_check
check (price_type = ANY (ARRAY ['calculated'::text, 'flat'::text]));
`)
this.addSql(
'create table if not exists "shipping_option_rule" ("id" text not null, "attribute" text not null, "operator" text check ("operator" in (\'in\', \'eq\', \'ne\', \'gt\', \'gte\', \'lt\', \'lte\', \'nin\')) not null, "value" jsonb null, "shipping_option_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_rule_pkey" primary key ("id"));'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_shipping_option_rule_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(`
alter table fulfillment
alter
column id type text using id::text;
alter table fulfillment
add packed_at timestamp with time zone;
alter table fulfillment
alter column data drop not null;
alter table fulfillment
add delivered_at timestamp with time zone;
alter table fulfillment
add shipping_option_id text;
alter table fulfillment
add delivery_address_id text;
alter table fulfillment
alter
column provider_id type text using provider_id::text;
alter table fulfillment
add deleted_at timestamp with time zone;
alter table fulfillment
alter
column location_id type text using location_id::text;
alter table fulfillment
alter column location_id set not null;
drop index if exists "IDX_d73e55964e0ff2db8f03807d52";
drop index if exists "IDX_a52e234f729db789cf473297a5";
drop index if exists "IDX_f129acc85e346a10eed12b86fc";
drop index if exists "IDX_beb35a6de60a6c4f91d5ae57e4";
create index if not exists "IDX_fulfillment_location_id"
on fulfillment (location_id) where (deleted_at IS NULL);
create index if not exists"IDX_fulfillment_provider_id"
on fulfillment (provider_id) where (deleted_at IS NULL);
create index if not exists"IDX_fulfillment_shipping_option_id"
on fulfillment (shipping_option_id) where (deleted_at IS NULL);
create index if not exists"IDX_fulfillment_deleted_at"
on fulfillment (deleted_at) where (deleted_at IS NOT NULL);
alter table fulfillment
add constraint fulfillment_delivery_address_id_unique
unique (delivery_address_id);
alter table fulfillment
drop
constraint "FK_a52e234f729db789cf473297a5c"; /* swap id */
alter table fulfillment
drop
constraint "FK_f129acc85e346a10eed12b86fca"; /* order id */
alter table fulfillment
drop
constraint "FK_beb35a6de60a6c4f91d5ae57e44"; /* provider id */
alter table fulfillment
drop
constraint "FK_d73e55964e0ff2db8f03807d52e"; /* claim id */
alter table fulfillment
add constraint fulfillment_provider_id_foreign
foreign key (provider_id) references fulfillment_provider
on update cascade on delete set null;
alter table fulfillment
add constraint fulfillment_shipping_option_id_foreign
foreign key (shipping_option_id) references shipping_option
on update cascade on delete set null;
alter table fulfillment
add constraint fulfillment_delivery_address_id_foreign
foreign key (delivery_address_id) references fulfillment_address
on update cascade on delete cascade;
`)
this.addSql(
'create table if not exists "fulfillment_label" ("id" text not null, "tracking_number" text not null, "tracking_url" text not null, "label_url" text not null, "fulfillment_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_label_pkey" primary key ("id"));'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_fulfillment_label_fulfillment_id" ON "fulfillment_label" (fulfillment_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_fulfillment_label_deleted_at" ON "fulfillment_label" (deleted_at) WHERE deleted_at IS NOT NULL;'
)
this.addSql(`
alter table fulfillment_item
add id text not null;
alter table fulfillment_item
add title text not null;
alter table fulfillment_item
add sku text not null;
alter table fulfillment_item
add barcode text not null;
alter table fulfillment_item
alter column quantity type numeric using quantity::numeric;
alter table fulfillment_item
add raw_quantity jsonb not null;
alter table fulfillment_item
add line_item_id text;
alter table fulfillment_item
add inventory_item_id text;
alter table fulfillment_item
alter column fulfillment_id type text using fulfillment_id::text;
alter table fulfillment_item
add created_at timestamp with time zone default now() not null;
alter table fulfillment_item
add updated_at timestamp with time zone default now() not null;
alter table fulfillment_item
add deleted_at timestamp with time zone;
create index if not exists "IDX_fulfillment_item_line_item_id"
on fulfillment_item (line_item_id)
where (deleted_at IS NULL);
create index if not exists "IDX_fulfillment_item_inventory_item_id"
on fulfillment_item (inventory_item_id)
where (deleted_at IS NULL);
create index if not exists "IDX_fulfillment_item_fulfillment_id"
on fulfillment_item (fulfillment_id)
where (deleted_at IS NULL);
create index if not exists "IDX_fulfillment_item_deleted_at"
on fulfillment_item (deleted_at)
where (deleted_at IS NOT NULL);
alter table fulfillment_item
drop constraint "FK_a033f83cc6bd7701a5687ab4b38"; /* fulfillment id */
alter table fulfillment_item
drop constraint "FK_e13ff60e74206b747a1896212d1"; /* item id */
alter table fulfillment_item
drop constraint "PK_bc3e8a388de75db146a249922e0"; /* item id + fulfillment id PK */
alter table fulfillment_item
alter column item_id drop not null;
alter table fulfillment_item
add constraint fulfillment_item_fulfillment_id_foreign
foreign key (fulfillment_id) references fulfillment
on update cascade on delete cascade;
alter table "fulfillment_item" add primary key ("id");
`)
this.addSql(
'alter table if exists "service_zone" add constraint "service_zone_fulfillment_set_id_foreign" foreign key ("fulfillment_set_id") references "fulfillment_set" ("id") on update cascade on delete cascade;'
)
this.addSql(
'alter table if exists "geo_zone" add constraint "geo_zone_service_zone_id_foreign" foreign key ("service_zone_id") references "service_zone" ("id") on update cascade 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 on delete 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 migration is for be initial setup of the fulfillment module in the case of
* a new database. It will create all the necessary tables and indexes.
*/
async function migrateUpModuleMigration(
this: Migration20240311145700_InitialSetupMigration
) {
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, "is_enabled" boolean not null default true, constraint "fulfillment_provider_pkey" primary key ("id"));'
)
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_zone" ("id" text not null, "name" text not null, "metadata" jsonb null, "fulfillment_set_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "service_zone_pkey" primary key ("id"));'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_service_zone_name_unique" ON "service_zone" (name) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_service_zone_fulfillment_set_id" ON "service_zone" (fulfillment_set_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_service_zone_deleted_at" ON "service_zone" (deleted_at) WHERE deleted_at IS NOT NULL;'
)
this.addSql(
'create table if not exists "geo_zone" ("id" text not null, "type" text check ("type" in (\'country\', \'province\', \'city\', \'zip\')) not null default \'country\', "country_code" text not null, "province_code" text null, "city" text null, "service_zone_id" text not null, "postal_expression" jsonb null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "geo_zone_pkey" primary key ("id"));'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_geo_zone_country_code" ON "geo_zone" (country_code) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_geo_zone_province_code" ON "geo_zone" (province_code) WHERE deleted_at IS NULL AND province_code IS NOT NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_geo_zone_city" ON "geo_zone" (city) WHERE deleted_at IS NULL AND city IS NOT NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_geo_zone_service_zone_id" ON "geo_zone" (service_zone_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_geo_zone_deleted_at" ON "geo_zone" (deleted_at) WHERE deleted_at IS NOT NULL;'
)
this.addSql(
'create table if not exists "shipping_option_type" ("id" text not null, "label" text not null, "description" text null, "code" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_type_pkey" primary key ("id"));'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_shipping_option_type_deleted_at" ON "shipping_option_type" (deleted_at) WHERE deleted_at IS NOT NULL;'
)
this.addSql(
'create table if not exists "shipping_profile" ("id" text not null, "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 "shipping_profile_pkey" primary key ("id"));'
)
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 \'flat\', "service_zone_id" text not null, "shipping_profile_id" text null, "provider_id" text null, "data" jsonb null, "metadata" jsonb null, "shipping_option_type_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_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_provider_id" ON "shipping_option" (provider_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_shipping_option_shipping_option_type_id" ON "shipping_option" (shipping_option_type_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_shipping_option_deleted_at" ON "shipping_option" (deleted_at) WHERE deleted_at IS NOT NULL;'
)
this.addSql(
'create table if not exists "shipping_option_rule" ("id" text not null, "attribute" text not null, "operator" text check ("operator" in (\'in\', \'eq\', \'ne\', \'gt\', \'gte\', \'lt\', \'lte\', \'nin\')) not null, "value" jsonb null, "shipping_option_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_rule_pkey" primary key ("id"));'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_shipping_option_rule_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 null, "shipping_option_id" text null, "metadata" jsonb null, "delivery_address_id" text 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;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_fulfillment_deleted_at" ON "fulfillment" (deleted_at) WHERE deleted_at IS NOT NULL;'
)
this.addSql(
'create table if not exists "fulfillment_label" ("id" text not null, "tracking_number" text not null, "tracking_url" text not null, "label_url" text not null, "fulfillment_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_label_pkey" primary key ("id"));'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_fulfillment_label_fulfillment_id" ON "fulfillment_label" (fulfillment_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_fulfillment_label_deleted_at" ON "fulfillment_label" (deleted_at) WHERE deleted_at IS NOT NULL;'
)
this.addSql(
'create table if not exists "fulfillment_item" ("id" text not null, "title" text not null, "sku" text not null, "barcode" text not null, "quantity" numeric not null, "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;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_fulfillment_item_deleted_at" ON "fulfillment_item" (deleted_at) WHERE deleted_at IS NOT NULL;'
)
this.addSql(
'alter table if exists "service_zone" add constraint "service_zone_fulfillment_set_id_foreign" foreign key ("fulfillment_set_id") references "fulfillment_set" ("id") on update cascade on delete cascade;'
)
this.addSql(
'alter table if exists "geo_zone" add constraint "geo_zone_service_zone_id_foreign" foreign key ("service_zone_id") references "service_zone" ("id") on update cascade on delete cascade;'
)
this.addSql(
'alter table if exists "shipping_option" add constraint "shipping_option_service_zone_id_foreign" foreign key ("service_zone_id") references "service_zone" ("id") on update cascade on delete 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_provider_id_foreign" foreign key ("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 on delete 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 on delete set null;'
)
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_delivery_address_id_foreign" foreign key ("delivery_address_id") references "fulfillment_address" ("id") on update cascade on delete 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 on delete cascade;'
)
}
@@ -0,0 +1,91 @@
import { DAL } from "@medusajs/types"
import {
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/utils"
import {
BeforeCreate,
Entity,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
type OptionalAddressProps = DAL.SoftDeletableEntityDateColumns
const FulfillmentDeletedAtIndex = createPsqlIndexStatementHelper({
tableName: "fulfillment_address",
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
})
@Entity({ tableName: "fulfillment_address" })
export default class Address {
[OptionalProps]: OptionalAddressProps
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "text", nullable: true })
company: string | null = null
@Property({ columnType: "text", nullable: true })
first_name: string | null = null
@Property({ columnType: "text", nullable: true })
last_name: string | null = null
@Property({ columnType: "text", nullable: true })
address_1: string | null = null
@Property({ columnType: "text", nullable: true })
address_2: string | null = null
@Property({ columnType: "text", nullable: true })
city: string | null = null
@Property({ columnType: "text", nullable: true })
country_code: string | null = null
@Property({ columnType: "text", nullable: true })
province: string | null = null
@Property({ columnType: "text", nullable: true })
postal_code: string | null = null
@Property({ columnType: "text", nullable: true })
phone: string | null = null
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Property({ columnType: "timestamptz", nullable: true })
@FulfillmentDeletedAtIndex.MikroORMIndex()
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "fuladdr")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "fuladdr")
}
}
@@ -0,0 +1,121 @@
import {
BigNumber,
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
MikroOrmBigNumberProperty,
} from "@medusajs/utils"
import { BigNumberRawValue, DAL } from "@medusajs/types"
import {
BeforeCreate,
Entity,
Filter,
ManyToOne,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import Fulfillment from "./fulfillment"
type FulfillmentItemOptionalProps = DAL.SoftDeletableEntityDateColumns
const FulfillmentIdIndex = createPsqlIndexStatementHelper({
tableName: "fulfillment_item",
columns: "fulfillment_id",
where: "deleted_at IS NULL",
})
const LineItemIdIndex = createPsqlIndexStatementHelper({
tableName: "fulfillment_item",
columns: "line_item_id",
where: "deleted_at IS NULL",
})
const InventoryItemIdIndex = createPsqlIndexStatementHelper({
tableName: "fulfillment_item",
columns: "inventory_item_id",
where: "deleted_at IS NULL",
})
const FulfillmentItemDeletedAtIndex = createPsqlIndexStatementHelper({
tableName: "fulfillment_item",
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
})
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class FulfillmentItem {
[OptionalProps]?: FulfillmentItemOptionalProps
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
title: string
@Property({ columnType: "text" })
sku: string
@Property({ columnType: "text" })
barcode: string
@MikroOrmBigNumberProperty()
quantity: BigNumber | number
@Property({ columnType: "jsonb" })
raw_quantity: BigNumberRawValue
@Property({ columnType: "text", nullable: true })
@LineItemIdIndex.MikroORMIndex()
line_item_id: string | null = null
@Property({ columnType: "text", nullable: true })
@InventoryItemIdIndex.MikroORMIndex()
inventory_item_id: string | null = null
@ManyToOne(() => Fulfillment, {
columnType: "text",
mapToPk: true,
fieldName: "fulfillment_id",
onDelete: "cascade",
})
@FulfillmentIdIndex.MikroORMIndex()
fulfillment_id: string
@ManyToOne(() => Fulfillment, { persist: false })
fulfillment: Fulfillment
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@FulfillmentItemDeletedAtIndex.MikroORMIndex()
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@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
}
}
@@ -0,0 +1,93 @@
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
} from "@medusajs/utils"
import { DAL } from "@medusajs/types"
import {
BeforeCreate,
Entity,
Filter,
ManyToOne,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import Fulfillment from "./fulfillment"
type FulfillmentLabelOptionalProps = DAL.SoftDeletableEntityDateColumns
const FulfillmentIdIndex = createPsqlIndexStatementHelper({
tableName: "fulfillment_label",
columns: "fulfillment_id",
where: "deleted_at IS NULL",
})
const DeletedAtIndex = createPsqlIndexStatementHelper({
tableName: "fulfillment_label",
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
})
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class FulfillmentLabel {
[OptionalProps]?: FulfillmentLabelOptionalProps
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
tracking_number: string
@Property({ columnType: "text" })
tracking_url: string
@Property({ columnType: "text" })
label_url: string
@ManyToOne(() => Fulfillment, {
columnType: "text",
mapToPk: true,
fieldName: "fulfillment_id",
onDelete: "cascade",
})
@FulfillmentIdIndex.MikroORMIndex()
fulfillment_id: string
@ManyToOne(() => Fulfillment, { persist: false })
fulfillment: Fulfillment
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Property({ columnType: "timestamptz", nullable: true })
@DeletedAtIndex.MikroORMIndex()
deleted_at: Date | null = null
@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
}
}
@@ -0,0 +1,27 @@
import { generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Entity,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
@Entity()
export default class FulfillmentProvider {
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "boolean", defaultRaw: "true" })
is_enabled: boolean = true
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "serpro")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "serpro")
}
}
@@ -0,0 +1,89 @@
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
} from "@medusajs/utils"
import { DAL } from "@medusajs/types"
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Filter,
OneToMany,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import ServiceZone from "./service-zone"
type FulfillmentSetOptionalProps = DAL.SoftDeletableEntityDateColumns
const DeletedAtIndex = createPsqlIndexStatementHelper({
tableName: "fulfillment_set",
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
})
const NameIndex = createPsqlIndexStatementHelper({
tableName: "fulfillment_set",
columns: "name",
unique: true,
where: "deleted_at IS NULL",
})
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class FulfillmentSet {
[OptionalProps]?: FulfillmentSetOptionalProps
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
@NameIndex.MikroORMIndex()
name: string
@Property({ columnType: "text" })
type: string
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@OneToMany(() => ServiceZone, "fulfillment_set", {
cascade: [Cascade.PERSIST, "soft-remove"] as any,
orphanRemoval: true,
})
service_zones = new Collection<ServiceZone>(this)
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Property({ columnType: "timestamptz", nullable: true })
@DeletedAtIndex.MikroORMIndex()
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "fuset")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "fuset")
}
}
@@ -0,0 +1,173 @@
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
} from "@medusajs/utils"
import { DAL } from "@medusajs/types"
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Filter,
ManyToOne,
OneToMany,
OneToOne,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import Address from "./address"
import FulfillmentItem from "./fulfillment-item"
import FulfillmentLabel from "./fulfillment-label"
import FulfillmentProvider from "./fulfillment-provider"
import ShippingOption from "./shipping-option"
type FulfillmentOptionalProps = DAL.SoftDeletableEntityDateColumns
const FulfillmentDeletedAtIndex = createPsqlIndexStatementHelper({
tableName: "fulfillment",
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
})
const FulfillmentProviderIdIndex = createPsqlIndexStatementHelper({
tableName: "fulfillment",
columns: "provider_id",
where: "deleted_at IS NULL",
})
const FulfillmentLocationIdIndex = createPsqlIndexStatementHelper({
tableName: "fulfillment",
columns: "location_id",
where: "deleted_at IS NULL",
})
const FulfillmentShippingOptionIdIndex = createPsqlIndexStatementHelper({
tableName: "fulfillment",
columns: "shipping_option_id",
where: "deleted_at IS NULL",
})
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class Fulfillment {
[OptionalProps]?: FulfillmentOptionalProps
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
@FulfillmentLocationIdIndex.MikroORMIndex()
location_id: string
@Property({
columnType: "timestamptz",
nullable: true,
})
packed_at: Date | null = null
@Property({
columnType: "timestamptz",
nullable: true,
})
shipped_at: Date | null = null
@Property({
columnType: "timestamptz",
nullable: true,
})
delivered_at: Date | null = null
@Property({
columnType: "timestamptz",
nullable: true,
})
canceled_at: Date | null = null
@Property({ columnType: "jsonb", nullable: true })
data: Record<string, unknown> | null = null
@ManyToOne(() => FulfillmentProvider, {
columnType: "text",
fieldName: "provider_id",
mapToPk: true,
nullable: true,
onDelete: "set null",
})
@FulfillmentProviderIdIndex.MikroORMIndex()
provider_id: string
@ManyToOne(() => ShippingOption, {
columnType: "text",
fieldName: "shipping_option_id",
nullable: true,
mapToPk: true,
onDelete: "set null",
})
@FulfillmentShippingOptionIdIndex.MikroORMIndex()
shipping_option_id: string | null = null
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@ManyToOne(() => ShippingOption, { persist: false })
shipping_option: ShippingOption | null
@ManyToOne(() => FulfillmentProvider, { persist: false })
provider: FulfillmentProvider
@OneToOne({
entity: () => Address,
owner: true,
cascade: [Cascade.PERSIST, "soft-remove"] as any,
nullable: true,
onDelete: "cascade",
})
delivery_address!: Address
@OneToMany(() => FulfillmentItem, (item) => item.fulfillment, {
cascade: [Cascade.PERSIST, "soft-remove"] as any,
orphanRemoval: true,
})
items = new Collection<FulfillmentItem>(this)
@OneToMany(() => FulfillmentLabel, (label) => label.fulfillment, {
cascade: [Cascade.PERSIST, "soft-remove"] as any,
orphanRemoval: true,
})
labels = new Collection<FulfillmentLabel>(this)
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@FulfillmentDeletedAtIndex.MikroORMIndex()
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "ful")
this.provider_id ??= this.provider_id ?? this.provider?.id
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "ful")
this.provider_id ??= this.provider_id ?? this.provider?.id
}
}
@@ -0,0 +1,127 @@
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
GeoZoneType,
} from "@medusajs/utils"
import { DAL } from "@medusajs/types"
import {
BeforeCreate,
Entity,
Enum,
Filter,
ManyToOne,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import ServiceZone from "./service-zone"
type GeoZoneOptionalProps = DAL.SoftDeletableEntityDateColumns
const DeletedAtIndex = createPsqlIndexStatementHelper({
tableName: "geo_zone",
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
})
const CountryCodeIndex = createPsqlIndexStatementHelper({
tableName: "geo_zone",
columns: "country_code",
where: "deleted_at IS NULL",
})
const ProvinceCodeIndex = createPsqlIndexStatementHelper({
tableName: "geo_zone",
columns: "province_code",
where: "deleted_at IS NULL AND province_code IS NOT NULL",
})
const CityIndex = createPsqlIndexStatementHelper({
tableName: "geo_zone",
columns: "city",
where: "deleted_at IS NULL AND city IS NOT NULL",
})
const ServiceZoneIdIndex = createPsqlIndexStatementHelper({
tableName: "geo_zone",
columns: "service_zone_id",
where: "deleted_at IS NULL",
})
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class GeoZone {
[OptionalProps]?: GeoZoneOptionalProps
@PrimaryKey({ columnType: "text" })
id: string
@Enum({ items: () => GeoZoneType, default: GeoZoneType.COUNTRY })
type: GeoZoneType
@CountryCodeIndex.MikroORMIndex()
@Property({ columnType: "text" })
country_code: string
@ProvinceCodeIndex.MikroORMIndex()
@Property({ columnType: "text", nullable: true })
province_code: string | null = null
@CityIndex.MikroORMIndex()
@Property({ columnType: "text", nullable: true })
city: string | null = null
@ManyToOne(() => ServiceZone, {
type: "text",
mapToPk: true,
fieldName: "service_zone_id",
onDelete: "cascade",
})
@ServiceZoneIdIndex.MikroORMIndex()
service_zone_id: string
@Property({ columnType: "jsonb", nullable: true })
postal_expression: Record<string, unknown> | null = null
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@ManyToOne(() => ServiceZone, {
persist: false,
})
service_zone: ServiceZone
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@DeletedAtIndex.MikroORMIndex()
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, " fgz")
this.service_zone_id ??= this.service_zone?.id
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "fgz")
this.service_zone_id ??= this.service_zone?.id
}
}
@@ -0,0 +1,13 @@
export { default as Address } from "./address"
export { default as Fulfillment } from "./fulfillment"
export { default as FulfillmentItem } from "./fulfillment-item"
export { default as FulfillmentLabel } from "./fulfillment-label"
export { default as FulfillmentProvider } from "./fulfillment-provider"
export { default as FulfillmentSet } from "./fulfillment-set"
export { default as GeoZone } from "./geo-zone"
export { default as ServiceZone } from "./service-zone"
export { default as ShippingOption } from "./shipping-option"
export { default as ShippingOptionRule } from "./shipping-option-rule"
export { default as ShippingOptionType } from "./shipping-option-type"
export { default as ShippingProfile } from "./shipping-profile"
@@ -0,0 +1,125 @@
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
} from "@medusajs/utils"
import { DAL } from "@medusajs/types"
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Filter,
Index,
ManyToOne,
OneToMany,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import FulfillmentSet from "./fulfillment-set"
import GeoZone from "./geo-zone"
import ShippingOption from "./shipping-option"
type ServiceZoneOptionalProps = DAL.SoftDeletableEntityDateColumns
const deletedAtIndexName = "IDX_service_zone_deleted_at"
const deletedAtIndexStatement = createPsqlIndexStatementHelper({
name: deletedAtIndexName,
tableName: "service_zone",
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
}).expression
const NameIndex = createPsqlIndexStatementHelper({
tableName: "service_zone",
columns: "name",
unique: true,
where: "deleted_at IS NULL",
})
const FulfillmentSetIdIndex = createPsqlIndexStatementHelper({
tableName: "service_zone",
columns: "fulfillment_set_id",
where: "deleted_at IS NULL",
})
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class ServiceZone {
[OptionalProps]?: ServiceZoneOptionalProps
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
@NameIndex.MikroORMIndex()
name: string
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@ManyToOne(() => FulfillmentSet, {
type: "text",
mapToPk: true,
fieldName: "fulfillment_set_id",
onDelete: "cascade",
})
@FulfillmentSetIdIndex.MikroORMIndex()
fulfillment_set_id: string
@ManyToOne(() => FulfillmentSet, { persist: false })
fulfillment_set: FulfillmentSet
@OneToMany(() => GeoZone, "service_zone", {
cascade: [Cascade.PERSIST, "soft-remove"] as any,
orphanRemoval: true,
})
geo_zones = new Collection<GeoZone>(this)
@OneToMany(
() => ShippingOption,
(shippingOption) => shippingOption.service_zone,
{
cascade: [Cascade.PERSIST, "soft-remove"] as any,
orphanRemoval: true,
}
)
shipping_options = new Collection<ShippingOption>(this)
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({
name: deletedAtIndexName,
expression: deletedAtIndexStatement,
})
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "serzo")
this.fulfillment_set_id ??= this.fulfillment_set?.id
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "serzo")
this.fulfillment_set_id ??= this.fulfillment_set?.id
}
}
@@ -0,0 +1,99 @@
import { DAL } from "@medusajs/types"
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
RuleOperator,
} from "@medusajs/utils"
import {
BeforeCreate,
Entity,
Enum,
Filter,
ManyToOne,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import ShippingOption from "./shipping-option"
type ShippingOptionRuleOptionalProps = DAL.SoftDeletableEntityDateColumns
const DeletedAtIndex = createPsqlIndexStatementHelper({
tableName: "shipping_option_rule",
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
})
const ShippingOptionIdIndex = createPsqlIndexStatementHelper({
tableName: "shipping_option_rule",
columns: "shipping_option_id",
where: "deleted_at IS NULL",
})
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class ShippingOptionRule {
[OptionalProps]?: ShippingOptionRuleOptionalProps
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
attribute: string
@Enum({
items: () => Object.values(RuleOperator),
columnType: "text",
})
operator: Lowercase<keyof typeof RuleOperator>
@Property({ columnType: "jsonb", nullable: true })
value: string | string[] | null = null
@ManyToOne(() => ShippingOption, {
type: "text",
mapToPk: true,
fieldName: "shipping_option_id",
onDelete: "cascade",
})
@ShippingOptionIdIndex.MikroORMIndex()
shipping_option_id: string
@ManyToOne(() => ShippingOption, {
persist: false,
})
shipping_option: ShippingOption
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@DeletedAtIndex.MikroORMIndex()
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "sorul")
this.shipping_option_id ??= this.shipping_option?.id
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "sorul")
this.shipping_option_id ??= this.shipping_option?.id
}
}
@@ -0,0 +1,85 @@
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
} from "@medusajs/utils"
import { DAL } from "@medusajs/types"
import {
BeforeCreate,
Entity,
Filter,
OneToOne,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import ShippingOption from "./shipping-option"
type ShippingOptionTypeOptionalProps = DAL.SoftDeletableEntityDateColumns
const DeletedAtIndex = createPsqlIndexStatementHelper({
tableName: "shipping_option_type",
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
})
const ShippingOptionIdIndex = createPsqlIndexStatementHelper({
tableName: "shipping_option_type",
columns: "shipping_option_id",
where: "deleted_at IS NULL",
})
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class ShippingOptionType {
[OptionalProps]?: ShippingOptionTypeOptionalProps
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
label: string
@Property({ columnType: "text", nullable: true })
description: string | null = null
@Property({ columnType: "text" })
code: string
@OneToOne(() => ShippingOption, (so) => so.type, {
type: "text",
onDelete: "cascade",
})
shipping_option: ShippingOption
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@DeletedAtIndex.MikroORMIndex()
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "sotype")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "sotype")
}
}
@@ -0,0 +1,179 @@
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
ShippingOptionPriceType,
} from "@medusajs/utils"
import { DAL } from "@medusajs/types"
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Enum,
Filter,
ManyToOne,
OneToMany,
OneToOne,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import Fulfillment from "./fulfillment"
import FulfillmentProvider from "./fulfillment-provider"
import ServiceZone from "./service-zone"
import ShippingOptionRule from "./shipping-option-rule"
import ShippingOptionType from "./shipping-option-type"
import ShippingProfile from "./shipping-profile"
type ShippingOptionOptionalProps = DAL.SoftDeletableEntityDateColumns
const DeletedAtIndex = createPsqlIndexStatementHelper({
tableName: "shipping_option",
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
})
const ServiceZoneIdIndex = createPsqlIndexStatementHelper({
tableName: "shipping_option",
columns: "service_zone_id",
where: "deleted_at IS NULL",
})
const ShippingProfileIdIndex = createPsqlIndexStatementHelper({
tableName: "shipping_option",
columns: "shipping_profile_id",
where: "deleted_at IS NULL",
})
const FulfillmentProviderIdIndex = createPsqlIndexStatementHelper({
tableName: "shipping_option",
columns: "provider_id",
where: "deleted_at IS NULL",
})
const ShippingOptionTypeIdIndex = createPsqlIndexStatementHelper({
tableName: "shipping_option",
columns: "shipping_option_type_id",
where: "deleted_at IS NULL",
})
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class ShippingOption {
[OptionalProps]?: ShippingOptionOptionalProps
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
name: string
@Enum({
items: () => ShippingOptionPriceType,
default: ShippingOptionPriceType.FLAT,
})
price_type: ShippingOptionPriceType
@ManyToOne(() => ServiceZone, {
type: "text",
fieldName: "service_zone_id",
mapToPk: true,
onDelete: "cascade",
})
@ServiceZoneIdIndex.MikroORMIndex()
service_zone_id: string
@ManyToOne(() => ShippingProfile, {
type: "text",
fieldName: "shipping_profile_id",
mapToPk: true,
nullable: true,
onDelete: "set null",
})
@ShippingProfileIdIndex.MikroORMIndex()
shipping_profile_id: string | null
@ManyToOne(() => FulfillmentProvider, {
type: "text",
fieldName: "provider_id",
mapToPk: true,
nullable: true,
})
@FulfillmentProviderIdIndex.MikroORMIndex()
provider_id: string
@Property({ columnType: "text", persist: false })
@ShippingOptionTypeIdIndex.MikroORMIndex()
shipping_option_type_id: string | null = null
@Property({ columnType: "jsonb", nullable: true })
data: Record<string, unknown> | null = null
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@ManyToOne(() => ServiceZone, { persist: false })
service_zone: ServiceZone
@ManyToOne(() => ShippingProfile, {
persist: false,
})
shipping_profile: ShippingProfile | null
@ManyToOne(() => FulfillmentProvider, {
persist: false,
})
provider: FulfillmentProvider | null
@OneToOne(() => ShippingOptionType, (so) => so.shipping_option, {
owner: true,
cascade: [Cascade.PERSIST, "soft-remove"] as any,
orphanRemoval: true,
fieldName: "shipping_option_type_id",
onDelete: "cascade",
})
type: ShippingOptionType
@OneToMany(() => ShippingOptionRule, "shipping_option", {
cascade: [Cascade.PERSIST, "soft-remove"] as any,
orphanRemoval: true,
})
rules = new Collection<ShippingOptionRule>(this)
@OneToMany(() => Fulfillment, (fulfillment) => fulfillment.shipping_option)
fulfillments = new Collection<Fulfillment>(this)
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@DeletedAtIndex.MikroORMIndex()
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "so")
this.shipping_option_type_id ??= this.type?.id
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "so")
this.shipping_option_type_id ??= this.type?.id
}
}
@@ -0,0 +1,91 @@
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
Searchable,
} from "@medusajs/utils"
import { DAL } from "@medusajs/types"
import {
BeforeCreate,
Collection,
Entity,
Filter,
OneToMany,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import ShippingOption from "./shipping-option"
type ShippingProfileOptionalProps = DAL.SoftDeletableEntityDateColumns
const DeletedAtIndex = createPsqlIndexStatementHelper({
tableName: "shipping_profile",
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
})
const ShippingProfileTypeIndex = createPsqlIndexStatementHelper({
tableName: "shipping_profile",
columns: "name",
unique: true,
where: "deleted_at IS NULL",
})
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class ShippingProfile {
[OptionalProps]?: ShippingProfileOptionalProps
@PrimaryKey({ columnType: "text" })
id: string
@Searchable()
@Property({ columnType: "text" })
@ShippingProfileTypeIndex.MikroORMIndex()
name: string
@Searchable()
@Property({ columnType: "text" })
type: string
@OneToMany(
() => ShippingOption,
(shippingOption) => shippingOption.shipping_profile
)
shipping_options = new Collection<ShippingOption>(this)
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@DeletedAtIndex.MikroORMIndex()
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "sp")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "sp")
}
}
@@ -0,0 +1,44 @@
import { ModuleExports } from "@medusajs/types"
import * as ModuleServices from "@services"
import { FulfillmentModuleService } from "@services"
import { Modules } from "@medusajs/modules-sdk"
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,
models: ModuleModels,
pathToMigrations: __dirname + "/migrations",
}
const runMigrations = ModulesSdkUtils.buildMigrationScript(
migrationScriptOptions
)
const revertMigration = ModulesSdkUtils.buildRevertMigrationScript(
migrationScriptOptions
)
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
moduleModels: ModuleModels,
moduleRepositories: ModuleRepositories,
moduleServices: ModuleServices,
})
const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({
moduleName: Modules.FULFILLMENT,
moduleModels: Object.values(ModuleModels),
migrationsPath: __dirname + "/migrations",
})
const service = FulfillmentModuleService
const loaders = [containerLoader, connectionLoader, loadProviders]
export const moduleDefinition: ModuleExports = {
service,
loaders,
revertMigration,
runMigrations,
}
@@ -0,0 +1,59 @@
/*
import { Context, FulfillmentTypes } from "@medusajs/types"
import { DALUtils, promiseAll } from "@medusajs/utils"
import { FulfillmentSet, ServiceZone } from "@models"
import { SqlEntityManager } from "@mikro-orm/postgresql"
interface CreateFulfillmentSetDTO
extends FulfillmentTypes.CreateFulfillmentSetDTO {
service_zones: { id: string; name: string }[]
}
export class FulfillmentSetRepository extends DALUtils.mikroOrmBaseRepositoryFactory<FulfillmentSet>(
FulfillmentSet
) {
async update(
data: {
entity: FulfillmentSet
update: FulfillmentTypes.FulfillmentSetDTO
}[],
context?: Context
): Promise<FulfillmentSet[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
// init all service zones collections
await promiseAll(
data.map(async ({ entity }) => {
return await entity.service_zones.init()
})
)
const flfillmentSetsToUpdate = data.map(({ entity, update }) => {
const { service_zones, ...restToUpdate } = update
const currentServiceZones = entity.service_zones.getItems()
const serviceZonesToDetach = currentServiceZones.filter(
(serviceZone) =>
!update.service_zones.find(
(newServiceZone) => newServiceZone.id === serviceZone.id
)
)
const serviceZonesToAttach = update.service_zones.filter(
(newServiceZone) =>
!currentServiceZones.find(
(serviceZone) => serviceZone.id === newServiceZone.id
)
)
entity.service_zones.remove(serviceZonesToDetach)
entity.service_zones.add(serviceZonesToAttach as unknown as ServiceZone[])
return manager.assign(entity, restToUpdate)
})
manager.persist(flfillmentSetsToUpdate)
return flfillmentSetsToUpdate
}
}
*/
@@ -0,0 +1 @@
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
@@ -0,0 +1,29 @@
#!/usr/bin/env node
import { ModulesSdkUtils } from "@medusajs/utils"
import { Modules } from "@medusajs/modules-sdk"
import * as Models from "@models"
import { EOL } from "os"
const args = process.argv
const path = args.pop() as string
export default (async () => {
const { config } = await import("dotenv")
config()
if (!path) {
throw new Error(
`filePath is required.${EOL}Example: medusa-fulfillment-seed <filePath>`
)
}
const run = ModulesSdkUtils.buildSeedScript({
moduleName: Modules.FULFILLMENT,
models: Models,
pathToMigrations: __dirname + "/../../migrations",
seedHandler: async ({ manager, data }) => {
// TODO: Add seed logic
},
})
await run({ path })
})()
File diff suppressed because it is too large Load Diff
@@ -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(providerId: string, data: Record<string, unknown>) {
const provider = this.retrieveProviderRegistration(providerId)
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)
}
}
@@ -0,0 +1,2 @@
export { default as FulfillmentModuleService } from "./fulfillment-module-service"
export { default as FulfillmentProviderService } from "./fulfillment-provider"
@@ -0,0 +1,33 @@
import {
IEventBusModuleService,
Logger,
ModuleProviderExports,
ModuleServiceInitializeOptions,
} from "@medusajs/types"
export type InitializeModuleInjectableDependencies = {
logger?: Logger
eventBusService?: IEventBusModuleService
}
export const FulfillmentIdentifiersRegistrationName =
"fulfillment_providers_identifier"
export type FulfillmentModuleOptions =
Partial<ModuleServiceInitializeOptions> & {
/**
* Providers to be registered
*/
providers?: {
/**
* The module provider to be registered
*/
resolve: string | ModuleProviderExports
options: {
/**
* key value pair of the provider name and the configuration to be passed to the provider constructor
*/
config: Record<string, unknown>
}
}[]
}
@@ -0,0 +1,6 @@
import { FulfillmentTypes } from "@medusajs/types"
export type UpdateShippingOptionsInput = Required<
Pick<FulfillmentTypes.UpdateShippingOptionDTO, "id">
> &
FulfillmentTypes.UpdateShippingOptionDTO
@@ -0,0 +1,249 @@
import { RuleOperator } from "@medusajs/utils"
import { isContextValid } from "../utils"
describe("isContextValidForRules", () => {
const context = {
attribute1: "value1",
attribute2: "value2",
attribute3: "value3",
}
const validRule = {
attribute: "attribute1",
operator: RuleOperator.EQ,
value: "value1",
}
const invalidRule = {
attribute: "attribute2",
operator: RuleOperator.EQ,
value: "wrongValue",
}
it("returns true when all rules are valid", () => {
const rules = [validRule, validRule]
expect(isContextValid(context, rules)).toBe(true)
})
it("returns true when some rules are valid", () => {
const rules = [validRule, validRule]
const options = { someAreValid: true }
expect(isContextValid(context, rules, options)).toBe(true)
})
it("returns true when some rules are valid and someAreValid is true", () => {
const rules = [validRule, invalidRule]
const options = { someAreValid: true }
expect(isContextValid(context, rules, options)).toBe(true)
})
it("returns false when some rules are valid", () => {
const rules = [validRule, invalidRule]
expect(isContextValid(context, rules)).toBe(false)
})
it("returns false when no rules are valid and someAreValid is true", () => {
const rules = [invalidRule, invalidRule]
const options = { someAreValid: true }
expect(isContextValid(context, rules, options)).toBe(false)
})
it("returns false when no rules are valid", () => {
const rules = [invalidRule, invalidRule]
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'gt' operator is valid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.GT,
value: "1", // 2 > 1
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(true)
})
it("returns false when the 'gt' operator is invalid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.GT,
value: "0", // 0 > 0
},
]
const context = { attribute1: "0" }
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'gte' operator is valid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.GTE,
value: "2", // 2 >= 2
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(true)
})
it("returns false when the 'gte' operator is invalid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.GTE,
value: "3", // 2 >= 3
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'lt' operator is valid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.LT,
value: "3", // 2 < 3
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(true)
})
it("returns false when the 'lt' operator is invalid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.LT,
value: "2", // 2 < 2
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'lte' operator is valid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.LTE,
value: "2", // 2 <= 2
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(true)
})
// ... existing tests ...
it("returns false when the 'lte' operator is invalid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.LTE,
value: "1", // 2 <= 1
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'in' operator is valid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.IN,
value: ["1", "2", "3"], // 2 in [1, 2, 3]
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(true)
})
it("returns false when the 'in' operator is invalid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.IN,
value: ["1", "3", "4"], // 2 in [1, 3, 4]
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'nin' operator is valid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.NIN,
value: ["1", "3", "4"], // 2 not in [1, 3, 4]
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(true)
})
it("returns false when the 'nin' operator is invalid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.NIN,
value: ["1", "2", "3"], // 2 not in [1, 2, 3]
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'ne' operator is valid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.NE,
value: "1", // 2 != 1
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(true)
})
it("returns false when the 'ne' operator is invalid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.NE,
value: "2", // 2 != 2
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'eq' operator is valid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.EQ,
value: "2", // 2 == 2
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(true)
})
it("returns false when the 'eq' operator is invalid", () => {
const rules = [
{
attribute: "attribute1",
operator: RuleOperator.EQ,
value: "1", // 2 == 1
},
]
const context = { attribute1: "2" }
expect(isContextValid(context, rules)).toBe(false)
})
})
@@ -0,0 +1 @@
export * from './utils'
@@ -0,0 +1,172 @@
import {
isObject,
isString,
MedusaError,
pickValueFromObject,
RuleOperator,
} from "@medusajs/utils"
/**
* The rule engine here is kept inside the module as of now, but it could be moved
* to the utils package and be used across the different modules that provides context
* based rule filtering.
*
* TODO: discussion around that should happen at some point
*/
export type Rule = {
attribute: string
operator: Lowercase<keyof typeof RuleOperator>
value: string | string[] | null
}
export const availableOperators = Object.values(RuleOperator)
const isDate = (str: string) => {
return !isNaN(Date.parse(str))
}
const operatorsPredicate = {
in: (contextValue: string, ruleValue: string[]) =>
ruleValue.includes(contextValue),
nin: (contextValue: string, ruleValue: string[]) =>
!ruleValue.includes(contextValue),
eq: (contextValue: string, ruleValue: string) => contextValue === ruleValue,
ne: (contextValue: string, ruleValue: string) => contextValue !== ruleValue,
gt: (contextValue: string, ruleValue: string) => {
if (isDate(contextValue) && isDate(ruleValue)) {
return new Date(contextValue) > new Date(ruleValue)
}
return Number(contextValue) > Number(ruleValue)
},
gte: (contextValue: string, ruleValue: string) => {
if (isDate(contextValue) && isDate(ruleValue)) {
return new Date(contextValue) >= new Date(ruleValue)
}
return Number(contextValue) >= Number(ruleValue)
},
lt: (contextValue: string, ruleValue: string) => {
if (isDate(contextValue) && isDate(ruleValue)) {
return new Date(contextValue) < new Date(ruleValue)
}
return Number(contextValue) < Number(ruleValue)
},
lte: (contextValue: string, ruleValue: string) => {
if (isDate(contextValue) && isDate(ruleValue)) {
return new Date(contextValue) <= new Date(ruleValue)
}
return Number(contextValue) <= Number(ruleValue)
},
}
/**
* Validate contextValue context object from contextValue set of rules.
* By default, all rules must be valid to return true unless the option atLeastOneValidRule is set to true.
* @param context
* @param rules
* @param options
*/
export function isContextValid(
context: Record<string, any>,
rules: Rule[],
options: {
someAreValid: boolean
} = {
someAreValid: false,
}
): boolean {
const { someAreValid } = options
const loopComparator = someAreValid ? rules.some : rules.every
const predicate = (rule) => {
const { attribute, operator, value } = rule
const contextValue = pickValueFromObject(attribute, context)
return operatorsPredicate[operator](
contextValue,
value as string & string[]
)
}
return loopComparator.apply(rules, [predicate])
}
/**
* Validate contextValue rule object
* @param rule
*/
export function validateRule(rule: Record<string, unknown>): boolean {
if (!rule.attribute || !rule.operator || !rule.value) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Rule must have an attribute, an operator and a value"
)
}
if (!isString(rule.attribute)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Rule attribute must be a string"
)
}
if (!isString(rule.operator)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Rule operator must be a string"
)
}
if (!availableOperators.includes(rule.operator as RuleOperator)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Rule operator ${
rule.operator
} is not supported. Must be one of ${availableOperators.join(", ")}`
)
}
if (rule.operator === RuleOperator.IN || rule.operator === RuleOperator.NIN) {
if (!Array.isArray(rule.value)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Rule value must be an array for in/nin operators"
)
}
} else {
if (Array.isArray(rule.value) || isObject(rule.value)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Rule value must be a string, bool, number value for the selected operator ${rule.operator}`
)
}
}
return true
}
export function normalizeRulesValue<T extends Partial<Rule>>(rules: T[]): void {
rules.forEach((rule) => {
/**
* If a string is provided, then we don't want jsonb to convert to the primitive value based on the RFC
*/
if (rule.value === "true" || rule.value === "false") {
rule.value = rule.value === "true" ? '"true"' : '"false"'
}
return rule
})
}
export function validateAndNormalizeRules<T extends Partial<Rule>>(rules: T[]) {
rules.forEach(validateRule)
normalizeRulesValue(rules)
}
/**
* Validate contextValue set of rules
* @param rules
*/
export function validateRules(rules: Record<string, unknown>[]): boolean {
rules.forEach(validateRule)
return true
}
@@ -0,0 +1,38 @@
{
"compilerOptions": {
"lib": ["es2020"],
"target": "es2020",
"outDir": "./dist",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": false,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"downlevelIteration": true, // to use ES5 specific tooling
"baseUrl": ".",
"resolveJsonModule": true,
"paths": {
"@models": ["./src/models"],
"@services": ["./src/services"],
"@repositories": ["./src/repositories"],
"@types": ["./src/types"],
"@utils": ["./src/utils"]
}
},
"include": ["src"],
"exclude": [
"dist",
"./src/**/__tests__",
"./src/**/__mocks__",
"./src/**/__fixtures__",
"node_modules"
]
}
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": ["src", "integration-tests"],
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"sourceMap": true
}
}