feat(fulfillment): Module service implementation first iteration (#6381)

This commit is contained in:
Adrien de Peretti
2024-02-14 18:43:42 +01:00
committed by GitHub
parent 02c53ec93f
commit fafde4f54d
18 changed files with 3986 additions and 74 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/types": patch
---
feat(fulfillment): Module service implementation first iteration

View File

@@ -1,5 +0,0 @@
describe("noop", function () {
it("should run", function () {
expect(true).toBe(true)
})
})

View File

@@ -2,17 +2,12 @@ import { TestDatabaseUtils } from "medusa-test-utils"
import * as Models from "@models"
const pathToMigrations = "../../src/migrations"
const mikroOrmEntities = Models as unknown as any[]
export const MikroOrmWrapper = TestDatabaseUtils.getMikroOrmWrapper(
mikroOrmEntities,
pathToMigrations
)
export const MikroOrmConfig = TestDatabaseUtils.getMikroOrmConfig(
mikroOrmEntities,
pathToMigrations
null,
process.env.MEDUSA_FULFILLMENT_DB_SCHEMA
)
export const DB_URL = TestDatabaseUtils.getDatabaseURL()

View File

@@ -1 +1,2 @@
export * from "./database"
export * from "./get-init-module-config"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20240214103108 extends Migration {
async up(): Promise<void> {
this.addSql('create table if not exists "fulfillment_address" ("id" text not null, "fulfillment_id" text null, "company" text null, "first_name" text null, "last_name" text null, "address_1" text null, "address_2" text null, "city" text null, "country_code" text null, "province" text null, "postal_code" text null, "phone" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_address_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_address_fulfillment_id" ON "fulfillment_address" (fulfillment_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_address_deleted_at" ON "fulfillment_address" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "fulfillment_set" ("id" text not null, "name" text not null, "type" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_set_pkey" primary key ("id"));');
this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_fulfillment_set_name_unique" ON "fulfillment_set" (name) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_set_deleted_at" ON "fulfillment_set" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "service_provider" ("id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "service_provider_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_service_provider_deleted_at" ON "service_provider" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "service_zone" ("id" text not null, "name" text not null, "metadata" jsonb null, "fulfillment_set_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "service_zone_pkey" primary key ("id"));');
this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_service_zone_name_unique" ON "service_zone" (name) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_service_zone_fulfillment_set_id" ON "service_zone" (fulfillment_set_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_service_zone_deleted_at" ON "service_zone" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "geo_zone" ("id" text not null, "type" text check ("type" in (\'country\', \'province\', \'city\', \'zip\')) not null default \'country\', "country_code" text not null, "province_code" text null, "city" text null, "service_zone_id" text not null, "postal_expression" jsonb null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "geo_zone_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_geo_zone_country_code" ON "geo_zone" (country_code) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_geo_zone_province_code" ON "geo_zone" (province_code) WHERE deleted_at IS NULL AND province_code IS NOT NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_geo_zone_city" ON "geo_zone" (city) WHERE deleted_at IS NULL AND city IS NOT NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_geo_zone_service_zone_id" ON "geo_zone" (service_zone_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_geo_zone_deleted_at" ON "geo_zone" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "shipping_option_type" ("id" text not null, "label" text not null, "description" text null, "code" text not null, "shipping_option_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_type_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_type_shipping_option_id" ON "shipping_option_type" (shipping_option_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_type_deleted_at" ON "shipping_option_type" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "shipping_profile" ("id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_profile_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_profile_deleted_at" ON "shipping_profile" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "shipping_option" ("id" text not null, "name" text not null, "price_type" text check ("price_type" in (\'calculated\', \'flat\')) not null default \'calculated\', "service_zone_id" text not null, "shipping_profile_id" text not null, "service_provider_id" text not null, "shipping_option_type_id" text null, "data" jsonb null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_pkey" primary key ("id"));');
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_shipping_option_type_id_unique" unique ("shipping_option_type_id");');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_service_zone_id" ON "shipping_option" (service_zone_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_shipping_profile_id" ON "shipping_option" (shipping_profile_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_service_provider_id" ON "shipping_option" (service_provider_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_shipping_option_type_id" ON "shipping_option" (shipping_option_type_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_deleted_at" ON "shipping_option" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "shipping_option_rule" ("id" text not null, "attribute" text not null, "operator" text not null, "value" jsonb null, "shipping_option_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_rule_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_rule_deleted_at" ON "shipping_option_rule" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "fulfillment" ("id" text not null, "location_id" text not null, "packed_at" timestamptz null, "shipped_at" timestamptz null, "delivered_at" timestamptz null, "canceled_at" timestamptz null, "data" jsonb null, "provider_id" text not null, "shipping_option_id" text null, "metadata" jsonb null, "delivery_address_id" text not null, "items_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_location_id" ON "fulfillment" (location_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_provider_id" ON "fulfillment" (provider_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_shipping_option_id" ON "fulfillment" (shipping_option_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_deleted_at" ON "fulfillment" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "fulfillment_label" ("id" text not null, "tracking_number" text not null, "tracking_url" text not null, "label_url" text not null, "fulfillment_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_label_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_label_fulfillment_id" ON "fulfillment_label" (fulfillment_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_label_deleted_at" ON "fulfillment_label" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "fulfillment_item" ("id" text not null, "title" text not null, "sku" text not null, "barcode" text not null, "quantity" numeric not null, "line_item_id" text null, "inventory_item_id" text null, "fulfillment_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_item_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_item_fulfillment_id" ON "fulfillment_item" (line_item_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_item_fulfillment_id" ON "fulfillment_item" (inventory_item_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_item_fulfillment_id" ON "fulfillment_item" (fulfillment_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_item_deleted_at" ON "fulfillment_item" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('alter table if exists "service_zone" add constraint "service_zone_fulfillment_set_id_foreign" foreign key ("fulfillment_set_id") references "fulfillment_set" ("id") on update cascade;');
this.addSql('alter table if exists "geo_zone" add constraint "geo_zone_service_zone_id_foreign" foreign key ("service_zone_id") references "service_zone" ("id") on update cascade;');
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_service_zone_id_foreign" foreign key ("service_zone_id") references "service_zone" ("id") on update cascade;');
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_shipping_profile_id_foreign" foreign key ("shipping_profile_id") references "shipping_profile" ("id") on update cascade;');
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_service_provider_id_foreign" foreign key ("service_provider_id") references "service_provider" ("id") on update cascade;');
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_shipping_option_type_id_foreign" foreign key ("shipping_option_type_id") references "shipping_option_type" ("id") on update cascade;');
this.addSql('alter table if exists "shipping_option_rule" add constraint "shipping_option_rule_shipping_option_id_foreign" foreign key ("shipping_option_id") references "shipping_option" ("id") on update cascade;');
this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_shipping_option_id_foreign" foreign key ("shipping_option_id") references "shipping_option" ("id") on update cascade on delete set null;');
this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_provider_id_foreign" foreign key ("provider_id") references "service_provider" ("id") on update cascade;');
this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_delivery_address_id_foreign" foreign key ("delivery_address_id") references "fulfillment_address" ("id") on update cascade;');
this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_items_id_foreign" foreign key ("items_id") references "fulfillment_item" ("id") on update cascade;');
this.addSql('alter table if exists "fulfillment_label" add constraint "fulfillment_label_fulfillment_id_foreign" foreign key ("fulfillment_id") references "fulfillment" ("id") on update cascade;');
this.addSql('alter table if exists "fulfillment_item" add constraint "fulfillment_item_fulfillment_id_foreign" foreign key ("fulfillment_id") references "fulfillment" ("id") on update cascade;');
}
}

View File

@@ -6,11 +6,12 @@ import {
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Filter,
Index,
ManyToMany,
OneToMany,
OnInit,
OptionalProps,
PrimaryKey,
@@ -29,6 +30,15 @@ const deletedAtIndexStatement = createPsqlIndexStatementHelper({
where: "deleted_at IS NOT NULL",
})
const nameIndexName = "IDX_fulfillment_set_name_unique"
const nameIndexStatement = createPsqlIndexStatementHelper({
name: nameIndexName,
tableName: "fulfillment_set",
columns: "name",
unique: true,
where: "deleted_at IS NULL",
})
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class FulfillmentSet {
@@ -38,6 +48,10 @@ export default class FulfillmentSet {
id: string
@Property({ columnType: "text" })
@Index({
name: nameIndexName,
expression: nameIndexStatement,
})
name: string
@Property({ columnType: "text" })
@@ -46,11 +60,9 @@ export default class FulfillmentSet {
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@ManyToMany(() => ServiceZone, "fulfillment_sets", {
owner: true,
pivotTable: "fulfillment_set_service_zones",
joinColumn: "fulfillment_set_id",
inverseJoinColumn: "service_zone_id",
@OneToMany(() => ServiceZone, "fulfillment_set", {
cascade: [Cascade.PERSIST, "soft-remove"] as any,
orphanRemoval: true,
})
service_zones = new Collection<ServiceZone>(this)

View File

@@ -7,12 +7,11 @@ import {
import {
BeforeCreate,
Collection,
Entity,
Enum,
Filter,
Index,
ManyToMany,
ManyToOne,
OnInit,
OptionalProps,
PrimaryKey,
@@ -55,6 +54,14 @@ const cityIndexStatement = createPsqlIndexStatementHelper({
where: "deleted_at IS NULL AND city IS NOT NULL",
})
const serviceZoneIdIndexName = "IDX_geo_zone_service_zone_id"
const serviceZoneIdStatement = createPsqlIndexStatementHelper({
name: serviceZoneIdIndexName,
tableName: "geo_zone",
columns: "service_zone_id",
where: "deleted_at IS NULL",
})
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class GeoZone {
@@ -87,6 +94,13 @@ export default class GeoZone {
@Property({ columnType: "text", nullable: true })
city: string | null = null
@Property({ columnType: "text" })
@Index({
name: serviceZoneIdIndexName,
expression: serviceZoneIdStatement,
})
service_zone_id: string
// TODO: Do we have an example or idea of what would be stored in this field? like lat/long for example?
@Property({ columnType: "jsonb", nullable: true })
postal_expression: Record<string, unknown> | null = null
@@ -94,8 +108,10 @@ export default class GeoZone {
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@ManyToMany(() => ServiceZone, (serviceZone) => serviceZone.geo_zones)
service_zones = new Collection<ServiceZone>(this)
@ManyToOne(() => ServiceZone, {
persist: false,
})
service_zone: ServiceZone
@Property({
onCreate: () => new Date(),
@@ -122,10 +138,12 @@ export default class GeoZone {
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, " fgz")
this.service_zone_id ??= this.service_zone?.id
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "fgz")
this.service_zone_id ??= this.service_zone?.id
}
}

View File

@@ -6,11 +6,12 @@ import {
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Filter,
Index,
ManyToMany,
ManyToOne,
OneToMany,
OnInit,
OptionalProps,
@@ -32,6 +33,23 @@ const deletedAtIndexStatement = createPsqlIndexStatementHelper({
where: "deleted_at IS NOT NULL",
})
const nameIndexName = "IDX_service_zone_name_unique"
const nameIndexStatement = createPsqlIndexStatementHelper({
name: nameIndexName,
tableName: "service_zone",
columns: "name",
unique: true,
where: "deleted_at IS NULL",
})
const fulfillmentSetIdIndexName = "IDX_service_zone_fulfillment_set_id"
const fulfillmentSetIdIndexStatement = createPsqlIndexStatementHelper({
name: fulfillmentSetIdIndexName,
tableName: "service_zone",
columns: "fulfillment_set_id",
where: "deleted_at IS NULL",
})
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class ServiceZone {
@@ -41,22 +59,28 @@ export default class ServiceZone {
id: string
@Property({ columnType: "text" })
@Index({
name: nameIndexName,
expression: nameIndexStatement,
})
name: string
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@ManyToMany(
() => FulfillmentSet,
(fulfillmentSet) => fulfillmentSet.service_zones
)
fulfillment_sets = new Collection<FulfillmentSet>(this)
@Property({ columnType: "text" })
@Index({
name: fulfillmentSetIdIndexName,
expression: fulfillmentSetIdIndexStatement,
})
fulfillment_set_id: string
@ManyToMany(() => GeoZone, "service_zones", {
owner: true,
pivotTable: "service_zone_geo_zones",
joinColumn: "service_zone_id",
inverseJoinColumn: "geo_zone_id",
@ManyToOne(() => FulfillmentSet, { persist: false })
fulfillment_set: FulfillmentSet
@OneToMany(() => GeoZone, "service_zone", {
cascade: [Cascade.PERSIST, "soft-remove"] as any,
orphanRemoval: true,
})
geo_zones = new Collection<GeoZone>(this)
@@ -91,10 +115,12 @@ export default class ServiceZone {
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "serzo")
this.fulfillment_set_id ??= this.fulfillment_set?.id
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "serzo")
this.fulfillment_set_id ??= this.fulfillment_set?.id
}
}

View File

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

View File

@@ -8,20 +8,32 @@ import {
ModulesSdkTypes,
UpdateFulfillmentSetDTO,
} from "@medusajs/types"
import { InjectTransactionManager, ModulesSdkUtils } from "@medusajs/utils"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
MedusaError,
ModulesSdkUtils,
promiseAll,
getSetDifference
} from "@medusajs/utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { FulfillmentSet, ServiceZone, ShippingOption } from "@models"
import { FulfillmentSet, GeoZone, ServiceZone, ShippingOption } from "@models"
const generateMethodForModels = [ServiceZone, ShippingOption]
const generateMethodForModels = [ServiceZone, ShippingOption, GeoZone]
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
fulfillmentService: ModulesSdkTypes.InternalModuleService<any>
fulfillmentSetService: ModulesSdkTypes.InternalModuleService<any>
serviceZoneService: ModulesSdkTypes.InternalModuleService<any>
geoZoneService: ModulesSdkTypes.InternalModuleService<any>
}
export default class FulfillmentModuleService<
TEntity extends FulfillmentSet = FulfillmentSet
TEntity extends FulfillmentSet = FulfillmentSet,
TServiceZoneEntity extends ServiceZone = ServiceZone,
TGeoZoneEntity extends GeoZone = GeoZone
>
extends ModulesSdkUtils.abstractModuleServiceFactory<
InjectedDependencies,
@@ -30,21 +42,31 @@ export default class FulfillmentModuleService<
FulfillmentSet: { dto: FulfillmentTypes.FulfillmentSetDTO }
ServiceZone: { dto: FulfillmentTypes.ServiceZoneDTO }
ShippingOption: { dto: FulfillmentTypes.ShippingOptionDTO }
GeoZone: { dto: FulfillmentTypes.GeoZoneDTO }
}
>(FulfillmentSet, generateMethodForModels, entityNameToLinkableKeysMap)
implements IFulfillmentModuleService
{
protected baseRepository_: DAL.RepositoryService
protected readonly fulfillmentService_: ModulesSdkTypes.InternalModuleService<TEntity>
protected readonly fulfillmentSetService_: ModulesSdkTypes.InternalModuleService<TEntity>
protected readonly serviceZoneService_: ModulesSdkTypes.InternalModuleService<TServiceZoneEntity>
protected readonly geoZoneService_: ModulesSdkTypes.InternalModuleService<TGeoZoneEntity>
constructor(
{ baseRepository, fulfillmentService }: InjectedDependencies,
{
baseRepository,
fulfillmentSetService,
serviceZoneService,
geoZoneService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
// @ts-ignore
super(...arguments)
this.baseRepository_ = baseRepository
this.fulfillmentService_ = fulfillmentService
this.fulfillmentSetService_ = fulfillmentSetService
this.serviceZoneService_ = serviceZoneService
this.geoZoneService_ = geoZoneService
}
__joinerConfig(): ModuleJoinerConfig {
@@ -60,16 +82,41 @@ export default class FulfillmentModuleService<
sharedContext?: Context
): Promise<FulfillmentTypes.FulfillmentSetDTO>
@InjectTransactionManager("baseRepository_")
@InjectManager("baseRepository_")
async create(
data:
| FulfillmentTypes.CreateFulfillmentSetDTO
| FulfillmentTypes.CreateFulfillmentSetDTO[],
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.FulfillmentSetDTO | FulfillmentTypes.FulfillmentSetDTO[]
> {
return []
const createdFulfillmentSets = await this.create_(data, sharedContext)
return await this.baseRepository_.serialize<
FulfillmentTypes.FulfillmentSetDTO | FulfillmentTypes.FulfillmentSetDTO[]
>(createdFulfillmentSets, {
populate: true,
})
}
@InjectTransactionManager("baseRepository_")
protected async create_(
data:
| FulfillmentTypes.CreateFulfillmentSetDTO
| FulfillmentTypes.CreateFulfillmentSetDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity | TEntity[]> {
const data_ = Array.isArray(data) ? data : [data]
const createdFulfillmentSets = await this.fulfillmentSetService_.create(
data_,
sharedContext
)
return Array.isArray(data)
? createdFulfillmentSets
: createdFulfillmentSets[0]
}
createServiceZones(
@@ -81,16 +128,46 @@ export default class FulfillmentModuleService<
sharedContext?: Context
): Promise<FulfillmentTypes.ServiceZoneDTO>
@InjectTransactionManager("baseRepository_")
@InjectManager("baseRepository_")
async createServiceZones(
data:
| FulfillmentTypes.CreateServiceZoneDTO[]
| FulfillmentTypes.CreateServiceZoneDTO,
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[]
> {
return []
const createdServiceZones = await this.createServiceZones_(
data,
sharedContext
)
return await this.baseRepository_.serialize<
FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[]
>(createdServiceZones, {
populate: true,
})
}
@InjectTransactionManager("baseRepository_")
protected async createServiceZones_(
data:
| FulfillmentTypes.CreateServiceZoneDTO[]
| FulfillmentTypes.CreateServiceZoneDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<TServiceZoneEntity | TServiceZoneEntity[]> {
let data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
return []
}
const createdServiceZones = await this.serviceZoneService_.create(
data_,
sharedContext
)
return Array.isArray(data) ? createdServiceZones : createdServiceZones[0]
}
createShippingOptions(
@@ -107,13 +184,42 @@ export default class FulfillmentModuleService<
data:
| FulfillmentTypes.CreateShippingOptionDTO[]
| FulfillmentTypes.CreateShippingOptionDTO,
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.ShippingOptionDTO | FulfillmentTypes.ShippingOptionDTO[]
> {
return []
}
createGeoZones(
data: FulfillmentTypes.CreateGeoZoneDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.GeoZoneDTO[]>
createGeoZones(
data: FulfillmentTypes.CreateGeoZoneDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.GeoZoneDTO>
@InjectManager("baseRepository_")
async createGeoZones(
data:
| FulfillmentTypes.CreateGeoZoneDTO
| FulfillmentTypes.CreateGeoZoneDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<FulfillmentTypes.GeoZoneDTO | FulfillmentTypes.GeoZoneDTO[]> {
const createdGeoZones = await this.geoZoneService_.create(
data,
sharedContext
)
return await this.baseRepository_.serialize<FulfillmentTypes.GeoZoneDTO[]>(
createdGeoZones,
{
populate: true,
}
)
}
update(
data: FulfillmentTypes.UpdateFulfillmentSetDTO[],
sharedContext?: Context
@@ -123,14 +229,170 @@ export default class FulfillmentModuleService<
sharedContext?: Context
): Promise<FulfillmentTypes.FulfillmentSetDTO>
@InjectTransactionManager("baseRepository_")
@InjectManager("baseRepository_")
async update(
data: UpdateFulfillmentSetDTO[] | UpdateFulfillmentSetDTO,
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.FulfillmentSetDTO[] | FulfillmentTypes.FulfillmentSetDTO
> {
return []
const updatedFulfillmentSets = await this.update_(data, sharedContext)
return await this.baseRepository_.serialize<
FulfillmentTypes.FulfillmentSetDTO | FulfillmentTypes.FulfillmentSetDTO[]
>(updatedFulfillmentSets, {
populate: true,
})
}
@InjectTransactionManager("baseRepository_")
protected async update_(
data: UpdateFulfillmentSetDTO[] | UpdateFulfillmentSetDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[] | TEntity> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
return []
}
const fulfillmentSetIds = data_.map((f) => f.id)
if (!fulfillmentSetIds.length) {
return []
}
const fulfillmentSets = await this.fulfillmentSetService_.list(
{
id: fulfillmentSetIds,
},
{
relations: ["service_zones", "service_zones.geo_zones"],
},
sharedContext
)
const fulfillmentSetSet = new Set(fulfillmentSets.map((f) => f.id))
const expectedFulfillmentSetSet = new Set(data_.map((f) => f.id))
const missingFulfillmentSetIds = getSetDifference(
expectedFulfillmentSetSet,
fulfillmentSetSet
)
if (missingFulfillmentSetIds.size) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`The following fulfillment sets does not exists: ${Array.from(
missingFulfillmentSetIds
).join(", ")}`
)
}
const fulfillmentSetMap = new Map<string, TEntity>(
fulfillmentSets.map((f) => [f.id, f])
)
// find service zones to delete
const serviceZoneIdsToDelete: string[] = []
const geoZoneIdsToDelete: string[] = []
data_.forEach((fulfillmentSet) => {
if (fulfillmentSet.service_zones) {
/**
* Detect and delete service zones that are not in the updated
*/
const existingFulfillmentSet = fulfillmentSetMap.get(fulfillmentSet.id)!
const existingServiceZones = existingFulfillmentSet.service_zones
const updatedServiceZones = fulfillmentSet.service_zones
const toDeleteServiceZoneIds = getSetDifference(
new Set(existingServiceZones.map((s) => s.id)),
new Set(
updatedServiceZones
.map((s) => "id" in s && s.id)
.filter((id): id is string => !!id)
)
)
if (toDeleteServiceZoneIds.size) {
serviceZoneIdsToDelete.push(...Array.from(toDeleteServiceZoneIds))
geoZoneIdsToDelete.push(
...existingServiceZones
.filter((s) => toDeleteServiceZoneIds.has(s.id))
.flatMap((s) => s.geo_zones.map((g) => g.id))
)
}
/**
* Detect and re assign service zones to the fulfillment set that are still present
*/
const serviceZonesMap = new Map(
existingFulfillmentSet.service_zones.map((serviceZone) => [
serviceZone.id,
serviceZone,
])
)
const serviceZonesSet = new Set(
existingServiceZones
.map((s) => "id" in s && s.id)
.filter((id): id is string => !!id)
)
const expectedServiceZoneSet = new Set(
fulfillmentSet.service_zones
.map((s) => "id" in s && s.id)
.filter((id): id is string => !!id)
)
const missingServiceZoneIds = getSetDifference(
expectedServiceZoneSet,
serviceZonesSet
)
if (missingServiceZoneIds.size) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`The following service zones does not exists: ${Array.from(
missingServiceZoneIds
).join(", ")}`
)
}
// re assign service zones to the fulfillment set
if (fulfillmentSet.service_zones) {
fulfillmentSet.service_zones = fulfillmentSet.service_zones.map(
(serviceZone) => {
if (!("id" in serviceZone)) {
return serviceZone
}
return serviceZonesMap.get(serviceZone.id)!
}
)
}
}
})
if (serviceZoneIdsToDelete.length) {
await promiseAll([
this.geoZoneService_.delete(
{
id: geoZoneIdsToDelete,
},
sharedContext
),
this.serviceZoneService_.delete(
{
id: serviceZoneIdsToDelete,
},
sharedContext
),
])
}
const updatedFulfillmentSets = await this.fulfillmentSetService_.update(
data_,
sharedContext
)
return Array.isArray(data)
? updatedFulfillmentSets
: updatedFulfillmentSets[0]
}
updateServiceZones(
@@ -142,16 +404,146 @@ export default class FulfillmentModuleService<
sharedContext?: Context
): Promise<FulfillmentTypes.ServiceZoneDTO>
@InjectTransactionManager("baseRepository_")
@InjectManager("baseRepository_")
async updateServiceZones(
data:
| FulfillmentTypes.UpdateServiceZoneDTO[]
| FulfillmentTypes.UpdateServiceZoneDTO,
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.ServiceZoneDTO[] | FulfillmentTypes.ServiceZoneDTO
> {
return []
const updatedServiceZones = await this.updateServiceZones_(
data,
sharedContext
)
return await this.baseRepository_.serialize<
FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[]
>(updatedServiceZones, {
populate: true,
})
}
@InjectTransactionManager("baseRepository_")
protected async updateServiceZones_(
data:
| FulfillmentTypes.UpdateServiceZoneDTO[]
| FulfillmentTypes.UpdateServiceZoneDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<TServiceZoneEntity | TServiceZoneEntity[]> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
return []
}
const serviceZoneIds = data_.map((s) => s.id)
if (!serviceZoneIds.length) {
return []
}
const serviceZones = await this.serviceZoneService_.list(
{
id: serviceZoneIds,
},
{
relations: ["geo_zones"],
},
sharedContext
)
const serviceZoneSet = new Set(serviceZones.map((s) => s.id))
const expectedServiceZoneSet = new Set(data_.map((s) => s.id))
const missingServiceZoneIds = getSetDifference(
expectedServiceZoneSet,
serviceZoneSet
)
if (missingServiceZoneIds.size) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`The following service zones does not exists: ${Array.from(
missingServiceZoneIds
).join(", ")}`
)
}
const serviceZoneMap = new Map<string, TServiceZoneEntity>(
serviceZones.map((s) => [s.id, s])
)
const serviceZoneIdsToDelete: string[] = []
const geoZoneIdsToDelete: string[] = []
data_.forEach((serviceZone) => {
if (serviceZone.geo_zones) {
const existingServiceZone = serviceZoneMap.get(serviceZone.id)!
const existingGeoZones = existingServiceZone.geo_zones
const updatedGeoZones = serviceZone.geo_zones
const toDeleteGeoZoneIds = getSetDifference(
new Set(existingGeoZones.map((g) => g.id)),
new Set(
updatedGeoZones
.map((g) => "id" in g && g.id)
.filter((id): id is string => !!id)
)
)
if (toDeleteGeoZoneIds.size) {
geoZoneIdsToDelete.push(...Array.from(toDeleteGeoZoneIds))
}
const geoZonesMap = new Map(
existingServiceZone.geo_zones.map((geoZone) => [geoZone.id, geoZone])
)
const geoZonesSet = new Set(
existingGeoZones
.map((g) => "id" in g && g.id)
.filter((id): id is string => !!id)
)
const expectedGeoZoneSet = new Set(
serviceZone.geo_zones
.map((g) => "id" in g && g.id)
.filter((id): id is string => !!id)
)
const missingGeoZoneIds = getSetDifference(
expectedGeoZoneSet,
geoZonesSet
)
if (missingGeoZoneIds.size) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`The following geo zones does not exists: ${Array.from(
missingGeoZoneIds
).join(", ")}`
)
}
serviceZone.geo_zones = serviceZone.geo_zones.map((geoZone) => {
if (!("id" in geoZone)) {
return geoZone
}
return geoZonesMap.get(geoZone.id)!
})
}
})
if (geoZoneIdsToDelete.length) {
await this.geoZoneService_.delete(
{
id: geoZoneIdsToDelete,
},
sharedContext
)
}
const updatedServiceZones = await this.serviceZoneService_.update(
data_,
sharedContext
)
return Array.isArray(data) ? updatedServiceZones : updatedServiceZones[0]
}
updateShippingOptions(
@@ -168,10 +560,40 @@ export default class FulfillmentModuleService<
data:
| FulfillmentTypes.UpdateShippingOptionDTO[]
| FulfillmentTypes.UpdateShippingOptionDTO,
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.ShippingOptionDTO[] | FulfillmentTypes.ShippingOptionDTO
> {
return []
}
updateGeoZones(
data: FulfillmentTypes.UpdateGeoZoneDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.GeoZoneDTO[]>
updateGeoZones(
data: FulfillmentTypes.UpdateGeoZoneDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.GeoZoneDTO>
@InjectManager("baseRepository_")
async updateGeoZones(
data:
| FulfillmentTypes.UpdateGeoZoneDTO
| FulfillmentTypes.UpdateGeoZoneDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<FulfillmentTypes.GeoZoneDTO | FulfillmentTypes.GeoZoneDTO[]> {
const updatedGeoZones = await this.geoZoneService_.update(
data,
sharedContext
)
const serialized = await this.baseRepository_.serialize<
FulfillmentTypes.GeoZoneDTO[]
>(updatedGeoZones, {
populate: true,
})
return Array.isArray(data) ? serialized : serialized[0]
}
}

View File

@@ -3,10 +3,12 @@ import { CreateServiceZoneDTO } from "./service-zone"
export interface CreateFulfillmentSetDTO {
name: string
type: string
service_zones: Omit<CreateServiceZoneDTO, "fulfillment_set_id">[]
service_zones?: Omit<CreateServiceZoneDTO, 'fulfillment_set_id'>[]
}
export interface UpdateFulfillmentSetDTO
extends Partial<CreateFulfillmentSetDTO> {
export interface UpdateFulfillmentSetDTO {
id: string
name?: string
type?: string
service_zones?: (Omit<CreateServiceZoneDTO, 'fulfillment_set_id'> | { id: string })[]
}

View File

@@ -9,24 +9,20 @@ interface CreateGeoZoneBaseDTO {
interface CreateCountryGeoZoneDTO extends CreateGeoZoneBaseDTO {
type: "country"
country_code: string
}
interface CreateProvinceGeoZoneDTO extends CreateGeoZoneBaseDTO {
type: "province"
country_code: string
province_code: string
}
interface CreateCityGeoZoneDTO extends CreateGeoZoneBaseDTO {
type: "city"
country_code: string
city: string
}
interface CreateZipGeoZoneDTO extends CreateGeoZoneBaseDTO {
type: "zip"
country_code: string
postal_expression: Record<string, any>
}
@@ -42,24 +38,20 @@ interface UpdateGeoZoneBaseDTO extends Partial<CreateGeoZoneBaseDTO> {
interface UpdateCountryGeoZoneDTO extends UpdateGeoZoneBaseDTO {
type: "country"
country_code: string
}
interface UpdateProvinceGeoZoneDTO extends UpdateGeoZoneBaseDTO {
type: "province"
country_code: string
province_code: string
}
interface UpdateCityGeoZoneDTO extends UpdateGeoZoneBaseDTO {
type: "city"
country_code: string
city: string
}
interface UpdateZipGeoZoneDTO extends UpdateGeoZoneBaseDTO {
type: "zip"
country_code: string
postal_expression: Record<string, any>
}

View File

@@ -1,14 +1,13 @@
import { CreateGeoZoneDTO, UpdateGeoZoneDTO } from "./geo-zone"
import { CreateGeoZoneDTO } from "./geo-zone"
export interface CreateServiceZoneDTO {
fulfillment_set_id: string
name: string
geo_zones: (
| Omit<CreateGeoZoneDTO, "service_zone_id">
| Omit<UpdateGeoZoneDTO, "service_zone_id">
)[]
fulfillment_set_id: string
geo_zones?: Omit<CreateGeoZoneDTO, "service_zone_id">[]
}
export interface UpdateServiceZoneDTO extends Partial<CreateServiceZoneDTO> {
export interface UpdateServiceZoneDTO {
id: string
name?: string
geo_zones?: (Omit<CreateGeoZoneDTO, "service_zone_id"> | { id: string })[]
}

View File

@@ -1,9 +1,11 @@
import { IModuleService } from "../modules-sdk"
import {
FilterableFulfillmentSetProps,
FilterableGeoZoneProps,
FilterableServiceZoneProps,
FilterableShippingOptionProps,
FulfillmentSetDTO,
GeoZoneDTO,
ServiceZoneDTO,
ShippingOptionDTO,
} from "./common"
@@ -12,9 +14,11 @@ import { Context } from "../shared-context"
import { RestoreReturn, SoftDeleteReturn } from "../dal"
import {
CreateFulfillmentSetDTO,
CreateGeoZoneDTO,
CreateServiceZoneDTO,
CreateShippingOptionDTO,
UpdateFulfillmentSetDTO,
UpdateGeoZoneDTO,
UpdateServiceZoneDTO,
UpdateShippingOptionDTO,
} from "./mutations"
@@ -62,6 +66,20 @@ export interface IFulfillmentModuleService extends IModuleService {
sharedContext?: Context
): Promise<ShippingOptionDTO>
/**
* Create a new geo zone
* @param data
* @param sharedContext
*/
createGeoZones(
data: CreateGeoZoneDTO[],
sharedContext?: Context
): Promise<GeoZoneDTO[]>
createGeoZones(
data: CreateGeoZoneDTO,
sharedContext?: Context
): Promise<GeoZoneDTO>
/**
* Update a fulfillment set
* @param data
@@ -104,6 +122,20 @@ export interface IFulfillmentModuleService extends IModuleService {
sharedContext?: Context
): Promise<ShippingOptionDTO>
/**
* Update a geo zone
* @param data
* @param sharedContext
*/
updateGeoZones(
data: UpdateGeoZoneDTO[],
sharedContext?: Context
): Promise<GeoZoneDTO[]>
updateGeoZones(
data: UpdateGeoZoneDTO,
sharedContext?: Context
): Promise<GeoZoneDTO>
/**
* Delete a fulfillment set
* @param ids
@@ -128,6 +160,14 @@ export interface IFulfillmentModuleService extends IModuleService {
deleteShippingOptions(ids: string[], sharedContext?: Context): Promise<void>
deleteShippingOptions(id: string, sharedContext?: Context): Promise<void>
/**
* Delete a geo zone
* @param ids
* @param sharedContext
*/
deleteGeoZones(ids: string[], sharedContext?: Context): Promise<void>
deleteGeoZones(id: string, sharedContext?: Context): Promise<void>
/**
* Retrieve a fulfillment set
* @param id
@@ -164,6 +204,18 @@ export interface IFulfillmentModuleService extends IModuleService {
sharedContext?: Context
): Promise<ShippingOptionDTO>
/**
* Retrieve a geo zone
* @param id
* @param config
* @param sharedContext
*/
retrieveGeoZone(
id: string,
config?: FindConfig<GeoZoneDTO>,
sharedContext?: Context
): Promise<GeoZoneDTO>
/**
* List fulfillment sets
* @param filters
@@ -200,6 +252,18 @@ export interface IFulfillmentModuleService extends IModuleService {
sharedContext?: Context
): Promise<ShippingOptionDTO[]>
/**
* List geo zones
* @param filters
* @param config
* @param sharedContext
*/
listGeoZones(
filters?: FilterableGeoZoneProps,
config?: FindConfig<GeoZoneDTO>,
sharedContext?: Context
): Promise<GeoZoneDTO[]>
/**
* List and count fulfillment sets
* @param filters
@@ -236,6 +300,18 @@ export interface IFulfillmentModuleService extends IModuleService {
sharedContext?: Context
): Promise<[ShippingOptionDTO[], number]>
/**
* List and count geo zones
* @param filters
* @param config
* @param sharedContext
*/
listAndCountGeoZones(
filters?: FilterableGeoZoneProps,
config?: FindConfig<GeoZoneDTO>,
sharedContext?: Context
): Promise<[GeoZoneDTO[], number]>
/**
* Soft delete fulfillment sets
* @param fulfillmentIds
@@ -272,6 +348,18 @@ export interface IFulfillmentModuleService extends IModuleService {
sharedContext?: Context
): Promise<Record<string, string[]> | void>
/**
* Soft delete geo zones
* @param geoZoneIds
* @param config
* @param sharedContext
*/
softDeleteGeoZones<TReturnableLinkableKeys extends string = string>(
geoZoneIds: string[],
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
restore<TReturnableLinkableKeys extends string = string>(
fulfillmentIds: string[],
config?: RestoreReturn<TReturnableLinkableKeys>,

View File

@@ -0,0 +1,19 @@
/**
* Get the difference between two sets. The difference is the elements that are in the original set but not in the compare set.
* @param orignalSet
* @param compareSet
*/
export function getSetDifference<T>(
orignalSet: Set<T>,
compareSet: Set<T>
): Set<T> {
const difference = new Set<T>()
orignalSet.forEach((element) => {
if (!compareSet.has(element)) {
difference.add(element)
}
})
return difference
}

View File

@@ -1,5 +1,6 @@
export * from "./alter-columns-helper"
export * from "./array-difference"
export * from "./get-set-difference"
export * from "./build-query"
export * from "./camel-to-snake-case"
export * from "./container"
@@ -46,4 +47,3 @@ export * from "./to-pascal-case"
export * from "./transaction"
export * from "./upper-case-first"
export * from "./wrap-handler"