@cogenta/commerce 0.2.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 (98) hide show
  1. package/LICENSE +373 -0
  2. package/dist/admin/permissions.d.ts +57 -0
  3. package/dist/admin/permissions.d.ts.map +1 -0
  4. package/dist/admin/permissions.js +87 -0
  5. package/dist/admin/permissions.js.map +1 -0
  6. package/dist/admin/router.d.ts +53 -0
  7. package/dist/admin/router.d.ts.map +1 -0
  8. package/dist/admin/router.js +476 -0
  9. package/dist/admin/router.js.map +1 -0
  10. package/dist/cart/store.d.ts +91 -0
  11. package/dist/cart/store.d.ts.map +1 -0
  12. package/dist/cart/store.js +342 -0
  13. package/dist/cart/store.js.map +1 -0
  14. package/dist/cart/totals.d.ts +73 -0
  15. package/dist/cart/totals.d.ts.map +1 -0
  16. package/dist/cart/totals.js +65 -0
  17. package/dist/cart/totals.js.map +1 -0
  18. package/dist/catalog/store.d.ts +53 -0
  19. package/dist/catalog/store.d.ts.map +1 -0
  20. package/dist/catalog/store.js +358 -0
  21. package/dist/catalog/store.js.map +1 -0
  22. package/dist/catalog/types.d.ts +95 -0
  23. package/dist/catalog/types.d.ts.map +1 -0
  24. package/dist/catalog/types.js +5 -0
  25. package/dist/catalog/types.js.map +1 -0
  26. package/dist/coupon/store.d.ts +103 -0
  27. package/dist/coupon/store.d.ts.map +1 -0
  28. package/dist/coupon/store.js +181 -0
  29. package/dist/coupon/store.js.map +1 -0
  30. package/dist/customer/store.d.ts +41 -0
  31. package/dist/customer/store.d.ts.map +1 -0
  32. package/dist/customer/store.js +103 -0
  33. package/dist/customer/store.js.map +1 -0
  34. package/dist/index.d.ts +42 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +23 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/invoice/pdf.d.ts +67 -0
  39. package/dist/invoice/pdf.d.ts.map +1 -0
  40. package/dist/invoice/pdf.js +449 -0
  41. package/dist/invoice/pdf.js.map +1 -0
  42. package/dist/invoice/store.d.ts +96 -0
  43. package/dist/invoice/store.d.ts.map +1 -0
  44. package/dist/invoice/store.js +221 -0
  45. package/dist/invoice/store.js.map +1 -0
  46. package/dist/money.d.ts +55 -0
  47. package/dist/money.d.ts.map +1 -0
  48. package/dist/money.js +146 -0
  49. package/dist/money.js.map +1 -0
  50. package/dist/order/store.d.ts +75 -0
  51. package/dist/order/store.d.ts.map +1 -0
  52. package/dist/order/store.js +301 -0
  53. package/dist/order/store.js.map +1 -0
  54. package/dist/order/types.d.ts +67 -0
  55. package/dist/order/types.d.ts.map +1 -0
  56. package/dist/order/types.js +71 -0
  57. package/dist/order/types.js.map +1 -0
  58. package/dist/payment/manual.d.ts +27 -0
  59. package/dist/payment/manual.d.ts.map +1 -0
  60. package/dist/payment/manual.js +105 -0
  61. package/dist/payment/manual.js.map +1 -0
  62. package/dist/payment/registry.d.ts +23 -0
  63. package/dist/payment/registry.d.ts.map +1 -0
  64. package/dist/payment/registry.js +30 -0
  65. package/dist/payment/registry.js.map +1 -0
  66. package/dist/payment/store.d.ts +38 -0
  67. package/dist/payment/store.d.ts.map +1 -0
  68. package/dist/payment/store.js +253 -0
  69. package/dist/payment/store.js.map +1 -0
  70. package/dist/payment/stripe.d.ts +10 -0
  71. package/dist/payment/stripe.d.ts.map +1 -0
  72. package/dist/payment/stripe.js +419 -0
  73. package/dist/payment/stripe.js.map +1 -0
  74. package/dist/payment/types.d.ts +124 -0
  75. package/dist/payment/types.d.ts.map +1 -0
  76. package/dist/payment/types.js +18 -0
  77. package/dist/payment/types.js.map +1 -0
  78. package/dist/rows.d.ts +26 -0
  79. package/dist/rows.d.ts.map +1 -0
  80. package/dist/rows.js +87 -0
  81. package/dist/rows.js.map +1 -0
  82. package/dist/shipping/store.d.ts +88 -0
  83. package/dist/shipping/store.d.ts.map +1 -0
  84. package/dist/shipping/store.js +165 -0
  85. package/dist/shipping/store.js.map +1 -0
  86. package/dist/subscription/store.d.ts +106 -0
  87. package/dist/subscription/store.d.ts.map +1 -0
  88. package/dist/subscription/store.js +310 -0
  89. package/dist/subscription/store.js.map +1 -0
  90. package/dist/tables.d.ts +41 -0
  91. package/dist/tables.d.ts.map +1 -0
  92. package/dist/tables.js +383 -0
  93. package/dist/tables.js.map +1 -0
  94. package/dist/tax/store.d.ts +68 -0
  95. package/dist/tax/store.d.ts.map +1 -0
  96. package/dist/tax/store.js +126 -0
  97. package/dist/tax/store.js.map +1 -0
  98. package/package.json +42 -0
