@blamejs/blamejs-shop 0.3.44 → 0.3.45
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 +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/product-bulk-ops.js +10 -6
- package/lib/sales-tax-filings.js +8 -12
- package/lib/subscription-analytics.js +30 -17
- package/lib/tax.js +20 -25
- package/lib/wishlist-alerts.js +3 -2
- package/lib/wishlist-digest.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.3.x
|
|
10
10
|
|
|
11
|
+
- v0.3.45 (2026-06-01) — **Tax, VAT, bulk price changes, and customer emails now compute money in exact integer arithmetic.** Several money calculations were performed with floating-point intermediates. On a large order subtotal the sales-tax math could lose a minor unit, and the customer-facing wishlist emails formatted prices by dividing by 100 — which is wrong for zero-decimal currencies like JPY (¥1,050 rendered as 10.5) and three-decimal currencies like BHD, and dropped trailing zeros ($10.00 shown as 10). All of these now run through the framework's exact BigInt money primitive: sales-tax and VAT/GST extraction, the per-rate owed figure on a tax filing, the admin bulk price-adjust, the wishlist price-drop and digest emails, and subscription monthly-revenue normalization. Prices in customer emails are now rendered with the correct currency exponent and symbol for every currency. **Changed:** *Bulk price-adjust rounds to even on exact half-unit ties* — The admin bulk price-adjust now rounds a price that lands exactly halfway between two minor units to the nearest even unit, matching the rounding used everywhere else in the money surface, instead of always rounding the half up. This only changes the result on an exact tie and by at most one minor unit (for example a price computed as 200.5 now becomes 200 rather than 201). If a pricing regime requires rounding halves up, that is a one-line option change. **Fixed:** *Exact sales-tax and VAT computation* — Sales tax, VAT-inclusive extraction, and VAT-exclusive addition are computed in exact integer arithmetic rather than with a floating-point intermediate, so the tax on a large order total is no longer off by a minor unit. The per-rate owed figure on a tax filing is likewise computed exactly, which matters when a filing window aggregates a very large taxable total. · *Currency-correct prices in wishlist emails* — Price-drop alert emails and the wishlist digest now format prices with each currency's own decimal places and symbol. Zero-decimal currencies (such as JPY) and three-decimal currencies (such as BHD) render correctly, and whole amounts keep their trailing zeros — for example $10.00 instead of 10. · *Exact subscription monthly-revenue normalization* — Normalizing a weekly or annual plan to a monthly figure for revenue reporting now uses exact rational arithmetic instead of a floating-point cadence factor, so the dashboard total is stable and does not drift by a cent between refreshes.
|
|
12
|
+
|
|
11
13
|
- v0.3.44 (2026-05-31) — **Customers can rate a delivered order, and the admin console moderates and replies.** There was no way for a customer to give feedback on how an order was fulfilled, and no way for an operator to act on it. Customers can now rate one of their own delivered orders on shipping, packaging, and how likely they are to recommend you, with an optional comment. The admin console gets a Ratings screen with a moderation queue of every flagged comment plus the most recent ratings, where an operator can flag a comment or post one public reply that the customer then sees on their order page. A customer can only rate an order they own, and every comment and reply is HTML-escaped wherever it is shown, so feedback text can't inject markup into another shopper's or an operator's page. **Added:** *Order ratings* — A customer viewing one of their own orders can rate it on three axes — shipping, packaging, and likelihood to recommend (1 to 5) — and optionally leave a comment. Each order can be rated once. The rating, and any public reply from the store, appear on the customer's order page. A customer can only rate an order tied to their own account; the rating is always recorded against the signed-in customer, never a value taken from the request. · *Ratings moderation in the admin console* — A new Ratings screen lists recent ratings with their scores and comments, alongside a moderation queue that surfaces every flagged comment regardless of its score, so a flagged comment on an otherwise high rating is never hidden. An operator can flag a comment for moderation and post a single public reply to a rating, which is then shown to the customer. Comments and replies are HTML-escaped everywhere they are rendered.
|
|
12
14
|
|
|
13
15
|
- v0.3.43 (2026-05-31) — **Export orders for a date range as CSV or NDJSON, with scheduled exports.** There was no way to pull a bulk export of orders over a date range. A new Exports screen in the admin console previews how many orders and how much revenue a date range covers, then downloads them as a CSV or NDJSON file. The export can also be scheduled and managed as a queued job. Customer email addresses are exported only as a hash, never in the clear, and any field that could be interpreted as a spreadsheet formula is neutralized so an exported file can't run code when opened. This release also updates the vendored blamejs runtime to v0.14.16. **Added:** *Order export* — An Exports screen in the admin console exports orders over a chosen date range. It first shows a preview — the order count, revenue, and average order value for the range — then downloads the orders as a CSV or NDJSON file with a standard set of columns. Exports can also be queued as scheduled jobs and cancelled before they run. Customer email is exported as a hash rather than in the clear, and any cell that begins with a character a spreadsheet would treat as a formula is escaped, so an exported file can't execute anything when opened in Excel or Sheets. **Changed:** *Vendored blamejs runtime updated to v0.14.16* — The bundled blamejs runtime is updated to v0.14.16, which validates connection ports at configuration time — a malformed port (a non-integer or out-of-range value) is now rejected with a clear error at startup instead of silently falling back to a default. No operator action is required.
|
package/lib/asset-manifest.json
CHANGED
package/lib/product-bulk-ops.js
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* bulkAdjustPrice — apply a percent delta (e.g. -1500 bps for a
|
|
17
17
|
* 15% markdown) to every matched variant's
|
|
18
18
|
* current price in the given currency. The new
|
|
19
|
-
* amount is rounded half-
|
|
20
|
-
*
|
|
19
|
+
* amount is rounded half-even in minor units
|
|
20
|
+
* via b.money; the floor is 0 (no negative
|
|
21
21
|
* prices). Variants without a current price in
|
|
22
22
|
* the target currency are skipped.
|
|
23
23
|
* bulkArchive — set products.status='archived' on every
|
|
@@ -489,8 +489,9 @@ function create(opts) {
|
|
|
489
489
|
// Apply a percent_bps delta to every matched variant's CURRENT
|
|
490
490
|
// price in the given currency. Variants without a current price
|
|
491
491
|
// in the target currency are skipped (they don't have a "before"
|
|
492
|
-
// amount to multiply against). The new amount
|
|
493
|
-
//
|
|
492
|
+
// amount to multiply against). The new amount = current ×
|
|
493
|
+
// (1 + percent_bps/10000), rounded half-even in minor units via
|
|
494
|
+
// b.money, and floored at 0.
|
|
494
495
|
bulkAdjustPrice: async function (input) {
|
|
495
496
|
if (!input || typeof input !== "object") throw new TypeError("productBulkOps.bulkAdjustPrice: input object required");
|
|
496
497
|
var filter = input.filter;
|
|
@@ -501,11 +502,14 @@ function create(opts) {
|
|
|
501
502
|
var variants = await _variantsForProducts(productIds);
|
|
502
503
|
var affected = 0;
|
|
503
504
|
var skipped = 0;
|
|
504
|
-
var multiplier = (10000 + input.percent_bps) / 10000;
|
|
505
505
|
for (var i = 0; i < variants.length; i += 1) {
|
|
506
506
|
var current = await catalog.prices.current(variants[i].id, input.currency);
|
|
507
507
|
if (!current) { skipped += 1; continue; }
|
|
508
|
-
var next =
|
|
508
|
+
var next = Number(
|
|
509
|
+
b.money.fromMinorUnits(BigInt(current.amount_minor), input.currency)
|
|
510
|
+
.multiply([BigInt(10000 + input.percent_bps), 10000n], { rounding: "half-even" })
|
|
511
|
+
.toMinorUnits()
|
|
512
|
+
);
|
|
509
513
|
if (next < 0) next = 0;
|
|
510
514
|
await catalog.prices.set(variants[i].id, {
|
|
511
515
|
currency: input.currency,
|
package/lib/sales-tax-filings.js
CHANGED
|
@@ -559,10 +559,9 @@ function create(opts) {
|
|
|
559
559
|
}
|
|
560
560
|
|
|
561
561
|
// Owed: re-derive from per-rate taxable totals using rate_bps.
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
// primitive may aggregate millions of orders.)
|
|
562
|
+
// owed = taxable × bps / 10000 per rate, half-even via b.money
|
|
563
|
+
// (BigInt — taxable_minor sums every order in the window and can
|
|
564
|
+
// exceed 2^53, so the multiply must not pass through a float).
|
|
566
565
|
var owed = 0;
|
|
567
566
|
var rateKeys = Object.keys(breakdown);
|
|
568
567
|
for (var rk = 0; rk < rateKeys.length; rk += 1) {
|
|
@@ -571,14 +570,11 @@ function create(opts) {
|
|
|
571
570
|
var bps = parseInt(key, 10);
|
|
572
571
|
if (!Number.isFinite(bps)) continue;
|
|
573
572
|
var bucket = breakdown[key];
|
|
574
|
-
var
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (frac > 0.5) rounded = floored + 1;
|
|
580
|
-
else if (frac < 0.5) rounded = floored;
|
|
581
|
-
else rounded = (floored % 2 === 0) ? floored : floored + 1;
|
|
573
|
+
var rounded = Number(
|
|
574
|
+
b.money.fromMinorUnits(BigInt(bucket.taxable_minor), "USD")
|
|
575
|
+
.multiply([BigInt(bps), 10000n], { rounding: "half-even" })
|
|
576
|
+
.toMinorUnits()
|
|
577
|
+
);
|
|
582
578
|
owed += rounded;
|
|
583
579
|
}
|
|
584
580
|
// When no rate rows resolved (taxRatesApi absent / no rates
|
|
@@ -185,6 +185,18 @@ var CADENCE_TO_MONTHLY = {
|
|
|
185
185
|
year: 1 / 12,
|
|
186
186
|
};
|
|
187
187
|
|
|
188
|
+
// Exact rational [numerator, denominator] forms of the cadence factors
|
|
189
|
+
// above, used by _monthlyMinor so the monthly-normalized minor-unit
|
|
190
|
+
// figure is computed in BigInt (half-even) with no float intermediate.
|
|
191
|
+
// The float CADENCE_TO_MONTHLY view above is retained only for the
|
|
192
|
+
// display-only dashboard export.
|
|
193
|
+
var CADENCE_TO_MONTHLY_RATIO = {
|
|
194
|
+
day: [30n, 1n],
|
|
195
|
+
week: [30n, 7n],
|
|
196
|
+
month: [1n, 1n],
|
|
197
|
+
year: [1n, 12n],
|
|
198
|
+
};
|
|
199
|
+
|
|
188
200
|
// Active = (status in active/trialing) AND not cancelled AND not paused.
|
|
189
201
|
var ACTIVE_STATUSES = ["active", "trialing"];
|
|
190
202
|
|
|
@@ -325,27 +337,28 @@ function _canonicalJson(value) {
|
|
|
325
337
|
|
|
326
338
|
// ---- cadence normalization ---------------------------------------------
|
|
327
339
|
//
|
|
328
|
-
// Plan amount × quantity normalized to a single monthly figure.
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
var floor = Math.floor(x);
|
|
335
|
-
var diff = x - floor;
|
|
336
|
-
if (diff < 0.5) return floor;
|
|
337
|
-
if (diff > 0.5) return floor + 1;
|
|
338
|
-
// diff === 0.5 — round to even
|
|
339
|
-
return floor % 2 === 0 ? floor : floor + 1;
|
|
340
|
-
}
|
|
340
|
+
// Plan amount × quantity normalized to a single monthly figure. The
|
|
341
|
+
// cadence factor (e.g. week → 30/7, year → 1/12) is applied as an
|
|
342
|
+
// exact rational in BigInt half-even via b.money, so the monthly
|
|
343
|
+
// minor-unit total carries no float rounding and two interval
|
|
344
|
+
// combinations producing e.g. 4499.5 minor units settle on a stable
|
|
345
|
+
// even value rather than oscillating the dashboard by a cent.
|
|
341
346
|
|
|
342
347
|
function _monthlyMinor(amountMinor, interval, intervalCount, quantity) {
|
|
343
|
-
var
|
|
344
|
-
if (
|
|
348
|
+
var ratio = CADENCE_TO_MONTHLY_RATIO[interval];
|
|
349
|
+
if (ratio == null) return 0;
|
|
350
|
+
if (!Number.isInteger(amountMinor)) return 0;
|
|
345
351
|
var qty = Number.isInteger(quantity) && quantity > 0 ? quantity : 1;
|
|
346
352
|
var ic = Number.isInteger(intervalCount) && intervalCount > 0 ? intervalCount : 1;
|
|
347
|
-
|
|
348
|
-
|
|
353
|
+
// monthly = amount × qty × (num/den) / ic
|
|
354
|
+
// = amount × qty × num / (den × ic)
|
|
355
|
+
var num = BigInt(amountMinor) * BigInt(qty) * ratio[0];
|
|
356
|
+
var den = ratio[1] * BigInt(ic);
|
|
357
|
+
return Number(
|
|
358
|
+
b.money.fromMinorUnits(num >= 0n ? num : 0n, "USD")
|
|
359
|
+
.multiply([1n, den], { rounding: "half-even" })
|
|
360
|
+
.toMinorUnits()
|
|
361
|
+
);
|
|
349
362
|
}
|
|
350
363
|
|
|
351
364
|
// ---- active-at predicate (in SQL) --------------------------------------
|
package/lib/tax.js
CHANGED
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
* additive `lines[].tax_class` field.
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
+
var b = require("./vendor/blamejs");
|
|
41
|
+
|
|
40
42
|
var COUNTRY_RE = /^[A-Z]{2}$/;
|
|
41
43
|
var STATE_RE = /^[A-Z0-9]{1,5}$/;
|
|
42
44
|
var POSTAL_RE = /^[A-Za-z0-9 -]{1,16}$/;
|
|
@@ -72,6 +74,19 @@ function _nonNegInt(n, label) {
|
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
// Multiply a non-negative minor-unit integer by the rational num/den
|
|
78
|
+
// using the framework's BigInt money primitive (half-even). The result
|
|
79
|
+
// is an integer minor-unit count in the SAME unit as `minor` — the
|
|
80
|
+
// divisor is currency-exponent-independent, so a fixed carrier currency
|
|
81
|
+
// is correct (see lib/quantity-discounts.js for the same technique).
|
|
82
|
+
function _mulRational(minor, num, den) {
|
|
83
|
+
return Number(
|
|
84
|
+
b.money.fromMinorUnits(BigInt(minor), "USD")
|
|
85
|
+
.multiply([BigInt(num), BigInt(den)], { rounding: "half-even" })
|
|
86
|
+
.toMinorUnits()
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
function _validateRule(rule, i) {
|
|
76
91
|
if (!rule || typeof rule !== "object") throw new TypeError("tax: rule[" + i + "] must be an object");
|
|
77
92
|
_country(rule.country, "rule[" + i + "].country");
|
|
@@ -121,17 +136,9 @@ function operatorTable(opts) {
|
|
|
121
136
|
if (_matches(rules[i], ctx.shipTo)) { matched = rules[i]; break; }
|
|
122
137
|
}
|
|
123
138
|
var rateBps = matched ? matched.rate_bps : 0;
|
|
124
|
-
// tax = subtotal × bps / 10000,
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
// so we implement banker's by hand.
|
|
128
|
-
var raw = ctx.subtotal_minor * rateBps / 10000;
|
|
129
|
-
var floor = Math.floor(raw);
|
|
130
|
-
var frac = raw - floor;
|
|
131
|
-
var tax;
|
|
132
|
-
if (frac < 0.5) tax = floor;
|
|
133
|
-
else if (frac > 0.5) tax = floor + 1;
|
|
134
|
-
else tax = (floor % 2 === 0) ? floor : floor + 1; // even
|
|
139
|
+
// tax = subtotal × bps / 10000, half-even via b.money (BigInt —
|
|
140
|
+
// no float intermediate, exact for large subtotals).
|
|
141
|
+
var tax = _mulRational(ctx.subtotal_minor, rateBps, 10000);
|
|
135
142
|
return {
|
|
136
143
|
tax_minor: tax,
|
|
137
144
|
rate_bps: rateBps,
|
|
@@ -171,18 +178,6 @@ function create(opts) {
|
|
|
171
178
|
// primitives compose against. The operator-table adapter still owns
|
|
172
179
|
// jurisdiction-rule selection.
|
|
173
180
|
|
|
174
|
-
// Banker's rounding (round-half-to-even) for non-negative `raw`.
|
|
175
|
-
// JS Math.round is round-half-up, which biases tax totals upward
|
|
176
|
-
// across many small carts; this matches the rounding mode the
|
|
177
|
-
// existing operatorTable adapter uses.
|
|
178
|
-
function _bankersRound(raw) {
|
|
179
|
-
var floor = Math.floor(raw);
|
|
180
|
-
var frac = raw - floor;
|
|
181
|
-
if (frac < 0.5) return floor;
|
|
182
|
-
if (frac > 0.5) return floor + 1;
|
|
183
|
-
return (floor % 2 === 0) ? floor : floor + 1;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
181
|
function _currency(c) {
|
|
187
182
|
if (typeof c !== "string" || !/^[A-Z]{3}$/.test(c)) {
|
|
188
183
|
throw new TypeError("tax: currency must be a 3-letter ISO 4217 code (uppercase), got " + JSON.stringify(c));
|
|
@@ -210,7 +205,7 @@ function calculateInclusive(args) {
|
|
|
210
205
|
_currency(args.currency);
|
|
211
206
|
var gross = args.amount_minor;
|
|
212
207
|
var rate = args.rate_bps;
|
|
213
|
-
var tax =
|
|
208
|
+
var tax = _mulRational(gross, rate, 10000 + rate);
|
|
214
209
|
return {
|
|
215
210
|
net_minor: gross - tax,
|
|
216
211
|
tax_minor: tax,
|
|
@@ -237,7 +232,7 @@ function calculateExclusive(args) {
|
|
|
237
232
|
_currency(args.currency);
|
|
238
233
|
var net = args.amount_minor;
|
|
239
234
|
var rate = args.rate_bps;
|
|
240
|
-
var tax =
|
|
235
|
+
var tax = _mulRational(net, rate, 10000);
|
|
241
236
|
return {
|
|
242
237
|
net_minor: net,
|
|
243
238
|
tax_minor: tax,
|
package/lib/wishlist-alerts.js
CHANGED
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
*/
|
|
90
90
|
|
|
91
91
|
var b = require("./vendor/blamejs");
|
|
92
|
+
var pricing = require("./pricing");
|
|
92
93
|
|
|
93
94
|
var C = b.constants;
|
|
94
95
|
|
|
@@ -770,10 +771,10 @@ function create(opts) {
|
|
|
770
771
|
var dispatchOk = false;
|
|
771
772
|
try {
|
|
772
773
|
var oldPriceStr = v.payload.old_amount_minor != null
|
|
773
|
-
?
|
|
774
|
+
? pricing.format(v.payload.old_amount_minor, v.payload.currency)
|
|
774
775
|
: "—";
|
|
775
776
|
var newPriceStr = v.payload.new_amount_minor != null
|
|
776
|
-
?
|
|
777
|
+
? pricing.format(v.payload.new_amount_minor, v.payload.currency)
|
|
777
778
|
: "—";
|
|
778
779
|
var pctStr = v.payload.discount_bps != null
|
|
779
780
|
? String(Math.floor(v.payload.discount_bps / 100)) + "%"
|
package/lib/wishlist-digest.js
CHANGED
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
*/
|
|
112
112
|
|
|
113
113
|
var b = require("./vendor/blamejs");
|
|
114
|
+
var pricing = require("./pricing");
|
|
114
115
|
|
|
115
116
|
var C = b.constants;
|
|
116
117
|
|
|
@@ -781,7 +782,9 @@ function create(opts) {
|
|
|
781
782
|
}
|
|
782
783
|
var priceStr = "—";
|
|
783
784
|
if (price && typeof price.amount_minor === "number") {
|
|
784
|
-
|
|
785
|
+
try {
|
|
786
|
+
priceStr = pricing.format(price.amount_minor, price.currency || currency);
|
|
787
|
+
} catch (_efmt) { priceStr = "—"; }
|
|
785
788
|
}
|
|
786
789
|
|
|
787
790
|
// Stock change — best-effort. The catalog dep doesn't guarantee
|
package/package.json
CHANGED