@porulle/core 0.7.0 → 0.8.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/dist/auth/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +1 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +5 -0
- package/dist/config/types.d.ts +17 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/generated/plugin-manifest.d.ts +3 -0
- package/dist/generated/plugin-manifest.d.ts.map +1 -1
- package/dist/generated/plugin-manifest.js +1 -0
- package/dist/hooks/checkout.d.ts.map +1 -1
- package/dist/hooks/checkout.js +16 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/interfaces/rest/index.d.ts.map +1 -1
- package/dist/interfaces/rest/index.js +12 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts +12 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/admin/staff.js +172 -0
- package/dist/interfaces/rest/routes/analytics.d.ts +5 -0
- package/dist/interfaces/rest/routes/analytics.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/analytics.js +24 -0
- package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/carts.js +39 -2
- package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/catalog.js +17 -1
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +17 -0
- package/dist/interfaces/rest/routes/documents.d.ts +10 -0
- package/dist/interfaces/rest/routes/documents.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/documents.js +40 -0
- package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/orders.js +98 -1
- package/dist/interfaces/rest/routes/pricing.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/pricing.js +36 -1
- package/dist/interfaces/rest/routes/settings.d.ts +5 -0
- package/dist/interfaces/rest/routes/settings.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/settings.js +50 -0
- package/dist/interfaces/rest/routes/shipping.d.ts +5 -0
- package/dist/interfaces/rest/routes/shipping.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/shipping.js +68 -0
- package/dist/interfaces/rest/routes/tax.d.ts +5 -0
- package/dist/interfaces/rest/routes/tax.d.ts.map +1 -0
- package/dist/interfaces/rest/routes/tax.js +68 -0
- package/dist/interfaces/rest/schemas/admin-staff.d.ts +899 -0
- package/dist/interfaces/rest/schemas/admin-staff.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/admin-staff.js +98 -0
- package/dist/interfaces/rest/schemas/analytics.d.ts +252 -0
- package/dist/interfaces/rest/schemas/analytics.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/analytics.js +33 -0
- package/dist/interfaces/rest/schemas/carts.d.ts +656 -0
- package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/carts.js +38 -0
- package/dist/interfaces/rest/schemas/catalog.d.ts +1218 -1
- package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/catalog.js +48 -0
- package/dist/interfaces/rest/schemas/checkout.d.ts +2 -0
- package/dist/interfaces/rest/schemas/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/checkout.js +4 -0
- package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
- package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/documents.d.ts +509 -0
- package/dist/interfaces/rest/schemas/documents.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/documents.js +65 -0
- package/dist/interfaces/rest/schemas/orders.d.ts +3110 -43
- package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/orders.js +233 -0
- package/dist/interfaces/rest/schemas/pricing.d.ts +405 -0
- package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/pricing.js +70 -0
- package/dist/interfaces/rest/schemas/responses.d.ts +156 -0
- package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/search.d.ts +17 -0
- package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/settings.d.ts +397 -0
- package/dist/interfaces/rest/schemas/settings.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/settings.js +79 -0
- package/dist/interfaces/rest/schemas/shipping.d.ts +1093 -0
- package/dist/interfaces/rest/schemas/shipping.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/shipping.js +146 -0
- package/dist/interfaces/rest/schemas/tax.d.ts +1074 -0
- package/dist/interfaces/rest/schemas/tax.d.ts.map +1 -0
- package/dist/interfaces/rest/schemas/tax.js +132 -0
- package/dist/kernel/database/schema.d.ts +4 -0
- package/dist/kernel/database/schema.d.ts.map +1 -1
- package/dist/kernel/database/schema.js +8 -0
- package/dist/kernel/hooks/types.d.ts +1 -1
- package/dist/kernel/hooks/types.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.d.ts +23 -0
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +17 -2
- package/dist/modules/analytics/module.d.ts.map +1 -1
- package/dist/modules/analytics/module.js +6 -0
- package/dist/modules/analytics/reports.d.ts +58 -0
- package/dist/modules/analytics/reports.d.ts.map +1 -0
- package/dist/modules/analytics/reports.js +249 -0
- package/dist/modules/analytics/service.d.ts +10 -0
- package/dist/modules/analytics/service.d.ts.map +1 -1
- package/dist/modules/analytics/service.js +14 -0
- package/dist/modules/cart/repository/index.d.ts +7 -0
- package/dist/modules/cart/repository/index.d.ts.map +1 -1
- package/dist/modules/cart/repository/index.js +25 -1
- package/dist/modules/cart/schema.d.ts +17 -0
- package/dist/modules/cart/schema.d.ts.map +1 -1
- package/dist/modules/cart/schema.js +2 -0
- package/dist/modules/cart/schemas.d.ts +1 -0
- package/dist/modules/cart/schemas.d.ts.map +1 -1
- package/dist/modules/cart/schemas.js +1 -0
- package/dist/modules/cart/service.d.ts +32 -0
- package/dist/modules/cart/service.d.ts.map +1 -1
- package/dist/modules/cart/service.js +58 -0
- package/dist/modules/catalog/entity-service.d.ts +21 -0
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +132 -3
- package/dist/modules/catalog/schema.d.ts +34 -0
- package/dist/modules/catalog/schema.d.ts.map +1 -1
- package/dist/modules/catalog/schema.js +5 -0
- package/dist/modules/catalog/schemas.d.ts +3 -0
- package/dist/modules/catalog/schemas.d.ts.map +1 -1
- package/dist/modules/catalog/schemas.js +3 -0
- package/dist/modules/catalog/service.d.ts +36 -0
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +6 -0
- package/dist/modules/documents/module.d.ts +7 -0
- package/dist/modules/documents/module.d.ts.map +1 -0
- package/dist/modules/documents/module.js +12 -0
- package/dist/modules/documents/pdf.d.ts +25 -0
- package/dist/modules/documents/pdf.d.ts.map +1 -0
- package/dist/modules/documents/pdf.js +80 -0
- package/dist/modules/documents/render.d.ts +32 -0
- package/dist/modules/documents/render.d.ts.map +1 -0
- package/dist/modules/documents/render.js +190 -0
- package/dist/modules/documents/repository/index.d.ts +25 -0
- package/dist/modules/documents/repository/index.d.ts.map +1 -0
- package/dist/modules/documents/repository/index.js +47 -0
- package/dist/modules/documents/schema.d.ts +214 -0
- package/dist/modules/documents/schema.d.ts.map +1 -0
- package/dist/modules/documents/schema.js +42 -0
- package/dist/modules/documents/service.d.ts +51 -0
- package/dist/modules/documents/service.d.ts.map +1 -0
- package/dist/modules/documents/service.js +119 -0
- package/dist/modules/fulfillment/service.d.ts +18 -0
- package/dist/modules/fulfillment/service.d.ts.map +1 -1
- package/dist/modules/fulfillment/service.js +85 -1
- package/dist/modules/orders/repository/index.d.ts +18 -1
- package/dist/modules/orders/repository/index.d.ts.map +1 -1
- package/dist/modules/orders/repository/index.js +86 -1
- package/dist/modules/orders/schema.d.ts +374 -0
- package/dist/modules/orders/schema.d.ts.map +1 -1
- package/dist/modules/orders/schema.js +52 -1
- package/dist/modules/orders/service.d.ts +86 -1
- package/dist/modules/orders/service.d.ts.map +1 -1
- package/dist/modules/orders/service.js +447 -20
- package/dist/modules/pricing/repository/index.d.ts +5 -0
- package/dist/modules/pricing/repository/index.d.ts.map +1 -1
- package/dist/modules/pricing/repository/index.js +19 -0
- package/dist/modules/pricing/service.d.ts +18 -0
- package/dist/modules/pricing/service.d.ts.map +1 -1
- package/dist/modules/pricing/service.js +38 -0
- package/dist/modules/settings/module.d.ts +6 -0
- package/dist/modules/settings/module.d.ts.map +1 -0
- package/dist/modules/settings/module.js +11 -0
- package/dist/modules/settings/repository/index.d.ts +16 -0
- package/dist/modules/settings/repository/index.d.ts.map +1 -0
- package/dist/modules/settings/repository/index.js +41 -0
- package/dist/modules/settings/schema.d.ts +121 -0
- package/dist/modules/settings/schema.d.ts.map +1 -0
- package/dist/modules/settings/schema.js +24 -0
- package/dist/modules/settings/service.d.ts +26 -0
- package/dist/modules/settings/service.d.ts.map +1 -0
- package/dist/modules/settings/service.js +51 -0
- package/dist/modules/shipping/calculator.d.ts +5 -0
- package/dist/modules/shipping/calculator.d.ts.map +1 -1
- package/dist/modules/shipping/calculator.js +10 -0
- package/dist/modules/shipping/module.d.ts +5 -1
- package/dist/modules/shipping/module.d.ts.map +1 -1
- package/dist/modules/shipping/module.js +4 -1
- package/dist/modules/shipping/repository/index.d.ts +27 -2
- package/dist/modules/shipping/repository/index.d.ts.map +1 -1
- package/dist/modules/shipping/repository/index.js +105 -1
- package/dist/modules/shipping/schema.d.ts +419 -0
- package/dist/modules/shipping/schema.d.ts.map +1 -0
- package/dist/modules/shipping/schema.js +56 -0
- package/dist/modules/shipping/service.d.ts +54 -0
- package/dist/modules/shipping/service.d.ts.map +1 -1
- package/dist/modules/shipping/service.js +161 -2
- package/dist/modules/tax/adapter.d.ts +13 -0
- package/dist/modules/tax/adapter.d.ts.map +1 -1
- package/dist/modules/tax/module.d.ts +5 -1
- package/dist/modules/tax/module.d.ts.map +1 -1
- package/dist/modules/tax/module.js +4 -1
- package/dist/modules/tax/repository/index.d.ts +38 -2
- package/dist/modules/tax/repository/index.d.ts.map +1 -1
- package/dist/modules/tax/repository/index.js +138 -1
- package/dist/modules/tax/schema.d.ts +355 -0
- package/dist/modules/tax/schema.d.ts.map +1 -0
- package/dist/modules/tax/schema.js +55 -0
- package/dist/modules/tax/service.d.ts +60 -1
- package/dist/modules/tax/service.d.ts.map +1 -1
- package/dist/modules/tax/service.js +227 -2
- package/dist/runtime/kernel-modules.d.ts +15 -2
- package/dist/runtime/kernel-modules.d.ts.map +1 -1
- package/dist/runtime/kernel-modules.js +4 -0
- package/dist/runtime/kernel-types.d.ts +5 -1
- package/dist/runtime/kernel-types.d.ts.map +1 -1
- package/dist/runtime/kernel-types.js +2 -0
- package/dist/runtime/server.js +1 -1
- package/dist/test-utils/create-plugin-test-app.d.ts +3 -0
- package/dist/test-utils/create-plugin-test-app.d.ts.map +1 -1
- package/dist/test-utils/create-plugin-test-app.js +15 -3
- package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
- package/dist/test-utils/rest-api-test-utils.js +6 -0
- package/package.json +3 -3
- package/src/auth/setup.ts +1 -0
- package/src/config/defaults.ts +5 -0
- package/src/config/types.ts +17 -1
- package/src/generated/plugin-capabilities.d.ts +1 -0
- package/src/generated/plugin-manifest.ts +1 -0
- package/src/generated/plugin-repositories.d.ts +1 -0
- package/src/hooks/checkout.ts +25 -3
- package/src/index.ts +1 -0
- package/src/interfaces/rest/index.ts +12 -0
- package/src/interfaces/rest/routes/admin/staff.ts +218 -0
- package/src/interfaces/rest/routes/analytics.ts +33 -0
- package/src/interfaces/rest/routes/carts.ts +52 -2
- package/src/interfaces/rest/routes/catalog.ts +34 -0
- package/src/interfaces/rest/routes/checkout.ts +22 -0
- package/src/interfaces/rest/routes/documents.ts +61 -0
- package/src/interfaces/rest/routes/orders.ts +137 -1
- package/src/interfaces/rest/routes/pricing.ts +44 -1
- package/src/interfaces/rest/routes/settings.ts +61 -0
- package/src/interfaces/rest/routes/shipping.ts +92 -0
- package/src/interfaces/rest/routes/tax.ts +90 -0
- package/src/interfaces/rest/schemas/admin-staff.ts +110 -0
- package/src/interfaces/rest/schemas/analytics.ts +39 -0
- package/src/interfaces/rest/schemas/carts.ts +40 -0
- package/src/interfaces/rest/schemas/catalog.ts +53 -0
- package/src/interfaces/rest/schemas/checkout.ts +4 -0
- package/src/interfaces/rest/schemas/documents.ts +72 -0
- package/src/interfaces/rest/schemas/orders.ts +257 -0
- package/src/interfaces/rest/schemas/pricing.ts +75 -0
- package/src/interfaces/rest/schemas/settings.ts +93 -0
- package/src/interfaces/rest/schemas/shipping.ts +164 -0
- package/src/interfaces/rest/schemas/tax.ts +148 -0
- package/src/kernel/database/schema.ts +12 -0
- package/src/kernel/hooks/types.ts +1 -0
- package/src/kernel/plugin/manifest.ts +38 -2
- package/src/modules/analytics/module.ts +10 -0
- package/src/modules/analytics/reports.ts +311 -0
- package/src/modules/analytics/service.ts +23 -0
- package/src/modules/cart/repository/index.ts +30 -1
- package/src/modules/cart/schema.ts +2 -0
- package/src/modules/cart/schemas.ts +1 -0
- package/src/modules/cart/service.ts +101 -0
- package/src/modules/catalog/entity-service.ts +168 -3
- package/src/modules/catalog/schema.ts +5 -0
- package/src/modules/catalog/schemas.ts +3 -0
- package/src/modules/catalog/service.ts +30 -0
- package/src/modules/documents/module.ts +19 -0
- package/src/modules/documents/pdf.ts +101 -0
- package/src/modules/documents/render.ts +231 -0
- package/src/modules/documents/repository/index.ts +68 -0
- package/src/modules/documents/schema.ts +44 -0
- package/src/modules/documents/service.ts +176 -0
- package/src/modules/fulfillment/service.ts +139 -1
- package/src/modules/orders/repository/index.ts +127 -1
- package/src/modules/orders/schema.ts +54 -1
- package/src/modules/orders/service.ts +654 -22
- package/src/modules/pricing/repository/index.ts +34 -0
- package/src/modules/pricing/service.ts +71 -0
- package/src/modules/settings/module.ts +18 -0
- package/src/modules/settings/repository/index.ts +59 -0
- package/src/modules/settings/schema.ts +25 -0
- package/src/modules/settings/service.ts +82 -0
- package/src/modules/shipping/calculator.ts +27 -0
- package/src/modules/shipping/module.ts +6 -2
- package/src/modules/shipping/repository/index.ts +136 -2
- package/src/modules/shipping/schema.ts +58 -0
- package/src/modules/shipping/service.ts +260 -1
- package/src/modules/tax/adapter.ts +9 -0
- package/src/modules/tax/module.ts +6 -2
- package/src/modules/tax/repository/index.ts +169 -2
- package/src/modules/tax/schema.ts +57 -0
- package/src/modules/tax/service.ts +313 -2
- package/src/runtime/kernel-modules.ts +4 -0
- package/src/runtime/kernel-types.ts +6 -0
- package/src/runtime/server.ts +1 -1
- package/src/test-utils/create-plugin-test-app.ts +17 -4
- package/src/test-utils/rest-api-test-utils.ts +6 -0
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveOrgId } from "../../auth/org.js";
|
|
2
|
+
import type { Actor } from "../../auth/types.js";
|
|
3
|
+
import type { TxContext } from "../../kernel/database/tx-context.js";
|
|
4
|
+
import { CommerceNotFoundError, CommerceValidationError } from "../../kernel/errors.js";
|
|
2
5
|
import { Err, Ok, type Result } from "../../kernel/result.js";
|
|
3
6
|
import type {
|
|
4
7
|
TaxAdapter,
|
|
@@ -7,19 +10,73 @@ import type {
|
|
|
7
10
|
TaxReportParams,
|
|
8
11
|
TaxVoidParams,
|
|
9
12
|
} from "./adapter.js";
|
|
13
|
+
import type { TaxClass, TaxRate, TaxRatesRepository } from "./repository/index.js";
|
|
10
14
|
|
|
11
15
|
interface TaxServiceDeps {
|
|
12
16
|
adapter: TaxAdapter | undefined;
|
|
17
|
+
repository?: TaxRatesRepository;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export class TaxService {
|
|
16
21
|
private adapter: TaxAdapter | undefined;
|
|
22
|
+
private repository: TaxRatesRepository | undefined;
|
|
17
23
|
|
|
18
24
|
constructor(deps: TaxServiceDeps) {
|
|
19
25
|
this.adapter = deps.adapter;
|
|
26
|
+
this.repository = deps.repository;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Calculates tax. Resolution order:
|
|
31
|
+
* 1. Product tax classes (issue #57) — when the org defines active classes,
|
|
32
|
+
* each line taxes by its class (variant taxClass beats entity taxClass;
|
|
33
|
+
* unclassed lines use the default class or 0), with `orderDiscount`
|
|
34
|
+
* pro-rated across lines first. Returns per-line results in `lines`.
|
|
35
|
+
* 2. Runtime tax rates for the org matching `toAddress` (issue #45) —
|
|
36
|
+
* state-specific matches beat country-level; matches at the winning
|
|
37
|
+
* specificity are summed (e.g. GST + PST).
|
|
38
|
+
* 3. The configured tax adapter.
|
|
39
|
+
* 4. Zero tax.
|
|
40
|
+
*/
|
|
41
|
+
async calculate(
|
|
42
|
+
params: TaxCalculationParams,
|
|
43
|
+
orgId?: string,
|
|
44
|
+
ctx?: TxContext,
|
|
45
|
+
): Promise<Result<TaxCalculationResult>> {
|
|
46
|
+
if (this.repository && orgId && params.lineItems.length > 0) {
|
|
47
|
+
const classes = await this.repository.findActiveClasses(orgId, ctx);
|
|
48
|
+
if (classes.length > 0) {
|
|
49
|
+
return Ok(await this.calculateByClasses(params, classes, ctx));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (this.repository && orgId && params.toAddress) {
|
|
54
|
+
const matched = await this.matchRuntimeRates(orgId, params.toAddress, ctx);
|
|
55
|
+
if (matched.length > 0) {
|
|
56
|
+
const taxableAmount = params.lineItems.reduce(
|
|
57
|
+
(sum, lineItem) =>
|
|
58
|
+
sum + lineItem.unitPrice * lineItem.quantity - (lineItem.discount ?? 0),
|
|
59
|
+
0,
|
|
60
|
+
);
|
|
61
|
+
let amountToCollect = 0;
|
|
62
|
+
const breakdown: Record<string, unknown> = {};
|
|
63
|
+
for (const rate of matched) {
|
|
64
|
+
const base =
|
|
65
|
+
taxableAmount + (rate.appliesToShipping ? params.shippingAmount : 0);
|
|
66
|
+
const amount = Math.max(0, Math.round((base * rate.rateBps) / 10_000));
|
|
67
|
+
amountToCollect += amount;
|
|
68
|
+
breakdown[rate.name] = { rateBps: rate.rateBps, amount };
|
|
69
|
+
}
|
|
70
|
+
const totalBps = matched.reduce((sum, r) => sum + r.rateBps, 0);
|
|
71
|
+
return Ok({
|
|
72
|
+
amountToCollect,
|
|
73
|
+
taxableAmount,
|
|
74
|
+
rate: totalBps / 10_000,
|
|
75
|
+
breakdown,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
23
80
|
if (!this.adapter) {
|
|
24
81
|
return Ok({
|
|
25
82
|
amountToCollect: 0,
|
|
@@ -34,6 +91,162 @@ export class TaxService {
|
|
|
34
91
|
return this.adapter.calculateTax(params);
|
|
35
92
|
}
|
|
36
93
|
|
|
94
|
+
/** Per-line class-based tax with cart-discount pro-ration (issue #57). */
|
|
95
|
+
private async calculateByClasses(
|
|
96
|
+
params: TaxCalculationParams,
|
|
97
|
+
classes: TaxClass[],
|
|
98
|
+
ctx?: TxContext,
|
|
99
|
+
): Promise<TaxCalculationResult> {
|
|
100
|
+
const byName = new Map(classes.map((c) => [c.name, c]));
|
|
101
|
+
const defaultClass = classes.find((c) => c.isDefault) ?? null;
|
|
102
|
+
|
|
103
|
+
const { byEntity, byVariant } = await this.repository!.resolveCatalogTaxClasses(
|
|
104
|
+
params.lineItems.map((li) => ({ entityId: li.entityId, variantId: li.variantId })),
|
|
105
|
+
ctx,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Pro-rate the cart-level discount across lines by value share; the last
|
|
109
|
+
// line absorbs the rounding remainder so the sum stays exact.
|
|
110
|
+
const lineValues = params.lineItems.map(
|
|
111
|
+
(li) => Math.max(0, li.unitPrice * li.quantity - (li.discount ?? 0)),
|
|
112
|
+
);
|
|
113
|
+
const totalValue = lineValues.reduce((sum, v) => sum + v, 0);
|
|
114
|
+
const orderDiscount = Math.min(params.orderDiscount ?? 0, totalValue);
|
|
115
|
+
const prorated = lineValues.map((value) =>
|
|
116
|
+
totalValue > 0 ? Math.round((orderDiscount * value) / totalValue) : 0,
|
|
117
|
+
);
|
|
118
|
+
if (lineValues.length > 0) {
|
|
119
|
+
const drift = orderDiscount - prorated.reduce((sum, v) => sum + v, 0);
|
|
120
|
+
prorated[prorated.length - 1]! += drift;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let amountToCollect = 0;
|
|
124
|
+
let taxableAmount = 0;
|
|
125
|
+
const breakdown: Record<string, unknown> = {};
|
|
126
|
+
const lines: Array<{ id: string; taxClass: string | null; taxAmount: number }> = [];
|
|
127
|
+
|
|
128
|
+
for (const [i, lineItem] of params.lineItems.entries()) {
|
|
129
|
+
const className =
|
|
130
|
+
(lineItem.variantId ? byVariant.get(lineItem.variantId) : null) ??
|
|
131
|
+
byEntity.get(lineItem.entityId) ??
|
|
132
|
+
defaultClass?.name ??
|
|
133
|
+
null;
|
|
134
|
+
const taxClass = className ? (byName.get(className) ?? defaultClass) : defaultClass;
|
|
135
|
+
const base = Math.max(0, lineValues[i]! - prorated[i]!);
|
|
136
|
+
const taxAmount = taxClass
|
|
137
|
+
? Math.max(0, Math.round((base * taxClass.rateBps) / 10_000))
|
|
138
|
+
: 0;
|
|
139
|
+
amountToCollect += taxAmount;
|
|
140
|
+
taxableAmount += base;
|
|
141
|
+
lines.push({ id: lineItem.id, taxClass: taxClass?.name ?? null, taxAmount });
|
|
142
|
+
if (taxClass) {
|
|
143
|
+
const entry = (breakdown[taxClass.name] as { rateBps: number; amount: number }) ?? {
|
|
144
|
+
rateBps: taxClass.rateBps,
|
|
145
|
+
amount: 0,
|
|
146
|
+
};
|
|
147
|
+
entry.amount += taxAmount;
|
|
148
|
+
breakdown[taxClass.name] = entry;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
amountToCollect,
|
|
154
|
+
taxableAmount,
|
|
155
|
+
rate: taxableAmount > 0 ? amountToCollect / taxableAmount : 0,
|
|
156
|
+
breakdown,
|
|
157
|
+
lines,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Tax class management (issue #57) ─────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
async createTaxClass(
|
|
164
|
+
input: { name: string; rateBps: number; isDefault?: boolean | undefined; isActive?: boolean | undefined },
|
|
165
|
+
actor?: Actor | null,
|
|
166
|
+
ctx?: TxContext,
|
|
167
|
+
): Promise<Result<TaxClass>> {
|
|
168
|
+
const repo = this.requireRepository();
|
|
169
|
+
if (!repo.ok) return repo;
|
|
170
|
+
if (input.rateBps < 0) {
|
|
171
|
+
return Err(new CommerceValidationError("rateBps must be non-negative."));
|
|
172
|
+
}
|
|
173
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
174
|
+
if (input.isDefault) await repo.value.clearDefaultClass(orgId, ctx);
|
|
175
|
+
const created = await repo.value.createClass(
|
|
176
|
+
{
|
|
177
|
+
organizationId: orgId,
|
|
178
|
+
name: input.name,
|
|
179
|
+
rateBps: input.rateBps,
|
|
180
|
+
isDefault: input.isDefault ?? false,
|
|
181
|
+
isActive: input.isActive ?? true,
|
|
182
|
+
},
|
|
183
|
+
ctx,
|
|
184
|
+
);
|
|
185
|
+
return Ok(created);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async listTaxClasses(actor?: Actor | null, ctx?: TxContext): Promise<Result<TaxClass[]>> {
|
|
189
|
+
const repo = this.requireRepository();
|
|
190
|
+
if (!repo.ok) return repo;
|
|
191
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
192
|
+
return Ok(await repo.value.findAllClasses(orgId, ctx));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async updateTaxClass(
|
|
196
|
+
id: string,
|
|
197
|
+
patch: { name?: string | undefined; rateBps?: number | undefined; isDefault?: boolean | undefined; isActive?: boolean | undefined },
|
|
198
|
+
actor?: Actor | null,
|
|
199
|
+
ctx?: TxContext,
|
|
200
|
+
): Promise<Result<TaxClass>> {
|
|
201
|
+
const repo = this.requireRepository();
|
|
202
|
+
if (!repo.ok) return repo;
|
|
203
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
204
|
+
const existing = await repo.value.findClassById(orgId, id, ctx);
|
|
205
|
+
if (!existing) return Err(new CommerceNotFoundError("Tax class not found."));
|
|
206
|
+
if (patch.isDefault) await repo.value.clearDefaultClass(orgId, ctx);
|
|
207
|
+
const updated = await repo.value.updateClass(
|
|
208
|
+
id,
|
|
209
|
+
{
|
|
210
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
211
|
+
...(patch.rateBps !== undefined ? { rateBps: patch.rateBps } : {}),
|
|
212
|
+
...(patch.isDefault !== undefined ? { isDefault: patch.isDefault } : {}),
|
|
213
|
+
...(patch.isActive !== undefined ? { isActive: patch.isActive } : {}),
|
|
214
|
+
},
|
|
215
|
+
ctx,
|
|
216
|
+
);
|
|
217
|
+
return Ok(updated!);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async deleteTaxClass(id: string, actor?: Actor | null, ctx?: TxContext): Promise<Result<{ deleted: true }>> {
|
|
221
|
+
const repo = this.requireRepository();
|
|
222
|
+
if (!repo.ok) return repo;
|
|
223
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
224
|
+
const existing = await repo.value.findClassById(orgId, id, ctx);
|
|
225
|
+
if (!existing) return Err(new CommerceNotFoundError("Tax class not found."));
|
|
226
|
+
await repo.value.deleteClass(id, ctx);
|
|
227
|
+
return Ok({ deleted: true });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private async matchRuntimeRates(
|
|
231
|
+
orgId: string,
|
|
232
|
+
toAddress: { country: string; state?: string },
|
|
233
|
+
ctx?: TxContext,
|
|
234
|
+
): Promise<TaxRate[]> {
|
|
235
|
+
const rates = await this.repository!.findActive(orgId, ctx);
|
|
236
|
+
if (rates.length === 0) return [];
|
|
237
|
+
const country = toAddress.country?.toUpperCase();
|
|
238
|
+
const state = toAddress.state?.toUpperCase();
|
|
239
|
+
|
|
240
|
+
const countryMatches = rates.filter(
|
|
241
|
+
(r) => r.country === "*" || r.country.toUpperCase() === country,
|
|
242
|
+
);
|
|
243
|
+
const stateMatches = countryMatches.filter(
|
|
244
|
+
(r) => r.state != null && state != null && r.state.toUpperCase() === state,
|
|
245
|
+
);
|
|
246
|
+
if (stateMatches.length > 0) return stateMatches;
|
|
247
|
+
return countryMatches.filter((r) => r.state == null);
|
|
248
|
+
}
|
|
249
|
+
|
|
37
250
|
async reportTransaction(params: TaxReportParams): Promise<Result<{ transactionId: string }>> {
|
|
38
251
|
if (!this.adapter) return Ok({ transactionId: params.transactionId });
|
|
39
252
|
return this.adapter.reportTransaction(params);
|
|
@@ -50,4 +263,102 @@ export class TaxService {
|
|
|
50
263
|
}
|
|
51
264
|
return Ok(this.adapter);
|
|
52
265
|
}
|
|
266
|
+
|
|
267
|
+
// ── Runtime tax-rate management (issue #45) ────────────────────────────
|
|
268
|
+
|
|
269
|
+
private requireRepository(): Result<TaxRatesRepository> {
|
|
270
|
+
if (!this.repository) {
|
|
271
|
+
return Err(new CommerceValidationError("Tax rate persistence is not available."));
|
|
272
|
+
}
|
|
273
|
+
return Ok(this.repository);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async createTaxRate(
|
|
277
|
+
input: {
|
|
278
|
+
name: string;
|
|
279
|
+
country: string;
|
|
280
|
+
state?: string | undefined;
|
|
281
|
+
rateBps: number;
|
|
282
|
+
appliesToShipping?: boolean | undefined;
|
|
283
|
+
priority?: number | undefined;
|
|
284
|
+
isActive?: boolean | undefined;
|
|
285
|
+
},
|
|
286
|
+
actor?: Actor | null,
|
|
287
|
+
ctx?: TxContext,
|
|
288
|
+
): Promise<Result<TaxRate>> {
|
|
289
|
+
const repo = this.requireRepository();
|
|
290
|
+
if (!repo.ok) return repo;
|
|
291
|
+
if (input.rateBps < 0) {
|
|
292
|
+
return Err(new CommerceValidationError("rateBps must be non-negative."));
|
|
293
|
+
}
|
|
294
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
295
|
+
const rate = await repo.value.create(
|
|
296
|
+
{
|
|
297
|
+
organizationId: orgId,
|
|
298
|
+
name: input.name,
|
|
299
|
+
country: input.country.toUpperCase(),
|
|
300
|
+
state: input.state?.toUpperCase() ?? null,
|
|
301
|
+
rateBps: input.rateBps,
|
|
302
|
+
appliesToShipping: input.appliesToShipping ?? true,
|
|
303
|
+
priority: input.priority ?? 100,
|
|
304
|
+
isActive: input.isActive ?? true,
|
|
305
|
+
},
|
|
306
|
+
ctx,
|
|
307
|
+
);
|
|
308
|
+
return Ok(rate);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async listTaxRates(actor?: Actor | null, ctx?: TxContext): Promise<Result<TaxRate[]>> {
|
|
312
|
+
const repo = this.requireRepository();
|
|
313
|
+
if (!repo.ok) return repo;
|
|
314
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
315
|
+
return Ok(await repo.value.findAll(orgId, ctx));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async updateTaxRate(
|
|
319
|
+
id: string,
|
|
320
|
+
patch: {
|
|
321
|
+
name?: string | undefined;
|
|
322
|
+
country?: string | undefined;
|
|
323
|
+
state?: string | null | undefined;
|
|
324
|
+
rateBps?: number | undefined;
|
|
325
|
+
appliesToShipping?: boolean | undefined;
|
|
326
|
+
priority?: number | undefined;
|
|
327
|
+
isActive?: boolean | undefined;
|
|
328
|
+
},
|
|
329
|
+
actor?: Actor | null,
|
|
330
|
+
ctx?: TxContext,
|
|
331
|
+
): Promise<Result<TaxRate>> {
|
|
332
|
+
const repo = this.requireRepository();
|
|
333
|
+
if (!repo.ok) return repo;
|
|
334
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
335
|
+
const existing = await repo.value.findById(orgId, id, ctx);
|
|
336
|
+
if (!existing) return Err(new CommerceNotFoundError("Tax rate not found."));
|
|
337
|
+
const updated = await repo.value.update(
|
|
338
|
+
id,
|
|
339
|
+
{
|
|
340
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
341
|
+
...(patch.country !== undefined ? { country: patch.country.toUpperCase() } : {}),
|
|
342
|
+
...(patch.state !== undefined ? { state: patch.state?.toUpperCase() ?? null } : {}),
|
|
343
|
+
...(patch.rateBps !== undefined ? { rateBps: patch.rateBps } : {}),
|
|
344
|
+
...(patch.appliesToShipping !== undefined
|
|
345
|
+
? { appliesToShipping: patch.appliesToShipping }
|
|
346
|
+
: {}),
|
|
347
|
+
...(patch.priority !== undefined ? { priority: patch.priority } : {}),
|
|
348
|
+
...(patch.isActive !== undefined ? { isActive: patch.isActive } : {}),
|
|
349
|
+
},
|
|
350
|
+
ctx,
|
|
351
|
+
);
|
|
352
|
+
return Ok(updated!);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async deleteTaxRate(id: string, actor?: Actor | null, ctx?: TxContext): Promise<Result<{ deleted: true }>> {
|
|
356
|
+
const repo = this.requireRepository();
|
|
357
|
+
if (!repo.ok) return repo;
|
|
358
|
+
const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
|
|
359
|
+
const existing = await repo.value.findById(orgId, id, ctx);
|
|
360
|
+
if (!existing) return Err(new CommerceNotFoundError("Tax rate not found."));
|
|
361
|
+
await repo.value.delete(id, ctx);
|
|
362
|
+
return Ok({ deleted: true });
|
|
363
|
+
}
|
|
53
364
|
}
|
|
@@ -5,6 +5,7 @@ import { analyticsModule } from "../modules/analytics/module.js";
|
|
|
5
5
|
import { cartModule } from "../modules/cart/module.js";
|
|
6
6
|
import { catalogModule } from "../modules/catalog/module.js";
|
|
7
7
|
import { customersModule } from "../modules/customers/index.js";
|
|
8
|
+
import { documentsModule } from "../modules/documents/module.js";
|
|
8
9
|
import { fulfillmentModule } from "../modules/fulfillment/module.js";
|
|
9
10
|
import { inventoryModule } from "../modules/inventory/module.js";
|
|
10
11
|
import { mediaModule } from "../modules/media/index.js";
|
|
@@ -14,12 +15,15 @@ import { paymentsModule } from "../modules/payments/module.js";
|
|
|
14
15
|
import { pricingModule } from "../modules/pricing/index.js";
|
|
15
16
|
import { promotionsModule } from "../modules/promotions/module.js";
|
|
16
17
|
import { searchModule } from "../modules/search/module.js";
|
|
18
|
+
import { settingsModule } from "../modules/settings/module.js";
|
|
17
19
|
import { shippingModule } from "../modules/shipping/module.js";
|
|
18
20
|
import { taxModule } from "../modules/tax/module.js";
|
|
19
21
|
import { webhooksModule } from "../modules/webhooks/index.js";
|
|
20
22
|
|
|
21
23
|
export const KERNEL_ALL_MODULES = {
|
|
22
24
|
audit: auditModule,
|
|
25
|
+
settings: settingsModule,
|
|
26
|
+
documents: documentsModule,
|
|
23
27
|
webhooks: webhooksModule,
|
|
24
28
|
media: mediaModule,
|
|
25
29
|
organization: organizationModule,
|
|
@@ -19,6 +19,8 @@ import { PromotionService } from "../modules/promotions/service.js";
|
|
|
19
19
|
import { TaxService } from "../modules/tax/service.js";
|
|
20
20
|
import { ShippingService } from "../modules/shipping/service.js";
|
|
21
21
|
import { SearchService } from "../modules/search/service.js";
|
|
22
|
+
import { SettingsService } from "../modules/settings/service.js";
|
|
23
|
+
import { DocumentsService } from "../modules/documents/service.js";
|
|
22
24
|
import type { AuditService } from "../modules/audit/service.js";
|
|
23
25
|
import { OrganizationService } from "../modules/organization/service.js";
|
|
24
26
|
import { createLogger } from "../utils/logger.js";
|
|
@@ -52,6 +54,8 @@ export interface Kernel {
|
|
|
52
54
|
tax: TaxService;
|
|
53
55
|
shipping: ShippingService;
|
|
54
56
|
search: SearchService;
|
|
57
|
+
settings: SettingsService;
|
|
58
|
+
documents: DocumentsService;
|
|
55
59
|
audit: AuditService;
|
|
56
60
|
compensationFailures: CompensationFailuresRepository;
|
|
57
61
|
email: CommerceConfig["email"];
|
|
@@ -77,6 +81,8 @@ export const KERNEL_REQUIRED_SERVICE_KEYS = [
|
|
|
77
81
|
"tax",
|
|
78
82
|
"shipping",
|
|
79
83
|
"search",
|
|
84
|
+
"settings",
|
|
85
|
+
"documents",
|
|
80
86
|
"audit",
|
|
81
87
|
"compensationFailures",
|
|
82
88
|
] as const satisfies Array<keyof Kernel["services"]>;
|
package/src/runtime/server.ts
CHANGED
|
@@ -341,7 +341,7 @@ export async function createServer(config: CommerceConfig) {
|
|
|
341
341
|
app.route("/api/me", createCustomerPortalRoutes(kernel));
|
|
342
342
|
|
|
343
343
|
if (config.routes) {
|
|
344
|
-
config.routes(app, kernel);
|
|
344
|
+
config.routes(app, kernel, auth);
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
// OpenAPI spec — disabled in production unless explicitly enabled
|
|
@@ -29,6 +29,8 @@ import { buildSchema } from "../kernel/database/migrate.js";
|
|
|
29
29
|
import { unwrapDb } from "../kernel/database/adapter.js";
|
|
30
30
|
import { ensureDefaultOrg } from "../auth/org.js";
|
|
31
31
|
import { mapErrorToStatus } from "../kernel/error-mapper.js";
|
|
32
|
+
import { createAuth, type AuthInstance } from "../auth/setup.js";
|
|
33
|
+
import { authMiddleware } from "../auth/middleware.js";
|
|
32
34
|
|
|
33
35
|
// drizzle-kit/api uses CJS internally; createRequire provides ESM compat.
|
|
34
36
|
const require = createRequire(import.meta.url);
|
|
@@ -58,6 +60,8 @@ export interface PluginTestApp {
|
|
|
58
60
|
kernel: Kernel;
|
|
59
61
|
/** Drizzle database instance for direct queries in test assertions. */
|
|
60
62
|
db: PgDatabase<PgQueryResultHKT, Record<string, unknown>>;
|
|
63
|
+
/** The Better Auth instance passed to plugin routes (mint/verify API keys in tests). */
|
|
64
|
+
auth: AuthInstance;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
/**
|
|
@@ -105,8 +109,11 @@ export async function createPluginTestApp(
|
|
|
105
109
|
// 5. Create OpenAPIHono --- matching production server.ts
|
|
106
110
|
// Plugin routes register via manifest.ts which calls app.openapi().
|
|
107
111
|
const app = new OpenAPIHono<TestAppEnv>();
|
|
112
|
+
const auth = createAuth(kernel.database, config);
|
|
108
113
|
|
|
109
|
-
// 6. Test actor middleware: parse x-test-actor header -> set on context
|
|
114
|
+
// 6. Test actor middleware: parse x-test-actor header -> set on context.
|
|
115
|
+
// Requests without the header fall through to the real auth middleware,
|
|
116
|
+
// so API keys minted by plugins (issue #51) authenticate like production.
|
|
110
117
|
app.use("*", async (c, next) => {
|
|
111
118
|
const header = c.req.header("x-test-actor");
|
|
112
119
|
if (header) {
|
|
@@ -116,12 +123,17 @@ export async function createPluginTestApp(
|
|
|
116
123
|
}
|
|
117
124
|
await next();
|
|
118
125
|
});
|
|
126
|
+
app.use("*", async (c, next) => {
|
|
127
|
+
if (c.get("actor")) return next();
|
|
128
|
+
return authMiddleware(auth, config)(c as never, next);
|
|
129
|
+
});
|
|
119
130
|
|
|
120
|
-
// 7. Register plugin routes (deferred via config.routes)
|
|
131
|
+
// 7. Register plugin routes (deferred via config.routes) — with the auth
|
|
132
|
+
// instance, matching server.ts's config.routes(app, kernel, auth).
|
|
121
133
|
const routes = config.routes as
|
|
122
|
-
| ((app: unknown, kernel: unknown) => void)
|
|
134
|
+
| ((app: unknown, kernel: unknown, auth?: unknown) => void)
|
|
123
135
|
| undefined;
|
|
124
|
-
routes?.(app, kernel);
|
|
136
|
+
routes?.(app, kernel, auth);
|
|
125
137
|
|
|
126
138
|
// 8. Error handler matching production server.ts — maps CommerceError
|
|
127
139
|
// subclasses (CommerceForbiddenError, CommerceNotFoundError, etc.)
|
|
@@ -149,5 +161,6 @@ export async function createPluginTestApp(
|
|
|
149
161
|
app,
|
|
150
162
|
kernel,
|
|
151
163
|
db: kernel.database.db as PgDatabase<PgQueryResultHKT, Record<string, unknown>>,
|
|
164
|
+
auth,
|
|
152
165
|
};
|
|
153
166
|
}
|
|
@@ -166,9 +166,15 @@ export const testActor: Actor = {
|
|
|
166
166
|
"cart:create",
|
|
167
167
|
"cart:update",
|
|
168
168
|
"cart:read",
|
|
169
|
+
"cart:manage",
|
|
169
170
|
"customers:update:self",
|
|
170
171
|
"webhooks:manage",
|
|
171
172
|
"pricing:manage",
|
|
173
|
+
"shipping:manage",
|
|
174
|
+
"tax:manage",
|
|
175
|
+
"settings:manage",
|
|
176
|
+
"analytics:read",
|
|
177
|
+
"staff:manage",
|
|
172
178
|
"promotions:manage",
|
|
173
179
|
"promotions:read",
|
|
174
180
|
"audit:read",
|