@endora-commerce/mod-taxes 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/admin/index.d.ts +25 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +34 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/TaxesPage.d.ts +4 -0
- package/dist/admin/pages/TaxesPage.d.ts.map +1 -0
- package/dist/admin/pages/TaxesPage.js +135 -0
- package/dist/admin/pages/TaxesPage.js.map +1 -0
- package/dist/backend/demo/reset.d.ts +13 -0
- package/dist/backend/demo/reset.d.ts.map +1 -0
- package/dist/backend/demo/reset.js +11 -0
- package/dist/backend/demo/reset.js.map +1 -0
- package/dist/backend/demo/rows.d.ts +33 -0
- package/dist/backend/demo/rows.d.ts.map +1 -0
- package/dist/backend/demo/rows.js +27 -0
- package/dist/backend/demo/rows.js.map +1 -0
- package/dist/backend/demo/seed.d.ts +16 -0
- package/dist/backend/demo/seed.d.ts.map +1 -0
- package/dist/backend/demo/seed.js +31 -0
- package/dist/backend/demo/seed.js.map +1 -0
- package/dist/backend/entities/tax.entity.d.ts +21 -0
- package/dist/backend/entities/tax.entity.d.ts.map +1 -0
- package/dist/backend/entities/tax.entity.js +83 -0
- package/dist/backend/entities/tax.entity.js.map +1 -0
- package/dist/backend/index.d.ts +76 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +86 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.d.ts +9 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +63 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/tax-country-reference.d.ts +12 -0
- package/dist/backend/services/tax-country-reference.d.ts.map +1 -0
- package/dist/backend/services/tax-country-reference.js +24 -0
- package/dist/backend/services/tax-country-reference.js.map +1 -0
- package/dist/backend/services/tax-service.d.ts +51 -0
- package/dist/backend/services/tax-service.d.ts.map +1 -0
- package/dist/backend/services/tax-service.js +169 -0
- package/dist/backend/services/tax-service.js.map +1 -0
- package/dist/manifest.d.ts +169 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +113 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260426T081516_taxes_promotions_init.d.ts +14 -0
- package/dist/migrations/20260426T081516_taxes_promotions_init.d.ts.map +1 -0
- package/dist/migrations/20260426T081516_taxes_promotions_init.js +63 -0
- package/dist/migrations/20260426T081516_taxes_promotions_init.js.map +1 -0
- package/dist/migrations/20260912T094654_taxes_sales_channel_taxes.d.ts +27 -0
- package/dist/migrations/20260912T094654_taxes_sales_channel_taxes.d.ts.map +1 -0
- package/dist/migrations/20260912T094654_taxes_sales_channel_taxes.js +44 -0
- package/dist/migrations/20260912T094654_taxes_sales_channel_taxes.js.map +1 -0
- package/dist/migrations/index.d.ts +28 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +31 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/taxes.md +75 -0
- package/i18n/en.json +3 -0
- package/i18n/pl.json +3 -0
- package/package.json +92 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taxes module — manifest backfill (Module Lifecycle, feature 018).
|
|
3
|
+
*
|
|
4
|
+
* Predates the lifecycle system; this manifest is the static record
|
|
5
|
+
* required so the module participates in the registry. No install /
|
|
6
|
+
* uninstall hook today — the module's schema is owned by earlier
|
|
7
|
+
* platform-wide migrations.
|
|
8
|
+
*/
|
|
9
|
+
export declare const manifest: {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
dependencies: string[];
|
|
14
|
+
description?: string | undefined;
|
|
15
|
+
acknowledgedDependencies?: {
|
|
16
|
+
moduleId: string;
|
|
17
|
+
port: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
}[] | undefined;
|
|
20
|
+
nonBindingDependencies?: {
|
|
21
|
+
moduleId: string;
|
|
22
|
+
name: string;
|
|
23
|
+
kind: "contributes-to" | "degrades-without" | "refuses-without";
|
|
24
|
+
reason: string;
|
|
25
|
+
whenAbsent?: string | undefined;
|
|
26
|
+
}[] | undefined;
|
|
27
|
+
activation?: {
|
|
28
|
+
settingCode: string;
|
|
29
|
+
default: boolean;
|
|
30
|
+
} | {
|
|
31
|
+
nonDeactivatable: true;
|
|
32
|
+
reason: string;
|
|
33
|
+
} | undefined;
|
|
34
|
+
settings?: {
|
|
35
|
+
moduleCode: string;
|
|
36
|
+
groups: {
|
|
37
|
+
code: string;
|
|
38
|
+
name: string;
|
|
39
|
+
salesChannelCodes?: string[] | undefined;
|
|
40
|
+
isSystemProtected?: boolean | undefined;
|
|
41
|
+
}[];
|
|
42
|
+
settings: {
|
|
43
|
+
code: string;
|
|
44
|
+
name: string;
|
|
45
|
+
valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
|
|
46
|
+
defaultValue: unknown;
|
|
47
|
+
description?: string | undefined;
|
|
48
|
+
groupCode?: string | undefined;
|
|
49
|
+
previousDefaultValues?: unknown[] | undefined;
|
|
50
|
+
salesChannelCodes?: string[] | undefined;
|
|
51
|
+
enumOptions?: string[] | undefined;
|
|
52
|
+
configurationType?: string | undefined;
|
|
53
|
+
hidden?: boolean | undefined;
|
|
54
|
+
}[];
|
|
55
|
+
} | undefined;
|
|
56
|
+
i18n?: {
|
|
57
|
+
bundlesDir: string;
|
|
58
|
+
} | undefined;
|
|
59
|
+
docs?: false | {
|
|
60
|
+
dir: string;
|
|
61
|
+
} | undefined;
|
|
62
|
+
demo?: false | {
|
|
63
|
+
summary: string;
|
|
64
|
+
seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
|
|
65
|
+
reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
|
|
66
|
+
after?: readonly string[] | undefined;
|
|
67
|
+
package?: string | undefined;
|
|
68
|
+
} | undefined;
|
|
69
|
+
actions?: {
|
|
70
|
+
id: string;
|
|
71
|
+
labelKey: string;
|
|
72
|
+
icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
|
|
73
|
+
targetRoute: string;
|
|
74
|
+
keywords: string[];
|
|
75
|
+
weight: number;
|
|
76
|
+
descriptionKey?: string | undefined;
|
|
77
|
+
requiredPermission?: string | undefined;
|
|
78
|
+
}[] | undefined;
|
|
79
|
+
permissions?: {
|
|
80
|
+
code: string;
|
|
81
|
+
label: string;
|
|
82
|
+
module?: string | undefined;
|
|
83
|
+
description?: string | undefined;
|
|
84
|
+
requires?: string[] | undefined;
|
|
85
|
+
}[] | undefined;
|
|
86
|
+
transactionalEmails?: {
|
|
87
|
+
code: string;
|
|
88
|
+
name: string;
|
|
89
|
+
variables: {
|
|
90
|
+
key: string;
|
|
91
|
+
label: string;
|
|
92
|
+
sampleValue?: string | undefined;
|
|
93
|
+
description?: string | undefined;
|
|
94
|
+
}[];
|
|
95
|
+
description?: string | undefined;
|
|
96
|
+
group?: string | undefined;
|
|
97
|
+
}[] | undefined;
|
|
98
|
+
capabilities?: string[] | undefined;
|
|
99
|
+
exclusiveCapabilities?: {
|
|
100
|
+
key: string;
|
|
101
|
+
errorCode: string;
|
|
102
|
+
}[] | undefined;
|
|
103
|
+
errorCodes?: {
|
|
104
|
+
code: string;
|
|
105
|
+
tokens?: string[] | undefined;
|
|
106
|
+
}[] | undefined;
|
|
107
|
+
blocks?: {
|
|
108
|
+
name: string;
|
|
109
|
+
labelKey: string;
|
|
110
|
+
category: string;
|
|
111
|
+
contexts: ("invoice" | "email" | "cms" | "newsletter")[];
|
|
112
|
+
fields: Record<string, {
|
|
113
|
+
type: "number" | "object" | "array" | "uuid" | "text" | "textarea" | "select" | "radio" | "external" | "richtext";
|
|
114
|
+
label?: string | undefined;
|
|
115
|
+
required?: boolean | undefined;
|
|
116
|
+
options?: {
|
|
117
|
+
label: string;
|
|
118
|
+
value: string | number;
|
|
119
|
+
}[] | undefined;
|
|
120
|
+
refKind?: string | undefined;
|
|
121
|
+
}>;
|
|
122
|
+
descriptionKey?: string | undefined;
|
|
123
|
+
defaultProps?: Record<string, unknown> | undefined;
|
|
124
|
+
responsiveFields?: string[] | undefined;
|
|
125
|
+
previewIcon?: string | undefined;
|
|
126
|
+
weight?: number | undefined;
|
|
127
|
+
}[] | undefined;
|
|
128
|
+
blockCategories?: {
|
|
129
|
+
key: string;
|
|
130
|
+
titleKey: string;
|
|
131
|
+
contexts: ("invoice" | "email" | "cms" | "newsletter")[];
|
|
132
|
+
weight?: number | undefined;
|
|
133
|
+
visible?: boolean | undefined;
|
|
134
|
+
}[] | undefined;
|
|
135
|
+
env?: {
|
|
136
|
+
name: string;
|
|
137
|
+
describes: {
|
|
138
|
+
en: string;
|
|
139
|
+
pl: string;
|
|
140
|
+
};
|
|
141
|
+
requirement: {
|
|
142
|
+
kind: "required";
|
|
143
|
+
} | {
|
|
144
|
+
kind: "requiredWhen";
|
|
145
|
+
input: string;
|
|
146
|
+
equals: string;
|
|
147
|
+
} | {
|
|
148
|
+
kind: "optional";
|
|
149
|
+
without: {
|
|
150
|
+
en: string;
|
|
151
|
+
pl: string;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
secret: boolean;
|
|
155
|
+
generable: boolean;
|
|
156
|
+
owner: {
|
|
157
|
+
kind: "platform";
|
|
158
|
+
} | {
|
|
159
|
+
kind: "application";
|
|
160
|
+
application: "admin" | "backend" | "storefront";
|
|
161
|
+
} | {
|
|
162
|
+
kind: "module";
|
|
163
|
+
moduleId: string;
|
|
164
|
+
};
|
|
165
|
+
consumers: ("admin" | "backend" | "storefront")[];
|
|
166
|
+
addressOf: "admin" | "backend" | "storefront" | null;
|
|
167
|
+
}[] | undefined;
|
|
168
|
+
};
|
|
169
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAwBA;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoFnB,CAAC"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { defineModuleManifest } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* The demo data this module owns (feature 113, T220 — contract §1.3).
|
|
4
|
+
*
|
|
5
|
+
* A typed `const` rather than an inline object literal, and that is not style:
|
|
6
|
+
* declared inline the parameter infers from the schema and is `never`, so the
|
|
7
|
+
* author loses `context.ctx: ModuleContext`.
|
|
8
|
+
*
|
|
9
|
+
* Both bodies are reached by a **relative `await import()`** (§1.4), in
|
|
10
|
+
* `cliCommands.run`'s shape and for `cliCommands`' reason: a manifest is loaded
|
|
11
|
+
* by every process that composes the platform and by the check scripts that
|
|
12
|
+
* import the generated index, so a demo body imported at the top of this file
|
|
13
|
+
* would be a service graph pulled into all of them. It needs no `exports`
|
|
14
|
+
* subpath and no `files` entry — a relative import inside the package lands in
|
|
15
|
+
* `dist` through the existing emit (§1.5).
|
|
16
|
+
*/
|
|
17
|
+
const demo = {
|
|
18
|
+
summary: 'The Polish standard VAT rule, so seeded prices carry tax.',
|
|
19
|
+
seed: async (context) => (await import('./backend/demo/seed.js')).seedDemo(context),
|
|
20
|
+
reset: async (context) => (await import('./backend/demo/reset.js')).resetDemo(context),
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Taxes module — manifest backfill (Module Lifecycle, feature 018).
|
|
24
|
+
*
|
|
25
|
+
* Predates the lifecycle system; this manifest is the static record
|
|
26
|
+
* required so the module participates in the registry. No install /
|
|
27
|
+
* uninstall hook today — the module's schema is owned by earlier
|
|
28
|
+
* platform-wide migrations.
|
|
29
|
+
*/
|
|
30
|
+
export const manifest = defineModuleManifest({
|
|
31
|
+
id: 'taxes',
|
|
32
|
+
name: 'Taxes',
|
|
33
|
+
description: 'Tax rate configuration and order tax computation.',
|
|
34
|
+
version: '1.0.0',
|
|
35
|
+
// `auth` owns the `requireAdmin` port the admin routes are gated by;
|
|
36
|
+
// `dictionaries` owns the validator that checks country and region codes.
|
|
37
|
+
// Feature 072 made both container resolutions rather than optional arguments.
|
|
38
|
+
dependencies: ['auth', 'dictionaries'],
|
|
39
|
+
/**
|
|
40
|
+
* The module's own authority (2026-08-28).
|
|
41
|
+
*
|
|
42
|
+
* All four admin routes used to enforce `catalog:write` — the two reads
|
|
43
|
+
* included — so whoever could edit a product could read the tax table,
|
|
44
|
+
* rewrite a rate, move the default rule and delete a rule outright. A rate is
|
|
45
|
+
* the figure every price, every order total and every invoice is computed
|
|
46
|
+
* from, and this module is the one the platform refuses to switch off on the
|
|
47
|
+
* ground that a shop which cannot compute tax cannot state a lawful price;
|
|
48
|
+
* the authority over that figure was the catalogue's. `catalog:write` was
|
|
49
|
+
* real, declared and enforced, so the permission inventory's two directions
|
|
50
|
+
* were clean over the site, and `check:action-route-permissions` never looked
|
|
51
|
+
* at all: this module declares no manifest action, so the check has nothing
|
|
52
|
+
* of its own to compare.
|
|
53
|
+
*
|
|
54
|
+
* A pair and no third code, spelled `<module id>:<read|write>` like the five
|
|
55
|
+
* gateway modules, `payment_methods`, `delivery_methods`, `returns` and
|
|
56
|
+
* `invoices`. A prefix that is not its owner's id is the mistake
|
|
57
|
+
* `PERMISSION_CATALOGUE` comments on twice (`integrations:manage`,
|
|
58
|
+
* `audit_log:read`), both frozen because they are persisted in role rows;
|
|
59
|
+
* getting it right on a code that does not exist yet is free. The codes stay
|
|
60
|
+
* here rather than in `PERMISSION_CATALOGUE`, which is for codes spanning
|
|
61
|
+
* modules — this module owns these outright.
|
|
62
|
+
*
|
|
63
|
+
* The read half is a capability that did not exist before rather than a
|
|
64
|
+
* rename of one: there was no read gate to move, so `taxes:read` is what
|
|
65
|
+
* finally lets an operator be shown a VAT rate without being handed the
|
|
66
|
+
* authority to change it.
|
|
67
|
+
*
|
|
68
|
+
* No data migration: see `test/contract/taxes/permission-authority.test.ts`.
|
|
69
|
+
*/
|
|
70
|
+
permissions: [
|
|
71
|
+
{ code: 'taxes:read', label: 'View tax rules' },
|
|
72
|
+
{ code: 'taxes:write', label: 'Configure tax rules' },
|
|
73
|
+
],
|
|
74
|
+
settings: {
|
|
75
|
+
moduleCode: 'taxes',
|
|
76
|
+
groups: [{ code: 'taxes', name: 'Taxes' }],
|
|
77
|
+
// The group is kept as a reservation while the module ships no settings of
|
|
78
|
+
// its own: `taxes.enabled` was the only one, and it went with the control
|
|
79
|
+
// it backed (feature 074). Dropping the group too would only turn it into
|
|
80
|
+
// an orphan the boot reconciler warns about at every start.
|
|
81
|
+
settings: [],
|
|
82
|
+
},
|
|
83
|
+
// Feature 074 (Constitution XVII), test C2 — functional base, and one of the
|
|
84
|
+
// escalation answers. 073 read this module as "a platform without
|
|
85
|
+
// configurable tax rates is a smaller platform, not a broken one", and the
|
|
86
|
+
// deciding fact is what that reading missed: the absence does not announce
|
|
87
|
+
// itself. Every price becomes tax-free with no operator-visible signal and
|
|
88
|
+
// the invoice carries a wrong figure. A platform that cannot compute tax
|
|
89
|
+
// cannot state a lawful price, which is a different product rather than a
|
|
90
|
+
// reduced one.
|
|
91
|
+
//
|
|
92
|
+
// This closes the *operator* route to that state. The platform route — a
|
|
93
|
+
// deployment that never installs this module — is still open and is a
|
|
94
|
+
// separate follow-up, which inherits one constraint: a correctly-configured
|
|
95
|
+
// 0% rate is a legitimate answer in some jurisdictions, so absence and a
|
|
96
|
+
// configured zero must stay distinguishable.
|
|
97
|
+
//
|
|
98
|
+
// `taxes.enabled` goes with the control. The existing rows are removed by a
|
|
99
|
+
// core data migration (feature 074, FR-010a).
|
|
100
|
+
// Feature 091 (Phase 4, batch 8) — this module ships a bundle now: its
|
|
101
|
+
// sidebar entry's `labelKey` is module-relative (R8) and resolves in this
|
|
102
|
+
// module's own namespace. Its screen's copy stays in `_i18n`'s `core`
|
|
103
|
+
// scope, which is batch 4's shape and not a new one.
|
|
104
|
+
i18n: { bundlesDir: 'i18n' },
|
|
105
|
+
docs: { dir: 'docs' },
|
|
106
|
+
demo,
|
|
107
|
+
activation: {
|
|
108
|
+
nonDeactivatable: true,
|
|
109
|
+
reason: 'Absent, every price becomes tax-free with no operator-visible signal and the invoice ' +
|
|
110
|
+
'carries a wrong figure. A platform that cannot compute tax cannot state a lawful price.',
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAA2B,MAAM,4BAA4B,CAAC;AAG3F;;;;;;;;;;;;;;GAcG;AACH,MAAM,IAAI,GAAsC;IAC9C,OAAO,EAAE,2DAA2D;IACpE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,wBAAwB,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;IACnF,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC;CACvF,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAC;IAC3C,EAAE,EAAE,OAAO;IACX,IAAI,EAAE,OAAO;IACb,WAAW,EACT,mDAAmD;IACrD,OAAO,EAAE,OAAO;IAChB,qEAAqE;IACrE,0EAA0E;IAC1E,8EAA8E;IAC9E,YAAY,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC;IACtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,WAAW,EAAE;QACX,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,gBAAgB,EAAE;QAC/C,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,qBAAqB,EAAE;KACtD;IACD,QAAQ,EAAE;QACR,UAAU,EAAE,OAAO;QACnB,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QAC1C,2EAA2E;QAC3E,0EAA0E;QAC1E,0EAA0E;QAC1E,4DAA4D;QAC5D,QAAQ,EAAE,EAAE;KACb;IACD,6EAA6E;IAC7E,kEAAkE;IAClE,2EAA2E;IAC3E,2EAA2E;IAC3E,2EAA2E;IAC3E,yEAAyE;IACzE,0EAA0E;IAC1E,eAAe;IACf,EAAE;IACF,yEAAyE;IACzE,sEAAsE;IACtE,4EAA4E;IAC5E,yEAAyE;IACzE,6CAA6C;IAC7C,EAAE;IACF,4EAA4E;IAC5E,8CAA8C;IAC9C,uEAAuE;IACvE,0EAA0E;IAC1E,sEAAsE;IACtE,qDAAqD;IACrD,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE;IAC5B,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE;IACrB,IAAI;IACJ,UAAU,EAAE;QACV,gBAAgB,EAAE,IAAI;QACtB,MAAM,EACJ,uFAAuF;YACvF,yFAAyF;KAC5F;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* Tax + Promotion init (T128, T129). Two tables, both keyed by their
|
|
4
|
+
* own UUID; `taxes.code` and `promotions.code` are unique. The default
|
|
5
|
+
* tax invariant is enforced by a partial unique index.
|
|
6
|
+
*
|
|
7
|
+
* Co-located in the `taxes` module folder to keep the migration
|
|
8
|
+
* sequence numerical.
|
|
9
|
+
*/
|
|
10
|
+
export declare class Migration20260426T081516TaxesPromotionsInit extends Migration {
|
|
11
|
+
up(): Promise<void>;
|
|
12
|
+
down(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=20260426T081516_taxes_promotions_init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260426T081516_taxes_promotions_init.d.ts","sourceRoot":"","sources":["../../src/migrations/20260426T081516_taxes_promotions_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;GAOG;AACH,qBAAa,2CAA4C,SAAQ,SAAS;IACzD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAmDnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAIrC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* Tax + Promotion init (T128, T129). Two tables, both keyed by their
|
|
4
|
+
* own UUID; `taxes.code` and `promotions.code` are unique. The default
|
|
5
|
+
* tax invariant is enforced by a partial unique index.
|
|
6
|
+
*
|
|
7
|
+
* Co-located in the `taxes` module folder to keep the migration
|
|
8
|
+
* sequence numerical.
|
|
9
|
+
*/
|
|
10
|
+
export class Migration20260426T081516TaxesPromotionsInit extends Migration {
|
|
11
|
+
async up() {
|
|
12
|
+
this.addSql(`
|
|
13
|
+
create table "taxes" (
|
|
14
|
+
"id" uuid not null,
|
|
15
|
+
"code" varchar(64) not null,
|
|
16
|
+
"name" varchar(160) not null,
|
|
17
|
+
"rate" numeric(6, 4) not null,
|
|
18
|
+
"country" varchar(2) null,
|
|
19
|
+
"product_type" varchar(16) null,
|
|
20
|
+
"applies_to_vat_statuses" jsonb not null default '[]'::jsonb,
|
|
21
|
+
"is_default" boolean not null default false,
|
|
22
|
+
"priority" int not null default 0,
|
|
23
|
+
"created_at" timestamptz not null,
|
|
24
|
+
"updated_at" timestamptz not null,
|
|
25
|
+
constraint "taxes_pkey" primary key ("id"),
|
|
26
|
+
constraint "taxes_code_unique" unique ("code")
|
|
27
|
+
);
|
|
28
|
+
`);
|
|
29
|
+
this.addSql('create index "taxes_country_index" on "taxes" ("country");');
|
|
30
|
+
this.addSql('create unique index "uniq_taxes_one_default" on "taxes" ("is_default") where "is_default" = true;');
|
|
31
|
+
this.addSql(`
|
|
32
|
+
create table "promotions" (
|
|
33
|
+
"id" uuid not null,
|
|
34
|
+
"code" varchar(64) null,
|
|
35
|
+
"name" varchar(160) not null,
|
|
36
|
+
"kind" varchar(24) not null,
|
|
37
|
+
"value" numeric(14, 4) not null,
|
|
38
|
+
"currency" varchar(3) null,
|
|
39
|
+
"min_cart_subtotal" numeric(14, 2) null,
|
|
40
|
+
"valid_from" timestamptz null,
|
|
41
|
+
"valid_until" timestamptz null,
|
|
42
|
+
"organization_id" uuid null,
|
|
43
|
+
"customer_group_id" uuid null,
|
|
44
|
+
"category_id" uuid null,
|
|
45
|
+
"product_id" uuid null,
|
|
46
|
+
"is_active" boolean not null default true,
|
|
47
|
+
"created_at" timestamptz not null,
|
|
48
|
+
"updated_at" timestamptz not null,
|
|
49
|
+
constraint "promotions_pkey" primary key ("id"),
|
|
50
|
+
constraint "promotions_code_unique" unique ("code")
|
|
51
|
+
);
|
|
52
|
+
`);
|
|
53
|
+
this.addSql('create index "promotions_kind_index" on "promotions" ("kind");');
|
|
54
|
+
this.addSql('create index "promotions_organization_id_index" on "promotions" ("organization_id");');
|
|
55
|
+
this.addSql('create index "promotions_customer_group_id_index" on "promotions" ("customer_group_id");');
|
|
56
|
+
this.addSql('create index "promotions_is_active_index" on "promotions" ("is_active");');
|
|
57
|
+
}
|
|
58
|
+
async down() {
|
|
59
|
+
this.addSql('drop table if exists "promotions" cascade;');
|
|
60
|
+
this.addSql('drop table if exists "taxes" cascade;');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=20260426T081516_taxes_promotions_init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260426T081516_taxes_promotions_init.js","sourceRoot":"","sources":["../../src/migrations/20260426T081516_taxes_promotions_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;GAOG;AACH,MAAM,OAAO,2CAA4C,SAAQ,SAAS;IAC/D,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;KAgBX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,4DAA4D,CAAC,CAAC;QAC1E,IAAI,CAAC,MAAM,CACT,mGAAmG,CACpG,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;KAqBX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,gEAAgE,CAAC,CAAC;QAC9E,IAAI,CAAC,MAAM,CAAC,sFAAsF,CAAC,CAAC;QACpG,IAAI,CAAC,MAAM,CAAC,0FAA0F,CAAC,CAAC;QACxG,IAAI,CAAC,MAAM,CAAC,0EAA0E,CAAC,CAAC;IAC1F,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,4CAA4C,CAAC,CAAC;QAC1D,IAAI,CAAC,MAAM,CAAC,uCAAuC,CAAC,CAAC;IACvD,CAAC;CACF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* The `sales_channel_taxes` sales-channel bridge — the tax classes a channel applies.
|
|
4
|
+
*
|
|
5
|
+
* It was created by the platform's frozen `Migration20260430T170044CoreSalesChannelsPromote`
|
|
6
|
+
* until `specs/120-migration-closure-bridge-ownership/` Phase 2. Under D-226 a
|
|
7
|
+
* bridge between an always-present near side (`sales_channels`, a kernel table)
|
|
8
|
+
* and a switchable far side belongs to the far side, so this module creates it:
|
|
9
|
+
* an instance that does not install `taxes` no longer has a migration corpus
|
|
10
|
+
* naming a table nothing builds.
|
|
11
|
+
*
|
|
12
|
+
* `if not exists`, because every database that has already applied the frozen
|
|
13
|
+
* migration has this table. The storage keys on the class name and holds no
|
|
14
|
+
* checksum, so nothing is re-offered there and this migration is the no-op it
|
|
15
|
+
* reads as; on a fresh database it is the creation.
|
|
16
|
+
*
|
|
17
|
+
* The statements are the frozen ones verbatim — same columns, same primary key,
|
|
18
|
+
* same two foreign keys, same index — so the two paths reach one schema. Its
|
|
19
|
+
* position needs nothing declared: `sales_channels` is the kernel's and
|
|
20
|
+
* `taxes` is created inside the frozen prefix, which every
|
|
21
|
+
* above-watermark migration runs after.
|
|
22
|
+
*/
|
|
23
|
+
export declare class Migration20260912T094654TaxesSalesChannelTaxes extends Migration {
|
|
24
|
+
up(): Promise<void>;
|
|
25
|
+
down(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=20260912T094654_taxes_sales_channel_taxes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260912T094654_taxes_sales_channel_taxes.d.ts","sourceRoot":"","sources":["../../src/migrations/20260912T094654_taxes_sales_channel_taxes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,8CAA+C,SAAQ,SAAS;IAC5D,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAGrC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* The `sales_channel_taxes` sales-channel bridge — the tax classes a channel applies.
|
|
4
|
+
*
|
|
5
|
+
* It was created by the platform's frozen `Migration20260430T170044CoreSalesChannelsPromote`
|
|
6
|
+
* until `specs/120-migration-closure-bridge-ownership/` Phase 2. Under D-226 a
|
|
7
|
+
* bridge between an always-present near side (`sales_channels`, a kernel table)
|
|
8
|
+
* and a switchable far side belongs to the far side, so this module creates it:
|
|
9
|
+
* an instance that does not install `taxes` no longer has a migration corpus
|
|
10
|
+
* naming a table nothing builds.
|
|
11
|
+
*
|
|
12
|
+
* `if not exists`, because every database that has already applied the frozen
|
|
13
|
+
* migration has this table. The storage keys on the class name and holds no
|
|
14
|
+
* checksum, so nothing is re-offered there and this migration is the no-op it
|
|
15
|
+
* reads as; on a fresh database it is the creation.
|
|
16
|
+
*
|
|
17
|
+
* The statements are the frozen ones verbatim — same columns, same primary key,
|
|
18
|
+
* same two foreign keys, same index — so the two paths reach one schema. Its
|
|
19
|
+
* position needs nothing declared: `sales_channels` is the kernel's and
|
|
20
|
+
* `taxes` is created inside the frozen prefix, which every
|
|
21
|
+
* above-watermark migration runs after.
|
|
22
|
+
*/
|
|
23
|
+
export class Migration20260912T094654TaxesSalesChannelTaxes extends Migration {
|
|
24
|
+
async up() {
|
|
25
|
+
this.addSql(`
|
|
26
|
+
create table if not exists "sales_channel_taxes" (
|
|
27
|
+
"sales_channel_id" uuid not null,
|
|
28
|
+
"tax_id" uuid not null,
|
|
29
|
+
constraint "sales_channel_taxes_pkey"
|
|
30
|
+
primary key ("sales_channel_id", "tax_id"),
|
|
31
|
+
constraint "sales_channel_taxes_channel_fk"
|
|
32
|
+
foreign key ("sales_channel_id") references "sales_channels" ("id") on delete cascade,
|
|
33
|
+
constraint "sales_channel_taxes_tax_fk"
|
|
34
|
+
foreign key ("tax_id") references "taxes" ("id") on delete cascade
|
|
35
|
+
);
|
|
36
|
+
`);
|
|
37
|
+
this.addSql('create index if not exists "sales_channel_taxes_tax_id_index" ' +
|
|
38
|
+
'on "sales_channel_taxes" ("tax_id");');
|
|
39
|
+
}
|
|
40
|
+
async down() {
|
|
41
|
+
this.addSql('drop table if exists "sales_channel_taxes" cascade;');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=20260912T094654_taxes_sales_channel_taxes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260912T094654_taxes_sales_channel_taxes.js","sourceRoot":"","sources":["../../src/migrations/20260912T094654_taxes_sales_channel_taxes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,OAAO,8CAA+C,SAAQ,SAAS;IAClE,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;KAWX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,gEAAgE;YAC9D,sCAAsC,CACzC,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,qDAAqD,CAAC,CAAC;IACrE,CAAC;CACF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `./migrations` subpath — every migration class this module owns, as one
|
|
3
|
+
* ordered `migrations` array.
|
|
4
|
+
*
|
|
5
|
+
* The array is what the platform reads when this module is **installed**:
|
|
6
|
+
* `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
|
|
7
|
+
* the package outright when it is absent (D-168).
|
|
8
|
+
*
|
|
9
|
+
* Listed in ascending timestamp, which is the order of this module's own
|
|
10
|
+
* migrations and of nothing else (feature 081): a manifest `dependencies` array
|
|
11
|
+
* is the only thing ordering this block against another module's.
|
|
12
|
+
*
|
|
13
|
+
* The **named** exports stay beside the array, and the asymmetry with
|
|
14
|
+
* `./backend` — which publishes an array and no named class (D-168) — is
|
|
15
|
+
* deliberate. `db/migrations-registry.generated.ts` imports each class by name
|
|
16
|
+
* from this specifier, and a migration class name is contract in a way an entity
|
|
17
|
+
* class name is not: `mikro_orm_migrations` persists it, so it is a string every
|
|
18
|
+
* already-migrated database holds.
|
|
19
|
+
*
|
|
20
|
+
* A class that is in neither the array nor the barrel is a migration that does
|
|
21
|
+
* not run: `migration:pending` reports nothing pending and the first symptom is
|
|
22
|
+
* a query against a table nobody created.
|
|
23
|
+
*/
|
|
24
|
+
import { Migration20260426T081516TaxesPromotionsInit } from './20260426T081516_taxes_promotions_init.js';
|
|
25
|
+
import { Migration20260912T094654TaxesSalesChannelTaxes } from './20260912T094654_taxes_sales_channel_taxes.js';
|
|
26
|
+
export declare const migrations: (typeof Migration20260426T081516TaxesPromotionsInit)[];
|
|
27
|
+
export { Migration20260426T081516TaxesPromotionsInit, Migration20260912T094654TaxesSalesChannelTaxes, };
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,2CAA2C,EAAE,MAAM,4CAA4C,CAAC;AACzG,OAAO,EAAE,8CAA8C,EAAE,MAAM,gDAAgD,CAAC;AAEhH,eAAO,MAAM,UAAU,wDAGtB,CAAC;AAEF,OAAO,EACL,2CAA2C,EAC3C,8CAA8C,GAC/C,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `./migrations` subpath — every migration class this module owns, as one
|
|
3
|
+
* ordered `migrations` array.
|
|
4
|
+
*
|
|
5
|
+
* The array is what the platform reads when this module is **installed**:
|
|
6
|
+
* `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
|
|
7
|
+
* the package outright when it is absent (D-168).
|
|
8
|
+
*
|
|
9
|
+
* Listed in ascending timestamp, which is the order of this module's own
|
|
10
|
+
* migrations and of nothing else (feature 081): a manifest `dependencies` array
|
|
11
|
+
* is the only thing ordering this block against another module's.
|
|
12
|
+
*
|
|
13
|
+
* The **named** exports stay beside the array, and the asymmetry with
|
|
14
|
+
* `./backend` — which publishes an array and no named class (D-168) — is
|
|
15
|
+
* deliberate. `db/migrations-registry.generated.ts` imports each class by name
|
|
16
|
+
* from this specifier, and a migration class name is contract in a way an entity
|
|
17
|
+
* class name is not: `mikro_orm_migrations` persists it, so it is a string every
|
|
18
|
+
* already-migrated database holds.
|
|
19
|
+
*
|
|
20
|
+
* A class that is in neither the array nor the barrel is a migration that does
|
|
21
|
+
* not run: `migration:pending` reports nothing pending and the first symptom is
|
|
22
|
+
* a query against a table nobody created.
|
|
23
|
+
*/
|
|
24
|
+
import { Migration20260426T081516TaxesPromotionsInit } from './20260426T081516_taxes_promotions_init.js';
|
|
25
|
+
import { Migration20260912T094654TaxesSalesChannelTaxes } from './20260912T094654_taxes_sales_channel_taxes.js';
|
|
26
|
+
export const migrations = [
|
|
27
|
+
Migration20260426T081516TaxesPromotionsInit,
|
|
28
|
+
Migration20260912T094654TaxesSalesChannelTaxes,
|
|
29
|
+
];
|
|
30
|
+
export { Migration20260426T081516TaxesPromotionsInit, Migration20260912T094654TaxesSalesChannelTaxes, };
|
|
31
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,2CAA2C,EAAE,MAAM,4CAA4C,CAAC;AACzG,OAAO,EAAE,8CAA8C,EAAE,MAAM,gDAAgD,CAAC;AAEhH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,2CAA2C;IAC3C,8CAA8C;CAC/C,CAAC;AAEF,OAAO,EACL,2CAA2C,EAC3C,8CAA8C,GAC/C,CAAC"}
|
package/docs/taxes.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: taxes
|
|
3
|
+
description: Tax-rate resolver narrowed by country / product type / VAT status
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# `taxes`
|
|
7
|
+
|
|
8
|
+
Tax-rate resolution. Each `Tax` row is a rule
|
|
9
|
+
narrowed by zero or more of `country`, `productType`, `appliesToVatStatuses`.
|
|
10
|
+
|
|
11
|
+
## Public surface
|
|
12
|
+
|
|
13
|
+
| Verb + Path | Audience | Purpose |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `GET /api/v1/admin/taxes` | admin (`taxes:read`) | List rules |
|
|
16
|
+
| `PUT /api/v1/admin/taxes/:code` | admin (`taxes:write`) | Upsert |
|
|
17
|
+
| `DELETE /api/v1/admin/taxes/:id` | admin (`taxes:write`) | Remove |
|
|
18
|
+
| `GET /api/v1/admin/taxes/preview?country&productType&vatStatus` | admin (`taxes:read`) | Resolve effective rate |
|
|
19
|
+
|
|
20
|
+
Every one of the four enforced `catalog:write` until 2026-08-28 — the two reads
|
|
21
|
+
included, so seeing a VAT rate required the authority to change it. The module
|
|
22
|
+
now owns `taxes:read` and `taxes:write`, declared in its manifest and therefore
|
|
23
|
+
grantable on `/admin-roles`. It is a **clean break**: a role that reached the tax
|
|
24
|
+
table through the catalogue's write code is granted `taxes:read` (and
|
|
25
|
+
`taxes:write`, to edit) explicitly. See
|
|
26
|
+
`backend/test/contract/taxes/permission-authority.test.ts`.
|
|
27
|
+
|
|
28
|
+
Nothing else reads a rate through these routes: `orders`, `carts`,
|
|
29
|
+
`product_feeds` and `quote_requests` resolve one in process through the
|
|
30
|
+
`taxService` port, which the permission change does not touch.
|
|
31
|
+
|
|
32
|
+
## Resolution algorithm
|
|
33
|
+
|
|
34
|
+
`TaxService.taxRateFor({ country, productType, vatStatus })`:
|
|
35
|
+
|
|
36
|
+
1. A rule **matches** when each narrowing field is either null
|
|
37
|
+
(unconstrained) or equal to the input.
|
|
38
|
+
2. Among matching rules, the one with the **most narrowed fields** wins.
|
|
39
|
+
3. Specificity ties → `priority` desc → `createdAt` asc.
|
|
40
|
+
4. When no rule matches, the row with `isDefault=true` wins.
|
|
41
|
+
5. When no default exists either, the resolver returns `{ source: 'none' }` —
|
|
42
|
+
an answer with **no `rate` field at all**.
|
|
43
|
+
|
|
44
|
+
Point 5 is a type, not a convention. A configured 0% rate is a
|
|
45
|
+
legitimate answer in some jurisdictions, so it comes back as
|
|
46
|
+
`{ source: 'default', rate: 0, taxId }` and prices an order like any other rate.
|
|
47
|
+
"Nothing is configured" is not an answer, so it carries no number a caller could
|
|
48
|
+
spend by accident: `ResolvedTax` is a discriminated union and `.rate` does not
|
|
49
|
+
compile until the caller narrows on `source`. The two used to share one shape,
|
|
50
|
+
`{ rate: 0, source: 'none' }`, and every consumer read `.rate` — which is how an
|
|
51
|
+
unconfigured deployment quoted 0% VAT onto real invoices.
|
|
52
|
+
|
|
53
|
+
A third state — the `taxes` module being absent — is deliberately not in the
|
|
54
|
+
union. Absence is not a value: the port gate raises the 503 `MODULE_DISABLED`
|
|
55
|
+
envelope before a resolution runs, so an order the platform cannot tax is
|
|
56
|
+
refused rather than taxed at a figure nobody chose.
|
|
57
|
+
|
|
58
|
+
The "at most one default" invariant is enforced by a partial unique index
|
|
59
|
+
`(is_default) WHERE is_default = true`. `upsertByCode` demotes any prior
|
|
60
|
+
default before promoting the new one so the swap stays consistent.
|
|
61
|
+
|
|
62
|
+
## Entities
|
|
63
|
+
|
|
64
|
+
`Tax` — `code`, `name`, `rate` (0..1), nullable `country` /
|
|
65
|
+
`productType`, JSONB `appliesToVatStatuses`, `isDefault`, `priority`.
|
|
66
|
+
|
|
67
|
+
## Extension points
|
|
68
|
+
|
|
69
|
+
- **VAT-OSS / one-stop-shop** — model the buyer's status separately and
|
|
70
|
+
emit different rates per country pair.
|
|
71
|
+
- **Reduced-rate triggers** — the `appliesToVatStatuses` array is the
|
|
72
|
+
natural place to add buyer-side discriminators (e.g. consumer vs B2B).
|
|
73
|
+
- **Per-currency rates** — today the rate is currency-agnostic. For VAT
|
|
74
|
+
in different reporting currencies, add a `currency` column and route
|
|
75
|
+
the resolver based on the order's currency.
|
package/i18n/en.json
ADDED
package/i18n/pl.json
ADDED