@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.
- package/CHANGELOG.md +6 -0
- package/lib/announcement-bar.js +753 -0
- package/lib/banner-ab-tests.js +806 -0
- package/lib/bin-locations.js +791 -0
- package/lib/blog-articles.js +1173 -0
- package/lib/carrier-accounts.js +805 -0
- package/lib/cart-recovery.js +1133 -0
- package/lib/category-navigation.js +934 -0
- package/lib/consent-ledger.js +539 -0
- package/lib/customer-impersonation.js +743 -0
- package/lib/customer-merge.js +879 -0
- package/lib/demand-forecast.js +1121 -0
- package/lib/dispute-resolution.js +886 -0
- package/lib/email-ab-tests.js +918 -0
- package/lib/email-engagement-score.js +649 -0
- package/lib/event-log.js +713 -0
- package/lib/fulfillment-sla.js +791 -0
- package/lib/index.js +41 -0
- package/lib/inventory-audits.js +852 -0
- package/lib/line-gift-wrap.js +430 -0
- package/lib/marketing-budget.js +792 -0
- package/lib/operator-activity-feed.js +977 -0
- package/lib/operator-approvals.js +942 -0
- package/lib/operator-help-center.js +1020 -0
- package/lib/operator-inbox.js +889 -0
- package/lib/operator-sessions.js +701 -0
- package/lib/order-exchanges.js +602 -0
- package/lib/product-compare.js +804 -0
- package/lib/pwa-manifest.js +1005 -0
- package/lib/referral-leaderboard.js +612 -0
- package/lib/sales-tax-filings.js +807 -0
- package/lib/search-ranking.js +859 -0
- package/lib/shipping-insurance.js +757 -0
- package/lib/shrinkage-report.js +1182 -0
- package/lib/sidebar-widgets.js +952 -0
- package/lib/smart-restocking.js +1048 -0
- package/lib/stock-receipts.js +834 -0
- package/lib/subscription-analytics.js +1032 -0
- package/lib/suggestion-box.js +921 -0
- package/lib/tax-remittance.js +625 -0
- package/lib/vendor-invoices.js +1021 -0
- package/lib/winback-campaigns.js +1350 -0
- package/lib/wishlist-digest.js +1133 -0
- 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
|
+
};
|