@blamejs/blamejs-shop 0.4.79 → 0.4.80
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/affiliates.js +24 -7
- package/lib/asset-manifest.json +1 -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.4.x
|
|
10
10
|
|
|
11
|
+
- v0.4.80 (2026-06-21) — **Recording an affiliate commission is idempotent under concurrency.** Recording an affiliate commission for an order now converges instead of failing when two attribution hooks fire for the same order at once. The recorder already returned the existing commission on a sequential repeat; under concurrency, both callers passed that pre-read and the second insert hit the (order_id, affiliate_id) uniqueness constraint and threw an unhandled error. The losing writer now catches the constraint violation and returns the committed row, so exactly one commission is recorded and both callers get it. No configuration changes; upgrade to pick it up. **Fixed:** *Concurrent affiliate-commission recording converges on one row* — recordCommissionEvent wraps its insert so that when a concurrent call for the same (order_id, affiliate_id) wins the race, the losing writer catches the uniqueness violation and returns the already-committed commission rather than throwing. The sequential idempotent behaviour is unchanged; this closes the gap where two simultaneous attribution events for one order surfaced an unhandled error instead of a single recorded commission.
|
|
12
|
+
|
|
11
13
|
- v0.4.79 (2026-06-21) — **Reversing a receipt no longer mints phantom stock, and restock recommendations accept lowercase location codes.** Two inventory correctness fixes. Reversing an applied goods-receipt now rolls back exactly the quantity the reversal removed: when a SKU had already been decremented elsewhere to below the received quantity, the reversal's forward step removes only what's on hand, and a mid-reversal failure now compensates by that exact amount instead of adding back the full received quantity — which previously minted phantom stock. Separately, restock recommendations now accept lowercase and dotted location codes, matching the location-code format the rest of the inventory surface already accepts; a stricter uppercase-only pattern had been rejecting valid codes. No configuration changes; upgrade to pick them up. **Fixed:** *Reversing a goods-receipt rolls back the exact quantity removed* — Receipt reversal now records how many units each line's decrement actually removed (floored at the quantity on hand) and, if a later line in the same reversal fails, compensates the already-decremented lines by that exact amount. Previously the forward decrement clamped at zero while the compensation added back the full received quantity, so reversing a receipt for a SKU that had since been drawn down below the received amount could leave more stock on hand than before the reversal. · *Restock recommendations accept lowercase and dotted location codes* — smart-restocking now validates location codes against the same pattern the inventory-locations primitive uses (mixed case, with dot/underscore/hyphen), so a recommendation request for a location like "aisle-3.bin-2" is accepted. A divergent uppercase-only pattern had been rejecting valid location codes that every other inventory operation accepts.
|
|
12
14
|
|
|
13
15
|
- v0.4.78 (2026-06-21) — **Voiding a gift card no longer clobbers a concurrent redemption, and a credit order can't be charged twice.** Two money-safety fixes that make terminal transitions exactly-once. Voiding a gift card is now a single atomic transition guarded on the card still being active, so a void that races a redemption can no longer overwrite the redemption's terminal state and silently discard the spent balance — whichever transition lands first wins and the other is refused. Separately, charging a credit-line order is now idempotent per order: the charge guard refuses a second charge for an order that already has one (and a retry returns the original charge rather than erroring), so the same order can't be charged twice against the customer's credit line. No configuration changes; upgrade to pick them up. **Fixed:** *Voiding a gift card is an atomic, fenced transition* — Gift-card void is now a single guarded UPDATE that only matches a card still in the active state, mirroring redeem. A void that races a redemption (or an expiry) can no longer overwrite the card's terminal state — the prior read-decide-write could void a card a concurrent redemption had just spent, discarding the remaining-balance debit. A void that loses the race is refused (or returns idempotently if the card was already voided). · *A credit-line order is charged at most once* — creditLimits.chargeOrder now refuses a second charge for an order that already carries one, enforced in the same atomic statement as the credit-cap check. A retried charge for an already-charged order returns the original charge instead of erroring or charging the line a second time, so a duplicate capture or retry can't double-draw the customer's credit.
|
package/lib/affiliates.js
CHANGED
|
@@ -796,13 +796,30 @@ function create(opts) {
|
|
|
796
796
|
);
|
|
797
797
|
|
|
798
798
|
var id = b.uuid.v7();
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
799
|
+
try {
|
|
800
|
+
await query(
|
|
801
|
+
"INSERT INTO affiliate_commissions " +
|
|
802
|
+
"(id, order_id, affiliate_id, order_total_minor, commission_minor, " +
|
|
803
|
+
" currency, status, occurred_at, paid_at, voided_at, payout_reference, void_reason) " +
|
|
804
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, 'pending', ?7, NULL, NULL, NULL, NULL)",
|
|
805
|
+
[id, orderId, affiliateId, orderTotal, commissionMinor, currency, occurredAt],
|
|
806
|
+
);
|
|
807
|
+
} catch (e) {
|
|
808
|
+
// Concurrent recordCommissionEvent for the same (order_id,
|
|
809
|
+
// affiliate_id): the pre-read above missed a racing insert, so this
|
|
810
|
+
// writer loses on the UNIQUE constraint. Matching on the error MESSAGE
|
|
811
|
+
// is unreliable — the production D1 service-binding redacts the SQLite
|
|
812
|
+
// "UNIQUE constraint failed" text to a generic "HTTP 500" — so converge
|
|
813
|
+
// message-agnostically: re-read, and if the row now exists the failure
|
|
814
|
+
// was the duplicate, so return it (idempotent). If no row exists the
|
|
815
|
+
// insert failed for some other reason, so re-throw.
|
|
816
|
+
var raced = await query(
|
|
817
|
+
"SELECT * FROM affiliate_commissions WHERE order_id = ?1 AND affiliate_id = ?2",
|
|
818
|
+
[orderId, affiliateId],
|
|
819
|
+
);
|
|
820
|
+
if (raced.rows.length) return raced.rows[0];
|
|
821
|
+
throw e;
|
|
822
|
+
}
|
|
806
823
|
return await _getCommissionRaw(id);
|
|
807
824
|
},
|
|
808
825
|
|
package/lib/asset-manifest.json
CHANGED
package/package.json
CHANGED