package/dist/tables.js ADDED
@@ -0,0 +1,383 @@
1
+ import { identifier, sql, unsafeRaw, } from '@cogenta/core';
2
+ /**
3
+ * Every table contract E owns.
4
+ *
5
+ * Prefixed `cogenta_commerce_` rather than `cogenta_`: a site's content tables
6
+ * are named after the collections **it** declares, so a shop that also declares
7
+ * a `product` collection for the storefront page must not collide with the
8
+ * commercial record of the same product. The two are deliberately different
9
+ * objects (ADR draft 0023) and they must be able to coexist in one database.
10
+ */
11
+ export const TABLES = {
12
+ products: 'cogenta_commerce_products',
13
+ variants: 'cogenta_commerce_variants',
14
+ customers: 'cogenta_commerce_customers',
15
+ carts: 'cogenta_commerce_carts',
16
+ cartLines: 'cogenta_commerce_cart_lines',
17
+ orders: 'cogenta_commerce_orders',
18
+ orderLines: 'cogenta_commerce_order_lines',
19
+ orderEvents: 'cogenta_commerce_order_events',
20
+ payments: 'cogenta_commerce_payments',
21
+ refunds: 'cogenta_commerce_refunds',
22
+ taxRules: 'cogenta_commerce_tax_rules',
23
+ shippingMethods: 'cogenta_commerce_shipping_methods',
24
+ coupons: 'cogenta_commerce_coupons',
25
+ couponRedemptions: 'cogenta_commerce_coupon_redemptions',
26
+ invoices: 'cogenta_commerce_invoices',
27
+ invoiceSequences: 'cogenta_commerce_invoice_sequences',
28
+ subscriptions: 'cogenta_commerce_subscriptions',
29
+ subscriptionCycles: 'cogenta_commerce_subscription_cycles',
30
+ };
31
+ /** `varchar` on Postgres/MySQL, `text` on SQLite — encapsulated once, here. */
32
+ function textColumn(dialect, length) {
33
+ return unsafeRaw(dialect === 'sqlite' ? 'text' : `varchar(${length})`);
34
+ }
35
+ function booleanColumn(dialect) {
36
+ return unsafeRaw(dialect === 'postgres' ? 'boolean' : 'tinyint');
37
+ }
38
+ /**
39
+ * Every amount and every quantity is a `bigint`.
40
+ *
41
+ * `integer` would hold any realistic price, and it is still the wrong choice:
42
+ * the one place a 32-bit overflow shows up is a yearly turnover total in a
43
+ * minor-unit currency, i.e. exactly the number an accountant looks at. Eight
44
+ * bytes to never think about it again.
45
+ */
46
+ function integerColumn() {
47
+ return unsafeRaw('bigint');
48
+ }
49
+ /**
50
+ * Creates everything this package owns, idempotently.
51
+ *
52
+ * Same shape as `ensureAuthTables`: `create table if not exists`, run once at
53
+ * startup, so a fresh install and an upgrade take the same path. Commerce is
54
+ * **not** part of contract A's migration engine on purpose — a site that never
55
+ * sells anything never creates these tables, which is the whole point of
56
+ * putting commerce in its own contract rather than raising `schema` to 3.0.
57
+ */
58
+ export async function ensureCommerceTables(db) {
59
+ const d = db.dialect;
60
+ const t1024 = textColumn(d, 1024);
61
+ const t255 = textColumn(d, 255);
62
+ const t64 = textColumn(d, 64);
63
+ const t8 = textColumn(d, 8);
64
+ const bool = booleanColumn(d);
65
+ const int = integerColumn();
66
+ await db.query(sql `
67
+ create table if not exists ${identifier(TABLES.products, d)} (
68
+ id ${t64} not null primary key,
69
+ -- Stable, human-readable, used in URLs and in the admin. Unique.
70
+ handle ${t255} not null unique,
71
+ title ${t255} not null,
72
+ status ${t64} not null,
73
+ -- The optional link to the editorial face of this product (contract A).
74
+ -- Deliberately NOT a foreign key: the entries table of a collection is
75
+ -- created by contract A's migration engine from the schema the *site*
76
+ -- declares, so its name is not known here. Checked in application code.
77
+ content_collection ${t255},
78
+ content_entry_id ${t64},
79
+ created_at ${t64} not null,
80
+ updated_at ${t64} not null
81
+ )`);
82
+ await db.query(sql `
83
+ create table if not exists ${identifier(TABLES.variants, d)} (
84
+ id ${t64} not null primary key,
85
+ product_id ${t64} not null,
86
+ sku ${t255} not null unique,
87
+ title ${t255} not null,
88
+ price_minor ${int} not null,
89
+ currency ${t8} not null,
90
+ -- Units physically available. Never negative: every write that lowers it
91
+ -- carries its own "on_hand >= quantity" guard (see catalog/store.ts).
92
+ on_hand ${int} not null,
93
+ -- When true this variant sells past zero. The guard is then skipped, and
94
+ -- that is the *only* way on_hand goes below zero — an explicit choice
95
+ -- per variant, never an accident.
96
+ allow_backorder ${bool} not null,
97
+ -- Grams. Integers again, and for the same reason as money.
98
+ weight_grams ${int} not null,
99
+ tax_category ${t64} not null,
100
+ position ${int} not null,
101
+ created_at ${t64} not null,
102
+ updated_at ${t64} not null
103
+ )`);
104
+ await db.query(sql `
105
+ create table if not exists ${identifier(TABLES.customers, d)} (
106
+ id ${t64} not null primary key,
107
+ email ${t255} not null unique,
108
+ name ${t255},
109
+ -- Optional link to a @cogenta/auth account. A customer is not a user:
110
+ -- most shops sell to people who never sign in to the admin.
111
+ user_id ${t64},
112
+ created_at ${t64} not null,
113
+ updated_at ${t64} not null
114
+ )`);
115
+ await db.query(sql `
116
+ create table if not exists ${identifier(TABLES.carts, d)} (
117
+ id ${t64} not null primary key,
118
+ customer_id ${t64},
119
+ -- An anonymous shopper. Opaque, supplied by the transport, never a
120
+ -- session token of @cogenta/auth: a cart must survive signing out.
121
+ session_key ${t255},
122
+ currency ${t8} not null,
123
+ coupon_code ${t64},
124
+ shipping_country ${t8},
125
+ shipping_region ${t64},
126
+ shipping_method_id ${t64},
127
+ status ${t64} not null,
128
+ order_id ${t64},
129
+ created_at ${t64} not null,
130
+ updated_at ${t64} not null,
131
+ expires_at ${t64} not null
132
+ )`);
133
+ await db.query(sql `
134
+ create table if not exists ${identifier(TABLES.cartLines, d)} (
135
+ id ${t64} not null primary key,
136
+ cart_id ${t64} not null,
137
+ variant_id ${t64} not null,
138
+ quantity ${int} not null,
139
+ -- The price when the line was added. A cart shows what the shopper was
140
+ -- told; the order re-reads the live price and says so if it moved.
141
+ unit_price_minor ${int} not null,
142
+ created_at ${t64} not null,
143
+ updated_at ${t64} not null
144
+ )`);
145
+ await db.query(sql `
146
+ create table if not exists ${identifier(TABLES.orders, d)} (
147
+ id ${t64} not null primary key,
148
+ -- Human-facing order reference. Unique, and unrelated to the invoice
149
+ -- number: an order that is never invoiced still needs to be named.
150
+ reference ${t64} not null unique,
151
+ customer_id ${t64},
152
+ email ${t255} not null,
153
+ status ${t64} not null,
154
+ currency ${t8} not null,
155
+ subtotal_minor ${int} not null,
156
+ discount_minor ${int} not null,
157
+ shipping_minor ${int} not null,
158
+ tax_minor ${int} not null,
159
+ total_minor ${int} not null,
160
+ coupon_code ${t64},
161
+ shipping_country ${t8},
162
+ shipping_region ${t64},
163
+ shipping_method_id ${t64},
164
+ shipping_method_label ${t255},
165
+ placed_at ${t64} not null,
166
+ updated_at ${t64} not null,
167
+ -- Set when a subscription cycle produced this order, null otherwise.
168
+ subscription_id ${t64}
169
+ )`);
170
+ await db.query(sql `
171
+ create table if not exists ${identifier(TABLES.orderLines, d)} (
172
+ id ${t64} not null primary key,
173
+ order_id ${t64} not null,
174
+ variant_id ${t64} not null,
175
+ -- Copied, not joined. A product renamed or deleted next year must not
176
+ -- change what an order from last year says it sold.
177
+ sku ${t255} not null,
178
+ title ${t255} not null,
179
+ quantity ${int} not null,
180
+ unit_price_minor ${int} not null,
181
+ subtotal_minor ${int} not null,
182
+ discount_minor ${int} not null,
183
+ tax_minor ${int} not null,
184
+ tax_rate_bp ${int} not null,
185
+ total_minor ${int} not null,
186
+ position ${int} not null
187
+ )`);
188
+ await db.query(sql `
189
+ create table if not exists ${identifier(TABLES.orderEvents, d)} (
190
+ id ${t64} not null primary key,
191
+ order_id ${t64} not null,
192
+ at ${t64} not null,
193
+ kind ${t64} not null,
194
+ from_status ${t64},
195
+ to_status ${t64},
196
+ actor_id ${t64},
197
+ note ${t1024}
198
+ )`);
199
+ await db.query(sql `
200
+ create table if not exists ${identifier(TABLES.payments, d)} (
201
+ id ${t64} not null primary key,
202
+ order_id ${t64} not null,
203
+ driver ${t64} not null,
204
+ -- The gateway's own id. Null for a driver that settles offline until a
205
+ -- human records the transfer reference.
206
+ external_id ${t255},
207
+ status ${t64} not null,
208
+ amount_minor ${int} not null,
209
+ currency ${t8} not null,
210
+ -- Free-form, driver-specific, shown to the shopper: bank details for a
211
+ -- transfer, a redirect URL for a gateway.
212
+ instructions ${t1024},
213
+ created_at ${t64} not null,
214
+ updated_at ${t64} not null
215
+ )`);
216
+ await db.query(sql `
217
+ create table if not exists ${identifier(TABLES.refunds, d)} (
218
+ id ${t64} not null primary key,
219
+ payment_id ${t64} not null,
220
+ order_id ${t64} not null,
221
+ external_id ${t255},
222
+ status ${t64} not null,
223
+ amount_minor ${int} not null,
224
+ currency ${t8} not null,
225
+ reason ${t255},
226
+ created_at ${t64} not null
227
+ )`);
228
+ await db.query(sql `
229
+ create table if not exists ${identifier(TABLES.taxRules, d)} (
230
+ id ${t64} not null primary key,
231
+ -- Null country means "everywhere the more specific rules do not cover".
232
+ country ${t8},
233
+ region ${t64},
234
+ tax_category ${t64} not null,
235
+ name ${t255} not null,
236
+ rate_bp ${int} not null,
237
+ -- True when the catalogue price already contains this tax (the European
238
+ -- convention). The order then shows the tax it *contains*, never adds it.
239
+ included_in_price ${bool} not null,
240
+ -- Higher wins. Makes "France, reduced rate" beat "anywhere, standard".
241
+ priority ${int} not null,
242
+ active ${bool} not null,
243
+ created_at ${t64} not null
244
+ )`);
245
+ await db.query(sql `
246
+ create table if not exists ${identifier(TABLES.shippingMethods, d)} (
247
+ id ${t64} not null primary key,
248
+ label ${t255} not null,
249
+ country ${t8},
250
+ region ${t64},
251
+ kind ${t64} not null,
252
+ currency ${t8} not null,
253
+ amount_minor ${int} not null,
254
+ per_kg_minor ${int} not null,
255
+ free_over_minor ${int},
256
+ -- Names a registered carrier driver, or null for a plain stored rate.
257
+ carrier ${t64},
258
+ position ${int} not null,
259
+ active ${bool} not null,
260
+ created_at ${t64} not null
261
+ )`);
262
+ await db.query(sql `
263
+ create table if not exists ${identifier(TABLES.coupons, d)} (
264
+ code ${t64} not null primary key,
265
+ kind ${t64} not null,
266
+ -- Basis points for a percentage coupon, minor units for a fixed one,
267
+ -- ignored for free shipping. One column because a coupon has exactly
268
+ -- one number, and three nullable ones would only invite two being set.
269
+ value ${int} not null,
270
+ currency ${t8},
271
+ min_subtotal_minor ${int} not null,
272
+ starts_at ${t64},
273
+ ends_at ${t64},
274
+ -- Null means unlimited. Zero means exhausted, not unlimited.
275
+ max_redemptions ${int},
276
+ redemptions ${int} not null,
277
+ active ${bool} not null,
278
+ created_at ${t64} not null
279
+ )`);
280
+ await db.query(sql `
281
+ create table if not exists ${identifier(TABLES.couponRedemptions, d)} (
282
+ id ${t64} not null primary key,
283
+ code ${t64} not null,
284
+ order_id ${t64} not null unique,
285
+ customer_id ${t64},
286
+ at ${t64} not null
287
+ )`);
288
+ await db.query(sql `
289
+ create table if not exists ${identifier(TABLES.invoices, d)} (
290
+ id ${t64} not null primary key,
291
+ -- One invoice per order. The unique constraint is the last line of
292
+ -- defence behind the transaction: a double issue burns a number.
293
+ order_id ${t64} not null unique,
294
+ series ${t64} not null,
295
+ seq ${int} not null,
296
+ number ${t64} not null unique,
297
+ issued_at ${t64} not null,
298
+ currency ${t8} not null,
299
+ total_minor ${int} not null,
300
+ -- The whole document, frozen at issue. An invoice must say what it said
301
+ -- the day it was issued, whatever the order becomes afterwards.
302
+ document text not null
303
+ )`);
304
+ await db.query(sql `
305
+ create table if not exists ${identifier(TABLES.invoiceSequences, d)} (
306
+ series ${t64} not null primary key,
307
+ next_seq ${int} not null
308
+ )`);
309
+ await db.query(sql `
310
+ create table if not exists ${identifier(TABLES.subscriptions, d)} (
311
+ id ${t64} not null primary key,
312
+ customer_id ${t64} not null,
313
+ variant_id ${t64} not null,
314
+ quantity ${int} not null,
315
+ status ${t64} not null,
316
+ interval_unit ${t64} not null,
317
+ interval_count ${int} not null,
318
+ price_minor ${int} not null,
319
+ currency ${t8} not null,
320
+ payment_driver ${t64} not null,
321
+ current_period_start ${t64} not null,
322
+ current_period_end ${t64} not null,
323
+ -- The clock this store reads to decide what is due. Kept as its own
324
+ -- column rather than derived, so a paused subscription simply stops
325
+ -- moving it forward.
326
+ next_billing_at ${t64} not null,
327
+ shipping_country ${t8},
328
+ shipping_region ${t64},
329
+ created_at ${t64} not null,
330
+ updated_at ${t64} not null,
331
+ cancelled_at ${t64}
332
+ )`);
333
+ await db.query(sql `
334
+ create table if not exists ${identifier(TABLES.subscriptionCycles, d)} (
335
+ id ${t64} not null primary key,
336
+ subscription_id ${t64} not null,
337
+ period_start ${t64} not null,
338
+ period_end ${t64} not null,
339
+ -- Unique per period: the guard that makes billing idempotent, so running
340
+ -- the biller twice in one minute cannot charge twice.
341
+ period_key ${t255} not null unique,
342
+ order_id ${t64},
343
+ status ${t64} not null,
344
+ created_at ${t64} not null
345
+ )`);
346
+ await ensureIndexes(db);
347
+ }
348
+ /**
349
+ * Indexes for the reads this package actually performs.
350
+ *
351
+ * Created separately and after the tables, because `create index if not
352
+ * exists` is the one piece of DDL the three dialects spell differently enough
353
+ * to be worth isolating: MySQL before 8.0.29 has no `if not exists` for
354
+ * indexes at all, so a duplicate-index error there is swallowed rather than
355
+ * guarded against.
356
+ */
357
+ async function ensureIndexes(db) {
358
+ const d = db.dialect;
359
+ const wanted = [
360
+ ['cogenta_commerce_variants_product', TABLES.variants, 'product_id'],
361
+ ['cogenta_commerce_cart_lines_cart', TABLES.cartLines, 'cart_id'],
362
+ ['cogenta_commerce_order_lines_order', TABLES.orderLines, 'order_id'],
363
+ ['cogenta_commerce_order_events_order', TABLES.orderEvents, 'order_id'],
364
+ ['cogenta_commerce_orders_customer', TABLES.orders, 'customer_id'],
365
+ ['cogenta_commerce_payments_order', TABLES.payments, 'order_id'],
366
+ ['cogenta_commerce_refunds_payment', TABLES.refunds, 'payment_id'],
367
+ ['cogenta_commerce_cycles_subscription', TABLES.subscriptionCycles, 'subscription_id'],
368
+ ];
369
+ for (const [name, table, column] of wanted) {
370
+ const statement = sql `create index ${identifier(name, d)} on ${identifier(table, d)} (${identifier(column, d)})`;
371
+ try {
372
+ await db.query(d === 'mysql'
373
+ ? statement
374
+ : sql `create index if not exists ${identifier(name, d)} on ${identifier(table, d)} (${identifier(column, d)})`);
375
+ }
376
+ catch {
377
+ // Already there. The only failure this swallows is "duplicate index",
378
+ // and a genuinely broken schema fails loudly on the next real query
379
+ // rather than here.
380
+ }
381
+ }
382
+ }
383
+ //# sourceMappingURL=tables.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.js","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EAEV,GAAG,EACH,SAAS,GACV,MAAM,eAAe,CAAA;AAEtB;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,QAAQ,EAAE,2BAA2B;IACrC,QAAQ,EAAE,2BAA2B;IACrC,SAAS,EAAE,4BAA4B;IACvC,KAAK,EAAE,wBAAwB;IAC/B,SAAS,EAAE,6BAA6B;IACxC,MAAM,EAAE,yBAAyB;IACjC,UAAU,EAAE,8BAA8B;IAC1C,WAAW,EAAE,+BAA+B;IAC5C,QAAQ,EAAE,2BAA2B;IACrC,OAAO,EAAE,0BAA0B;IACnC,QAAQ,EAAE,4BAA4B;IACtC,eAAe,EAAE,mCAAmC;IACpD,OAAO,EAAE,0BAA0B;IACnC,iBAAiB,EAAE,qCAAqC;IACxD,QAAQ,EAAE,2BAA2B;IACrC,gBAAgB,EAAE,oCAAoC;IACtD,aAAa,EAAE,gCAAgC;IAC/C,kBAAkB,EAAE,sCAAsC;CAClD,CAAA;AAEV,+EAA+E;AAC/E,SAAS,UAAU,CAAC,OAAwB,EAAE,MAAc;IAC1D,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,MAAM,GAAG,CAAC,CAAA;AACxE,CAAC;AAED,SAAS,aAAa,CAAC,OAAwB;IAC7C,OAAO,SAAS,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AAClE,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,aAAa;IACpB,OAAO,SAAS,CAAC,QAAQ,CAAC,CAAA;AAC5B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,EAAkB;IAC3D,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAA;IACpB,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IACjC,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IAC/B,MAAM,GAAG,GAAG,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAC7B,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAC3B,MAAM,IAAI,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;IAC7B,MAAM,GAAG,GAAG,aAAa,EAAE,CAAA;IAE3B,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;WACpD,GAAG;;eAEC,IAAI;cACL,IAAI;eACH,GAAG;;;;;2BAKS,IAAI;yBACN,GAAG;mBACT,GAAG;mBACH,GAAG;MAChB,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;WACpD,GAAG;mBACK,GAAG;YACV,IAAI;cACF,IAAI;oBACE,GAAG;iBACN,EAAE;;;gBAGH,GAAG;;;;wBAIK,IAAI;;qBAEP,GAAG;qBACH,GAAG;iBACP,GAAG;mBACD,GAAG;mBACH,GAAG;MAChB,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;WACrD,GAAG;cACA,IAAI;aACL,IAAI;;;gBAGD,GAAG;mBACA,GAAG;mBACH,GAAG;MAChB,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;WACjD,GAAG;oBACM,GAAG;;;oBAGH,IAAI;iBACP,EAAE;oBACC,GAAG;yBACE,EAAE;wBACH,GAAG;2BACA,GAAG;eACf,GAAG;iBACD,GAAG;mBACD,GAAG;mBACH,GAAG;mBACH,GAAG;MAChB,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;WACrD,GAAG;gBACE,GAAG;mBACA,GAAG;iBACL,GAAG;;;yBAGK,GAAG;mBACT,GAAG;mBACH,GAAG;MAChB,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;WAClD,GAAG;;;kBAGI,GAAG;oBACD,GAAG;cACT,IAAI;eACH,GAAG;iBACD,EAAE;uBACI,GAAG;uBACH,GAAG;uBACH,GAAG;kBACR,GAAG;oBACD,GAAG;oBACH,GAAG;yBACE,EAAE;wBACH,GAAG;2BACA,GAAG;8BACA,IAAI;kBAChB,GAAG;mBACF,GAAG;;wBAEE,GAAG;MACrB,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;WACtD,GAAG;iBACG,GAAG;mBACD,GAAG;;;YAGV,IAAI;cACF,IAAI;iBACD,GAAG;yBACK,GAAG;uBACL,GAAG;uBACH,GAAG;kBACR,GAAG;oBACD,GAAG;oBACH,GAAG;iBACN,GAAG;MACd,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;WACvD,GAAG;iBACG,GAAG;WACT,GAAG;aACD,GAAG;oBACI,GAAG;kBACL,GAAG;iBACJ,GAAG;aACP,KAAK;MACZ,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;WACpD,GAAG;iBACG,GAAG;eACL,GAAG;;;oBAGE,IAAI;eACT,GAAG;qBACG,GAAG;iBACP,EAAE;;;qBAGE,KAAK;mBACP,GAAG;mBACH,GAAG;MAChB,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;WACnD,GAAG;mBACK,GAAG;iBACL,GAAG;oBACA,IAAI;eACT,GAAG;qBACG,GAAG;iBACP,EAAE;eACJ,IAAI;mBACA,GAAG;MAChB,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;WACpD,GAAG;;gBAEE,EAAE;eACH,GAAG;qBACG,GAAG;aACX,IAAI;gBACD,GAAG;;;0BAGO,IAAI;;iBAEb,GAAG;eACL,IAAI;mBACA,GAAG;MAChB,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC,CAAC;WAC3D,GAAG;cACA,IAAI;gBACF,EAAE;eACH,GAAG;aACL,GAAG;iBACC,EAAE;qBACE,GAAG;qBACH,GAAG;wBACA,GAAG;;gBAEX,GAAG;iBACF,GAAG;eACL,IAAI;mBACA,GAAG;MAChB,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;aACjD,GAAG;aACH,GAAG;;;;cAIF,GAAG;iBACA,EAAE;2BACQ,GAAG;kBACZ,GAAG;gBACL,GAAG;;wBAEK,GAAG;oBACP,GAAG;eACR,IAAI;mBACA,GAAG;MAChB,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC;WAC7D,GAAG;aACD,GAAG;iBACC,GAAG;oBACA,GAAG;WACZ,GAAG;MACR,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;WACpD,GAAG;;;iBAGG,GAAG;eACL,GAAG;YACN,GAAG;eACA,GAAG;kBACA,GAAG;iBACJ,EAAE;oBACC,GAAG;;;;MAIjB,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC;eACxD,GAAG;iBACD,GAAG;MACd,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;WACzD,GAAG;oBACM,GAAG;mBACJ,GAAG;iBACL,GAAG;eACL,GAAG;sBACI,GAAG;uBACF,GAAG;oBACN,GAAG;iBACN,EAAE;uBACI,GAAG;6BACG,GAAG;2BACL,GAAG;;;;wBAIN,GAAG;yBACF,EAAE;wBACH,GAAG;mBACR,GAAG;mBACH,GAAG;qBACD,GAAG;MAClB,CAAC,CAAA;IAEL,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;iCACa,UAAU,CAAC,MAAM,CAAC,kBAAkB,EAAE,CAAC,CAAC;WAC9D,GAAG;wBACU,GAAG;qBACN,GAAG;mBACL,GAAG;;;mBAGH,IAAI;iBACN,GAAG;eACL,GAAG;mBACC,GAAG;MAChB,CAAC,CAAA;IAEL,MAAM,aAAa,CAAC,EAAE,CAAC,CAAA;AACzB,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,aAAa,CAAC,EAAkB;IAC7C,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAA;IACpB,MAAM,MAAM,GAAmD;QAC7D,CAAC,mCAAmC,EAAE,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC;QACpE,CAAC,kCAAkC,EAAE,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC;QACjE,CAAC,oCAAoC,EAAE,MAAM,CAAC,UAAU,EAAE,UAAU,CAAC;QACrE,CAAC,qCAAqC,EAAE,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC;QACvE,CAAC,kCAAkC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC;QAClE,CAAC,iCAAiC,EAAE,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC;QAChE,CAAC,kCAAkC,EAAE,MAAM,CAAC,OAAO,EAAE,YAAY,CAAC;QAClE,CAAC,sCAAsC,EAAE,MAAM,CAAC,kBAAkB,EAAE,iBAAiB,CAAC;KACvF,CAAA;IAED,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QAC3C,MAAM,SAAS,GAAG,GAAG,CAAA,gBAAgB,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAA;QAChH,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,KAAK,CACZ,CAAC,KAAK,OAAO;gBACX,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,GAAG,CAAA,8BAA8B,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CACjH,CAAA;QACH,CAAC;QAAC,MAAM,CAAC;YACP,sEAAsE;YACtE,oEAAoE;YACpE,oBAAoB;QACtB,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,68 @@
1
+ import { type DatabaseHandle } from '@cogenta/core';
2
+ /**
3
+ * Where a shopper is, as far as tax is concerned.
4
+ *
5
+ * Country and an optional region, and nothing else. Not a postcode: postcode
6
+ * tax (US local sales tax) is a database problem, not a rules problem, and
7
+ * pretending to solve it with three stored rows would be worse than saying so.
8
+ * A site that needs it registers a carrier-style rule source of its own.
9
+ */
10
+ export interface TaxZone {
11
+ /** ISO 3166-1 alpha-2, upper case. */
12
+ readonly country: string;
13
+ /** A state, province or county code. Free text, matched exactly. */
14
+ readonly region?: string | null;
15
+ }
16
+ export interface TaxRule {
17
+ readonly id: string;
18
+ readonly country: string | null;
19
+ readonly region: string | null;
20
+ readonly taxCategory: string;
21
+ readonly name: string;
22
+ readonly rateBp: number;
23
+ /**
24
+ * True when the catalogue price already contains this tax — the European
25
+ * convention. The order then shows the tax it *contains*; it never adds it.
26
+ */
27
+ readonly includedInPrice: boolean;
28
+ readonly priority: number;
29
+ readonly active: boolean;
30
+ readonly createdAt: string;
31
+ }
32
+ export interface CreateTaxRuleInput {
33
+ readonly country?: string | null;
34
+ readonly region?: string | null;
35
+ readonly taxCategory?: string;
36
+ readonly name: string;
37
+ readonly rateBp: number;
38
+ readonly includedInPrice?: boolean;
39
+ readonly priority?: number;
40
+ readonly active?: boolean;
41
+ }
42
+ /** What one line owes. */
43
+ export interface TaxOutcome {
44
+ readonly rateBp: number;
45
+ readonly taxMinor: number;
46
+ readonly includedInPrice: boolean;
47
+ readonly ruleName: string | null;
48
+ }
49
+ export interface TaxStore {
50
+ createRule(input: CreateTaxRuleInput): Promise<TaxRule>;
51
+ deleteRule(id: string): Promise<void>;
52
+ listRules(): Promise<readonly TaxRule[]>;
53
+ /** The one rule that applies, or null when nothing does. */
54
+ resolve(zone: TaxZone | null, taxCategory: string): Promise<TaxRule | null>;
55
+ }
56
+ export declare function createTaxStore(db: DatabaseHandle, now?: () => number): TaxStore;
57
+ /**
58
+ * What one taxable amount owes under a rule.
59
+ *
60
+ * Two arithmetics, and getting them the wrong way round is the classic
61
+ * European VAT bug. When tax is **added**, it is `amount × rate`. When it is
62
+ * **included**, the amount already contains it, so the tax is
63
+ * `amount − amount / (1 + rate)`, computed in integers as
64
+ * `amount × rate / (10000 + rate)`. Applying the first formula to a
65
+ * tax-inclusive price overcharges by the tax on the tax.
66
+ */
67
+ export declare function taxFor(amountMinor: number, rule: TaxRule | null): TaxOutcome;
68
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/tax/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAKzF;;;;;;;GAOG;AACH,MAAM,WAAW,OAAO;IACtB,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAA;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAA;IAClC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED,0BAA0B;AAC1B,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAA;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACvD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,SAAS,IAAI,OAAO,CAAC,SAAS,OAAO,EAAE,CAAC,CAAA;IACxC,4DAA4D;IAC5D,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAA;CAC5E;AAoDD,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,GAAE,MAAM,MAAiB,GAAG,QAAQ,CAmEzF;AAED;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,GAAG,UAAU,CAe5E"}
@@ -0,0 +1,126 @@
1
+ import { CogentaError, identifier, newId, sql } from '@cogenta/core';
2
+ import { applyBasisPoints } from '../money.js';
3
+ import { fromBool, toBool, toInt, toNullableText, toText } from '../rows.js';
4
+ import { TABLES } from '../tables.js';
5
+ function decode(row) {
6
+ return {
7
+ id: toText(row.id, 'tax_rule.id'),
8
+ country: toNullableText(row.country),
9
+ region: toNullableText(row.region),
10
+ taxCategory: toText(row.tax_category, 'tax_rule.tax_category'),
11
+ name: toText(row.name, 'tax_rule.name'),
12
+ rateBp: toInt(row.rate_bp, 'tax_rule.rate_bp'),
13
+ includedInPrice: toBool(row.included_in_price),
14
+ priority: toInt(row.priority, 'tax_rule.priority'),
15
+ active: toBool(row.active),
16
+ createdAt: toText(row.created_at, 'tax_rule.created_at'),
17
+ };
18
+ }
19
+ /**
20
+ * How specific a rule is for a given zone, or null when it does not apply.
21
+ *
22
+ * Specificity, not order in the table: "France, Corsica" must beat "France",
23
+ * which must beat "anywhere". Resolving by insertion order would make the
24
+ * answer depend on the sequence an operator happened to type the rules in,
25
+ * which is exactly the kind of invisible dependency a shop discovers at the
26
+ * worst moment.
27
+ */
28
+ function specificity(rule, zone) {
29
+ if (rule.country !== null) {
30
+ if (zone === null)
31
+ return null;
32
+ if (rule.country !== zone.country)
33
+ return null;
34
+ }
35
+ if (rule.region !== null) {
36
+ const region = zone?.region ?? null;
37
+ if (region === null || rule.region !== region)
38
+ return null;
39
+ return 2;
40
+ }
41
+ return rule.country === null ? 0 : 1;
42
+ }
43
+ export function createTaxStore(db, now = Date.now) {
44
+ const d = db.dialect;
45
+ const table = identifier(TABLES.taxRules, d);
46
+ return {
47
+ createRule: async (input) => {
48
+ if (!Number.isInteger(input.rateBp) || input.rateBp < 0 || input.rateBp > 100_000) {
49
+ throw new CogentaError({
50
+ code: 'COMMERCE_TAX_RULE_INVALID',
51
+ message: `A tax rate must be a whole number of basis points between 0 and 100000, got ${String(input.rateBp)}.`,
52
+ hint: '20 % is 2000 basis points. 5.5 % is 550.',
53
+ });
54
+ }
55
+ const id = newId(now);
56
+ await db.query(sql `
57
+ insert into ${table} (id, country, region, tax_category, name, rate_bp, included_in_price, priority, active, created_at)
58
+ values (${id}, ${input.country?.toUpperCase() ?? null}, ${input.region ?? null},
59
+ ${input.taxCategory ?? 'standard'}, ${input.name}, ${input.rateBp},
60
+ ${fromBool(input.includedInPrice ?? true, d)}, ${input.priority ?? 0},
61
+ ${fromBool(input.active ?? true, d)}, ${new Date(now()).toISOString()})`);
62
+ const result = await db.query(sql `select * from ${table} where id = ${id}`);
63
+ const row = result.rows[0];
64
+ if (row === undefined) {
65
+ throw new CogentaError({
66
+ code: 'COMMERCE_TAX_RULE_INVALID',
67
+ message: 'The tax rule was not stored.',
68
+ hint: 'Check that the commerce tables exist (ensureCommerceTables).',
69
+ });
70
+ }
71
+ return decode(row);
72
+ },
73
+ deleteRule: async (id) => {
74
+ await db.query(sql `delete from ${table} where id = ${id}`);
75
+ },
76
+ listRules: async () => {
77
+ const result = await db.query(sql `select * from ${table} order by priority desc, created_at asc`);
78
+ return result.rows.map(decode);
79
+ },
80
+ resolve: async (zone, taxCategory) => {
81
+ const result = await db.query(sql `select * from ${table} where tax_category = ${taxCategory}`);
82
+ let best = null;
83
+ let bestScore = -1;
84
+ for (const rule of result.rows.map(decode)) {
85
+ if (!rule.active)
86
+ continue;
87
+ const score = specificity(rule, zone);
88
+ if (score === null)
89
+ continue;
90
+ // Priority first, then specificity, then oldest — a total order, so
91
+ // two equally good rules always resolve the same way.
92
+ const combined = rule.priority * 10 + score;
93
+ if (combined > bestScore) {
94
+ best = rule;
95
+ bestScore = combined;
96
+ }
97
+ }
98
+ return best;
99
+ },
100
+ };
101
+ }
102
+ /**
103
+ * What one taxable amount owes under a rule.
104
+ *
105
+ * Two arithmetics, and getting them the wrong way round is the classic
106
+ * European VAT bug. When tax is **added**, it is `amount × rate`. When it is
107
+ * **included**, the amount already contains it, so the tax is
108
+ * `amount − amount / (1 + rate)`, computed in integers as
109
+ * `amount × rate / (10000 + rate)`. Applying the first formula to a
110
+ * tax-inclusive price overcharges by the tax on the tax.
111
+ */
112
+ export function taxFor(amountMinor, rule) {
113
+ if (rule === null) {
114
+ return { rateBp: 0, taxMinor: 0, includedInPrice: false, ruleName: null };
115
+ }
116
+ const taxMinor = rule.includedInPrice
117
+ ? Math.floor((amountMinor * rule.rateBp + (10_000 + rule.rateBp) / 2) / (10_000 + rule.rateBp))
118
+ : applyBasisPoints(amountMinor, rule.rateBp);
119
+ return {
120
+ rateBp: rule.rateBp,
121
+ taxMinor,
122
+ includedInPrice: rule.includedInPrice,
123
+ ruleName: rule.name,
124
+ };
125
+ }
126
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/tax/store.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@cogenta/commerce",
3
+ "version": "0.2.0",
4
+ "description": "Contract E: products, carts, orders, payments, tax, shipping, coupons, invoices and subscriptions.",
5
+ "license": "MPL-2.0",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "engines": {
18
+ "node": ">=22.11.0"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "provenance": true
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/cogenta-cms/cogenta.git",
27
+ "directory": "packages/commerce"
28
+ },
29
+ "dependencies": {
30
+ "@cogenta/core": "0.4.0"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^7.0.2",
34
+ "vitest": "^4.1.10"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.json",
38
+ "typecheck": "tsc -p tsconfig.typecheck.json",
39
+ "test": "vitest run --passWithNoTests",
40
+ "test:integration": "vitest run --config vitest.integration.config.ts --passWithNoTests"
41
+ }
42
+ }