chore(): Reorganize modules (#7210)

**What**
Move all modules to the modules directory
This commit is contained in:
Adrien de Peretti
2024-05-02 17:33:34 +02:00
committed by GitHub
parent 7a351eef09
commit 4eae25e1ef
870 changed files with 91 additions and 62 deletions

View File

@@ -0,0 +1,13 @@
import { initializeFactory, Modules } from "@medusajs/modules-sdk"
import { moduleDefinition } from "./module-definition"
export * from "./models"
export * from "./services"
export const initialize = initializeFactory({
moduleName: Modules.TAX,
moduleDefinition,
})
export const runMigrations = moduleDefinition.runMigrations
export const revertMigration = moduleDefinition.revertMigration
export default moduleDefinition

View File

@@ -0,0 +1,57 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { MapToConfig } from "@medusajs/utils"
import { TaxProvider, TaxRate, TaxRateRule, TaxRegion } from "@models"
export const LinkableKeys: Record<string, string> = {
tax_rate_id: TaxRate.name,
tax_region_id: TaxRegion.name,
tax_rate_rule_id: TaxRateRule.name,
tax_provider_id: TaxProvider.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.TAX,
primaryKeys: ["id"],
linkableKeys: LinkableKeys,
alias: [
{
name: ["tax_rate", "tax_rates"],
args: {
entity: TaxRate.name,
},
},
{
name: ["tax_region", "tax_regions"],
args: {
entity: TaxRegion.name,
methodSuffix: "TaxRegions",
},
},
{
name: ["tax_rate_rule", "tax_rate_rules"],
args: {
entity: TaxRateRule.name,
methodSuffix: "TaxRateRules",
},
},
{
name: ["tax_provider", "tax_providers"],
args: {
entity: TaxProvider.name,
methodSuffix: "TaxProviders",
},
},
],
} as ModuleJoinerConfig

View File

@@ -0,0 +1,43 @@
import { moduleProviderLoader } from "@medusajs/modules-sdk"
import { LoaderOptions, ModuleProvider, ModulesSdkTypes } from "@medusajs/types"
import { Lifetime, asFunction } from "awilix"
import * as providers from "../providers"
const registrationFn = async (klass, container, pluginOptions) => {
container.register({
[`tp_${klass.identifier}`]: asFunction(
(cradle) => new klass(cradle, pluginOptions),
{ lifetime: klass.LIFE_TIME || Lifetime.SINGLETON }
),
})
container.registerAdd(
"tax_providers",
asFunction((cradle) => new klass(cradle, pluginOptions), {
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
})
)
}
export default async ({
container,
options,
}: LoaderOptions<
(
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
) & { providers: ModuleProvider[] }
>): Promise<void> => {
// Local providers
for (const provider of Object.values(providers)) {
await registrationFn(provider, container, {})
}
await moduleProviderLoader({
container,
providers: options?.providers || [],
registerServiceFn: registrationFn,
})
}

View File

@@ -0,0 +1,558 @@
{
"namespaces": [
"public"
],
"name": "public",
"tables": [
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"is_enabled": {
"name": "is_enabled",
"type": "boolean",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "true",
"mappedType": "boolean"
}
},
"name": "tax_provider",
"schema": "public",
"indexes": [
{
"keyName": "tax_provider_pkey",
"columnNames": [
"id"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {}
},
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"provider_id": {
"name": "provider_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"country_code": {
"name": "country_code",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"province_code": {
"name": "province_code",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"parent_id": {
"name": "parent_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"created_by": {
"name": "created_by",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"deleted_at": {
"name": "deleted_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
}
},
"name": "tax_region",
"schema": "public",
"indexes": [
{
"columnNames": [
"parent_id"
],
"composite": false,
"keyName": "IDX_tax_region_parent_id",
"primary": false,
"unique": false
},
{
"keyName": "IDX_tax_region_deleted_at",
"columnNames": [
"deleted_at"
],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_tax_region_deleted_at\" ON \"tax_region\" (deleted_at) WHERE deleted_at IS NOT NULL"
},
{
"keyName": "IDX_tax_region_unique_country_province",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_tax_region_unique_country_province\" ON \"tax_region\" (country_code, province_code)"
},
{
"keyName": "tax_region_pkey",
"columnNames": [
"id"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [
{
"name": "CK_tax_region_country_top_level",
"expression": "parent_id IS NULL OR province_code IS NOT NULL",
"definition": "check ((parent_id IS NULL OR province_code IS NOT NULL))"
},
{
"name": "CK_tax_region_provider_top_level",
"expression": "parent_id IS NULL OR provider_id IS NULL",
"definition": "check ((parent_id IS NULL OR provider_id IS NULL))"
}
],
"foreignKeys": {
"tax_region_provider_id_foreign": {
"constraintName": "tax_region_provider_id_foreign",
"columnNames": [
"provider_id"
],
"localTableName": "public.tax_region",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.tax_provider",
"deleteRule": "set null",
"updateRule": "cascade"
},
"tax_region_parent_id_foreign": {
"constraintName": "tax_region_parent_id_foreign",
"columnNames": [
"parent_id"
],
"localTableName": "public.tax_region",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.tax_region",
"deleteRule": "cascade",
"updateRule": "cascade"
}
}
},
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"rate": {
"name": "rate",
"type": "real",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "float"
},
"code": {
"name": "code",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"name": {
"name": "name",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"is_default": {
"name": "is_default",
"type": "bool",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "false",
"mappedType": "boolean"
},
"is_combinable": {
"name": "is_combinable",
"type": "bool",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "false",
"mappedType": "boolean"
},
"tax_region_id": {
"name": "tax_region_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"created_by": {
"name": "created_by",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"deleted_at": {
"name": "deleted_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
}
},
"name": "tax_rate",
"schema": "public",
"indexes": [
{
"keyName": "IDX_tax_rate_tax_region_id",
"columnNames": [
"tax_region_id"
],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_tax_rate_tax_region_id\" ON \"tax_rate\" (tax_region_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_tax_rate_deleted_at",
"columnNames": [
"deleted_at"
],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_tax_rate_deleted_at\" ON \"tax_rate\" (deleted_at) WHERE deleted_at IS NOT NULL"
},
{
"keyName": "IDX_single_default_region",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_single_default_region\" ON \"tax_rate\" (tax_region_id) WHERE is_default = true AND deleted_at IS NULL"
},
{
"keyName": "tax_rate_pkey",
"columnNames": [
"id"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"tax_rate_tax_region_id_foreign": {
"constraintName": "tax_rate_tax_region_id_foreign",
"columnNames": [
"tax_region_id"
],
"localTableName": "public.tax_rate",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.tax_region",
"deleteRule": "cascade",
"updateRule": "cascade"
}
}
},
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"tax_rate_id": {
"name": "tax_rate_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"reference_id": {
"name": "reference_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"reference": {
"name": "reference",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"created_by": {
"name": "created_by",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"deleted_at": {
"name": "deleted_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
}
},
"name": "tax_rate_rule",
"schema": "public",
"indexes": [
{
"keyName": "IDX_tax_rate_rule_tax_rate_id",
"columnNames": [
"tax_rate_id"
],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_tax_rate_rule_tax_rate_id\" ON \"tax_rate_rule\" (tax_rate_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_tax_rate_rule_reference_id",
"columnNames": [
"reference_id"
],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_tax_rate_rule_reference_id\" ON \"tax_rate_rule\" (reference_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_tax_rate_rule_deleted_at",
"columnNames": [
"deleted_at"
],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_tax_rate_rule_deleted_at\" ON \"tax_rate_rule\" (deleted_at) WHERE deleted_at IS NOT NULL"
},
{
"keyName": "IDX_tax_rate_rule_unique_rate_reference",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_tax_rate_rule_unique_rate_reference\" ON \"tax_rate_rule\" (tax_rate_id, reference_id) WHERE deleted_at IS NULL"
},
{
"keyName": "tax_rate_rule_pkey",
"columnNames": [
"id"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"tax_rate_rule_tax_rate_id_foreign": {
"constraintName": "tax_rate_rule_tax_rate_id_foreign",
"columnNames": [
"tax_rate_id"
],
"localTableName": "public.tax_rate_rule",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.tax_rate",
"deleteRule": "cascade",
"updateRule": "cascade"
}
}
}
]
}

View File

@@ -0,0 +1,114 @@
import { generatePostgresAlterColummnIfExistStatement } from "@medusajs/utils"
import { Migration } from "@mikro-orm/migrations"
export class Migration20240227090331 extends Migration {
async up(): Promise<void> {
// Adjust tax_provider table
this.addSql(
`ALTER TABLE IF EXISTS "tax_provider" ADD COLUMN IF NOT EXISTS "is_enabled" bool not null default true;`
)
this.addSql(
`CREATE TABLE IF NOT EXISTS "tax_provider" ("id" text not null, "is_enabled" boolean not null default true, CONSTRAINT "tax_provider_pkey" PRIMARY KEY ("id"));`
)
// Create or update tax_region table
this.addSql(
`CREATE TABLE IF NOT EXISTS "tax_region" ("id" text not null, "provider_id" text null, "country_code" text not null, "province_code" text null, "parent_id" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "created_by" text null, "deleted_at" timestamptz null, CONSTRAINT "tax_region_pkey" PRIMARY KEY ("id"), CONSTRAINT "CK_tax_region_country_top_level" CHECK (parent_id IS NULL OR province_code IS NOT NULL), CONSTRAINT "CK_tax_region_provider_top_level" CHECK (parent_id IS NULL OR provider_id IS NULL));`
)
// Indexes for tax_region
this.addSql(
`CREATE INDEX IF NOT EXISTS "IDX_tax_region_parent_id" ON "tax_region" ("parent_id");`
)
this.addSql(
`CREATE INDEX IF NOT EXISTS "IDX_tax_region_deleted_at" ON "tax_region" ("deleted_at") WHERE deleted_at IS NOT NULL;`
)
this.addSql(
`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_tax_region_unique_country_province" ON "tax_region" ("country_code", "province_code");`
)
// Old foreign key on region_id
this.addSql(
generatePostgresAlterColummnIfExistStatement(
"tax_rate",
["region_id"],
"DROP NOT NULL"
)
)
this.addSql(
`ALTER TABLE IF EXISTS "tax_rate" DROP CONSTRAINT IF EXISTS "FK_b95a1e03b051993d208366cb960";`
)
this.addSql(
`ALTER TABLE IF EXISTS "tax_rate" ADD COLUMN IF NOT EXISTS "tax_region_id" text not null;`
)
this.addSql(
`ALTER TABLE IF EXISTS "tax_rate" ADD COLUMN IF NOT EXISTS "deleted_at" timestamptz null;`
)
this.addSql(
`ALTER TABLE IF EXISTS "tax_rate" ADD COLUMN IF NOT EXISTS "created_by" text null;`
)
this.addSql(
`ALTER TABLE IF EXISTS "tax_rate" ADD COLUMN IF NOT EXISTS "is_default" bool not null default false;`
)
this.addSql(
`ALTER TABLE IF EXISTS "tax_rate" ADD COLUMN IF NOT EXISTS "is_combinable" bool not null default false;`
)
this.addSql(
`create table if not exists "tax_rate" ("id" text not null, "rate" real null, "code" text null, "name" text not null, "is_default" bool not null default false, "is_combinable" bool not null default false, "tax_region_id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "created_by" text null, "deleted_at" timestamptz null, constraint "tax_rate_pkey" primary key ("id"));`
)
// Indexes for tax_rate
this.addSql(
`CREATE INDEX IF NOT EXISTS "IDX_tax_rate_tax_region_id" ON "tax_rate" ("tax_region_id") WHERE deleted_at IS NULL;`
)
this.addSql(
`CREATE INDEX IF NOT EXISTS "IDX_tax_rate_deleted_at" ON "tax_rate" ("deleted_at") WHERE deleted_at IS NOT NULL;`
)
this.addSql(
`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_single_default_region" ON "tax_rate" ("tax_region_id") WHERE is_default = true AND deleted_at IS NULL;`
)
// Adjust or create tax_rate_rule table
this.addSql(
`CREATE TABLE IF NOT EXISTS "tax_rate_rule" ("id" text not null, "tax_rate_id" text not null, "reference_id" text not null, "reference" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "created_by" text null, "deleted_at" timestamptz null, CONSTRAINT "tax_rate_rule_pkey" PRIMARY KEY ("id"));`
)
// Indexes for tax_rate_rule
this.addSql(
`CREATE INDEX IF NOT EXISTS "IDX_tax_rate_rule_tax_rate_id" ON "tax_rate_rule" ("tax_rate_id") WHERE deleted_at IS NULL;`
)
this.addSql(
`CREATE INDEX IF NOT EXISTS "IDX_tax_rate_rule_reference_id" ON "tax_rate_rule" ("reference_id") WHERE deleted_at IS NULL;`
)
this.addSql(
`CREATE INDEX IF NOT EXISTS "IDX_tax_rate_rule_deleted_at" ON "tax_rate_rule" ("deleted_at") WHERE deleted_at IS NOT NULL;`
)
this.addSql(
`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_tax_rate_rule_unique_rate_reference" ON "tax_rate_rule" ("tax_rate_id", "reference_id") WHERE deleted_at IS NULL;`
)
// Foreign keys adjustments
this.addSql(
`ALTER TABLE "tax_region" ADD CONSTRAINT "FK_tax_region_provider_id" FOREIGN KEY ("provider_id") REFERENCES "tax_provider" ("id") ON DELETE SET NULL;`
)
this.addSql(
`ALTER TABLE "tax_region" ADD CONSTRAINT "FK_tax_region_parent_id" FOREIGN KEY ("parent_id") REFERENCES "tax_region" ("id") ON DELETE CASCADE;`
)
this.addSql(
`ALTER TABLE "tax_rate" ADD CONSTRAINT "FK_tax_rate_tax_region_id" FOREIGN KEY ("tax_region_id") REFERENCES "tax_region" ("id") ON DELETE CASCADE;`
)
this.addSql(
`ALTER TABLE "tax_rate_rule" ADD CONSTRAINT "FK_tax_rate_rule_tax_rate_id" FOREIGN KEY ("tax_rate_id") REFERENCES "tax_rate" ("id") ON DELETE CASCADE;`
)
// remove old tax related foreign key constraints
this.addSql(
`ALTER TABLE IF EXISTS "product_tax_rate" DROP CONSTRAINT IF EXISTS "FK_346e0016cf045b9980747747645";`
)
this.addSql(
`ALTER TABLE IF EXISTS "product_type_tax_rate" DROP CONSTRAINT IF EXISTS "FK_ece65a774192b34253abc4cd672";`
)
this.addSql(
`ALTER TABLE IF EXISTS "shipping_tax_rate" DROP CONSTRAINT IF EXISTS "FK_346e0016cf045b9980747747645";`
)
}
}

View File

@@ -0,0 +1,4 @@
export { default as TaxRate } from "./tax-rate"
export { default as TaxRegion } from "./tax-region"
export { default as TaxRateRule } from "./tax-rate-rule"
export { default as TaxProvider } from "./tax-provider"

View File

@@ -0,0 +1,16 @@
import { Entity, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core"
const TABLE_NAME = "tax_provider"
@Entity({ tableName: TABLE_NAME })
export default class TaxProvider {
[OptionalProps]?: "is_enabled"
@PrimaryKey({ columnType: "text" })
id: string
@Property({
default: true,
columnType: "boolean",
})
is_enabled: boolean = true
}

View File

@@ -0,0 +1,116 @@
import { DAL } from "@medusajs/types"
import {
DALUtils,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/utils"
import {
Cascade,
Entity,
ManyToOne,
PrimaryKey,
Property,
Filter,
OptionalProps,
BeforeCreate,
OnInit,
} from "@mikro-orm/core"
import TaxRate from "./tax-rate"
const TABLE_NAME = "tax_rate_rule"
type OptionalRuleProps = DAL.SoftDeletableEntityDateColumns
const taxRateIdIndexName = "IDX_tax_rate_rule_tax_rate_id"
const taxRateIdIndexStatement = createPsqlIndexStatementHelper({
name: taxRateIdIndexName,
tableName: TABLE_NAME,
columns: "tax_rate_id",
where: "deleted_at IS NULL",
})
const referenceIdIndexName = "IDX_tax_rate_rule_reference_id"
const referenceIdIndexStatement = createPsqlIndexStatementHelper({
name: referenceIdIndexName,
tableName: TABLE_NAME,
columns: "reference_id",
where: "deleted_at IS NULL",
})
export const uniqueRateReferenceIndexName =
"IDX_tax_rate_rule_unique_rate_reference"
const uniqueRateReferenceIndexStatement = createPsqlIndexStatementHelper({
name: uniqueRateReferenceIndexName,
tableName: TABLE_NAME,
columns: ["tax_rate_id", "reference_id"],
unique: true,
where: "deleted_at IS NULL",
})
@Entity({ tableName: TABLE_NAME })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
@uniqueRateReferenceIndexStatement.MikroORMIndex()
export default class TaxRateRule {
[OptionalProps]?: OptionalRuleProps
@PrimaryKey({ columnType: "text" })
id!: string
@ManyToOne(() => TaxRate, {
type: "text",
fieldName: "tax_rate_id",
mapToPk: true,
onDelete: "cascade",
})
@taxRateIdIndexStatement.MikroORMIndex()
tax_rate_id: string
@Property({ columnType: "text" })
@referenceIdIndexStatement.MikroORMIndex()
reference_id: string
@Property({ columnType: "text" })
reference: string
@ManyToOne(() => TaxRate, { persist: false })
tax_rate: TaxRate
@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: "text", nullable: true })
created_by: string | null = null
@createPsqlIndexStatementHelper({
tableName: TABLE_NAME,
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
}).MikroORMIndex()
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "txrule")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "txrule")
}
}

View File

@@ -0,0 +1,126 @@
import { DAL } from "@medusajs/types"
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
Searchable,
} from "@medusajs/utils"
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Filter,
ManyToOne,
OneToMany,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import TaxRateRule from "./tax-rate-rule"
import TaxRegion from "./tax-region"
type OptionalTaxRateProps = DAL.SoftDeletableEntityDateColumns
const TABLE_NAME = "tax_rate"
export const singleDefaultRegionIndexName = "IDX_single_default_region"
const singleDefaultRegionIndexStatement = createPsqlIndexStatementHelper({
name: singleDefaultRegionIndexName,
tableName: TABLE_NAME,
columns: "tax_region_id",
unique: true,
where: "is_default = true AND deleted_at IS NULL",
})
const taxRegionIdIndexName = "IDX_tax_rate_tax_region_id"
const taxRegionIdIndexStatement = createPsqlIndexStatementHelper({
name: taxRegionIdIndexName,
tableName: TABLE_NAME,
columns: "tax_region_id",
where: "deleted_at IS NULL",
})
@singleDefaultRegionIndexStatement.MikroORMIndex()
@Entity({ tableName: TABLE_NAME })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class TaxRate {
[OptionalProps]?: OptionalTaxRateProps
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "real", nullable: true })
rate: number | null = null
@Searchable()
@Property({ columnType: "text", nullable: true })
code: string | null = null
@Searchable()
@Property({ columnType: "text" })
name: string
@Property({ columnType: "bool", default: false })
is_default = false
@Property({ columnType: "bool", default: false })
is_combinable = false
@ManyToOne(() => TaxRegion, {
columnType: "text",
fieldName: "tax_region_id",
mapToPk: true,
onDelete: "cascade",
})
@taxRegionIdIndexStatement.MikroORMIndex()
tax_region_id: string
@ManyToOne({ entity: () => TaxRegion, persist: false })
tax_region: TaxRegion
@OneToMany(() => TaxRateRule, (rule) => rule.tax_rate, {
cascade: ["soft-remove" as Cascade],
})
rules = new Collection<TaxRateRule>(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
@Property({ columnType: "text", nullable: true })
created_by: string | null = null
@createPsqlIndexStatementHelper({
tableName: TABLE_NAME,
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
}).MikroORMIndex()
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "txr")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "txr")
}
}

View File

@@ -0,0 +1,138 @@
import { DAL } from "@medusajs/types"
import {
DALUtils,
Searchable,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/utils"
import {
BeforeCreate,
Cascade,
Check,
Collection,
Entity,
Filter,
ManyToOne,
OnInit,
OneToMany,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import TaxProvider from "./tax-provider"
import TaxRate from "./tax-rate"
type OptionalTaxRegionProps = DAL.SoftDeletableEntityDateColumns
const TABLE_NAME = "tax_region"
export const countryCodeProvinceIndexName =
"IDX_tax_region_unique_country_province"
const countryCodeProvinceIndexStatement = createPsqlIndexStatementHelper({
name: countryCodeProvinceIndexName,
tableName: TABLE_NAME,
columns: ["country_code", "province_code"],
unique: true,
})
export const taxRegionProviderTopLevelCheckName =
"CK_tax_region_provider_top_level"
export const taxRegionCountryTopLevelCheckName =
"CK_tax_region_country_top_level"
@Check({
name: taxRegionProviderTopLevelCheckName,
expression: `parent_id IS NULL OR provider_id IS NULL`,
})
@Check({
name: taxRegionCountryTopLevelCheckName,
expression: `parent_id IS NULL OR province_code IS NOT NULL`,
})
@countryCodeProvinceIndexStatement.MikroORMIndex()
@Entity({ tableName: TABLE_NAME })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class TaxRegion {
[OptionalProps]?: OptionalTaxRegionProps
@PrimaryKey({ columnType: "text" })
id!: string
@ManyToOne(() => TaxProvider, {
fieldName: "provider_id",
mapToPk: true,
nullable: true,
})
provider_id: string | null = null
@ManyToOne(() => TaxProvider, { persist: false })
provider: TaxProvider
@Searchable()
@Property({ columnType: "text" })
country_code: string
@Searchable()
@Property({ columnType: "text", nullable: true })
province_code: string | null = null
@ManyToOne(() => TaxRegion, {
index: "IDX_tax_region_parent_id",
fieldName: "parent_id",
onDelete: "cascade",
mapToPk: true,
nullable: true,
})
parent_id: string | null = null
@ManyToOne(() => TaxRegion, { persist: false })
parent: TaxRegion
@OneToMany(() => TaxRate, (label) => label.tax_region, {
cascade: ["soft-remove" as Cascade],
})
tax_rates = new Collection<TaxRate>(this)
@OneToMany(() => TaxRegion, (label) => label.parent, {
cascade: ["soft-remove" as Cascade],
})
children = new Collection<TaxRegion>(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
@Property({ columnType: "text", nullable: true })
created_by: string | null = null
@createPsqlIndexStatementHelper({
tableName: TABLE_NAME,
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
}).MikroORMIndex()
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "txreg")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "txreg")
}
}

View File

@@ -0,0 +1,44 @@
import { MikroOrmBaseRepository, ModulesSdkUtils } from "@medusajs/utils"
import { Modules } from "@medusajs/modules-sdk"
import { ModuleExports } from "@medusajs/types"
import * as Models from "@models"
import * as ModuleModels from "@models"
import * as ModuleServices from "@services"
import { TaxModuleService } from "@services"
import loadProviders from "./loaders/providers"
const migrationScriptOptions = {
moduleName: Modules.TAX,
models: Models,
pathToMigrations: __dirname + "/migrations",
}
const runMigrations = ModulesSdkUtils.buildMigrationScript(
migrationScriptOptions
)
const revertMigration = ModulesSdkUtils.buildRevertMigrationScript(
migrationScriptOptions
)
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
moduleModels: ModuleModels,
moduleRepositories: { BaseRepository: MikroOrmBaseRepository },
moduleServices: ModuleServices,
})
const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({
moduleName: Modules.TAX,
moduleModels: Object.values(Models),
migrationsPath: __dirname + "/migrations",
})
const service = TaxModuleService
const loaders = [containerLoader, connectionLoader, loadProviders] as any
export const moduleDefinition: ModuleExports = {
service,
loaders,
revertMigration,
runMigrations,
}

View File

@@ -0,0 +1 @@
export { default as SystemTaxProvider } from "./system"

View File

@@ -0,0 +1,42 @@
import { ITaxProvider, TaxTypes } from "@medusajs/types"
export default class SystemTaxService implements ITaxProvider {
static identifier = "system"
getIdentifier(): string {
return SystemTaxService.identifier
}
async getTaxLines(
itemLines: TaxTypes.ItemTaxCalculationLine[],
shippingLines: TaxTypes.ShippingTaxCalculationLine[],
_: TaxTypes.TaxCalculationContext
): Promise<(TaxTypes.ItemTaxLineDTO | TaxTypes.ShippingTaxLineDTO)[]> {
let taxLines: (TaxTypes.ItemTaxLineDTO | TaxTypes.ShippingTaxLineDTO)[] =
itemLines.flatMap((l) => {
return l.rates.map((r) => ({
rate_id: r.id,
rate: r.rate || 0,
name: r.name,
code: r.code,
line_item_id: l.line_item.id,
provider_id: this.getIdentifier(),
}))
})
taxLines = taxLines.concat(
shippingLines.flatMap((l) => {
return l.rates.map((r) => ({
rate_id: r.id,
rate: r.rate || 0,
name: r.name,
code: r.code,
shipping_line_id: l.shipping_line.id,
provider_id: this.getIdentifier(),
}))
})
)
return taxLines
}
}

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env node
import { Modules } from "@medusajs/modules-sdk"
import { ModulesSdkUtils } from "@medusajs/utils"
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-tax-seed <filePath>`
)
}
const run = ModulesSdkUtils.buildSeedScript({
moduleName: Modules.TAX,
models: Models,
pathToMigrations: __dirname + "/../../migrations",
seedHandler: async ({ manager, data }) => {
// TODO: Add seed logic
},
})
await run({ path })
})()

View File

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

View File

@@ -0,0 +1 @@
export { default as TaxModuleService } from "./tax-module-service"

View File

@@ -0,0 +1,750 @@
import {
Context,
DAL,
ITaxModuleService,
ITaxProvider,
InternalModuleDeclaration,
ModuleJoinerConfig,
ModulesSdkTypes,
TaxRegionDTO,
TaxTypes,
} from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
MedusaError,
ModulesSdkUtils,
isDefined,
isString,
promiseAll,
} from "@medusajs/utils"
import { TaxProvider, TaxRate, TaxRateRule, TaxRegion } from "@models"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
taxRateService: ModulesSdkTypes.InternalModuleService<any>
taxRegionService: ModulesSdkTypes.InternalModuleService<any>
taxRateRuleService: ModulesSdkTypes.InternalModuleService<any>
taxProviderService: ModulesSdkTypes.InternalModuleService<any>
[key: `tp_${string}`]: ITaxProvider
}
const generateForModels = [TaxRegion, TaxRateRule, TaxProvider]
type ItemWithRates = {
rates: TaxRate[]
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO
}
export default class TaxModuleService<
TTaxRate extends TaxRate = TaxRate,
TTaxRegion extends TaxRegion = TaxRegion,
TTaxRateRule extends TaxRateRule = TaxRateRule,
TTaxProvider extends TaxProvider = TaxProvider
>
extends ModulesSdkUtils.abstractModuleServiceFactory<
InjectedDependencies,
TaxTypes.TaxRateDTO,
{
TaxRegion: { dto: TaxTypes.TaxRegionDTO }
TaxRateRule: { dto: TaxTypes.TaxRateRuleDTO }
TaxProvider: { dto: TaxTypes.TaxProviderDTO }
}
>(TaxRate, generateForModels, entityNameToLinkableKeysMap)
implements ITaxModuleService
{
protected readonly container_: InjectedDependencies
protected baseRepository_: DAL.RepositoryService
protected taxRateService_: ModulesSdkTypes.InternalModuleService<TTaxRate>
protected taxRegionService_: ModulesSdkTypes.InternalModuleService<TTaxRegion>
protected taxRateRuleService_: ModulesSdkTypes.InternalModuleService<TTaxRateRule>
protected taxProviderService_: ModulesSdkTypes.InternalModuleService<TTaxProvider>
constructor(
{
baseRepository,
taxRateService,
taxRegionService,
taxRateRuleService,
taxProviderService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
// @ts-ignore
super(...arguments)
this.container_ = arguments[0]
this.baseRepository_ = baseRepository
this.taxRateService_ = taxRateService
this.taxRegionService_ = taxRegionService
this.taxRateRuleService_ = taxRateRuleService
this.taxProviderService_ = taxProviderService
}
__joinerConfig(): ModuleJoinerConfig {
return joinerConfig
}
async create(
data: TaxTypes.CreateTaxRateDTO[],
sharedContext?: Context
): Promise<TaxTypes.TaxRateDTO[]>
async create(
data: TaxTypes.CreateTaxRateDTO,
sharedContext?: Context
): Promise<TaxTypes.TaxRateDTO>
@InjectManager("baseRepository_")
async create(
data: TaxTypes.CreateTaxRateDTO[] | TaxTypes.CreateTaxRateDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<TaxTypes.TaxRateDTO[] | TaxTypes.TaxRateDTO> {
const input = Array.isArray(data) ? data : [data]
const rates = await this.create_(input, sharedContext)
return Array.isArray(data) ? rates : rates[0]
}
@InjectTransactionManager("baseRepository_")
protected async create_(
data: TaxTypes.CreateTaxRateDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const [rules, rateData] = data.reduce(
(acc, region) => {
const { rules, ...rest } = region
acc[0].push(rules)
acc[1].push(rest)
return acc
},
[[], []] as [
(Omit<TaxTypes.CreateTaxRateRuleDTO, "tax_rate_id">[] | undefined)[],
Partial<TaxTypes.CreateTaxRegionDTO>[]
]
)
const rates = await this.taxRateService_.create(rateData, sharedContext)
const rulesToCreate = rates
.reduce((acc, rate, i) => {
const rateRules = rules[i]
if (isDefined(rateRules)) {
acc.push(
rateRules.map((r) => {
return {
...r,
created_by: rate.created_by,
tax_rate_id: rate.id,
}
})
)
}
return acc
}, [] as TaxTypes.CreateTaxRateRuleDTO[][])
.flat()
if (rulesToCreate.length > 0) {
await this.taxRateRuleService_.create(rulesToCreate, sharedContext)
}
return await this.baseRepository_.serialize<TaxTypes.TaxRateDTO[]>(rates, {
populate: true,
})
}
async update(
id: string,
data: TaxTypes.UpdateTaxRateDTO,
sharedContext?: Context
): Promise<TaxTypes.TaxRateDTO>
async update(
ids: string[],
data: TaxTypes.UpdateTaxRateDTO,
sharedContext?: Context
): Promise<TaxTypes.TaxRateDTO[]>
async update(
selector: TaxTypes.FilterableTaxRateProps,
data: TaxTypes.UpdateTaxRateDTO,
sharedContext?: Context
): Promise<TaxTypes.TaxRateDTO[]>
@InjectManager("baseRepository_")
async update(
selector: string | string[] | TaxTypes.FilterableTaxRateProps,
data: TaxTypes.UpdateTaxRateDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<TaxTypes.TaxRateDTO | TaxTypes.TaxRateDTO[]> {
const rates = await this.update_(selector, data, sharedContext)
const serialized = await this.baseRepository_.serialize<
TaxTypes.TaxRateDTO[]
>(rates, { populate: true })
return isString(selector) ? serialized[0] : serialized
}
@InjectTransactionManager("baseRepository_")
protected async update_(
idOrSelector: string | string[] | TaxTypes.FilterableTaxRateProps,
data: TaxTypes.UpdateTaxRateDTO,
@MedusaContext() sharedContext: Context = {}
) {
const selector =
Array.isArray(idOrSelector) || isString(idOrSelector)
? { id: idOrSelector }
: idOrSelector
if (data.rules) {
await this.setTaxRateRulesForTaxRates(
idOrSelector,
data.rules,
data.updated_by,
sharedContext
)
delete data.rules
}
return await this.taxRateService_.update({ selector, data }, sharedContext)
}
private async setTaxRateRulesForTaxRates(
idOrSelector: string | string[] | TaxTypes.FilterableTaxRateProps,
rules: Omit<TaxTypes.CreateTaxRateRuleDTO, "tax_rate_id">[],
createdBy?: string,
sharedContext: Context = {}
) {
const selector =
Array.isArray(idOrSelector) || isString(idOrSelector)
? { id: idOrSelector }
: idOrSelector
await this.taxRateRuleService_.softDelete(
{ tax_rate: selector },
sharedContext
)
// TODO: this is a temporary solution seems like mikro-orm doesn't persist
// the soft delete which results in the creation below breaking the unique
// constraint
await this.taxRateRuleService_.list(
{ tax_rate: selector },
{ select: ["id"] },
sharedContext
)
if (rules.length === 0) {
return
}
const rateIds = await this.getTaxRateIdsFromSelector(idOrSelector)
const toCreate = rateIds
.map((id) => {
return rules.map((r) => {
return {
...r,
created_by: createdBy,
tax_rate_id: id,
}
})
})
.flat()
return await this.createTaxRateRules(toCreate, sharedContext)
}
private async getTaxRateIdsFromSelector(
idOrSelector: string | string[] | TaxTypes.FilterableTaxRateProps,
sharedContext: Context = {}
) {
if (Array.isArray(idOrSelector)) {
return idOrSelector
}
if (isString(idOrSelector)) {
return [idOrSelector]
}
const rates = await this.taxRateService_.list(
idOrSelector,
{ select: ["id"] },
sharedContext
)
return rates.map((r) => r.id)
}
async upsert(
data: TaxTypes.UpsertTaxRateDTO[],
sharedContext?: Context
): Promise<TaxTypes.TaxRateDTO[]>
async upsert(
data: TaxTypes.UpsertTaxRateDTO,
sharedContext?: Context
): Promise<TaxTypes.TaxRateDTO>
@InjectTransactionManager("baseRepository_")
async upsert(
data: TaxTypes.UpsertTaxRateDTO | TaxTypes.UpsertTaxRateDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TaxTypes.TaxRateDTO | TaxTypes.TaxRateDTO[]> {
const result = await this.taxRateService_.upsert(data, sharedContext)
const serialized = await this.baseRepository_.serialize<
TaxTypes.TaxRateDTO[]
>(result, { populate: true })
return Array.isArray(data) ? serialized : serialized[0]
}
createTaxRegions(
data: TaxTypes.CreateTaxRegionDTO,
sharedContext?: Context
): Promise<TaxRegionDTO>
createTaxRegions(
data: TaxTypes.CreateTaxRegionDTO[],
sharedContext?: Context
): Promise<TaxRegionDTO[]>
@InjectManager("baseRepository_")
async createTaxRegions(
data: TaxTypes.CreateTaxRegionDTO | TaxTypes.CreateTaxRegionDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const input = Array.isArray(data) ? data : [data]
const result = await this.createTaxRegions_(input, sharedContext)
return Array.isArray(data) ? result : result[0]
}
async createTaxRegions_(
data: TaxTypes.CreateTaxRegionDTO[],
sharedContext: Context = {}
) {
const { defaultRates, regionData } =
this.prepareTaxRegionInputForCreate(data)
await this.verifyProvinceToCountryMatch(regionData, sharedContext)
const regions = await this.taxRegionService_.create(
regionData,
sharedContext
)
const rates = regions
.map((region, i) => {
if (!defaultRates[i]) {
return false
}
return {
...defaultRates[i],
tax_region_id: region.id,
}
})
.filter(Boolean) as TaxTypes.CreateTaxRateDTO[]
if (rates.length !== 0) {
await this.create(rates, sharedContext)
}
return await this.baseRepository_.serialize<TaxTypes.TaxRegionDTO[]>(
regions,
{ populate: true }
)
}
createTaxRateRules(
data: TaxTypes.CreateTaxRateRuleDTO,
sharedContext?: Context
): Promise<TaxTypes.TaxRateRuleDTO>
createTaxRateRules(
data: TaxTypes.CreateTaxRateRuleDTO[],
sharedContext?: Context
): Promise<TaxTypes.TaxRateRuleDTO[]>
@InjectManager("baseRepository_")
async createTaxRateRules(
data: TaxTypes.CreateTaxRateRuleDTO | TaxTypes.CreateTaxRateRuleDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const input = Array.isArray(data) ? data : [data]
const result = await this.createTaxRateRules_(input, sharedContext)
return Array.isArray(data) ? result : result[0]
}
@InjectTransactionManager("baseRepository_")
async createTaxRateRules_(
data: TaxTypes.CreateTaxRateRuleDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const rules = await this.taxRateRuleService_.create(data, sharedContext)
return await this.baseRepository_.serialize<TaxTypes.TaxRateRuleDTO[]>(
rules,
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async getTaxLines(
items: (TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO)[],
calculationContext: TaxTypes.TaxCalculationContext,
@MedusaContext() sharedContext: Context = {}
): Promise<(TaxTypes.ItemTaxLineDTO | TaxTypes.ShippingTaxLineDTO)[]> {
const normalizedContext =
this.normalizeTaxCalculationContext(calculationContext)
const regions = await this.taxRegionService_.list(
{
$or: [
{
country_code: normalizedContext.address.country_code,
province_code: null,
},
{
country_code: normalizedContext.address.country_code,
province_code: normalizedContext.address.province_code,
},
],
},
{},
sharedContext
)
const parentRegion = regions.find((r) => r.province_code === null)
if (!parentRegion) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"No parent region found for country"
)
}
const toReturn = await promiseAll(
items.map(async (item) => {
const regionIds = regions.map((r) => r.id)
const rateQuery = this.getTaxRateQueryForItem(item, regionIds)
const candidateRates = await this.taxRateService_.list(
rateQuery,
{
relations: ["tax_region", "rules"],
},
sharedContext
)
const applicableRates = await this.getTaxRatesForItem(
item,
candidateRates
)
return {
rates: applicableRates,
item,
}
})
)
const taxLines = await this.getTaxLinesFromProvider(
parentRegion.provider_id,
toReturn,
calculationContext
)
return taxLines
}
private async getTaxLinesFromProvider(
rawProviderId: string | null,
items: ItemWithRates[],
calculationContext: TaxTypes.TaxCalculationContext
) {
const providerId = rawProviderId || "system"
let provider: ITaxProvider
try {
provider = this.container_[`tp_${providerId}`] as ITaxProvider
} catch (err) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Failed to resolve Tax Provider with id: ${providerId}. Make sure it's installed and configured in the Tax Module's options.`
)
}
const [itemLines, shippingLines] = items.reduce(
(acc, line) => {
if ("shipping_option_id" in line.item) {
acc[1].push({
shipping_line: line.item,
rates: line.rates,
})
} else {
acc[0].push({
line_item: line.item,
rates: line.rates,
})
}
return acc
},
[[], []] as [
TaxTypes.ItemTaxCalculationLine[],
TaxTypes.ShippingTaxCalculationLine[]
]
)
const itemTaxLines = await provider.getTaxLines(
itemLines,
shippingLines,
calculationContext
)
return itemTaxLines
}
private normalizeTaxCalculationContext(
context: TaxTypes.TaxCalculationContext
): TaxTypes.TaxCalculationContext {
return {
...context,
address: {
...context.address,
country_code: this.normalizeRegionCodes(context.address.country_code),
province_code: context.address.province_code
? this.normalizeRegionCodes(context.address.province_code)
: null,
},
}
}
private prepareTaxRegionInputForCreate(
data: TaxTypes.CreateTaxRegionDTO | TaxTypes.CreateTaxRegionDTO[]
) {
const regionsWithDefaultRate = Array.isArray(data) ? data : [data]
const defaultRates: (Omit<
TaxTypes.CreateTaxRateDTO,
"tax_region_id"
> | null)[] = []
const regionData: TaxTypes.CreateTaxRegionDTO[] = []
for (const region of regionsWithDefaultRate) {
const { default_tax_rate, ...rest } = region
if (!default_tax_rate) {
defaultRates.push(null)
} else {
defaultRates.push({
...default_tax_rate,
is_default: true,
created_by: region.created_by,
})
}
regionData.push({
...rest,
province_code: rest.province_code
? this.normalizeRegionCodes(rest.province_code)
: null,
country_code: this.normalizeRegionCodes(rest.country_code),
})
}
return { defaultRates, regionData }
}
private async verifyProvinceToCountryMatch(
regionsToVerify: TaxTypes.CreateTaxRegionDTO[],
sharedContext: Context = {}
) {
const parentIds = regionsToVerify.map((i) => i.parent_id).filter(isDefined)
if (parentIds.length > 0) {
const parentRegions = await this.taxRegionService_.list(
{ id: { $in: parentIds } },
{ select: ["id", "country_code"] },
sharedContext
)
for (const region of regionsToVerify) {
if (isDefined(region.parent_id)) {
const parentRegion = parentRegions.find(
(r) => r.id === region.parent_id
)
if (!isDefined(parentRegion)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Province region must belong to a parent region. You are trying to create a province region with (country: ${region.country_code}, province: ${region.province_code}) but parent does not exist`
)
}
if (parentRegion.country_code !== region.country_code) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Province region must belong to a parent region with the same country code. You are trying to create a province region with (country: ${region.country_code}, province: ${region.province_code}) but parent expects (country: ${parentRegion.country_code})`
)
}
}
}
}
}
private async getTaxRatesForItem(
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO,
rates: TTaxRate[]
): Promise<TTaxRate[]> {
if (!rates.length) {
return []
}
const prioritizedRates = this.prioritizeRates(rates, item)
const rate = prioritizedRates[0]
const ratesToReturn = [rate]
// If the rate can be combined we need to find the rate's
// parent region and add that rate too. If not we can return now.
if (!(rate.is_combinable && rate.tax_region.parent_id)) {
return ratesToReturn
}
// First parent region rate in prioritized rates
// will be the most granular rate.
const parentRate = prioritizedRates.find(
(r) => r.tax_region.id === rate.tax_region.parent_id
)
if (parentRate) {
ratesToReturn.push(parentRate)
}
return ratesToReturn
}
private getTaxRateQueryForItem(
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO,
regionIds: string[]
) {
const isShipping = "shipping_option_id" in item
let ruleQuery = isShipping
? [
{
reference: "shipping_option",
reference_id: item.shipping_option_id,
},
]
: [
{
reference: "product",
reference_id: item.product_id,
},
{
reference: "product_type",
reference_id: item.product_type_id,
},
]
return {
$and: [
{ tax_region_id: regionIds },
{ $or: [{ is_default: true }, { rules: { $or: ruleQuery } }] },
],
}
}
private checkRuleMatches(
rate: TTaxRate,
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO
) {
if (rate.rules.length === 0) {
return {
isProductMatch: false,
isProductTypeMatch: false,
isShippingMatch: false,
}
}
let isProductMatch = false
const isShipping = "shipping_option_id" in item
const matchingRules = rate.rules.filter((rule) => {
if (isShipping) {
return (
rule.reference === "shipping" &&
rule.reference_id === item.shipping_option_id
)
}
return (
(rule.reference === "product" &&
rule.reference_id === item.product_id) ||
(rule.reference === "product_type" &&
rule.reference_id === item.product_type_id)
)
})
if (matchingRules.some((rule) => rule.reference === "product")) {
isProductMatch = true
}
return {
isProductMatch,
isProductTypeMatch: matchingRules.length > 0,
isShippingMatch: isShipping && matchingRules.length > 0,
}
}
private prioritizeRates(
rates: TTaxRate[],
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO
) {
const decoratedRates: (TTaxRate & {
priority_score: number
})[] = rates.map((rate) => {
const { isProductMatch, isProductTypeMatch, isShippingMatch } =
this.checkRuleMatches(rate, item)
const isProvince = rate.tax_region.province_code !== null
const isDefault = rate.is_default
const decoratedRate = {
...rate,
priority_score: 7,
}
if ((isShippingMatch || isProductMatch) && isProvince) {
decoratedRate.priority_score = 1
} else if (isProductTypeMatch && isProvince) {
decoratedRate.priority_score = 2
} else if (isDefault && isProvince) {
decoratedRate.priority_score = 3
} else if ((isShippingMatch || isProductMatch) && !isProvince) {
decoratedRate.priority_score = 4
} else if (isProductTypeMatch && !isProvince) {
decoratedRate.priority_score = 5
} else if (isDefault && !isProvince) {
decoratedRate.priority_score = 6
}
return decoratedRate
})
return decoratedRates.sort(
(a, b) => (a as any).priority_score - (b as any).priority_score
)
}
private normalizeRegionCodes(code: string) {
return code.toLowerCase()
}
// @InjectTransactionManager("baseRepository_")
// async createProvidersOnLoad(@MedusaContext() sharedContext: Context = {}) {
// const providersToLoad = this.container_["tax_providers"] as ITaxProvider[]
// const ids = providersToLoad.map((p) => p.getIdentifier())
// const existing = await this.taxProviderService_.update(
// { selector: { id: { $in: ids } }, data: { is_enabled: true } },
// sharedContext
// )
// const existingIds = existing.map((p) => p.id)
// const diff = arrayDifference(ids, existingIds)
// await this.taxProviderService_.create(
// diff.map((id) => ({ id, is_enabled: true }))
// )
// await this.taxProviderService_.update({
// selector: { id: { $nin: ids } },
// data: { is_enabled: false },
// })
// }
}