@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,76 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
3
|
+
import type { ModuleContext } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
5
|
+
import type { SalesChannelMembershipPort } from '@endora-commerce/platform/kernel';
|
|
6
|
+
import type { ChannelMemberEntityType } from '@endora-commerce/contracts';
|
|
7
|
+
import type { DictionaryValidator } from '@endora-commerce/contracts';
|
|
8
|
+
import { TaxService } from './services/tax-service.js';
|
|
9
|
+
import { Tax } from './entities/tax.entity.js';
|
|
10
|
+
/**
|
|
11
|
+
* `taxes` — three optional arguments, all of which change what a write means
|
|
12
|
+
* (feature 072, wave 2, T119).
|
|
13
|
+
*
|
|
14
|
+
* `TaxService` took its channel-membership binder, its dictionary validator and
|
|
15
|
+
* its audit writer as optional constructor arguments. Omit them and a tax rate
|
|
16
|
+
* is created that binds to no sales channel, validates no country or region
|
|
17
|
+
* code, and records nothing — a write that looks identical from the API and is
|
|
18
|
+
* wrong in three separate ways. Both roots passed all three, so nothing was
|
|
19
|
+
* live; this is the eleventh, twelfth and thirteenth removal of that shape in
|
|
20
|
+
* this transition, and the argument for removing it does not depend on any of
|
|
21
|
+
* them having fired.
|
|
22
|
+
*
|
|
23
|
+
* `taxService` is a **port**: `orders` and `carts` resolve it across a module
|
|
24
|
+
* boundary to price a line, so an operator switching taxes off should get the
|
|
25
|
+
* explicit 503 envelope rather than a service that quietly returns no rate —
|
|
26
|
+
* which, on a checkout path, is a discount.
|
|
27
|
+
*/
|
|
28
|
+
export interface TaxesCradle {
|
|
29
|
+
/**
|
|
30
|
+
* Feature 120 (FR-015) — the platform's channel-bridge registry, resolved by
|
|
31
|
+
* **container name** rather than imported. The shape is the narrow one this
|
|
32
|
+
* module needs, which is what `ctx.cradle<C>()` asks an author to declare;
|
|
33
|
+
* the registry itself is the kernel's and is published on no barrel.
|
|
34
|
+
*/
|
|
35
|
+
readonly salesChannelBridgeRegistry: {
|
|
36
|
+
register(bridge: {
|
|
37
|
+
entityType: ChannelMemberEntityType;
|
|
38
|
+
table: string;
|
|
39
|
+
entityIdColumn: string;
|
|
40
|
+
}): void;
|
|
41
|
+
};
|
|
42
|
+
readonly emFactory: () => EntityManager;
|
|
43
|
+
readonly auditLogService: AuditPort;
|
|
44
|
+
readonly requireAdmin: RequireAdminFactory;
|
|
45
|
+
readonly salesChannelMembershipPort: SalesChannelMembershipPort;
|
|
46
|
+
readonly dictionaryValidator: DictionaryValidator | undefined;
|
|
47
|
+
readonly taxService: TaxService;
|
|
48
|
+
}
|
|
49
|
+
export declare function registerModule(ctx: ModuleContext): void;
|
|
50
|
+
/**
|
|
51
|
+
* The module's persisted entity classes, on the `./backend` subpath, as one
|
|
52
|
+
* array and **no named class export** (D-168).
|
|
53
|
+
*
|
|
54
|
+
* This is the shape the platform reads when the package is *installed*: the
|
|
55
|
+
* boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
|
|
56
|
+
* and the static declaration reader (`scripts/lib/package-declarations.ts`),
|
|
57
|
+
* which is the third source of `check:module-boundary`'s `table→owner` map and
|
|
58
|
+
* the package pass of `check-entity-tenant-classification`. A missing array is
|
|
59
|
+
* answered with `[]` — zero entities registered, no error anywhere.
|
|
60
|
+
*/
|
|
61
|
+
export declare const entities: (typeof Tax)[];
|
|
62
|
+
/**
|
|
63
|
+
* The sales-channel membership bridge this module owns (feature 120, FR-015).
|
|
64
|
+
*
|
|
65
|
+
* Exported as well as registered because the owning module is the only honest
|
|
66
|
+
* source for it: a caller that composes no platform — the bare-database test
|
|
67
|
+
* harness, which constructs the kernel's membership service directly — declares
|
|
68
|
+
* these from here rather than from a list of nine it keeps itself, which is
|
|
69
|
+
* exactly the total map this feature deleted.
|
|
70
|
+
*/
|
|
71
|
+
export declare const salesChannelBridges: ReadonlyArray<{
|
|
72
|
+
readonly entityType: ChannelMemberEntityType;
|
|
73
|
+
readonly table: string;
|
|
74
|
+
readonly entityIdColumn: string;
|
|
75
|
+
}>;
|
|
76
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACnF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,KAAK,EACM,mBAAmB,EAEpC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAGvD,OAAO,EAAE,GAAG,EAAE,MAAM,0BAA0B,CAAC;AAE/C;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,0BAA0B,EAAE;QACnC,QAAQ,CAAC,MAAM,EAAE;YACf,UAAU,EAAE,uBAAuB,CAAC;YACpC,KAAK,EAAE,MAAM,CAAC;YACd,cAAc,EAAE,MAAM,CAAC;SACxB,GAAG,IAAI,CAAC;KACV,CAAC;IACF,QAAQ,CAAC,SAAS,EAAE,MAAM,aAAa,CAAC;IACxC,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,0BAA0B,EAAE,0BAA0B,CAAC;IAChE,QAAQ,CAAC,mBAAmB,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAC9D,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;CACjC;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAwEvD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,gBAEpB,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,EAAE,aAAa,CAAC;IAC9C,QAAQ,CAAC,UAAU,EAAE,uBAAuB,CAAC;IAC7C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC,CAEA,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { lazyPort } from '@endora-commerce/platform/kernel';
|
|
2
|
+
import { TaxService } from './services/tax-service.js';
|
|
3
|
+
import { registerTaxRoutes } from './routes.js';
|
|
4
|
+
import { registerTaxCountryReferences } from './services/tax-country-reference.js';
|
|
5
|
+
import { Tax } from './entities/tax.entity.js';
|
|
6
|
+
export function registerModule(ctx) {
|
|
7
|
+
/**
|
|
8
|
+
* Feature 120 (FR-015) — the sales-channel membership bridge this module owns.
|
|
9
|
+
*
|
|
10
|
+
* The platform used to hold a map **total** over
|
|
11
|
+
* `ChannelMemberEntityTypeSchema`, naming this module's bridge table from
|
|
12
|
+
* inside the kernel. That is D-52/D-53's prohibition expressed in a contract
|
|
13
|
+
* instead of an import, and it meant a membership call for 'tax' on an
|
|
14
|
+
* instance that never installed this module executed SQL against a relation
|
|
15
|
+
* that is not there (D-226). The module whose migration creates the table is
|
|
16
|
+
* the one that says where it is; a member nothing registers now refuses at
|
|
17
|
+
* the call instead.
|
|
18
|
+
*
|
|
19
|
+
* A **contribution** hook and deliberately **unprobed** (D-62/D-68): a
|
|
20
|
+
* registration is an inert statement about the schema rather than a
|
|
21
|
+
* capability, the rows outlive an operator switching this module off, and
|
|
22
|
+
* probing would make a runtime activation flip require a restart. It is
|
|
23
|
+
* declared first in this module so any later hook of its own that binds a
|
|
24
|
+
* membership finds the bridge already there.
|
|
25
|
+
*/
|
|
26
|
+
ctx.onBoot(() => {
|
|
27
|
+
const { salesChannelBridgeRegistry } = ctx.cradle();
|
|
28
|
+
for (const bridge of salesChannelBridges)
|
|
29
|
+
salesChannelBridgeRegistry.register(bridge);
|
|
30
|
+
});
|
|
31
|
+
ctx.di.providePort('taxService', ctx
|
|
32
|
+
.asFunction(({ emFactory, auditLogService, }) => new TaxService(emFactory, lazyPort(ctx, 'salesChannelMembershipPort'), lazyPort(ctx, 'dictionaryValidator'), auditLogService))
|
|
33
|
+
.singleton());
|
|
34
|
+
ctx.routes(async (app) => {
|
|
35
|
+
const { requireAdmin } = ctx.cradle();
|
|
36
|
+
await registerTaxRoutes(app, {
|
|
37
|
+
// Lazily, even though the module owns this port: route *registration* runs
|
|
38
|
+
// inside `buildServer` whatever the module's effective state is, so
|
|
39
|
+
// destructuring the gate here would stop the next start instead of
|
|
40
|
+
// stopping the routes (D-40).
|
|
41
|
+
taxService: lazyPort(ctx, 'taxService'),
|
|
42
|
+
requireAdmin,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* This module's rows carry a country code, so it answers "who still points at
|
|
47
|
+
* this country?" about its own tables (feature 077, D-87), where the owner used
|
|
48
|
+
* to count them with SQL naming this module's tables.
|
|
49
|
+
*
|
|
50
|
+
* A **contribution** hook: it pushes an inert descriptor into `countryReferenceRegistry`,
|
|
51
|
+
* an ungated registry, and carries no presence probe (D-62/D-68). Probing
|
|
52
|
+
* would be wrong in the dangerous direction — a switched-off module still owns
|
|
53
|
+
* the rows, so its country must still refuse the delete, which is the
|
|
54
|
+
* enumeration policy the registry states.
|
|
55
|
+
*/
|
|
56
|
+
ctx.onBoot(() => {
|
|
57
|
+
registerTaxCountryReferences(lazyPort(ctx, 'countryReferenceRegistry'), ctx.cradle().emFactory);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The module's persisted entity classes, on the `./backend` subpath, as one
|
|
62
|
+
* array and **no named class export** (D-168).
|
|
63
|
+
*
|
|
64
|
+
* This is the shape the platform reads when the package is *installed*: the
|
|
65
|
+
* boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
|
|
66
|
+
* and the static declaration reader (`scripts/lib/package-declarations.ts`),
|
|
67
|
+
* which is the third source of `check:module-boundary`'s `table→owner` map and
|
|
68
|
+
* the package pass of `check-entity-tenant-classification`. A missing array is
|
|
69
|
+
* answered with `[]` — zero entities registered, no error anywhere.
|
|
70
|
+
*/
|
|
71
|
+
export const entities = [
|
|
72
|
+
Tax,
|
|
73
|
+
];
|
|
74
|
+
/**
|
|
75
|
+
* The sales-channel membership bridge this module owns (feature 120, FR-015).
|
|
76
|
+
*
|
|
77
|
+
* Exported as well as registered because the owning module is the only honest
|
|
78
|
+
* source for it: a caller that composes no platform — the bare-database test
|
|
79
|
+
* harness, which constructs the kernel's membership service directly — declares
|
|
80
|
+
* these from here rather than from a list of nine it keeps itself, which is
|
|
81
|
+
* exactly the total map this feature deleted.
|
|
82
|
+
*/
|
|
83
|
+
export const salesChannelBridges = [
|
|
84
|
+
{ entityType: 'tax', table: 'sales_channel_taxes', entityIdColumn: 'tax_id' },
|
|
85
|
+
];
|
|
86
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAQ5D,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,4BAA4B,EAAE,MAAM,qCAAqC,CAAC;AACnF,OAAO,EAAE,GAAG,EAAE,MAAM,0BAA0B,CAAC;AA2C/C,MAAM,UAAU,cAAc,CAAC,GAAkB;IAC/C;;;;;;;;;;;;;;;;;;OAkBG;IACH,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE;QACd,MAAM,EAAE,0BAA0B,EAAE,GAAG,GAAG,CAAC,MAAM,EAAe,CAAC;QACjE,KAAK,MAAM,MAAM,IAAI,mBAAmB;YAAE,0BAA0B,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxF,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,EAAE,CAAC,WAAW,CAChB,YAAY,EACZ,GAAG;SACA,UAAU,CACT,CAAC,EACC,SAAS,EACT,eAAe,GACH,EAAE,EAAE,CAChB,IAAI,UAAU,CACZ,SAAS,EACT,QAAQ,CAA6B,GAAG,EAAE,4BAA4B,CAAC,EACvE,QAAQ,CAAsB,GAAG,EAAE,qBAAqB,CAAC,EACzD,eAAe,CAChB,CACJ;SACA,SAAS,EAAE,CACf,CAAC;IAEF,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACvB,MAAM,EAAE,YAAY,EAAE,GAAG,GAAG,CAAC,MAAM,EAAe,CAAC;QACnD,MAAM,iBAAiB,CAAC,GAAG,EAAE;YAC3B,2EAA2E;YAC3E,oEAAoE;YACpE,mEAAmE;YACnE,8BAA8B;YAC9B,UAAU,EAAE,QAAQ,CAAa,GAAG,EAAE,YAAY,CAAC;YACnD,YAAY;SACb,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH;;;;;;;;;;OAUG;IACH,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE;QACd,4BAA4B,CAC1B,QAAQ,CAAkC,GAAG,EAAE,0BAA0B,CAAC,EAC1E,GAAG,CAAC,MAAM,EAAe,CAAC,SAAS,CACpC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,GAAG;CACJ,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAI3B;IACH,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,QAAQ,EAAE;CAC9E,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { TaxService } from './services/tax-service.js';
|
|
3
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
4
|
+
export interface TaxRoutesDeps {
|
|
5
|
+
taxService: TaxService;
|
|
6
|
+
requireAdmin: RequireAdminFactory;
|
|
7
|
+
}
|
|
8
|
+
export declare function registerTaxRoutes(app: FastifyInstance, deps: TaxRoutesDeps): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,UAAU,CAAC;IACvB,YAAY,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAiEf"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { taxResolutionInputSchema, upsertTaxRequestSchema } from '@endora-commerce/contracts';
|
|
2
|
+
export async function registerTaxRoutes(app, deps) {
|
|
3
|
+
const { taxService, requireAdmin } = deps;
|
|
4
|
+
app.get('/api/v1/admin/taxes', { preHandler: requireAdmin('taxes:read') }, async () => {
|
|
5
|
+
const rows = await taxService.list();
|
|
6
|
+
return { data: rows.map(serialize) };
|
|
7
|
+
});
|
|
8
|
+
app.put('/api/v1/admin/taxes/:code', {
|
|
9
|
+
preHandler: requireAdmin('taxes:write'),
|
|
10
|
+
schema: { body: upsertTaxRequestSchema },
|
|
11
|
+
}, async (request) => {
|
|
12
|
+
const body = upsertTaxRequestSchema.parse(request.body);
|
|
13
|
+
const row = await taxService.upsertByCode({
|
|
14
|
+
code: request.params.code,
|
|
15
|
+
name: body.name,
|
|
16
|
+
rate: body.rate,
|
|
17
|
+
...(body.country !== undefined ? { country: body.country } : {}),
|
|
18
|
+
...(body.productType !== undefined ? { productType: body.productType } : {}),
|
|
19
|
+
...(body.appliesToVatStatuses !== undefined
|
|
20
|
+
? { appliesToVatStatuses: body.appliesToVatStatuses }
|
|
21
|
+
: {}),
|
|
22
|
+
...(body.isDefault !== undefined ? { isDefault: body.isDefault } : {}),
|
|
23
|
+
...(body.priority !== undefined ? { priority: body.priority } : {}),
|
|
24
|
+
});
|
|
25
|
+
return { data: serialize(row) };
|
|
26
|
+
});
|
|
27
|
+
app.delete('/api/v1/admin/taxes/:id', { preHandler: requireAdmin('taxes:write') }, async (request, reply) => {
|
|
28
|
+
await taxService.remove(request.params.id);
|
|
29
|
+
return reply.status(204).send();
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* `taxes:read`, with the list, and not `taxes:write`: the preview reads the
|
|
33
|
+
* configuration one resolution further on — "what rate would this order line
|
|
34
|
+
* be charged" — and writes nothing. All four routes were `catalog:write`
|
|
35
|
+
* until 2026-08-28, so seeing the tax table at all required the authority to
|
|
36
|
+
* rewrite it; the pair is what separates the two.
|
|
37
|
+
*/
|
|
38
|
+
app.get('/api/v1/admin/taxes/preview', { preHandler: requireAdmin('taxes:read') }, async (request) => {
|
|
39
|
+
const input = taxResolutionInputSchema.parse({
|
|
40
|
+
country: request.query.country,
|
|
41
|
+
productType: request.query.productType,
|
|
42
|
+
vatStatus: request.query.vatStatus,
|
|
43
|
+
});
|
|
44
|
+
const result = await taxService.taxRateFor(input);
|
|
45
|
+
return { data: result };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function serialize(row) {
|
|
49
|
+
return {
|
|
50
|
+
id: row.id,
|
|
51
|
+
code: row.code,
|
|
52
|
+
name: row.name,
|
|
53
|
+
rate: Number(row.rate),
|
|
54
|
+
country: row.country ?? null,
|
|
55
|
+
productType: row.productType ?? null,
|
|
56
|
+
appliesToVatStatuses: row.appliesToVatStatuses,
|
|
57
|
+
isDefault: row.isDefault,
|
|
58
|
+
priority: row.priority,
|
|
59
|
+
createdAt: row.createdAt.toISOString(),
|
|
60
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DictionaryReferenceRegistryPort } from '@endora-commerce/contracts';
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
3
|
+
/**
|
|
4
|
+
* Who in `taxes` still points at a country (feature 077, D-87).
|
|
5
|
+
*
|
|
6
|
+
* `dictionaries` used to count this table itself and join it a second time
|
|
7
|
+
* for the orphan report, naming it in strings no import-level boundary check
|
|
8
|
+
* can see. A tax rule is scoped by country, so a country the platform drops
|
|
9
|
+
* leaves the rule unapplicable.
|
|
10
|
+
*/
|
|
11
|
+
export declare function registerTaxCountryReferences(registry: DictionaryReferenceRegistryPort, emFactory: () => EntityManager): void;
|
|
12
|
+
//# sourceMappingURL=tax-country-reference.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tax-country-reference.d.ts","sourceRoot":"","sources":["../../../src/backend/services/tax-country-reference.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,4BAA4B,CAAC;AAClF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,+BAA+B,EACzC,SAAS,EAAE,MAAM,aAAa,GAC7B,IAAI,CAoBN"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who in `taxes` still points at a country (feature 077, D-87).
|
|
3
|
+
*
|
|
4
|
+
* `dictionaries` used to count this table itself and join it a second time
|
|
5
|
+
* for the orphan report, naming it in strings no import-level boundary check
|
|
6
|
+
* can see. A tax rule is scoped by country, so a country the platform drops
|
|
7
|
+
* leaves the rule unapplicable.
|
|
8
|
+
*/
|
|
9
|
+
export function registerTaxCountryReferences(registry, emFactory) {
|
|
10
|
+
registry.register({
|
|
11
|
+
ownerModuleId: 'taxes',
|
|
12
|
+
consumer: 'taxes',
|
|
13
|
+
tableName: 'taxes',
|
|
14
|
+
columnName: 'country',
|
|
15
|
+
blocking: true,
|
|
16
|
+
countReferences: async (code) => {
|
|
17
|
+
const rows = (await emFactory().execute(`select count(*)::int as n from "taxes" where "country" = ?`, [code]));
|
|
18
|
+
return rows[0]?.n ?? 0;
|
|
19
|
+
},
|
|
20
|
+
usedCodes: async () => (await emFactory().execute(`select "country" as code, count(*)::int as count
|
|
21
|
+
from "taxes" where "country" is not null group by "country"`)),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=tax-country-reference.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tax-country-reference.js","sourceRoot":"","sources":["../../../src/backend/services/tax-country-reference.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,MAAM,UAAU,4BAA4B,CAC1C,QAAyC,EACzC,SAA8B;IAE9B,QAAQ,CAAC,QAAQ,CAAC;QAChB,aAAa,EAAE,OAAO;QACtB,QAAQ,EAAE,OAAO;QACjB,SAAS,EAAE,OAAO;QAClB,UAAU,EAAE,SAAS;QACrB,QAAQ,EAAE,IAAI;QACd,eAAe,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YAC9B,MAAM,IAAI,GAAG,CAAC,MAAM,SAAS,EAAE,CAAC,OAAO,CACrC,4DAA4D,EAC5D,CAAC,IAAI,CAAC,CACP,CAAyB,CAAC;YAC3B,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;QACD,SAAS,EAAE,KAAK,IAAI,EAAE,CACpB,CAAC,MAAM,SAAS,EAAE,CAAC,OAAO,CACxB;uEAC+D,CAChE,CAA2C;KAC/C,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type DictionaryValidator, type ResolvedTax, type TaxResolutionInput } from '@endora-commerce/contracts';
|
|
3
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import type { SalesChannelMembershipPort } from '@endora-commerce/platform/kernel';
|
|
5
|
+
import { Tax } from '../entities/tax.entity.js';
|
|
6
|
+
/**
|
|
7
|
+
* TaxService (T131 / FR-051).
|
|
8
|
+
*
|
|
9
|
+
* `taxRateFor({ country, productType, vatStatus })` returns the applicable
|
|
10
|
+
* rate. Resolution rules:
|
|
11
|
+
* - A rule matches when each of its narrowing fields is either null
|
|
12
|
+
* (unconstrained) or equal to the corresponding input field.
|
|
13
|
+
* - Among matching rules, the one with the most narrowed fields wins.
|
|
14
|
+
* - Ties are broken by `priority` desc, then `createdAt` asc.
|
|
15
|
+
* - When no rule matches, the row with `isDefault=true` wins.
|
|
16
|
+
* - When no default exists either, the resolver returns `{ source: 'none' }` —
|
|
17
|
+
* an answer that carries no rate, because there is none to carry.
|
|
18
|
+
*
|
|
19
|
+
* The last line is the whole point of the union (issue #124). A configured 0%
|
|
20
|
+
* rate comes back as `{ source: 'rule' | 'default', rate: 0 }` and prices an
|
|
21
|
+
* order; "nothing is configured" comes back without a `rate` at all, so no
|
|
22
|
+
* consumer can spend it as zero by accident. Absence of the module itself is
|
|
23
|
+
* neither: the port gate refuses the resolution before it starts.
|
|
24
|
+
*/
|
|
25
|
+
export declare class TaxService {
|
|
26
|
+
#private;
|
|
27
|
+
private readonly emFactory;
|
|
28
|
+
/** Feature 005 / T027b — auto-bind newly-created Taxes to the system default. */
|
|
29
|
+
private readonly salesChannelMembership?;
|
|
30
|
+
private readonly dictionaryValidator?;
|
|
31
|
+
private readonly auditLog?;
|
|
32
|
+
constructor(emFactory: () => EntityManager,
|
|
33
|
+
/** Feature 005 / T027b — auto-bind newly-created Taxes to the system default. */
|
|
34
|
+
salesChannelMembership?: SalesChannelMembershipPort | undefined, dictionaryValidator?: DictionaryValidator | undefined, auditLog?: AuditPort | undefined);
|
|
35
|
+
list(): Promise<Tax[]>;
|
|
36
|
+
getById(id: string): Promise<Tax>;
|
|
37
|
+
upsertByCode(input: {
|
|
38
|
+
code: string;
|
|
39
|
+
name: string;
|
|
40
|
+
rate: number;
|
|
41
|
+
country?: string | null;
|
|
42
|
+
productType?: 'simple' | 'configurable' | 'grouped' | 'bundle' | 'virtual' | null;
|
|
43
|
+
appliesToVatStatuses?: Array<'vat_payer' | 'vat_exempt' | 'reverse_charge'>;
|
|
44
|
+
isDefault?: boolean;
|
|
45
|
+
priority?: number;
|
|
46
|
+
}): Promise<Tax>;
|
|
47
|
+
remove(id: string): Promise<void>;
|
|
48
|
+
taxRateFor(input: TaxResolutionInput): Promise<ResolvedTax>;
|
|
49
|
+
private validateCountry;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=tax-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tax-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/tax-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAIL,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACxB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACnF,OAAO,EAAE,GAAG,EAAE,MAAM,2BAA2B,CAAC;AAEhD;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,UAAU;;IAEnB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAC;IACxC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAJT,SAAS,EAAE,MAAM,aAAa;IAC/C,iFAAiF;IAChE,sBAAsB,CAAC,EAAE,0BAA0B,YAAA,EACnD,mBAAmB,CAAC,EAAE,mBAAmB,YAAA,EACzC,QAAQ,CAAC,EAAE,SAAS,YAAA;IASjC,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAItB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAMjC,YAAY,CAAC,KAAK,EAAE;QACxB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,WAAW,CAAC,EAAE,QAAQ,GAAG,cAAc,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC;QAClF,oBAAoB,CAAC,EAAE,KAAK,CAAC,WAAW,GAAG,YAAY,GAAG,gBAAgB,CAAC,CAAC;QAC5E,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,GAAG,CAAC;IAoDV,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQjC,UAAU,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,CAAC;YAkCnD,eAAe;CAqB9B"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { DictionaryReferenceError, ERROR_CODES, dispatchValidatorMode, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { recordAuditFromContext } from '@endora-commerce/platform/commands';
|
|
4
|
+
import { Tax } from '../entities/tax.entity.js';
|
|
5
|
+
/**
|
|
6
|
+
* TaxService (T131 / FR-051).
|
|
7
|
+
*
|
|
8
|
+
* `taxRateFor({ country, productType, vatStatus })` returns the applicable
|
|
9
|
+
* rate. Resolution rules:
|
|
10
|
+
* - A rule matches when each of its narrowing fields is either null
|
|
11
|
+
* (unconstrained) or equal to the corresponding input field.
|
|
12
|
+
* - Among matching rules, the one with the most narrowed fields wins.
|
|
13
|
+
* - Ties are broken by `priority` desc, then `createdAt` asc.
|
|
14
|
+
* - When no rule matches, the row with `isDefault=true` wins.
|
|
15
|
+
* - When no default exists either, the resolver returns `{ source: 'none' }` —
|
|
16
|
+
* an answer that carries no rate, because there is none to carry.
|
|
17
|
+
*
|
|
18
|
+
* The last line is the whole point of the union (issue #124). A configured 0%
|
|
19
|
+
* rate comes back as `{ source: 'rule' | 'default', rate: 0 }` and prices an
|
|
20
|
+
* order; "nothing is configured" comes back without a `rate` at all, so no
|
|
21
|
+
* consumer can spend it as zero by accident. Absence of the module itself is
|
|
22
|
+
* neither: the port gate refuses the resolution before it starts.
|
|
23
|
+
*/
|
|
24
|
+
export class TaxService {
|
|
25
|
+
emFactory;
|
|
26
|
+
salesChannelMembership;
|
|
27
|
+
dictionaryValidator;
|
|
28
|
+
auditLog;
|
|
29
|
+
constructor(emFactory,
|
|
30
|
+
/** Feature 005 / T027b — auto-bind newly-created Taxes to the system default. */
|
|
31
|
+
salesChannelMembership, dictionaryValidator, auditLog) {
|
|
32
|
+
this.emFactory = emFactory;
|
|
33
|
+
this.salesChannelMembership = salesChannelMembership;
|
|
34
|
+
this.dictionaryValidator = dictionaryValidator;
|
|
35
|
+
this.auditLog = auditLog;
|
|
36
|
+
}
|
|
37
|
+
#audit(em, action, objectId, stateBefore, stateAfter) {
|
|
38
|
+
if (this.auditLog) {
|
|
39
|
+
recordAuditFromContext(this.auditLog, em, { action, objectType: 'tax', objectId, stateBefore, stateAfter });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async list() {
|
|
43
|
+
return this.emFactory().find(Tax, {}, { orderBy: { priority: 'desc', code: 'asc' } });
|
|
44
|
+
}
|
|
45
|
+
async getById(id) {
|
|
46
|
+
const row = await this.emFactory().findOne(Tax, { id });
|
|
47
|
+
if (!row)
|
|
48
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, `Tax ${id} not found.`);
|
|
49
|
+
return row;
|
|
50
|
+
}
|
|
51
|
+
async upsertByCode(input) {
|
|
52
|
+
const em = this.emFactory();
|
|
53
|
+
if (input.isDefault) {
|
|
54
|
+
await em.getConnection().execute(`update taxes set is_default = false where is_default = true and code <> ?`, [input.code]);
|
|
55
|
+
}
|
|
56
|
+
const existing = await em.findOne(Tax, { code: input.code });
|
|
57
|
+
if (input.country != null) {
|
|
58
|
+
await this.validateCountry(input.country, existing
|
|
59
|
+
? dispatchValidatorMode(existing.country, input.country)
|
|
60
|
+
: 'create-or-change');
|
|
61
|
+
}
|
|
62
|
+
if (existing) {
|
|
63
|
+
existing.name = input.name;
|
|
64
|
+
existing.rate = String(input.rate);
|
|
65
|
+
if (input.country !== undefined)
|
|
66
|
+
existing.country = input.country;
|
|
67
|
+
if (input.productType !== undefined)
|
|
68
|
+
existing.productType = input.productType;
|
|
69
|
+
if (input.appliesToVatStatuses !== undefined) {
|
|
70
|
+
existing.appliesToVatStatuses = input.appliesToVatStatuses;
|
|
71
|
+
}
|
|
72
|
+
if (input.isDefault !== undefined)
|
|
73
|
+
existing.isDefault = input.isDefault;
|
|
74
|
+
if (input.priority !== undefined)
|
|
75
|
+
existing.priority = input.priority;
|
|
76
|
+
this.#audit(em, 'tax.upsert', existing.id, null, { code: existing.code, rate: existing.rate });
|
|
77
|
+
await em.flush();
|
|
78
|
+
return existing;
|
|
79
|
+
}
|
|
80
|
+
const row = em.create(Tax, {
|
|
81
|
+
code: input.code,
|
|
82
|
+
name: input.name,
|
|
83
|
+
rate: String(input.rate),
|
|
84
|
+
...(input.country !== undefined ? { country: input.country } : {}),
|
|
85
|
+
...(input.productType !== undefined ? { productType: input.productType } : {}),
|
|
86
|
+
...(input.appliesToVatStatuses !== undefined
|
|
87
|
+
? { appliesToVatStatuses: input.appliesToVatStatuses }
|
|
88
|
+
: {}),
|
|
89
|
+
...(input.isDefault !== undefined ? { isDefault: input.isDefault } : {}),
|
|
90
|
+
...(input.priority !== undefined ? { priority: input.priority } : {}),
|
|
91
|
+
});
|
|
92
|
+
em.persist(row);
|
|
93
|
+
this.#audit(em, 'tax.upsert', row.id, null, { code: row.code, rate: row.rate });
|
|
94
|
+
await em.flush();
|
|
95
|
+
if (this.salesChannelMembership) {
|
|
96
|
+
await this.salesChannelMembership.bindToDefaultIfEmpty('tax', row.id);
|
|
97
|
+
}
|
|
98
|
+
return row;
|
|
99
|
+
}
|
|
100
|
+
async remove(id) {
|
|
101
|
+
const em = this.emFactory();
|
|
102
|
+
const row = await em.findOne(Tax, { id });
|
|
103
|
+
if (!row)
|
|
104
|
+
return;
|
|
105
|
+
this.#audit(em, 'tax.delete', row.id, { code: row.code }, null);
|
|
106
|
+
await em.removeAndFlush(row);
|
|
107
|
+
}
|
|
108
|
+
async taxRateFor(input) {
|
|
109
|
+
const em = this.emFactory();
|
|
110
|
+
const all = await em.find(Tax, {});
|
|
111
|
+
const matching = all
|
|
112
|
+
.filter((t) => !t.isDefault)
|
|
113
|
+
.filter((t) => {
|
|
114
|
+
if (t.country != null && t.country !== input.country)
|
|
115
|
+
return false;
|
|
116
|
+
if (t.productType != null && t.productType !== input.productType)
|
|
117
|
+
return false;
|
|
118
|
+
if (t.appliesToVatStatuses.length > 0 &&
|
|
119
|
+
!t.appliesToVatStatuses.includes(input.vatStatus)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
});
|
|
124
|
+
if (matching.length > 0) {
|
|
125
|
+
matching.sort((a, b) => {
|
|
126
|
+
const specA = specificity(a);
|
|
127
|
+
const specB = specificity(b);
|
|
128
|
+
if (specA !== specB)
|
|
129
|
+
return specB - specA;
|
|
130
|
+
if (a.priority !== b.priority)
|
|
131
|
+
return b.priority - a.priority;
|
|
132
|
+
return a.createdAt.getTime() - b.createdAt.getTime();
|
|
133
|
+
});
|
|
134
|
+
const winner = matching[0];
|
|
135
|
+
return { rate: Number(winner.rate), taxId: winner.id, source: 'rule' };
|
|
136
|
+
}
|
|
137
|
+
const defaultRule = all.find((t) => t.isDefault);
|
|
138
|
+
if (defaultRule) {
|
|
139
|
+
return { rate: Number(defaultRule.rate), taxId: defaultRule.id, source: 'default' };
|
|
140
|
+
}
|
|
141
|
+
return { source: 'none' };
|
|
142
|
+
}
|
|
143
|
+
async validateCountry(country, mode) {
|
|
144
|
+
if (!this.dictionaryValidator)
|
|
145
|
+
return;
|
|
146
|
+
try {
|
|
147
|
+
await this.dictionaryValidator.validateCountryCode(country, mode);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
if (err instanceof DictionaryReferenceError) {
|
|
151
|
+
throw new HttpError(409, err.code, err.code === 'DICTIONARY_ENTRY_INACTIVE'
|
|
152
|
+
? `Country code ${err.entryCode} is no longer available for tax rules.`
|
|
153
|
+
: `Country code ${err.entryCode} is not recognised.`, [{ path: 'country', issue: err.code }]);
|
|
154
|
+
}
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function specificity(t) {
|
|
160
|
+
let n = 0;
|
|
161
|
+
if (t.country != null)
|
|
162
|
+
n += 1;
|
|
163
|
+
if (t.productType != null)
|
|
164
|
+
n += 1;
|
|
165
|
+
if (t.appliesToVatStatuses.length > 0)
|
|
166
|
+
n += 1;
|
|
167
|
+
return n;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=tax-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tax-service.js","sourceRoot":"","sources":["../../../src/backend/services/tax-service.ts"],"names":[],"mappings":"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"}
|