@blamejs/blamejs-shop 0.3.44 → 0.3.46

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.
@@ -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. Uses
329
- // banker's rounding (round-half-to-even) on the final minor-unit total
330
- // so two interval combinations that produce e.g. 4499.5 minor units
331
- // don't oscillate the dashboard total by one cent between refreshes.
332
-
333
- function _bankersRound(x) {
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 factor = CADENCE_TO_MONTHLY[interval];
344
- if (factor == null) return 0;
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
- var raw = (amountMinor * qty * factor) / ic;
348
- return _bankersRound(raw);
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, rounded to nearest minor unit.
125
- // Round-half-to-even (banker's) avoids systematic upward bias
126
- // across many small carts; Math.round in JS is round-half-up,
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 = _bankersRound(gross * rate / (10000 + rate));
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 = _bankersRound(net * rate / 10000);
235
+ var tax = _mulRational(net, rate, 10000);
241
236
  return {
242
237
  net_minor: net,
243
238
  tax_minor: tax,
@@ -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
- ? String(v.payload.old_amount_minor / 100) + " " + v.payload.currency
774
+ ? pricing.format(v.payload.old_amount_minor, v.payload.currency)
774
775
  : "—";
775
776
  var newPriceStr = v.payload.new_amount_minor != null
776
- ? String(v.payload.new_amount_minor / 100) + " " + v.payload.currency
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)) + "%"
@@ -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
- priceStr = (price.amount_minor / 100).toFixed(2) + " " + (price.currency || currency);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.3.44",
3
+ "version": "0.3.46",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {