@blamejs/blamejs-shop 0.0.72 → 0.0.75

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 (44) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/announcement-bar.js +753 -0
  3. package/lib/banner-ab-tests.js +806 -0
  4. package/lib/bin-locations.js +791 -0
  5. package/lib/blog-articles.js +1173 -0
  6. package/lib/carrier-accounts.js +805 -0
  7. package/lib/cart-recovery.js +1133 -0
  8. package/lib/category-navigation.js +934 -0
  9. package/lib/consent-ledger.js +539 -0
  10. package/lib/customer-impersonation.js +743 -0
  11. package/lib/customer-merge.js +879 -0
  12. package/lib/demand-forecast.js +1121 -0
  13. package/lib/dispute-resolution.js +886 -0
  14. package/lib/email-ab-tests.js +918 -0
  15. package/lib/email-engagement-score.js +649 -0
  16. package/lib/event-log.js +713 -0
  17. package/lib/fulfillment-sla.js +791 -0
  18. package/lib/index.js +41 -0
  19. package/lib/inventory-audits.js +852 -0
  20. package/lib/line-gift-wrap.js +430 -0
  21. package/lib/marketing-budget.js +792 -0
  22. package/lib/operator-activity-feed.js +977 -0
  23. package/lib/operator-approvals.js +942 -0
  24. package/lib/operator-help-center.js +1020 -0
  25. package/lib/operator-inbox.js +889 -0
  26. package/lib/operator-sessions.js +701 -0
  27. package/lib/order-exchanges.js +602 -0
  28. package/lib/product-compare.js +804 -0
  29. package/lib/pwa-manifest.js +1005 -0
  30. package/lib/referral-leaderboard.js +612 -0
  31. package/lib/sales-tax-filings.js +807 -0
  32. package/lib/search-ranking.js +859 -0
  33. package/lib/shipping-insurance.js +757 -0
  34. package/lib/shrinkage-report.js +1182 -0
  35. package/lib/sidebar-widgets.js +952 -0
  36. package/lib/smart-restocking.js +1048 -0
  37. package/lib/stock-receipts.js +834 -0
  38. package/lib/subscription-analytics.js +1032 -0
  39. package/lib/suggestion-box.js +921 -0
  40. package/lib/tax-remittance.js +625 -0
  41. package/lib/vendor-invoices.js +1021 -0
  42. package/lib/winback-campaigns.js +1350 -0
  43. package/lib/wishlist-digest.js +1133 -0
  44. package/package.json +1 -1
