@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.
Files changed (289) hide show
  1. package/dist/auth/setup.d.ts.map +1 -1
  2. package/dist/auth/setup.js +1 -0
  3. package/dist/config/defaults.d.ts.map +1 -1
  4. package/dist/config/defaults.js +5 -0
  5. package/dist/config/types.d.ts +17 -1
  6. package/dist/config/types.d.ts.map +1 -1
  7. package/dist/generated/plugin-manifest.d.ts +3 -0
  8. package/dist/generated/plugin-manifest.d.ts.map +1 -1
  9. package/dist/generated/plugin-manifest.js +1 -0
  10. package/dist/hooks/checkout.d.ts.map +1 -1
  11. package/dist/hooks/checkout.js +16 -1
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -0
  15. package/dist/interfaces/rest/index.d.ts.map +1 -1
  16. package/dist/interfaces/rest/index.js +12 -0
  17. package/dist/interfaces/rest/routes/admin/staff.d.ts +12 -0
  18. package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -0
  19. package/dist/interfaces/rest/routes/admin/staff.js +172 -0
  20. package/dist/interfaces/rest/routes/analytics.d.ts +5 -0
  21. package/dist/interfaces/rest/routes/analytics.d.ts.map +1 -0
  22. package/dist/interfaces/rest/routes/analytics.js +24 -0
  23. package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
  24. package/dist/interfaces/rest/routes/carts.js +39 -2
  25. package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
  26. package/dist/interfaces/rest/routes/catalog.js +17 -1
  27. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  28. package/dist/interfaces/rest/routes/checkout.js +17 -0
  29. package/dist/interfaces/rest/routes/documents.d.ts +10 -0
  30. package/dist/interfaces/rest/routes/documents.d.ts.map +1 -0
  31. package/dist/interfaces/rest/routes/documents.js +40 -0
  32. package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
  33. package/dist/interfaces/rest/routes/orders.js +98 -1
  34. package/dist/interfaces/rest/routes/pricing.d.ts.map +1 -1
  35. package/dist/interfaces/rest/routes/pricing.js +36 -1
  36. package/dist/interfaces/rest/routes/settings.d.ts +5 -0
  37. package/dist/interfaces/rest/routes/settings.d.ts.map +1 -0
  38. package/dist/interfaces/rest/routes/settings.js +50 -0
  39. package/dist/interfaces/rest/routes/shipping.d.ts +5 -0
  40. package/dist/interfaces/rest/routes/shipping.d.ts.map +1 -0
  41. package/dist/interfaces/rest/routes/shipping.js +68 -0
  42. package/dist/interfaces/rest/routes/tax.d.ts +5 -0
  43. package/dist/interfaces/rest/routes/tax.d.ts.map +1 -0
  44. package/dist/interfaces/rest/routes/tax.js +68 -0
  45. package/dist/interfaces/rest/schemas/admin-staff.d.ts +899 -0
  46. package/dist/interfaces/rest/schemas/admin-staff.d.ts.map +1 -0
  47. package/dist/interfaces/rest/schemas/admin-staff.js +98 -0
  48. package/dist/interfaces/rest/schemas/analytics.d.ts +252 -0
  49. package/dist/interfaces/rest/schemas/analytics.d.ts.map +1 -0
  50. package/dist/interfaces/rest/schemas/analytics.js +33 -0
  51. package/dist/interfaces/rest/schemas/carts.d.ts +656 -0
  52. package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
  53. package/dist/interfaces/rest/schemas/carts.js +38 -0
  54. package/dist/interfaces/rest/schemas/catalog.d.ts +1218 -1
  55. package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
  56. package/dist/interfaces/rest/schemas/catalog.js +48 -0
  57. package/dist/interfaces/rest/schemas/checkout.d.ts +2 -0
  58. package/dist/interfaces/rest/schemas/checkout.d.ts.map +1 -1
  59. package/dist/interfaces/rest/schemas/checkout.js +4 -0
  60. package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
  61. package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
  62. package/dist/interfaces/rest/schemas/documents.d.ts +509 -0
  63. package/dist/interfaces/rest/schemas/documents.d.ts.map +1 -0
  64. package/dist/interfaces/rest/schemas/documents.js +65 -0
  65. package/dist/interfaces/rest/schemas/orders.d.ts +3110 -43
  66. package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
  67. package/dist/interfaces/rest/schemas/orders.js +233 -0
  68. package/dist/interfaces/rest/schemas/pricing.d.ts +405 -0
  69. package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
  70. package/dist/interfaces/rest/schemas/pricing.js +70 -0
  71. package/dist/interfaces/rest/schemas/responses.d.ts +156 -0
  72. package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
  73. package/dist/interfaces/rest/schemas/search.d.ts +17 -0
  74. package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
  75. package/dist/interfaces/rest/schemas/settings.d.ts +397 -0
  76. package/dist/interfaces/rest/schemas/settings.d.ts.map +1 -0
  77. package/dist/interfaces/rest/schemas/settings.js +79 -0
  78. package/dist/interfaces/rest/schemas/shipping.d.ts +1093 -0
  79. package/dist/interfaces/rest/schemas/shipping.d.ts.map +1 -0
  80. package/dist/interfaces/rest/schemas/shipping.js +146 -0
  81. package/dist/interfaces/rest/schemas/tax.d.ts +1074 -0
  82. package/dist/interfaces/rest/schemas/tax.d.ts.map +1 -0
  83. package/dist/interfaces/rest/schemas/tax.js +132 -0
  84. package/dist/kernel/database/schema.d.ts +4 -0
  85. package/dist/kernel/database/schema.d.ts.map +1 -1
  86. package/dist/kernel/database/schema.js +8 -0
  87. package/dist/kernel/hooks/types.d.ts +1 -1
  88. package/dist/kernel/hooks/types.d.ts.map +1 -1
  89. package/dist/kernel/plugin/manifest.d.ts +23 -0
  90. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  91. package/dist/kernel/plugin/manifest.js +17 -2
  92. package/dist/modules/analytics/module.d.ts.map +1 -1
  93. package/dist/modules/analytics/module.js +6 -0
  94. package/dist/modules/analytics/reports.d.ts +58 -0
  95. package/dist/modules/analytics/reports.d.ts.map +1 -0
  96. package/dist/modules/analytics/reports.js +249 -0
  97. package/dist/modules/analytics/service.d.ts +10 -0
  98. package/dist/modules/analytics/service.d.ts.map +1 -1
  99. package/dist/modules/analytics/service.js +14 -0
  100. package/dist/modules/cart/repository/index.d.ts +7 -0
  101. package/dist/modules/cart/repository/index.d.ts.map +1 -1
  102. package/dist/modules/cart/repository/index.js +25 -1
  103. package/dist/modules/cart/schema.d.ts +17 -0
  104. package/dist/modules/cart/schema.d.ts.map +1 -1
  105. package/dist/modules/cart/schema.js +2 -0
  106. package/dist/modules/cart/schemas.d.ts +1 -0
  107. package/dist/modules/cart/schemas.d.ts.map +1 -1
  108. package/dist/modules/cart/schemas.js +1 -0
  109. package/dist/modules/cart/service.d.ts +32 -0
  110. package/dist/modules/cart/service.d.ts.map +1 -1
  111. package/dist/modules/cart/service.js +58 -0
  112. package/dist/modules/catalog/entity-service.d.ts +21 -0
  113. package/dist/modules/catalog/entity-service.d.ts.map +1 -1
  114. package/dist/modules/catalog/entity-service.js +132 -3
  115. package/dist/modules/catalog/schema.d.ts +34 -0
  116. package/dist/modules/catalog/schema.d.ts.map +1 -1
  117. package/dist/modules/catalog/schema.js +5 -0
  118. package/dist/modules/catalog/schemas.d.ts +3 -0
  119. package/dist/modules/catalog/schemas.d.ts.map +1 -1
  120. package/dist/modules/catalog/schemas.js +3 -0
  121. package/dist/modules/catalog/service.d.ts +36 -0
  122. package/dist/modules/catalog/service.d.ts.map +1 -1
  123. package/dist/modules/catalog/service.js +6 -0
  124. package/dist/modules/documents/module.d.ts +7 -0
  125. package/dist/modules/documents/module.d.ts.map +1 -0
  126. package/dist/modules/documents/module.js +12 -0
  127. package/dist/modules/documents/pdf.d.ts +25 -0
  128. package/dist/modules/documents/pdf.d.ts.map +1 -0
  129. package/dist/modules/documents/pdf.js +80 -0
  130. package/dist/modules/documents/render.d.ts +32 -0
  131. package/dist/modules/documents/render.d.ts.map +1 -0
  132. package/dist/modules/documents/render.js +190 -0
  133. package/dist/modules/documents/repository/index.d.ts +25 -0
  134. package/dist/modules/documents/repository/index.d.ts.map +1 -0
  135. package/dist/modules/documents/repository/index.js +47 -0
  136. package/dist/modules/documents/schema.d.ts +214 -0
  137. package/dist/modules/documents/schema.d.ts.map +1 -0
  138. package/dist/modules/documents/schema.js +42 -0
  139. package/dist/modules/documents/service.d.ts +51 -0
  140. package/dist/modules/documents/service.d.ts.map +1 -0
  141. package/dist/modules/documents/service.js +119 -0
  142. package/dist/modules/fulfillment/service.d.ts +18 -0
  143. package/dist/modules/fulfillment/service.d.ts.map +1 -1
  144. package/dist/modules/fulfillment/service.js +85 -1
  145. package/dist/modules/orders/repository/index.d.ts +18 -1
  146. package/dist/modules/orders/repository/index.d.ts.map +1 -1
  147. package/dist/modules/orders/repository/index.js +86 -1
  148. package/dist/modules/orders/schema.d.ts +374 -0
  149. package/dist/modules/orders/schema.d.ts.map +1 -1
  150. package/dist/modules/orders/schema.js +52 -1
  151. package/dist/modules/orders/service.d.ts +86 -1
  152. package/dist/modules/orders/service.d.ts.map +1 -1
  153. package/dist/modules/orders/service.js +447 -20
  154. package/dist/modules/pricing/repository/index.d.ts +5 -0
  155. package/dist/modules/pricing/repository/index.d.ts.map +1 -1
  156. package/dist/modules/pricing/repository/index.js +19 -0
  157. package/dist/modules/pricing/service.d.ts +18 -0
  158. package/dist/modules/pricing/service.d.ts.map +1 -1
  159. package/dist/modules/pricing/service.js +38 -0
  160. package/dist/modules/settings/module.d.ts +6 -0
  161. package/dist/modules/settings/module.d.ts.map +1 -0
  162. package/dist/modules/settings/module.js +11 -0
  163. package/dist/modules/settings/repository/index.d.ts +16 -0
  164. package/dist/modules/settings/repository/index.d.ts.map +1 -0
  165. package/dist/modules/settings/repository/index.js +41 -0
  166. package/dist/modules/settings/schema.d.ts +121 -0
  167. package/dist/modules/settings/schema.d.ts.map +1 -0
  168. package/dist/modules/settings/schema.js +24 -0
  169. package/dist/modules/settings/service.d.ts +26 -0
  170. package/dist/modules/settings/service.d.ts.map +1 -0
  171. package/dist/modules/settings/service.js +51 -0
  172. package/dist/modules/shipping/calculator.d.ts +5 -0
  173. package/dist/modules/shipping/calculator.d.ts.map +1 -1
  174. package/dist/modules/shipping/calculator.js +10 -0
  175. package/dist/modules/shipping/module.d.ts +5 -1
  176. package/dist/modules/shipping/module.d.ts.map +1 -1
  177. package/dist/modules/shipping/module.js +4 -1
  178. package/dist/modules/shipping/repository/index.d.ts +27 -2
  179. package/dist/modules/shipping/repository/index.d.ts.map +1 -1
  180. package/dist/modules/shipping/repository/index.js +105 -1
  181. package/dist/modules/shipping/schema.d.ts +419 -0
  182. package/dist/modules/shipping/schema.d.ts.map +1 -0
  183. package/dist/modules/shipping/schema.js +56 -0
  184. package/dist/modules/shipping/service.d.ts +54 -0
  185. package/dist/modules/shipping/service.d.ts.map +1 -1
  186. package/dist/modules/shipping/service.js +161 -2
  187. package/dist/modules/tax/adapter.d.ts +13 -0
  188. package/dist/modules/tax/adapter.d.ts.map +1 -1
  189. package/dist/modules/tax/module.d.ts +5 -1
  190. package/dist/modules/tax/module.d.ts.map +1 -1
  191. package/dist/modules/tax/module.js +4 -1
  192. package/dist/modules/tax/repository/index.d.ts +38 -2
  193. package/dist/modules/tax/repository/index.d.ts.map +1 -1
  194. package/dist/modules/tax/repository/index.js +138 -1
  195. package/dist/modules/tax/schema.d.ts +355 -0
  196. package/dist/modules/tax/schema.d.ts.map +1 -0
  197. package/dist/modules/tax/schema.js +55 -0
  198. package/dist/modules/tax/service.d.ts +60 -1
  199. package/dist/modules/tax/service.d.ts.map +1 -1
  200. package/dist/modules/tax/service.js +227 -2
  201. package/dist/runtime/kernel-modules.d.ts +15 -2
  202. package/dist/runtime/kernel-modules.d.ts.map +1 -1
  203. package/dist/runtime/kernel-modules.js +4 -0
  204. package/dist/runtime/kernel-types.d.ts +5 -1
  205. package/dist/runtime/kernel-types.d.ts.map +1 -1
  206. package/dist/runtime/kernel-types.js +2 -0
  207. package/dist/runtime/server.js +1 -1
  208. package/dist/test-utils/create-plugin-test-app.d.ts +3 -0
  209. package/dist/test-utils/create-plugin-test-app.d.ts.map +1 -1
  210. package/dist/test-utils/create-plugin-test-app.js +15 -3
  211. package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
  212. package/dist/test-utils/rest-api-test-utils.js +6 -0
  213. package/package.json +3 -3
  214. package/src/auth/setup.ts +1 -0
  215. package/src/config/defaults.ts +5 -0
  216. package/src/config/types.ts +17 -1
  217. package/src/generated/plugin-capabilities.d.ts +1 -0
  218. package/src/generated/plugin-manifest.ts +1 -0
  219. package/src/generated/plugin-repositories.d.ts +1 -0
  220. package/src/hooks/checkout.ts +25 -3
  221. package/src/index.ts +1 -0
  222. package/src/interfaces/rest/index.ts +12 -0
  223. package/src/interfaces/rest/routes/admin/staff.ts +218 -0
  224. package/src/interfaces/rest/routes/analytics.ts +33 -0
  225. package/src/interfaces/rest/routes/carts.ts +52 -2
  226. package/src/interfaces/rest/routes/catalog.ts +34 -0
  227. package/src/interfaces/rest/routes/checkout.ts +22 -0
  228. package/src/interfaces/rest/routes/documents.ts +61 -0
  229. package/src/interfaces/rest/routes/orders.ts +137 -1
  230. package/src/interfaces/rest/routes/pricing.ts +44 -1
  231. package/src/interfaces/rest/routes/settings.ts +61 -0
  232. package/src/interfaces/rest/routes/shipping.ts +92 -0
  233. package/src/interfaces/rest/routes/tax.ts +90 -0
  234. package/src/interfaces/rest/schemas/admin-staff.ts +110 -0
  235. package/src/interfaces/rest/schemas/analytics.ts +39 -0
  236. package/src/interfaces/rest/schemas/carts.ts +40 -0
  237. package/src/interfaces/rest/schemas/catalog.ts +53 -0
  238. package/src/interfaces/rest/schemas/checkout.ts +4 -0
  239. package/src/interfaces/rest/schemas/documents.ts +72 -0
  240. package/src/interfaces/rest/schemas/orders.ts +257 -0
  241. package/src/interfaces/rest/schemas/pricing.ts +75 -0
  242. package/src/interfaces/rest/schemas/settings.ts +93 -0
  243. package/src/interfaces/rest/schemas/shipping.ts +164 -0
  244. package/src/interfaces/rest/schemas/tax.ts +148 -0
  245. package/src/kernel/database/schema.ts +12 -0
  246. package/src/kernel/hooks/types.ts +1 -0
  247. package/src/kernel/plugin/manifest.ts +38 -2
  248. package/src/modules/analytics/module.ts +10 -0
  249. package/src/modules/analytics/reports.ts +311 -0
  250. package/src/modules/analytics/service.ts +23 -0
  251. package/src/modules/cart/repository/index.ts +30 -1
  252. package/src/modules/cart/schema.ts +2 -0
  253. package/src/modules/cart/schemas.ts +1 -0
  254. package/src/modules/cart/service.ts +101 -0
  255. package/src/modules/catalog/entity-service.ts +168 -3
  256. package/src/modules/catalog/schema.ts +5 -0
  257. package/src/modules/catalog/schemas.ts +3 -0
  258. package/src/modules/catalog/service.ts +30 -0
  259. package/src/modules/documents/module.ts +19 -0
  260. package/src/modules/documents/pdf.ts +101 -0
  261. package/src/modules/documents/render.ts +231 -0
  262. package/src/modules/documents/repository/index.ts +68 -0
  263. package/src/modules/documents/schema.ts +44 -0
  264. package/src/modules/documents/service.ts +176 -0
  265. package/src/modules/fulfillment/service.ts +139 -1
  266. package/src/modules/orders/repository/index.ts +127 -1
  267. package/src/modules/orders/schema.ts +54 -1
  268. package/src/modules/orders/service.ts +654 -22
  269. package/src/modules/pricing/repository/index.ts +34 -0
  270. package/src/modules/pricing/service.ts +71 -0
  271. package/src/modules/settings/module.ts +18 -0
  272. package/src/modules/settings/repository/index.ts +59 -0
  273. package/src/modules/settings/schema.ts +25 -0
  274. package/src/modules/settings/service.ts +82 -0
  275. package/src/modules/shipping/calculator.ts +27 -0
  276. package/src/modules/shipping/module.ts +6 -2
  277. package/src/modules/shipping/repository/index.ts +136 -2
  278. package/src/modules/shipping/schema.ts +58 -0
  279. package/src/modules/shipping/service.ts +260 -1
  280. package/src/modules/tax/adapter.ts +9 -0
  281. package/src/modules/tax/module.ts +6 -2
  282. package/src/modules/tax/repository/index.ts +169 -2
  283. package/src/modules/tax/schema.ts +57 -0
  284. package/src/modules/tax/service.ts +313 -2
  285. package/src/runtime/kernel-modules.ts +4 -0
  286. package/src/runtime/kernel-types.ts +6 -0
  287. package/src/runtime/server.ts +1 -1
  288. package/src/test-utils/create-plugin-test-app.ts +17 -4
  289. package/src/test-utils/rest-api-test-utils.ts +6 -0
@@ -1,4 +1,7 @@
1
- import { CommerceValidationError } from "../../kernel/errors.js";
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
- async calculate(params: TaxCalculationParams): Promise<Result<TaxCalculationResult>> {
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"]>;
@@ -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",