@blamejs/blamejs-shop 0.4.116 → 0.4.118
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 +4 -0
- package/lib/affiliates.js +14 -1
- package/lib/asset-manifest.json +1 -1
- package/lib/cost-layers.js +33 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.4.x
|
|
10
10
|
|
|
11
|
+
- v0.4.118 (2026-06-26) — **Changing an affiliate's commission kind now revalidates the commission value against the new kind's cap.** An affiliate's commission value is validated against its kind's ceiling when it is set: a percentage commission is capped at 100% (10000 basis points), while a flat per-order amount may be far larger. Changing only the commission kind — without resupplying the value — skipped that revalidation. Switching a flat per-order amount to a percentage therefore left the large flat value in place as a percentage rate: a $500-per-order flat amount became a 5,000,000% rate that computes a runaway commission on the next recorded order and feeds it straight into the pending-payout total. Updating the commission kind now revalidates the effective commission value (the resupplied one, or the stored one) against the new kind, so a change that would push the value over the new kind's cap is rejected with the same error as setting it directly, instead of being silently persisted. **Fixed:** *An affiliate commission-kind change revalidates the stored value against the new kind's cap* — Updating an affiliate re-checked the commission value against the (possibly new) kind's cap only when the value itself was part of the same update. A kind-only change left the stored value untouched and unvalidated, so switching a flat per-order amount to a percentage kept a value that is legal as a flat amount (up to the large flat ceiling) but thousands of times over the 100%-basis-point percentage cap — producing a grossly inflated commission on the next order and an inflated pending payout. The update path now revalidates the effective commission value against the effective kind whenever either the kind or the value changes, rejecting an over-cap combination rather than persisting it.
|
|
12
|
+
|
|
13
|
+
- v0.4.117 (2026-06-26) — **Weighted-average COGS reports reconstruct each sale's cost exactly, with no penny drift.** The weighted-average cost-of-goods-sold attribution stored a per-unit cost rounded to the nearest minor unit on each attribution row. The COGS reports (per-order and per-period) recover each line's cost as quantity times that per-unit price — there is no stored line total — so the residual attribution row, whose quantity does not always divide its cost evenly, reconstructed a total that differed from the cost the sale itself recorded, by up to one minor unit per multi-unit residual row. The drift could be a gain or a loss and accumulated across every weighted-average sale into the per-order profit figure and the period COGS report. Each attribution now splits a debit whose cost does not divide evenly into a floor-priced remainder and a ceil-priced sliver, so quantity times unit price summed over the rows reproduces the sale's cost exactly. FIFO and LIFO costing, and weighted-average debits that divide evenly, are unchanged. **Fixed:** *Weighted-average COGS attribution no longer drifts from the sale's recorded cost* — A weighted-average sale records the cost it consumes, then writes per-layer attribution rows the COGS reports sum back up. The attribution stored a single integer per-unit price, rounded — and because the reports reconstruct each row as quantity times that price (there is no stored line-total column), the residual-bearing row reconstructed quantity-times-rounded-unit, which differs from the attributed cost whenever the quantity does not divide it evenly (for example three units carrying a cost of 23 stored 8 per unit and reconstructed 24). A debit whose cost does not divide evenly across its units is now split into a floor-priced remainder and a ceil-priced sliver — (quantity minus r) units at the floor price and r units at the floor-plus-one price, where r is the leftover — so the rows reconstruct the attributed cost exactly. The per-order and per-period COGS reports now equal the cost each sale recorded; layer reversal restores the same quantity and value.
|
|
14
|
+
|
|
11
15
|
- v0.4.116 (2026-06-26) — **Sales-tax filings net partial refunds and derive owed tax from the jurisdiction's default rate.** Two correctness fixes to the sales-tax filing computation. A partial refund leaves its order in a paid / fulfilling / shipped / delivered status (there is no partially-refunded state), so the order still counts toward the filing for the period it sold in — but the filing counted the order's FULL tax and subtotal, ignoring the refunded portion, which over-stated collected tax and the amount remitted to the authority. A partial refund recorded inside the filing window now reduces that order's contribution to net taxable sales and net collected tax, apportioned across tax and subtotal by the order's own grand-total structure. Separately, the owed re-derivation attributed every order to the first rate row covering its date regardless of category, so a reduced category rate (e.g. food) defined alongside the jurisdiction's default rate in the same window could be applied to all revenue — and which rate won depended on the database's row order, making the owed figure wrong and non-deterministic. The owed re-derivation now models the jurisdiction's default (category-less) rate only; the collected figure still reflects the actual per-order tax each order was charged at its own rate. **Fixed:** *Partial refunds are netted out of a sales-tax filing instead of counting full tax* — A partial refund is recorded as a same-status order transition that does not change the order's status, subtotal, or tax columns, so the order remained in the filing with its pre-refund tax and subtotal counted in full — over-stating collected tax and over-remitting. computeFiling now sums the partial refunds recorded inside the filing window per order and nets each order's taxable subtotal and collected tax by the refunded share, apportioned across tax and subtotal by the order's grand total (half-even, integer minor units). Refunds are attributed to the period they occur in; an order with no in-window refund is unaffected, and recomputing a filing is idempotent. · *Owed tax is re-derived from the jurisdiction's default rate, deterministically* — The owed re-derivation took the first tax-rate row covering an order's date with no regard for category. Because a category-specific rate (such as a reduced food rate) can legally coexist with the jurisdiction's default rate in the same window, whichever row the database returned first claimed all revenue — yielding a wrong owed figure that flipped with the database's row order. The re-derivation now selects only the jurisdiction's default (category-less) rate, so the owed figure is deterministic and modelled on the jurisdiction's primary rate. The collected figure is unchanged: it remains the sum of the actual tax each order was charged at its own category-aware rate at order time.
|
|
12
16
|
|
|
13
17
|
- v0.4.115 (2026-06-26) — **Adding a bundle with a multi-quantity member no longer charges above the advertised bundle price.** A bundle's discounted price is split across its members and stored as a single integer per-unit price on each cart line, so a member's realized cost is its quantity times that per-unit price. When a member's quantity is greater than one and its allocated share is not divisible by that quantity, the leftover cents could not be returned without raising the per-unit price by a whole quantity-step — and the allocator did exactly that, pushing the realized cart subtotal a few cents ABOVE the bundle price the customer saw on the product page. The allocation now returns leftover cents only in whole quantity-steps and leaves any final sub-step residual off, so the realized subtotal is at most the advertised bundle price, never above it (a residual of fewer than the member's quantity in cents is dropped in the customer's favour in the cases a single integer per-unit price cannot represent exactly). Bundles whose members are all single-unit, and bundles whose discounted allocation divides evenly, are unaffected. **Fixed:** *Bundle with a quantity-greater-than-one member never charges above the quoted price* — The bundle-to-cart allocation converts each member's share of the discounted bundle price into a single integer per-unit price (floor) and returns the cents lost to that floor. A single-unit member absorbs the shortfall exactly; otherwise the cents are returned in whole quantity-sized steps. The final sub-step residual — strictly fewer cents than any remaining member's quantity — was being recovered by bumping a multi-unit member's per-unit price up by one, which raised that line's total by a whole quantity-step and pushed the realized subtotal above the quoted bundle price. That residual is now left off instead: the realized subtotal is at most the advertised price (a sub-quantity-cent undercharge in the customer's favour when an integer per-unit price cannot land on the quote exactly), and never exceeds it.
|
package/lib/affiliates.js
CHANGED
|
@@ -607,7 +607,20 @@ function create(opts) {
|
|
|
607
607
|
if (patch.payout_method != null) _set("payout_method", _payoutMethod(patch.payout_method));
|
|
608
608
|
if (patch.payout_address != null) _set("payout_address", _payoutAddress(patch.payout_address));
|
|
609
609
|
if (patch.commission_kind != null) _set("commission_kind", effectiveKind);
|
|
610
|
-
if (patch.commission_value != null)
|
|
610
|
+
if (patch.commission_value != null) {
|
|
611
|
+
// Value supplied — validate against the effective kind and write it.
|
|
612
|
+
_set("commission_value", _commissionValue(patch.commission_value, effectiveKind));
|
|
613
|
+
} else if (patch.commission_kind != null) {
|
|
614
|
+
// Kind-only patch: re-validate the STORED value against the new kind
|
|
615
|
+
// so an over-cap combination is rejected (a flat amount, legal up to
|
|
616
|
+
// the 1e11 flat cap, switched to percent_bps where the cap is 10000 =
|
|
617
|
+
// 100%, throws here instead of silently persisting a runaway
|
|
618
|
+
// multiplier). Do NOT add commission_value to the SET list: writing
|
|
619
|
+
// the read-before value would clobber a concurrent value-only update,
|
|
620
|
+
// and when the (re)validation passes the stored value is already
|
|
621
|
+
// correct for the new kind, so it is left untouched.
|
|
622
|
+
_commissionValue(Number(current.commission_value), effectiveKind);
|
|
623
|
+
}
|
|
611
624
|
if (patch.attribution_window_days != null) _set("attribution_window_days", _attributionWindow(patch.attribution_window_days));
|
|
612
625
|
|
|
613
626
|
var ts = _now();
|
package/lib/asset-manifest.json
CHANGED
package/lib/cost-layers.js
CHANGED
|
@@ -493,24 +493,43 @@ function create(opts) {
|
|
|
493
493
|
if (attrCogs > residual) attrCogs = residual;
|
|
494
494
|
residual -= attrCogs;
|
|
495
495
|
}
|
|
496
|
-
|
|
496
|
+
// Debit the layer by the full consumed quantity (one update).
|
|
497
497
|
await query(
|
|
498
498
|
"UPDATE cost_layers SET quantity_remaining = quantity_remaining - ?1 WHERE id = ?2",
|
|
499
499
|
[dRow.qty, dRow.layer.id],
|
|
500
500
|
);
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
)
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
501
|
+
// Attribute the debit's cost so `qty * unit_cost_minor` summed over
|
|
502
|
+
// its attribution rows reconstructs attrCogs EXACTLY — the COGS
|
|
503
|
+
// reports (cogsForOrder / cogsForPeriod) recover each line's cost
|
|
504
|
+
// that way and have no line-total column to read instead. A single
|
|
505
|
+
// integer per-unit price cannot represent attrCogs when the debit
|
|
506
|
+
// quantity does not divide it (e.g. 23 over 3 units), so split the
|
|
507
|
+
// debit into a floor-priced remainder and a ceil-priced sliver:
|
|
508
|
+
// (qty - r) units at floor, r units at floor+1, where
|
|
509
|
+
// r = attrCogs - floor*qty. (qty-r)*floor + r*(floor+1) =
|
|
510
|
+
// qty*floor + r = attrCogs — no penny drift between the sale's COGS
|
|
511
|
+
// and the persisted attributions (the prior single rounded per-unit
|
|
512
|
+
// drifted by up to qty-1 minor units on the residual-bearing row).
|
|
513
|
+
var floorUnit = dRow.qty > 0 ? Math.floor(attrCogs / dRow.qty) : avgUnitCost;
|
|
514
|
+
var rem = dRow.qty > 0 ? attrCogs - (floorUnit * dRow.qty) : 0;
|
|
515
|
+
var parts = [];
|
|
516
|
+
if (dRow.qty - rem > 0) parts.push({ qty: dRow.qty - rem, unit: floorUnit });
|
|
517
|
+
if (rem > 0) parts.push({ qty: rem, unit: floorUnit + 1 });
|
|
518
|
+
for (var p = 0; p < parts.length; p += 1) {
|
|
519
|
+
var attrId = b.uuid.v7();
|
|
520
|
+
await query(
|
|
521
|
+
"INSERT INTO cogs_attributions " +
|
|
522
|
+
"(id, order_id, line_id, sku, layer_id, qty, unit_cost_minor, " +
|
|
523
|
+
"currency, reversed, reversal_reason, occurred_at) " +
|
|
524
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 0, NULL, ?9)",
|
|
525
|
+
[attrId, orderId, lineId, sku, dRow.layer.id, parts[p].qty, parts[p].unit, currency, attributionTs],
|
|
526
|
+
);
|
|
527
|
+
consumed.push({
|
|
528
|
+
layer_id: dRow.layer.id,
|
|
529
|
+
qty: parts[p].qty,
|
|
530
|
+
unit_cost_minor: parts[p].unit,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
514
533
|
totalCogs += attrCogs;
|
|
515
534
|
}
|
|
516
535
|
// Edge case: residual unspent (impossible given the math
|
package/package.json
CHANGED