@@ -0,0 +1,807 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.salesTaxFilings
4
+ * @title Sales tax filings — periodic remittance preparation
5
+ *
6
+ * @intro
7
+ * Sales tax obligations don't end at checkout — every jurisdiction
8
+ * the operator collected tax for expects a periodic filing (monthly,
9
+ * quarterly, or annually) that reconciles the operator's books to
10
+ * the authority's. This primitive owns the lifecycle of one such
11
+ * filing.
12
+ *
13
+ * The shape:
14
+ *
15
+ * 1. `defineFilingPeriod({ jurisdiction, kind, period_start,
16
+ * period_end, due_date })` opens a row in `draft` status. One
17
+ * (jurisdiction, kind, period_start) tuple yields one row — a
18
+ * UNIQUE index at the DB boundary refuses a duplicate open
19
+ * even if two operators race the call.
20
+ *
21
+ * 2. `computeFiling({ filing_id })` snapshots the orders that
22
+ * fell inside the period and computes:
23
+ *
24
+ * gross_revenue_minor — sum of order subtotals in window
25
+ * taxable_revenue_minor — gross minus exempt-customer revenue
26
+ * exempt_revenue_minor — revenue attributed to customers
27
+ * with an approved tax-exemption for
28
+ * the filing's jurisdiction
29
+ * tax_collected_minor — sum of order tax_minor in window
30
+ * tax_owed_minor — re-derived from taxable revenue +
31
+ * rates effective during the window
32
+ * by_rate_breakdown — per-rate-bps split of taxable
33
+ * revenue + collected tax (operators
34
+ * need this for the filing form)
35
+ *
36
+ * Status moves draft -> computed. The snapshot is rerunnable;
37
+ * re-running overwrites the snapshot until status leaves
38
+ * `computed`.
39
+ *
40
+ * 3. `recordSubmission({ filing_id, submission_ref, submitted_at,
41
+ * submitted_by })` moves computed -> submitted. `submission_ref`
42
+ * is the authority's confirmation number (DR-123-456); the
43
+ * primitive does not interpret it beyond a length + control-
44
+ * byte gate.
45
+ *
46
+ * 4. `recordPayment({ filing_id, payment_minor, payment_ref,
47
+ * paid_at })` moves submitted -> paid. Payment minor doesn't
48
+ * have to equal tax_owed_minor — a jurisdiction occasionally
49
+ * accepts a partial / installment payment; the audit trail
50
+ * preserves both numbers.
51
+ *
52
+ * 5. `markAmended({ filing_id, reason })` moves a row that has
53
+ * landed in `computed`, `submitted`, or `paid` back to an
54
+ * explicit `amended` status. The original snapshot stays; the
55
+ * reason captures why. Operators re-open a new filing for the
56
+ * same period via defineFilingPeriod with the same tuple — the
57
+ * UNIQUE index prevents this without first amending the
58
+ * existing row.
59
+ *
60
+ * Order composition:
61
+ *
62
+ * The primitive walks orders that landed inside the window via
63
+ * `query()` against the project's `orders` schema (`status`,
64
+ * `currency`, `subtotal_minor`, `tax_minor`, `ship_to_json`).
65
+ * The window predicate uses `created_at` so a late-arriving
66
+ * fulfillment doesn't pull an old order back into a new filing.
67
+ *
68
+ * Orders contribute when their `ship_to.country` (and, for
69
+ * subdivision-keyed jurisdictions, `ship_to.region`) matches the
70
+ * filing's jurisdiction. Cancelled / refunded orders are excluded
71
+ * so the filing reflects net sales the authority cares about.
72
+ *
73
+ * Exemption composition:
74
+ *
75
+ * When `taxExempt` is wired, each contributing order's
76
+ * customer_id is checked via `taxExempt.isExempt({ customer_id,
77
+ * jurisdiction })`. Exempt customers shift revenue from the
78
+ * taxable bucket to the exempt bucket; they don't drop from the
79
+ * filing entirely (the authority wants to see the gross + the
80
+ * exempt split). Anonymous orders (no customer_id) are never
81
+ * exempt.
82
+ *
83
+ * Rate composition:
84
+ *
85
+ * When `taxRates` is wired, the rates effective during the window
86
+ * for the filing's jurisdiction are loaded via raw query against
87
+ * `tax_rates`. The per-rate breakdown attributes each order's
88
+ * contribution to the (single) rate row that covered its
89
+ * created_at timestamp. Orders whose created_at falls inside the
90
+ * window but outside every rate row's effective range fall into
91
+ * the special `__none__` breakdown key — operators see the gap
92
+ * before they file.
93
+ *
94
+ * Composes:
95
+ * - `b.guardUuid` — strict UUID gate on filing_id.
96
+ * - `b.uuid.v7` — filing row primary key (monotonic
97
+ * lexicographic so list-by-id sorts by open
98
+ * order).
99
+ *
100
+ * Storage: `migrations-d1/0184_sales_tax_filings.sql`.
101
+ *
102
+ * @primitive salesTaxFilings
103
+ * @related order, taxRates, taxExempt, b.guardUuid, b.uuid
104
+ */
105
+
106
+ var bShop;
107
+ function _b() {
108
+ if (!bShop) bShop = require("./index");
109
+ return bShop.framework;
110
+ }
111
+
112
+ // ---- constants ----------------------------------------------------------
113
+
114
+ var KINDS = Object.freeze(["monthly", "quarterly", "annual"]);
115
+ var STATUSES = Object.freeze(["draft", "computed", "submitted", "paid", "amended"]);
116
+
117
+ var JURISDICTION_RE = /^[A-Z]{2}(-[A-Z0-9]{1,3})?$/;
118
+
119
+ var MAX_SUBMISSION_REF_LEN = 200;
120
+ var MAX_PAYMENT_REF_LEN = 200;
121
+ var MAX_SUBMITTED_BY_LEN = 200;
122
+ var MAX_AMENDED_REASON_LEN = 1000;
123
+
124
+ var DEFAULT_LIMIT = 50;
125
+ var MAX_LIMIT = 500;
126
+ var MAX_DAYS_AHEAD = 365;
127
+
128
+ var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
129
+
130
+ // FSM transition map. The keys are (from_status, action) → to_status.
131
+ // Defined as a table so a misuse surfaces with the actual current
132
+ // status in the error rather than an "unexpected" surprise from a
133
+ // silent UPDATE that no-ops.
134
+ var FSM_TRANSITIONS = Object.freeze({
135
+ "draft|compute": "computed",
136
+ "computed|compute": "computed", // recompute pre-submit allowed
137
+ "computed|submit": "submitted",
138
+ "submitted|pay": "paid",
139
+ "computed|amend": "amended",
140
+ "submitted|amend": "amended",
141
+ "paid|amend": "amended",
142
+ });
143
+
144
+ // ---- monotonic clock ----------------------------------------------------
145
+ //
146
+ // Filings move through their lifecycle one operator action at a time
147
+ // and the audit trail (created_at / computed_at / submitted_at /
148
+ // paid_at / amended_at / updated_at) wants a strictly-increasing
149
+ // timeline so a sort-by-timestamp read returns events in the order
150
+ // they were issued. On a fast machine a defineFilingPeriod followed
151
+ // immediately by computeFiling can land in the same Date.now() bucket;
152
+ // bumping by 1ms on a tie keeps the ordering deterministic without an
153
+ // extra tiebreaker column.
154
+
155
+ var _lastTs = 0;
156
+ function _now() {
157
+ var t = Date.now();
158
+ if (t <= _lastTs) { t = _lastTs + 1; }
159
+ _lastTs = t;
160
+ return t;
161
+ }
162
+
163
+ // ---- validators ---------------------------------------------------------
164
+
165
+ function _jurisdiction(s) {
166
+ if (typeof s !== "string" || !JURISDICTION_RE.test(s)) {
167
+ throw new TypeError(
168
+ "salesTaxFilings: jurisdiction must match /^[A-Z]{2}(-[A-Z0-9]{1,3})?$/ " +
169
+ "(ISO 3166-1 alpha-2 + optional ISO 3166-2 subdivision), got " +
170
+ JSON.stringify(s)
171
+ );
172
+ }
173
+ return s;
174
+ }
175
+
176
+ function _kind(s) {
177
+ if (typeof s !== "string" || KINDS.indexOf(s) < 0) {
178
+ throw new TypeError("salesTaxFilings: kind must be one of " + KINDS.join(", "));
179
+ }
180
+ return s;
181
+ }
182
+
183
+ function _status(s, label) {
184
+ if (typeof s !== "string" || STATUSES.indexOf(s) < 0) {
185
+ throw new TypeError("salesTaxFilings: " + (label || "status") +
186
+ " must be one of " + STATUSES.join(", "));
187
+ }
188
+ return s;
189
+ }
190
+
191
+ function _epoch(n, label) {
192
+ if (!Number.isInteger(n) || n < 0) {
193
+ throw new TypeError("salesTaxFilings: " + label +
194
+ " must be a non-negative integer (ms epoch)");
195
+ }
196
+ return n;
197
+ }
198
+
199
+ function _epochOpt(n, label) {
200
+ if (n == null) return null;
201
+ return _epoch(n, label);
202
+ }
203
+
204
+ function _nonNegInt(n, label) {
205
+ if (!Number.isInteger(n) || n < 0) {
206
+ throw new TypeError("salesTaxFilings: " + label +
207
+ " must be a non-negative integer (minor units)");
208
+ }
209
+ return n;
210
+ }
211
+
212
+ function _shortText(s, label, max) {
213
+ if (typeof s !== "string" || !s.length || s.length > max) {
214
+ throw new TypeError("salesTaxFilings: " + label +
215
+ " must be a non-empty string <= " + max + " chars");
216
+ }
217
+ if (CONTROL_BYTE_RE.test(s)) {
218
+ throw new TypeError("salesTaxFilings: " + label + " must not contain control bytes");
219
+ }
220
+ return s;
221
+ }
222
+
223
+ function _filingId(s) {
224
+ try { return _b().guardUuid.sanitize(s, { profile: "strict" }); }
225
+ catch (e) {
226
+ throw new TypeError("salesTaxFilings: filing_id — " +
227
+ (e && e.message || "invalid UUID"));
228
+ }
229
+ }
230
+
231
+ function _limit(n) {
232
+ if (n == null) return DEFAULT_LIMIT;
233
+ if (!Number.isInteger(n) || n <= 0 || n > MAX_LIMIT) {
234
+ throw new TypeError("salesTaxFilings: limit must be an integer in [1, " + MAX_LIMIT + "]");
235
+ }
236
+ return n;
237
+ }
238
+
239
+ function _daysAhead(n) {
240
+ if (!Number.isInteger(n) || n <= 0 || n > MAX_DAYS_AHEAD) {
241
+ throw new TypeError("salesTaxFilings: days_ahead must be an integer in [1, " +
242
+ MAX_DAYS_AHEAD + "]");
243
+ }
244
+ return n;
245
+ }
246
+
247
+ // ---- jurisdiction coverage ---------------------------------------------
248
+ //
249
+ // An order with ship_to.country = "US" and ship_to.region = "CA"
250
+ // satisfies a "US-CA" filing AND a "US" filing. Match rule: split
251
+ // the filing jurisdiction on the dash; the country must match
252
+ // ship_to.country; if a subdivision is present, ship_to.region (or
253
+ // ship_to.subdivision / ship_to.state — accept any of the common
254
+ // keys) must match it.
255
+
256
+ function _shipToMatches(shipTo, jurisdiction) {
257
+ if (!shipTo || typeof shipTo !== "object") return false;
258
+ var country = typeof shipTo.country === "string" ? shipTo.country : null;
259
+ if (!country) return false;
260
+ var dash = jurisdiction.indexOf("-");
261
+ if (dash < 0) {
262
+ return country === jurisdiction;
263
+ }
264
+ var jurCountry = jurisdiction.slice(0, dash);
265
+ var jurSub = jurisdiction.slice(dash + 1);
266
+ if (country !== jurCountry) return false;
267
+ var sub = shipTo.region != null ? shipTo.region :
268
+ shipTo.subdivision != null ? shipTo.subdivision :
269
+ shipTo.state != null ? shipTo.state :
270
+ null;
271
+ return sub === jurSub;
272
+ }
273
+
274
+ // ---- factory ------------------------------------------------------------
275
+
276
+ function create(opts) {
277
+ opts = opts || {};
278
+ var query = opts.query;
279
+ if (!query) {
280
+ query = function (sql, params) { return _b().externalDb.query(sql, params); };
281
+ }
282
+ // Optional composition handles. When omitted, the primitive degrades
283
+ // gracefully — exemption checks become no-ops, the per-rate
284
+ // breakdown collapses to a single `__unknown__` key.
285
+ var taxExemptApi = opts.taxExempt || null;
286
+ var taxRatesApi = opts.taxRates || null;
287
+ // `order` is reserved for future use (e.g. listing filing-eligible
288
+ // orders directly via the primitive's API instead of raw SQL). The
289
+ // current implementation queries the `orders` table directly because
290
+ // the read shape (filter by created_at + status + ship_to_json) is
291
+ // not exposed on order.js.
292
+ // (no-op assignment to keep the option in the factory signature
293
+ // documented without lint complaining about an unused var)
294
+ if (opts.order) { /* reserved */ }
295
+
296
+ // ---- internal helpers -------------------------------------------------
297
+
298
+ function _decodeFiling(row) {
299
+ if (!row) return null;
300
+ var breakdown;
301
+ try { breakdown = JSON.parse(row.breakdown_json); }
302
+ catch (_e) { breakdown = {}; }
303
+ return {
304
+ id: row.id,
305
+ jurisdiction: row.jurisdiction,
306
+ kind: row.kind,
307
+ period_start: Number(row.period_start),
308
+ period_end: Number(row.period_end),
309
+ due_date: Number(row.due_date),
310
+ status: row.status,
311
+ gross_revenue_minor: Number(row.gross_revenue_minor),
312
+ taxable_revenue_minor: Number(row.taxable_revenue_minor),
313
+ exempt_revenue_minor: Number(row.exempt_revenue_minor),
314
+ tax_collected_minor: Number(row.tax_collected_minor),
315
+ tax_owed_minor: Number(row.tax_owed_minor),
316
+ by_rate_breakdown: breakdown,
317
+ submission_ref: row.submission_ref,
318
+ submitted_at: row.submitted_at != null ? Number(row.submitted_at) : null,
319
+ submitted_by: row.submitted_by,
320
+ payment_minor: row.payment_minor != null ? Number(row.payment_minor) : null,
321
+ payment_ref: row.payment_ref,
322
+ paid_at: row.paid_at != null ? Number(row.paid_at) : null,
323
+ amended_at: row.amended_at != null ? Number(row.amended_at) : null,
324
+ amended_reason: row.amended_reason,
325
+ computed_at: row.computed_at != null ? Number(row.computed_at) : null,
326
+ created_at: Number(row.created_at),
327
+ updated_at: Number(row.updated_at),
328
+ };
329
+ }
330
+
331
+ async function _getRaw(id) {
332
+ var r = await query("SELECT * FROM sales_tax_filings WHERE id = ?1", [id]);
333
+ return r.rows[0] || null;
334
+ }
335
+
336
+ function _expect(row, action) {
337
+ if (!row) {
338
+ var miss = new Error("salesTaxFilings: filing not found");
339
+ miss.code = "SALES_TAX_FILING_NOT_FOUND";
340
+ throw miss;
341
+ }
342
+ var key = row.status + "|" + action;
343
+ var next = FSM_TRANSITIONS[key];
344
+ if (!next) {
345
+ var bad = new Error("salesTaxFilings: action '" + action +
346
+ "' not permitted from status '" + row.status + "'");
347
+ bad.code = "SALES_TAX_FILING_BAD_TRANSITION";
348
+ throw bad;
349
+ }
350
+ return next;
351
+ }
352
+
353
+ // Load orders that fall inside the filing window AND ship to the
354
+ // filing's jurisdiction. The DB-level filter is intentionally
355
+ // narrow on (created_at, status) — the ship_to_json match runs in
356
+ // JS because the schema stores ship_to as opaque JSON. For typical
357
+ // filing-period volumes (thousands of orders) this is comfortably
358
+ // fast; high-volume operators can layer a column projection later
359
+ // without changing the primitive's contract.
360
+ async function _ordersInWindow(jurisdiction, periodStart, periodEnd) {
361
+ var sql =
362
+ "SELECT id, customer_id, currency, subtotal_minor, tax_minor, " +
363
+ " ship_to_json, status, created_at " +
364
+ "FROM orders " +
365
+ "WHERE created_at >= ?1 AND created_at < ?2 " +
366
+ " AND status IN ('paid', 'fulfilling', 'shipped', 'delivered')";
367
+ var r = await query(sql, [periodStart, periodEnd]);
368
+ var out = [];
369
+ for (var i = 0; i < r.rows.length; i += 1) {
370
+ var row = r.rows[i];
371
+ var shipTo;
372
+ try { shipTo = JSON.parse(row.ship_to_json); }
373
+ catch (_e) { shipTo = null; }
374
+ if (!_shipToMatches(shipTo, jurisdiction)) continue;
375
+ out.push({
376
+ id: row.id,
377
+ customer_id: row.customer_id,
378
+ currency: row.currency,
379
+ subtotal_minor: Number(row.subtotal_minor),
380
+ tax_minor: Number(row.tax_minor),
381
+ status: row.status,
382
+ created_at: Number(row.created_at),
383
+ ship_to: shipTo,
384
+ });
385
+ }
386
+ return out;
387
+ }
388
+
389
+ async function _ratesInWindow(jurisdiction, periodStart, periodEnd) {
390
+ // Pull every tax_rates row for the jurisdiction that overlaps the
391
+ // window. The lib filters down to the rate covering each order's
392
+ // created_at; the SQL is widely permissive so a future query
393
+ // optimization stays in one place.
394
+ if (!taxRatesApi) return [];
395
+ try {
396
+ var r = await query(
397
+ "SELECT id, jurisdiction, category, rate_bps, effective_from, effective_until " +
398
+ "FROM tax_rates " +
399
+ "WHERE jurisdiction = ?1 AND archived_at IS NULL " +
400
+ " AND effective_from < ?2 " +
401
+ " AND (effective_until IS NULL OR effective_until > ?3)",
402
+ [jurisdiction, periodEnd, periodStart],
403
+ );
404
+ var out = [];
405
+ for (var i = 0; i < r.rows.length; i += 1) {
406
+ var row = r.rows[i];
407
+ out.push({
408
+ id: row.id,
409
+ rate_bps: Number(row.rate_bps),
410
+ effective_from: Number(row.effective_from),
411
+ effective_until: row.effective_until == null ? null : Number(row.effective_until),
412
+ });
413
+ }
414
+ return out;
415
+ } catch (_e) {
416
+ // Schema may not have tax_rates table in the harness — degrade
417
+ // to no per-rate breakdown rather than failing the filing.
418
+ return [];
419
+ }
420
+ }
421
+
422
+ function _rateForOrder(rates, orderCreatedAt) {
423
+ for (var i = 0; i < rates.length; i += 1) {
424
+ var r = rates[i];
425
+ if (r.effective_from <= orderCreatedAt &&
426
+ (r.effective_until == null || r.effective_until > orderCreatedAt)) {
427
+ return r;
428
+ }
429
+ }
430
+ return null;
431
+ }
432
+
433
+ // ---- defineFilingPeriod -----------------------------------------------
434
+
435
+ async function defineFilingPeriod(input) {
436
+ if (!input || typeof input !== "object") {
437
+ throw new TypeError("salesTaxFilings.defineFilingPeriod: input object required");
438
+ }
439
+ var jurisdiction = _jurisdiction(input.jurisdiction);
440
+ var kind = _kind(input.kind);
441
+ var periodStart = _epoch(input.period_start, "period_start");
442
+ var periodEnd = _epoch(input.period_end, "period_end");
443
+ if (periodEnd <= periodStart) {
444
+ throw new TypeError("salesTaxFilings.defineFilingPeriod: period_end must be > period_start");
445
+ }
446
+ var dueDate = _epoch(input.due_date, "due_date");
447
+ if (dueDate < periodEnd) {
448
+ throw new TypeError("salesTaxFilings.defineFilingPeriod: due_date must be >= period_end");
449
+ }
450
+
451
+ var id = _b().uuid.v7();
452
+ var ts = _now();
453
+ try {
454
+ await query(
455
+ "INSERT INTO sales_tax_filings " +
456
+ "(id, jurisdiction, kind, period_start, period_end, due_date, status, " +
457
+ " gross_revenue_minor, taxable_revenue_minor, exempt_revenue_minor, " +
458
+ " tax_collected_minor, tax_owed_minor, breakdown_json, " +
459
+ " created_at, updated_at) " +
460
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, 'draft', 0, 0, 0, 0, 0, '{}', ?7, ?7)",
461
+ [id, jurisdiction, kind, periodStart, periodEnd, dueDate, ts],
462
+ );
463
+ } catch (e) {
464
+ // Surface a recognizable error code on the UNIQUE collision so
465
+ // the operator can detect "this period is already open" without
466
+ // pattern-matching the underlying driver's error message.
467
+ var msg = String(e && e.message || e);
468
+ if (/UNIQUE|unique/i.test(msg)) {
469
+ var dup = new Error("salesTaxFilings.defineFilingPeriod: filing already exists for " +
470
+ jurisdiction + " " + kind + " period_start=" + periodStart);
471
+ dup.code = "SALES_TAX_FILING_DUPLICATE";
472
+ dup.cause = e;
473
+ throw dup;
474
+ }
475
+ throw e;
476
+ }
477
+ return _decodeFiling(await _getRaw(id));
478
+ }
479
+
480
+ // ---- computeFiling ----------------------------------------------------
481
+
482
+ async function computeFiling(input) {
483
+ if (!input || typeof input !== "object") {
484
+ throw new TypeError("salesTaxFilings.computeFiling: input object required");
485
+ }
486
+ var id = _filingId(input.filing_id);
487
+ var raw = await _getRaw(id);
488
+ var next = _expect(raw, "compute");
489
+
490
+ var orders = await _ordersInWindow(raw.jurisdiction, Number(raw.period_start), Number(raw.period_end));
491
+ var rates = await _ratesInWindow(raw.jurisdiction, Number(raw.period_start), Number(raw.period_end));
492
+
493
+ // Resolve exemption per unique customer in a single pass so
494
+ // multiple orders from the same exempt customer don't refire the
495
+ // taxExempt.isExempt call.
496
+ var exemptByCustomer = Object.create(null);
497
+ if (taxExemptApi && typeof taxExemptApi.isExempt === "function") {
498
+ var seen = Object.create(null);
499
+ for (var oi = 0; oi < orders.length; oi += 1) {
500
+ var custId = orders[oi].customer_id;
501
+ if (!custId) continue;
502
+ if (seen[custId]) continue;
503
+ seen[custId] = true;
504
+ try {
505
+ exemptByCustomer[custId] = await taxExemptApi.isExempt({
506
+ customer_id: custId,
507
+ jurisdiction: raw.jurisdiction,
508
+ });
509
+ } catch (_e) {
510
+ // Exemption-check failure leaves the customer in the
511
+ // taxable bucket — surfaces in the filing rather than
512
+ // silently flipping an exempt customer back to taxable
513
+ // post-fact via the caller's retry.
514
+ exemptByCustomer[custId] = false;
515
+ }
516
+ }
517
+ }
518
+
519
+ var gross = 0;
520
+ var taxable = 0;
521
+ var exempt = 0;
522
+ var collected = 0;
523
+ var breakdown = Object.create(null); // rate_bps (or "__none__") -> { taxable_minor, tax_minor, order_count }
524
+
525
+ function _bucket(key) {
526
+ if (!breakdown[key]) {
527
+ breakdown[key] = { taxable_minor: 0, tax_minor: 0, order_count: 0 };
528
+ }
529
+ return breakdown[key];
530
+ }
531
+
532
+ for (var i = 0; i < orders.length; i += 1) {
533
+ var o = orders[i];
534
+ gross += o.subtotal_minor;
535
+ collected += o.tax_minor;
536
+ var isExempt = o.customer_id && exemptByCustomer[o.customer_id] === true;
537
+ if (isExempt) {
538
+ exempt += o.subtotal_minor;
539
+ } else {
540
+ taxable += o.subtotal_minor;
541
+ }
542
+
543
+ // Per-rate breakdown — only taxable contributions land in the
544
+ // rate-keyed bucket. Exempt revenue gets its own
545
+ // pseudo-bucket so the operator sees the magnitude without
546
+ // double-counting against a rate row.
547
+ var bucketKey;
548
+ if (isExempt) {
549
+ bucketKey = "__exempt__";
550
+ } else {
551
+ var rate = _rateForOrder(rates, o.created_at);
552
+ bucketKey = rate ? String(rate.rate_bps) : "__none__";
553
+ }
554
+ var b = _bucket(bucketKey);
555
+ if (isExempt) {
556
+ b.taxable_minor += 0;
557
+ } else {
558
+ b.taxable_minor += o.subtotal_minor;
559
+ b.tax_minor += o.tax_minor;
560
+ }
561
+ b.order_count += 1;
562
+ }
563
+
564
+ // Owed: re-derive from per-rate taxable totals using rate_bps.
565
+ // Integer math: taxable * bps / 10000, banker's-rounded so
566
+ // half-cent ties don't drift the sum. (Same rounding rule as
567
+ // b.tax — banker's rounding is the conservative choice when the
568
+ // primitive may aggregate millions of orders.)
569
+ var owed = 0;
570
+ var rateKeys = Object.keys(breakdown);
571
+ for (var rk = 0; rk < rateKeys.length; rk += 1) {
572
+ var key = rateKeys[rk];
573
+ if (key === "__none__" || key === "__exempt__") continue;
574
+ var bps = parseInt(key, 10);
575
+ if (!Number.isFinite(bps)) continue;
576
+ var bucket = breakdown[key];
577
+ var rawOwed = bucket.taxable_minor * bps / 10000;
578
+ // Banker's round
579
+ var floored = Math.floor(rawOwed);
580
+ var frac = rawOwed - floored;
581
+ var rounded;
582
+ if (frac > 0.5) rounded = floored + 1;
583
+ else if (frac < 0.5) rounded = floored;
584
+ else rounded = (floored % 2 === 0) ? floored : floored + 1;
585
+ owed += rounded;
586
+ }
587
+ // When no rate rows resolved (taxRatesApi absent / no rates
588
+ // defined) fall back to collected as the owed figure. Operators
589
+ // see the bare collected number rather than a misleading zero.
590
+ if (rates.length === 0 || rateKeys.every(function (k) { return k === "__none__" || k === "__exempt__"; })) {
591
+ owed = collected;
592
+ }
593
+
594
+ var ts = _now();
595
+ await query(
596
+ "UPDATE sales_tax_filings SET " +
597
+ "status = ?1, gross_revenue_minor = ?2, taxable_revenue_minor = ?3, " +
598
+ "exempt_revenue_minor = ?4, tax_collected_minor = ?5, tax_owed_minor = ?6, " +
599
+ "breakdown_json = ?7, computed_at = ?8, updated_at = ?8 " +
600
+ "WHERE id = ?9",
601
+ [next, gross, taxable, exempt, collected, owed, JSON.stringify(breakdown), ts, id],
602
+ );
603
+ return _decodeFiling(await _getRaw(id));
604
+ }
605
+
606
+ // ---- recordSubmission -------------------------------------------------
607
+
608
+ async function recordSubmission(input) {
609
+ if (!input || typeof input !== "object") {
610
+ throw new TypeError("salesTaxFilings.recordSubmission: input object required");
611
+ }
612
+ var id = _filingId(input.filing_id);
613
+ var submissionRef = _shortText(input.submission_ref, "submission_ref", MAX_SUBMISSION_REF_LEN);
614
+ var submittedAt = input.submitted_at == null ? _now()
615
+ : _epoch(input.submitted_at, "submitted_at");
616
+ var submittedBy = _shortText(input.submitted_by, "submitted_by", MAX_SUBMITTED_BY_LEN);
617
+
618
+ var raw = await _getRaw(id);
619
+ var next = _expect(raw, "submit");
620
+ var ts = _now();
621
+ await query(
622
+ "UPDATE sales_tax_filings SET " +
623
+ "status = ?1, submission_ref = ?2, submitted_at = ?3, submitted_by = ?4, " +
624
+ "updated_at = ?5 WHERE id = ?6",
625
+ [next, submissionRef, submittedAt, submittedBy, ts, id],
626
+ );
627
+ return _decodeFiling(await _getRaw(id));
628
+ }
629
+
630
+ // ---- recordPayment ----------------------------------------------------
631
+
632
+ async function recordPayment(input) {
633
+ if (!input || typeof input !== "object") {
634
+ throw new TypeError("salesTaxFilings.recordPayment: input object required");
635
+ }
636
+ var id = _filingId(input.filing_id);
637
+ var amount = _nonNegInt(input.payment_minor, "payment_minor");
638
+ var paymentRef = _shortText(input.payment_ref, "payment_ref", MAX_PAYMENT_REF_LEN);
639
+ var paidAt = input.paid_at == null ? _now() : _epoch(input.paid_at, "paid_at");
640
+
641
+ var raw = await _getRaw(id);
642
+ var next = _expect(raw, "pay");
643
+ var ts = _now();
644
+ await query(
645
+ "UPDATE sales_tax_filings SET " +
646
+ "status = ?1, payment_minor = ?2, payment_ref = ?3, paid_at = ?4, " +
647
+ "updated_at = ?5 WHERE id = ?6",
648
+ [next, amount, paymentRef, paidAt, ts, id],
649
+ );
650
+ return _decodeFiling(await _getRaw(id));
651
+ }
652
+
653
+ // ---- markAmended ------------------------------------------------------
654
+
655
+ async function markAmended(input) {
656
+ if (!input || typeof input !== "object") {
657
+ throw new TypeError("salesTaxFilings.markAmended: input object required");
658
+ }
659
+ var id = _filingId(input.filing_id);
660
+ var reason = _shortText(input.reason, "reason", MAX_AMENDED_REASON_LEN);
661
+
662
+ var raw = await _getRaw(id);
663
+ var next = _expect(raw, "amend");
664
+ var ts = _now();
665
+ await query(
666
+ "UPDATE sales_tax_filings SET " +
667
+ "status = ?1, amended_reason = ?2, amended_at = ?3, updated_at = ?3 " +
668
+ "WHERE id = ?4",
669
+ [next, reason, ts, id],
670
+ );
671
+ return _decodeFiling(await _getRaw(id));
672
+ }
673
+
674
+ // ---- getFiling --------------------------------------------------------
675
+
676
+ async function getFiling(id) {
677
+ var cid = _filingId(id);
678
+ return _decodeFiling(await _getRaw(cid));
679
+ }
680
+
681
+ // ---- listFilings ------------------------------------------------------
682
+
683
+ async function listFilings(listOpts) {
684
+ listOpts = listOpts || {};
685
+ var jurisdiction = listOpts.jurisdiction == null ? null : _jurisdiction(listOpts.jurisdiction);
686
+ var status = listOpts.status == null ? null : _status(listOpts.status, "status");
687
+ var limit = _limit(listOpts.limit);
688
+
689
+ var where = [];
690
+ var params = [];
691
+ var idx = 1;
692
+ if (jurisdiction) { where.push("jurisdiction = ?" + idx); params.push(jurisdiction); idx += 1; }
693
+ if (status) { where.push("status = ?" + idx); params.push(status); idx += 1; }
694
+ var sql = "SELECT * FROM sales_tax_filings" +
695
+ (where.length ? " WHERE " + where.join(" AND ") : "") +
696
+ " ORDER BY due_date ASC, id ASC LIMIT ?" + idx;
697
+ params.push(limit);
698
+
699
+ var r = await query(sql, params);
700
+ var out = [];
701
+ for (var i = 0; i < r.rows.length; i += 1) {
702
+ out.push(_decodeFiling(r.rows[i]));
703
+ }
704
+ return out;
705
+ }
706
+
707
+ // ---- auditReportForJurisdiction ---------------------------------------
708
+
709
+ async function auditReportForJurisdiction(input) {
710
+ if (!input || typeof input !== "object") {
711
+ throw new TypeError("salesTaxFilings.auditReportForJurisdiction: input object required");
712
+ }
713
+ var jurisdiction = _jurisdiction(input.jurisdiction);
714
+ var from = _epoch(input.from, "from");
715
+ var to = _epoch(input.to, "to");
716
+ if (to <= from) {
717
+ throw new TypeError("salesTaxFilings.auditReportForJurisdiction: to must be > from");
718
+ }
719
+
720
+ // Pull every filing whose period intersects [from, to]. Intersect
721
+ // rule: filing.period_start < to AND filing.period_end > from.
722
+ var r = await query(
723
+ "SELECT * FROM sales_tax_filings " +
724
+ "WHERE jurisdiction = ?1 AND period_start < ?2 AND period_end > ?3 " +
725
+ "ORDER BY period_start ASC",
726
+ [jurisdiction, to, from],
727
+ );
728
+
729
+ var filings = [];
730
+ var totalGross = 0;
731
+ var totalTaxable = 0;
732
+ var totalExempt = 0;
733
+ var totalCollected = 0;
734
+ var totalOwed = 0;
735
+ var totalPaid = 0;
736
+ for (var i = 0; i < r.rows.length; i += 1) {
737
+ var f = _decodeFiling(r.rows[i]);
738
+ filings.push(f);
739
+ totalGross += f.gross_revenue_minor;
740
+ totalTaxable += f.taxable_revenue_minor;
741
+ totalExempt += f.exempt_revenue_minor;
742
+ totalCollected += f.tax_collected_minor;
743
+ totalOwed += f.tax_owed_minor;
744
+ if (f.payment_minor != null) totalPaid += f.payment_minor;
745
+ }
746
+ return {
747
+ jurisdiction: jurisdiction,
748
+ from: from,
749
+ to: to,
750
+ filing_count: filings.length,
751
+ filings: filings,
752
+ total_gross_revenue_minor: totalGross,
753
+ total_taxable_revenue_minor: totalTaxable,
754
+ total_exempt_revenue_minor: totalExempt,
755
+ total_tax_collected_minor: totalCollected,
756
+ total_tax_owed_minor: totalOwed,
757
+ total_tax_paid_minor: totalPaid,
758
+ };
759
+ }
760
+
761
+ // ---- upcomingDue ------------------------------------------------------
762
+
763
+ async function upcomingDue(input) {
764
+ if (!input || typeof input !== "object") {
765
+ throw new TypeError("salesTaxFilings.upcomingDue: input object required");
766
+ }
767
+ var days = _daysAhead(input.days_ahead);
768
+ var nowTs = input.now == null ? _now() : _epoch(input.now, "now");
769
+ var horizon = nowTs + (days * 24 * 60 * 60 * 1000);
770
+
771
+ var r = await query(
772
+ "SELECT * FROM sales_tax_filings " +
773
+ "WHERE status IN ('draft', 'computed', 'submitted') " +
774
+ " AND due_date <= ?1 " +
775
+ "ORDER BY due_date ASC, id ASC",
776
+ [horizon],
777
+ );
778
+ var out = [];
779
+ for (var i = 0; i < r.rows.length; i += 1) {
780
+ out.push(_decodeFiling(r.rows[i]));
781
+ }
782
+ return out;
783
+ }
784
+
785
+ return {
786
+ KINDS: KINDS.slice(),
787
+ STATUSES: STATUSES.slice(),
788
+ JURISDICTION_RE: JURISDICTION_RE,
789
+
790
+ defineFilingPeriod: defineFilingPeriod,
791
+ computeFiling: computeFiling,
792
+ recordSubmission: recordSubmission,
793
+ recordPayment: recordPayment,
794
+ markAmended: markAmended,
795
+ getFiling: getFiling,
796
+ listFilings: listFilings,
797
+ auditReportForJurisdiction: auditReportForJurisdiction,
798
+ upcomingDue: upcomingDue,
799
+ };
800
+ }
801
+
802
+ module.exports = {
803
+ create: create,
804
+ KINDS: KINDS,
805
+ STATUSES: STATUSES,
806
+ JURISDICTION_RE: JURISDICTION_RE,
807
+ };