@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,625 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.taxRemittance
4
+ * @title Tax remittance — per-jurisdiction payment tracking +
5
+ * filing reconciliation
6
+ *
7
+ * @intro
8
+ * `salesTaxFilings` owns the lifecycle of a filing (open period,
9
+ * compute snapshot, record submission, record nominal payment).
10
+ * This primitive owns the lifecycle of the actual money — the
11
+ * bank-side artifact that proves the operator paid the authority
12
+ * the amount the filing said they owed. The two surfaces are
13
+ * deliberately separate:
14
+ *
15
+ * * A filing carries the operator's view of what's owed (taxable
16
+ * revenue + per-rate breakdown + due date). It's a tax-form-
17
+ * shaped object.
18
+ * * A remittance carries the bank's view of what cleared (which
19
+ * payment method, which reference number, when it settled). It's
20
+ * a ledger-shaped object.
21
+ *
22
+ * Operators reconcile the two via `reconcileWithFiling({
23
+ * remittance_id })` — the read sums every non-voided remittance for
24
+ * the filing and reports the variance against the filing's
25
+ * `tax_owed_minor`. Variance > 0 means the operator's still on the
26
+ * hook; variance < 0 means the authority owes a refund or applied
27
+ * the overage to the next period.
28
+ *
29
+ * API:
30
+ *
31
+ * - `recordRemittance({ filing_id, jurisdiction, amount_minor,
32
+ * currency, payment_method, payment_ref, paid_at })`
33
+ * Inserts a row in `paid` status. payment_method is one of
34
+ * bank_transfer / credit_card / ach / wire / check. Returns
35
+ * the decoded row.
36
+ * - `getRemittance(id)` — by-id lookup; returns null on miss.
37
+ * - `remittancesForJurisdiction({ jurisdiction, from, to })` —
38
+ * every row whose paid_at falls in [from, to), ordered by
39
+ * paid_at DESC. Voided rows included so the operator sees
40
+ * them.
41
+ * - `unpaidObligations({ as_of, jurisdiction? })` — every filing
42
+ * whose status is submitted (or paid via the salesTaxFilings
43
+ * primitive, but where the actual remittance sum is short of
44
+ * tax_owed_minor) and whose due_date <= as_of. Requires the
45
+ * `salesTaxFilings` API to be wired so the read can compare
46
+ * owed vs. actually-remitted.
47
+ * - `lateRemittances({ as_of, days_late_min })` — rows where
48
+ * paid_at - filing.due_date >= days_late_min * 86_400_000.
49
+ * Requires salesTaxFilings to resolve due dates.
50
+ * - `reconcileWithFiling({ remittance_id })` — looks up the
51
+ * remittance's filing, sums every non-voided remittance for
52
+ * it, returns `{ filing_id, owed_minor, paid_minor,
53
+ * variance_minor }`. variance > 0 = underpaid; < 0 = overpaid;
54
+ * == 0 = settled exactly.
55
+ * - `metricsForJurisdiction({ jurisdiction, from, to })` —
56
+ * rollup over the window: total_paid_minor, total_penalty_minor,
57
+ * remittance_count, voided_count, on_time_count, late_count,
58
+ * on_time_rate (on_time / (on_time + late) — 1.0 when no late
59
+ * remittances). On-time = paid_at <= filing.due_date.
60
+ * - `markVoided({ remittance_id, reason })` — flips status to
61
+ * voided, stamps voided_at + void_reason. Idempotent on
62
+ * already-voided.
63
+ * - `recordPenalty({ remittance_id, penalty_minor, reason })` —
64
+ * attaches a penalty after the authority assesses one. Amends
65
+ * the same row (rather than opening a sibling) so the per-row
66
+ * metrics read sees one canonical artifact per payment.
67
+ *
68
+ * Composes:
69
+ * - `b.guardUuid` — strict UUID gate on every id input.
70
+ * - `b.uuid.v7` — primary key for tax_remittances. Monotonic
71
+ * lexicographic so list-by-id sorts in issue
72
+ * order without a secondary timestamp column.
73
+ * - `salesTaxFilings` (optional handle) — used by
74
+ * `unpaidObligations`, `lateRemittances`, `reconcileWithFiling`,
75
+ * and `metricsForJurisdiction` to resolve due dates + owed
76
+ * totals. The primitive degrades gracefully when the handle is
77
+ * absent: reads that need it surface a clear error code; reads
78
+ * that don't (recordRemittance, getRemittance,
79
+ * remittancesForJurisdiction, markVoided, recordPenalty) keep
80
+ * working.
81
+ *
82
+ * Storage: `migrations-d1/0204_tax_remittance.sql`.
83
+ *
84
+ * @primitive taxRemittance
85
+ * @related salesTaxFilings, b.guardUuid, b.uuid
86
+ */
87
+
88
+ var bShop;
89
+ function _b() {
90
+ if (!bShop) bShop = require("./index");
91
+ return bShop.framework;
92
+ }
93
+
94
+ // ---- constants ----------------------------------------------------------
95
+
96
+ var PAYMENT_METHODS = Object.freeze([
97
+ "bank_transfer", "credit_card", "ach", "wire", "check",
98
+ ]);
99
+ var STATUSES = Object.freeze(["paid", "voided"]);
100
+
101
+ var JURISDICTION_RE = /^[A-Z]{2}(-[A-Z0-9]{1,3})?$/;
102
+ var CURRENCY_RE = /^[A-Z]{3}$/;
103
+
104
+ var MAX_PAYMENT_REF_LEN = 200;
105
+ var MAX_VOID_REASON_LEN = 1000;
106
+ var MAX_PENALTY_REASON_LEN = 1000;
107
+
108
+ var DEFAULT_DAYS_LATE_MIN = 1;
109
+ var MAX_DAYS_LATE_MIN = 3650; // ~10 years
110
+ var MS_PER_DAY = 24 * 60 * 60 * 1000;
111
+
112
+ var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
113
+
114
+ // ---- monotonic clock ----------------------------------------------------
115
+ //
116
+ // Remittance rows carry `paid_at` (provided by the caller from the
117
+ // bank's settlement timestamp) AND `created_at` (the audit stamp for
118
+ // when the operator recorded the row). On a fast machine a sequence
119
+ // of `recordRemittance` calls — e.g. an operator batch-recording the
120
+ // month's remittances — can land in the same Date.now() bucket. A
121
+ // strict-monotonic clock guarantees the audit trail's
122
+ // `created_at` values are strictly increasing so list-by-creation
123
+ // reads return rows in the order they were issued, without an extra
124
+ // tiebreaker column.
125
+
126
+ var _lastTs = 0;
127
+ function _now() {
128
+ var t = Date.now();
129
+ if (t <= _lastTs) { t = _lastTs + 1; }
130
+ _lastTs = t;
131
+ return t;
132
+ }
133
+
134
+ // ---- validators ---------------------------------------------------------
135
+
136
+ function _jurisdiction(s) {
137
+ if (typeof s !== "string" || !JURISDICTION_RE.test(s)) {
138
+ throw new TypeError(
139
+ "taxRemittance: jurisdiction must match /^[A-Z]{2}(-[A-Z0-9]{1,3})?$/ " +
140
+ "(ISO 3166-1 alpha-2 + optional ISO 3166-2 subdivision), got " +
141
+ JSON.stringify(s)
142
+ );
143
+ }
144
+ return s;
145
+ }
146
+
147
+ function _currency(s) {
148
+ if (typeof s !== "string" || !CURRENCY_RE.test(s)) {
149
+ throw new TypeError("taxRemittance: currency must be a 3-letter ISO 4217 code (e.g. USD)");
150
+ }
151
+ return s;
152
+ }
153
+
154
+ function _paymentMethod(s) {
155
+ if (typeof s !== "string" || PAYMENT_METHODS.indexOf(s) < 0) {
156
+ throw new TypeError("taxRemittance: payment_method must be one of " + PAYMENT_METHODS.join(", "));
157
+ }
158
+ return s;
159
+ }
160
+
161
+ function _epoch(n, label) {
162
+ if (!Number.isInteger(n) || n < 0) {
163
+ throw new TypeError("taxRemittance: " + label + " must be a non-negative integer (ms epoch)");
164
+ }
165
+ return n;
166
+ }
167
+
168
+ function _positiveInt(n, label) {
169
+ if (!Number.isInteger(n) || n <= 0) {
170
+ throw new TypeError("taxRemittance: " + label + " must be a positive integer (minor units)");
171
+ }
172
+ return n;
173
+ }
174
+
175
+ function _nonNegInt(n, label) {
176
+ if (!Number.isInteger(n) || n < 0) {
177
+ throw new TypeError("taxRemittance: " + label + " must be a non-negative integer (minor units)");
178
+ }
179
+ return n;
180
+ }
181
+
182
+ function _shortText(s, label, max) {
183
+ if (typeof s !== "string" || !s.length || s.length > max) {
184
+ throw new TypeError("taxRemittance: " + label +
185
+ " must be a non-empty string <= " + max + " chars");
186
+ }
187
+ if (CONTROL_BYTE_RE.test(s)) {
188
+ throw new TypeError("taxRemittance: " + label + " must not contain control bytes");
189
+ }
190
+ return s;
191
+ }
192
+
193
+ function _uuid(s, label) {
194
+ try { return _b().guardUuid.sanitize(s, { profile: "strict" }); }
195
+ catch (e) {
196
+ throw new TypeError("taxRemittance: " + label + " — " + (e && e.message || "invalid UUID"));
197
+ }
198
+ }
199
+
200
+ function _daysLateMin(n) {
201
+ if (n == null) return DEFAULT_DAYS_LATE_MIN;
202
+ if (!Number.isInteger(n) || n < 0 || n > MAX_DAYS_LATE_MIN) {
203
+ throw new TypeError("taxRemittance: days_late_min must be an integer in [0, " +
204
+ MAX_DAYS_LATE_MIN + "]");
205
+ }
206
+ return n;
207
+ }
208
+
209
+ // ---- factory ------------------------------------------------------------
210
+
211
+ function create(opts) {
212
+ opts = opts || {};
213
+ var query = opts.query;
214
+ if (!query) {
215
+ query = function (sql, params) { return _b().externalDb.query(sql, params); };
216
+ }
217
+
218
+ // Optional composition handle. `salesTaxFilings` is required for any
219
+ // read that needs to resolve filing.tax_owed_minor / filing.due_date
220
+ // (reconcileWithFiling, unpaidObligations, lateRemittances,
221
+ // metricsForJurisdiction). When absent, those reads surface a clear
222
+ // error rather than returning misleading partial data.
223
+ var filingsApi = null;
224
+ if (opts.salesTaxFilings != null) {
225
+ if (!opts.salesTaxFilings || typeof opts.salesTaxFilings.getFiling !== "function") {
226
+ throw new TypeError("taxRemittance: salesTaxFilings handle must expose getFiling()");
227
+ }
228
+ filingsApi = opts.salesTaxFilings;
229
+ }
230
+
231
+ function _requireFilings(label) {
232
+ if (!filingsApi) {
233
+ var e = new Error("taxRemittance." + label + ": salesTaxFilings must be wired");
234
+ e.code = "TAX_REMITTANCE_FILINGS_NOT_WIRED";
235
+ throw e;
236
+ }
237
+ return filingsApi;
238
+ }
239
+
240
+ function _decodeRemittance(row) {
241
+ if (!row) return null;
242
+ return {
243
+ id: row.id,
244
+ filing_id: row.filing_id,
245
+ jurisdiction: row.jurisdiction,
246
+ amount_minor: Number(row.amount_minor),
247
+ currency: row.currency,
248
+ payment_method: row.payment_method,
249
+ payment_ref: row.payment_ref,
250
+ paid_at: Number(row.paid_at),
251
+ status: row.status,
252
+ voided_at: row.voided_at != null ? Number(row.voided_at) : null,
253
+ void_reason: row.void_reason,
254
+ penalty_minor: row.penalty_minor != null ? Number(row.penalty_minor) : null,
255
+ penalty_reason: row.penalty_reason,
256
+ created_at: Number(row.created_at),
257
+ };
258
+ }
259
+
260
+ async function _getRaw(id) {
261
+ var r = await query("SELECT * FROM tax_remittances WHERE id = ?1", [id]);
262
+ return r.rows[0] || null;
263
+ }
264
+
265
+ // ---- recordRemittance -------------------------------------------------
266
+
267
+ async function recordRemittance(input) {
268
+ if (!input || typeof input !== "object") {
269
+ throw new TypeError("taxRemittance.recordRemittance: input object required");
270
+ }
271
+ var filingId = _uuid(input.filing_id, "filing_id");
272
+ var jur = _jurisdiction(input.jurisdiction);
273
+ var amount = _positiveInt(input.amount_minor, "amount_minor");
274
+ var currency = _currency(input.currency);
275
+ var method = _paymentMethod(input.payment_method);
276
+ var paymentRef = _shortText(input.payment_ref, "payment_ref", MAX_PAYMENT_REF_LEN);
277
+ var paidAt = _epoch(input.paid_at, "paid_at");
278
+
279
+ var id = _b().uuid.v7();
280
+ var ts = _now();
281
+ await query(
282
+ "INSERT INTO tax_remittances " +
283
+ "(id, filing_id, jurisdiction, amount_minor, currency, payment_method, payment_ref, " +
284
+ " paid_at, status, voided_at, void_reason, penalty_minor, penalty_reason, created_at) " +
285
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 'paid', NULL, NULL, NULL, NULL, ?9)",
286
+ [id, filingId, jur, amount, currency, method, paymentRef, paidAt, ts],
287
+ );
288
+ return _decodeRemittance(await _getRaw(id));
289
+ }
290
+
291
+ // ---- getRemittance ----------------------------------------------------
292
+
293
+ async function getRemittance(id) {
294
+ var cid = _uuid(id, "id");
295
+ return _decodeRemittance(await _getRaw(cid));
296
+ }
297
+
298
+ // ---- remittancesForJurisdiction ---------------------------------------
299
+
300
+ async function remittancesForJurisdiction(input) {
301
+ if (!input || typeof input !== "object") {
302
+ throw new TypeError("taxRemittance.remittancesForJurisdiction: input object required");
303
+ }
304
+ var jur = _jurisdiction(input.jurisdiction);
305
+ var from = _epoch(input.from, "from");
306
+ var to = _epoch(input.to, "to");
307
+ if (to <= from) {
308
+ throw new TypeError("taxRemittance.remittancesForJurisdiction: to must be > from");
309
+ }
310
+ var r = await query(
311
+ "SELECT * FROM tax_remittances " +
312
+ "WHERE jurisdiction = ?1 AND paid_at >= ?2 AND paid_at < ?3 " +
313
+ "ORDER BY paid_at DESC, id DESC",
314
+ [jur, from, to],
315
+ );
316
+ var out = [];
317
+ for (var i = 0; i < r.rows.length; i += 1) {
318
+ out.push(_decodeRemittance(r.rows[i]));
319
+ }
320
+ return out;
321
+ }
322
+
323
+ // ---- _sumPaidForFiling ------------------------------------------------
324
+ //
325
+ // Sum every non-voided remittance for the filing. Used by
326
+ // reconcileWithFiling and unpaidObligations.
327
+
328
+ async function _sumPaidForFiling(filingId) {
329
+ var r = await query(
330
+ "SELECT amount_minor FROM tax_remittances " +
331
+ "WHERE filing_id = ?1 AND status = 'paid'",
332
+ [filingId],
333
+ );
334
+ var total = 0;
335
+ for (var i = 0; i < r.rows.length; i += 1) {
336
+ total += Number(r.rows[i].amount_minor);
337
+ }
338
+ return total;
339
+ }
340
+
341
+ // ---- reconcileWithFiling ----------------------------------------------
342
+
343
+ async function reconcileWithFiling(input) {
344
+ if (!input || typeof input !== "object") {
345
+ throw new TypeError("taxRemittance.reconcileWithFiling: input object required");
346
+ }
347
+ var api = _requireFilings("reconcileWithFiling");
348
+ var rid = _uuid(input.remittance_id, "remittance_id");
349
+ var row = await _getRaw(rid);
350
+ if (!row) {
351
+ var miss = new Error("taxRemittance.reconcileWithFiling: remittance not found");
352
+ miss.code = "TAX_REMITTANCE_NOT_FOUND";
353
+ throw miss;
354
+ }
355
+ var filing = await api.getFiling(row.filing_id);
356
+ if (!filing) {
357
+ var noFiling = new Error("taxRemittance.reconcileWithFiling: filing not found");
358
+ noFiling.code = "TAX_REMITTANCE_FILING_NOT_FOUND";
359
+ throw noFiling;
360
+ }
361
+ var owed = Number(filing.tax_owed_minor || 0);
362
+ var paid = await _sumPaidForFiling(row.filing_id);
363
+ return {
364
+ remittance_id: rid,
365
+ filing_id: row.filing_id,
366
+ jurisdiction: row.jurisdiction,
367
+ owed_minor: owed,
368
+ paid_minor: paid,
369
+ variance_minor: owed - paid,
370
+ };
371
+ }
372
+
373
+ // ---- unpaidObligations ------------------------------------------------
374
+ //
375
+ // Sweep every filing whose due_date <= as_of and whose sum of
376
+ // non-voided remittances is short of tax_owed_minor. Requires the
377
+ // salesTaxFilings API to be wired; without it, the operator has no
378
+ // owed-side number to compare against.
379
+
380
+ async function unpaidObligations(input) {
381
+ if (!input || typeof input !== "object") {
382
+ throw new TypeError("taxRemittance.unpaidObligations: input object required");
383
+ }
384
+ var api = _requireFilings("unpaidObligations");
385
+ var asOf = _epoch(input.as_of, "as_of");
386
+ var jur = input.jurisdiction == null ? null : _jurisdiction(input.jurisdiction);
387
+
388
+ // The filings primitive doesn't expose a "list by due_date <= X"
389
+ // read directly — `upcomingDue` covers near-future windows, and
390
+ // `listFilings` covers status filters. Compose listFilings here
391
+ // and post-filter by due_date so the read works against the
392
+ // primitive's contract rather than a private SQL join.
393
+ var filings = [];
394
+ if (typeof api.listFilings === "function") {
395
+ var page = await api.listFilings({
396
+ jurisdiction: jur || undefined,
397
+ limit: 500,
398
+ });
399
+ for (var i = 0; i < page.length; i += 1) {
400
+ if (Number(page[i].due_date) <= asOf) filings.push(page[i]);
401
+ }
402
+ } else {
403
+ // The handle is partial — fall back to upcomingDue + an
404
+ // explicit horizon. listFilings is the canonical read; this
405
+ // branch keeps the primitive working against a slimmed-down
406
+ // handle in tests.
407
+ var rows = await api.upcomingDue({ days_ahead: 365, now: asOf });
408
+ for (var j = 0; j < rows.length; j += 1) {
409
+ if (jur && rows[j].jurisdiction !== jur) continue;
410
+ if (Number(rows[j].due_date) <= asOf) filings.push(rows[j]);
411
+ }
412
+ }
413
+
414
+ var out = [];
415
+ for (var k = 0; k < filings.length; k += 1) {
416
+ var f = filings[k];
417
+ var owed = Number(f.tax_owed_minor || 0);
418
+ // Filings still in draft have no owed number yet; skip — the
419
+ // operator hasn't run computeFiling on them.
420
+ if (f.status === "draft") continue;
421
+ var paid = await _sumPaidForFiling(f.id);
422
+ if (paid >= owed) continue;
423
+ out.push({
424
+ filing_id: f.id,
425
+ jurisdiction: f.jurisdiction,
426
+ due_date: Number(f.due_date),
427
+ owed_minor: owed,
428
+ paid_minor: paid,
429
+ variance_minor: owed - paid,
430
+ status: f.status,
431
+ });
432
+ }
433
+ // Operator-facing sort: most-overdue first.
434
+ out.sort(function (a, b) { return a.due_date - b.due_date; });
435
+ return out;
436
+ }
437
+
438
+ // ---- lateRemittances --------------------------------------------------
439
+
440
+ async function lateRemittances(input) {
441
+ if (!input || typeof input !== "object") {
442
+ throw new TypeError("taxRemittance.lateRemittances: input object required");
443
+ }
444
+ var api = _requireFilings("lateRemittances");
445
+ var asOf = _epoch(input.as_of, "as_of");
446
+ var daysLateMin = _daysLateMin(input.days_late_min);
447
+ var thresholdMs = daysLateMin * MS_PER_DAY;
448
+
449
+ // Every non-voided remittance up to as_of.
450
+ var r = await query(
451
+ "SELECT * FROM tax_remittances " +
452
+ "WHERE status = 'paid' AND paid_at <= ?1 " +
453
+ "ORDER BY paid_at ASC, id ASC",
454
+ [asOf],
455
+ );
456
+
457
+ var out = [];
458
+ for (var i = 0; i < r.rows.length; i += 1) {
459
+ var row = r.rows[i];
460
+ var filing;
461
+ try { filing = await api.getFiling(row.filing_id); }
462
+ catch (_e) { filing = null; }
463
+ if (!filing) continue;
464
+ var dueDate = Number(filing.due_date);
465
+ var paidAt = Number(row.paid_at);
466
+ if (paidAt - dueDate < thresholdMs) continue;
467
+ var dec = _decodeRemittance(row);
468
+ dec.due_date = dueDate;
469
+ dec.days_late = Math.floor((paidAt - dueDate) / MS_PER_DAY);
470
+ out.push(dec);
471
+ }
472
+ return out;
473
+ }
474
+
475
+ // ---- metricsForJurisdiction -------------------------------------------
476
+
477
+ async function metricsForJurisdiction(input) {
478
+ if (!input || typeof input !== "object") {
479
+ throw new TypeError("taxRemittance.metricsForJurisdiction: input object required");
480
+ }
481
+ var api = _requireFilings("metricsForJurisdiction");
482
+ var jur = _jurisdiction(input.jurisdiction);
483
+ var from = _epoch(input.from, "from");
484
+ var to = _epoch(input.to, "to");
485
+ if (to <= from) {
486
+ throw new TypeError("taxRemittance.metricsForJurisdiction: to must be > from");
487
+ }
488
+
489
+ var r = await query(
490
+ "SELECT * FROM tax_remittances " +
491
+ "WHERE jurisdiction = ?1 AND paid_at >= ?2 AND paid_at < ?3",
492
+ [jur, from, to],
493
+ );
494
+
495
+ var totalPaid = 0;
496
+ var totalPenalty = 0;
497
+ var remCount = 0;
498
+ var voidedCount = 0;
499
+ var onTime = 0;
500
+ var late = 0;
501
+ var filingCache = Object.create(null);
502
+
503
+ for (var i = 0; i < r.rows.length; i += 1) {
504
+ var row = r.rows[i];
505
+ remCount += 1;
506
+ if (row.status === "voided") {
507
+ voidedCount += 1;
508
+ continue;
509
+ }
510
+ totalPaid += Number(row.amount_minor);
511
+ if (row.penalty_minor != null) totalPenalty += Number(row.penalty_minor);
512
+
513
+ // Resolve due date per filing — cache so multiple installments
514
+ // against the same filing don't refire the lookup.
515
+ var filing = filingCache[row.filing_id];
516
+ if (filing === undefined) {
517
+ try { filing = await api.getFiling(row.filing_id); }
518
+ catch (_e) { filing = null; }
519
+ filingCache[row.filing_id] = filing;
520
+ }
521
+ if (!filing) continue;
522
+ var dueDate = Number(filing.due_date);
523
+ if (Number(row.paid_at) <= dueDate) onTime += 1;
524
+ else late += 1;
525
+ }
526
+
527
+ var rateBase = onTime + late;
528
+ var onTimeRate = rateBase === 0 ? 1 : onTime / rateBase;
529
+
530
+ return {
531
+ jurisdiction: jur,
532
+ from: from,
533
+ to: to,
534
+ remittance_count: remCount,
535
+ voided_count: voidedCount,
536
+ total_paid_minor: totalPaid,
537
+ total_penalty_minor: totalPenalty,
538
+ on_time_count: onTime,
539
+ late_count: late,
540
+ on_time_rate: Math.round(onTimeRate * 10000) / 10000,
541
+ };
542
+ }
543
+
544
+ // ---- markVoided -------------------------------------------------------
545
+
546
+ async function markVoided(input) {
547
+ if (!input || typeof input !== "object") {
548
+ throw new TypeError("taxRemittance.markVoided: input object required");
549
+ }
550
+ var id = _uuid(input.remittance_id, "remittance_id");
551
+ var reason = _shortText(input.reason, "reason", MAX_VOID_REASON_LEN);
552
+
553
+ var existing = await _getRaw(id);
554
+ if (!existing) {
555
+ var miss = new Error("taxRemittance.markVoided: remittance not found");
556
+ miss.code = "TAX_REMITTANCE_NOT_FOUND";
557
+ throw miss;
558
+ }
559
+ if (existing.status === "voided") {
560
+ // Idempotent — return the row as-is so a retry doesn't surface
561
+ // as a failure.
562
+ return _decodeRemittance(existing);
563
+ }
564
+ var ts = _now();
565
+ await query(
566
+ "UPDATE tax_remittances SET status = 'voided', voided_at = ?1, void_reason = ?2 " +
567
+ "WHERE id = ?3",
568
+ [ts, reason, id],
569
+ );
570
+ return _decodeRemittance(await _getRaw(id));
571
+ }
572
+
573
+ // ---- recordPenalty ----------------------------------------------------
574
+
575
+ async function recordPenalty(input) {
576
+ if (!input || typeof input !== "object") {
577
+ throw new TypeError("taxRemittance.recordPenalty: input object required");
578
+ }
579
+ var id = _uuid(input.remittance_id, "remittance_id");
580
+ var penalty = _nonNegInt(input.penalty_minor, "penalty_minor");
581
+ var reason = _shortText(input.reason, "reason", MAX_PENALTY_REASON_LEN);
582
+
583
+ var existing = await _getRaw(id);
584
+ if (!existing) {
585
+ var miss = new Error("taxRemittance.recordPenalty: remittance not found");
586
+ miss.code = "TAX_REMITTANCE_NOT_FOUND";
587
+ throw miss;
588
+ }
589
+ if (existing.status === "voided") {
590
+ var bad = new Error("taxRemittance.recordPenalty: cannot attach penalty to voided remittance");
591
+ bad.code = "TAX_REMITTANCE_VOIDED";
592
+ throw bad;
593
+ }
594
+ await query(
595
+ "UPDATE tax_remittances SET penalty_minor = ?1, penalty_reason = ?2 WHERE id = ?3",
596
+ [penalty, reason, id],
597
+ );
598
+ return _decodeRemittance(await _getRaw(id));
599
+ }
600
+
601
+ return {
602
+ PAYMENT_METHODS: PAYMENT_METHODS.slice(),
603
+ STATUSES: STATUSES.slice(),
604
+ JURISDICTION_RE: JURISDICTION_RE,
605
+ CURRENCY_RE: CURRENCY_RE,
606
+
607
+ recordRemittance: recordRemittance,
608
+ getRemittance: getRemittance,
609
+ remittancesForJurisdiction: remittancesForJurisdiction,
610
+ unpaidObligations: unpaidObligations,
611
+ lateRemittances: lateRemittances,
612
+ reconcileWithFiling: reconcileWithFiling,
613
+ metricsForJurisdiction: metricsForJurisdiction,
614
+ markVoided: markVoided,
615
+ recordPenalty: recordPenalty,
616
+ };
617
+ }
618
+
619
+ module.exports = {
620
+ create: create,
621
+ PAYMENT_METHODS: PAYMENT_METHODS,
622
+ STATUSES: STATUSES,
623
+ JURISDICTION_RE: JURISDICTION_RE,
624
+ CURRENCY_RE: CURRENCY_RE,
625
+ };