@blamejs/blamejs-shop 0.4.80 → 0.4.82

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 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.82 (2026-06-22) — **Return-authorization and shipping-label state transitions are exactly-once under concurrency.** Two fulfillment surfaces gain the atomic-claim discipline the refund path already uses. Approving, receiving, and rejecting a return authorization now gate the transition on the observed status inside the UPDATE, so two operators (or a double-clicked action) acting on the same return can no longer both apply — exactly one transition wins and the other is refused, instead of a read-then-write letting both pass last-write-wins. Likewise, recording a shipping label as purchased, voided, or used is now a single guarded claim, so two concurrent broker callbacks for the same label can't both record (which previously left conflicting tracking numbers and costs last-write-wins). The losing caller gets the same transition-refused error a sequential illegal transition already produced. No configuration changes; upgrade to pick them up. **Fixed:** *Return-authorization transitions are atomic claims* — returns approve, markReceived, and reject now fold the expected source status into the UPDATE (approve from pending, markReceived from approved, reject from pending or approved) and treat the row count as the claim signal, matching the refund path. Two concurrent transitions on the same return — two operators, or a double-submit — can no longer both write last-write-wins; exactly one wins and the loser is refused with the same transition-refused error, mapped to a 409. Sequential not-found and illegal-transition errors are unchanged. · *Shipping-label transitions are atomic claims* — shipping-labels markPurchased, voidLabel, and markUsed now gate on the observed status inside the UPDATE. Two concurrent broker callbacks marking the same pending label purchased (a worker-drain double-fire) can no longer both record, leaving conflicting tracking numbers and costs by last-write-wins — exactly one records and the other is refused. The void 30-day window and not-found / wrong-status refusals are unchanged.
12
+
13
+ - v0.4.81 (2026-06-22) — **Notification transitions are atomic, a missing insurance policy no longer crashes claim approval, and a returns summary no longer zeroes its refund totals under a status filter.** Three correctness fixes on the fulfillment surface. Notification status transitions (mark-sent / mark-failed / mark-read) are now single atomic claims that fold the expected status into the write, so two concurrent delivery callbacks can't both stamp the row last-write-wins, a mark-failed can't clobber a mark-sent, and two concurrent mark-failed calls each count once (the retry counter is incremented in the database rather than read-then-written). Approving a shipping-insurance claim whose parent policy row is missing now returns a clear typed error instead of dereferencing null and throwing an opaque crash. And the operator returns summary builds its refund-value totals from a status-free base filter, so supplying a status filter (e.g. to view rejected returns) no longer collapses the refunded and pending payout totals to zero through a self-contradictory query. No configuration changes; upgrade to pick them up. **Fixed:** *Notification status transitions are atomic claims* — mark-sent, mark-failed, and mark-read now gate the transition on the observed status inside the UPDATE and treat the row count as the claim signal, instead of checking status in a prior read and writing unconditionally. Concurrent delivery callbacks therefore can't both transition one notification last-write-wins, a mark-failed can't overwrite a mark-sent, and the retry counter is incremented in the database (count + 1) so two simultaneous failures can't lose an increment. mark-sent now applies only to a pending notification: a failed delivery is a durable failure record (the scheduler only re-dispatches pending notifications), so a stale or duplicate success callback can no longer flip a failed notification to sent and clear its error — retrying a failed notification is an explicit re-queue. · *Approving an insurance claim with a missing parent policy fails cleanly* — shipping-insurance claim approval now refuses with a typed not-found error when the claim's parent insurance row can't be loaded, matching the null-guard the rest of the module already applies, instead of dereferencing a null row while computing the payout ceiling. · *A returns summary keeps its refund totals when a status filter is applied* — The operator returns summary computes its refunded and pending payout totals from a base filter scoped only to the date window, so they pin their own status independently of the caller's status filter. Previously, supplying a status filter grafted a second, conflicting status predicate onto the payout queries (for example status = 'rejected' AND status = 'refunded'), silently zeroing both rollups; the per-status counts were always correct and remain so.
14
+
11
15
  - 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
16
 
13
17
  - 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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.80",
2
+ "version": "0.4.82",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-imfe0otYErcB8rr2h6KLSGTtStirysptpXETSPY4zLv3bZoIT75Lo1dOvkOav+xL",
@@ -241,18 +241,29 @@ function create(opts) {
241
241
  _uuid(id, "notification id");
242
242
  options = options || {};
243
243
  var sentAt = options.sent_at == null ? _now() : _scheduledAt(options.sent_at, _now());
244
- var existing = (await query("SELECT status FROM notifications WHERE id = ?1", [id])).rows[0];
245
- if (!existing) return null;
246
- if (existing.status !== "pending" && existing.status !== "failed") {
247
- var err = new Error("notifications.markSent: cannot transition from " + existing.status + " to sent");
248
- err.code = "NOTIFICATION_BAD_TRANSITION";
249
- throw err;
250
- }
251
- await query(
252
- "UPDATE notifications SET status = 'sent', sent_at = ?1, last_error = NULL, updated_at = ?1 WHERE id = ?2",
244
+ // Atomic claim, pending sent ONLY. Folding the status into the UPDATE
245
+ // makes the transition a single serialized claim (no read-then-write
246
+ // last-write-wins). Crucially the claim does NOT match a 'failed' row: a
247
+ // delivery that failed is a durable failure record (the scheduler only
248
+ // re-dispatches 'pending', so retrying means an explicit re-queue), and
249
+ // accepting 'failed' here let a stale or duplicate success callback flip
250
+ // a failed notification to sent and clear its last_error — silently
251
+ // masking the failure. rowCount is the claim signal.
252
+ var upd = await query(
253
+ "UPDATE notifications SET status = 'sent', sent_at = ?1, last_error = NULL, updated_at = ?1 " +
254
+ "WHERE id = ?2 AND status = 'pending'",
253
255
  [sentAt, id],
254
256
  );
255
- return (await query("SELECT * FROM notifications WHERE id = ?1", [id])).rows[0];
257
+ if (Number(upd.rowCount || 0) === 1) {
258
+ return (await query("SELECT * FROM notifications WHERE id = ?1", [id])).rows[0];
259
+ }
260
+ // Lost the claim: the row is gone or already left pending/failed. Report
261
+ // exactly as the prior JS guard did.
262
+ var row = (await query("SELECT status FROM notifications WHERE id = ?1", [id])).rows[0];
263
+ if (!row) return null;
264
+ var err = new Error("notifications.markSent: cannot transition from " + row.status + " to sent");
265
+ err.code = "NOTIFICATION_BAD_TRANSITION";
266
+ throw err;
256
267
  },
257
268
 
258
269
  markFailed: async function (id, options) {
@@ -266,26 +277,42 @@ function create(opts) {
266
277
  if (!Number.isInteger(increment) || increment < 0) {
267
278
  throw new TypeError("notifications.markFailed: retry_count_increment must be a non-negative integer");
268
279
  }
269
- var existing = (await query("SELECT status, retry_count FROM notifications WHERE id = ?1", [id])).rows[0];
270
- if (!existing) return null;
271
- if (existing.status !== "pending" && existing.status !== "failed") {
272
- var err = new Error("notifications.markFailed: cannot transition from " + existing.status + " to failed");
273
- err.code = "NOTIFICATION_BAD_TRANSITION";
274
- throw err;
275
- }
276
- var newCount = Number(existing.retry_count) + increment;
277
280
  var ts = _now();
278
- await query(
279
- "UPDATE notifications SET status = 'failed', retry_count = ?1, last_error = ?2, updated_at = ?3 WHERE id = ?4",
280
- [newCount, errMsg, ts, id],
281
+ // Atomic claim with an in-SQL increment: the retry_count bump is computed
282
+ // in the UPDATE (retry_count + ?) and gated on the observed status, so two
283
+ // concurrent markFailed calls can't read the same retry_count and lose an
284
+ // increment, and a markFailed can't clobber a markSent. rowCount is the
285
+ // claim signal.
286
+ var upd = await query(
287
+ "UPDATE notifications SET status = 'failed', retry_count = retry_count + ?1, last_error = ?2, updated_at = ?3 " +
288
+ "WHERE id = ?4 AND status IN ('pending', 'failed')",
289
+ [increment, errMsg, ts, id],
281
290
  );
282
- return (await query("SELECT * FROM notifications WHERE id = ?1", [id])).rows[0];
291
+ if (Number(upd.rowCount || 0) === 1) {
292
+ return (await query("SELECT * FROM notifications WHERE id = ?1", [id])).rows[0];
293
+ }
294
+ var row = (await query("SELECT status FROM notifications WHERE id = ?1", [id])).rows[0];
295
+ if (!row) return null;
296
+ var err = new Error("notifications.markFailed: cannot transition from " + row.status + " to failed");
297
+ err.code = "NOTIFICATION_BAD_TRANSITION";
298
+ throw err;
283
299
  },
284
300
 
285
301
  markRead: async function (id, options) {
286
302
  _uuid(id, "notification id");
287
303
  options = options || {};
288
304
  var readAt = options.read_at == null ? _now() : _scheduledAt(options.read_at, _now());
305
+ // Atomic claim: fold the in-app channel + observed-status predicate into
306
+ // the UPDATE so the read transition is a single serialized claim. rowCount
307
+ // is the claim signal; the loser re-reads to report the precise reason.
308
+ var upd = await query(
309
+ "UPDATE notifications SET status = 'read', read_at = ?1, updated_at = ?1 " +
310
+ "WHERE id = ?2 AND channel = 'in-app' AND status IN ('sent', 'pending')",
311
+ [readAt, id],
312
+ );
313
+ if (Number(upd.rowCount || 0) === 1) {
314
+ return (await query("SELECT * FROM notifications WHERE id = ?1", [id])).rows[0];
315
+ }
289
316
  var existing = (await query("SELECT status, channel FROM notifications WHERE id = ?1", [id])).rows[0];
290
317
  if (!existing) return null;
291
318
  if (existing.channel !== "in-app") {
@@ -293,16 +320,9 @@ function create(opts) {
293
320
  err.code = "NOTIFICATION_BAD_CHANNEL";
294
321
  throw err;
295
322
  }
296
- if (existing.status !== "sent" && existing.status !== "pending") {
297
- var err2 = new Error("notifications.markRead: cannot transition from " + existing.status + " to read");
298
- err2.code = "NOTIFICATION_BAD_TRANSITION";
299
- throw err2;
300
- }
301
- await query(
302
- "UPDATE notifications SET status = 'read', read_at = ?1, updated_at = ?1 WHERE id = ?2",
303
- [readAt, id],
304
- );
305
- return (await query("SELECT * FROM notifications WHERE id = ?1", [id])).rows[0];
323
+ var err2 = new Error("notifications.markRead: cannot transition from " + existing.status + " to read");
324
+ err2.code = "NOTIFICATION_BAD_TRANSITION";
325
+ throw err2;
306
326
  },
307
327
 
308
328
  dismiss: async function (id) {
package/lib/returns.js CHANGED
@@ -335,17 +335,26 @@ function create(opts) {
335
335
  var refundCurrency = input.refund_currency == null ? "USD" : _currency(input.refund_currency);
336
336
  var operatorNotes = _boundedString(input.operator_notes, "operator_notes", MAX_NOTE_LEN);
337
337
 
338
- var current = await this._currentStatus(rmaId);
339
- _assertTransition(current, "approve");
340
-
341
338
  var ts = _now();
342
- await query(
339
+ // Atomic claim — `AND status = 'pending'` is the serialization point, so
340
+ // two concurrent approve/reject POSTs on the same RMA can't both write
341
+ // (the prior read-then-write let both pass). A zero-row result means the
342
+ // RMA isn't pending: the read below surfaces RMA_NOT_FOUND vs
343
+ // RMA_TRANSITION_REFUSED so the request layer's 404/409 mapping holds.
344
+ var upd = await query(
343
345
  "UPDATE return_authorizations SET status = 'approved', " +
344
346
  "refund_amount_minor = ?1, refund_currency = ?2, approved_at = ?3, " +
345
347
  "operator_notes = CASE WHEN ?4 = '' THEN operator_notes ELSE ?4 END, " +
346
- "updated_at = ?3 WHERE id = ?5",
348
+ "updated_at = ?3 WHERE id = ?5 AND status = 'pending'",
347
349
  [input.refund_amount_minor, refundCurrency, ts, operatorNotes, rmaId],
348
350
  );
351
+ if (Number(upd.rowCount || 0) === 0) {
352
+ var current = await this._currentStatus(rmaId);
353
+ _assertTransition(current, "approve");
354
+ var raced = new Error("returns: approve already claimed for rma " + rmaId);
355
+ raced.code = "RMA_TRANSITION_REFUSED";
356
+ throw raced;
357
+ }
349
358
  return await this.get(rmaId);
350
359
  },
351
360
 
@@ -355,18 +364,25 @@ function create(opts) {
355
364
  var receivedAt = _epochOrNull(input.received_at, "received_at");
356
365
  var operatorNotes = _boundedString(input.operator_notes, "operator_notes", MAX_NOTE_LEN);
357
366
 
358
- var current = await this._currentStatus(rmaId);
359
- _assertTransition(current, "markReceived");
360
-
361
367
  var ts = _now();
362
368
  var rxAt = receivedAt == null ? ts : receivedAt;
363
- await query(
369
+ // Atomic claim — `AND status = 'approved'` serializes the transition so a
370
+ // concurrent markReceived/reject can't both write. Zero rows → re-read to
371
+ // surface RMA_NOT_FOUND vs RMA_TRANSITION_REFUSED unchanged.
372
+ var upd = await query(
364
373
  "UPDATE return_authorizations SET status = 'received', " +
365
374
  "received_at = ?1, " +
366
375
  "operator_notes = CASE WHEN ?2 = '' THEN operator_notes ELSE ?2 END, " +
367
- "updated_at = ?3 WHERE id = ?4",
376
+ "updated_at = ?3 WHERE id = ?4 AND status = 'approved'",
368
377
  [rxAt, operatorNotes, ts, rmaId],
369
378
  );
379
+ if (Number(upd.rowCount || 0) === 0) {
380
+ var current = await this._currentStatus(rmaId);
381
+ _assertTransition(current, "markReceived");
382
+ var raced = new Error("returns: markReceived already claimed for rma " + rmaId);
383
+ raced.code = "RMA_TRANSITION_REFUSED";
384
+ throw raced;
385
+ }
370
386
  return await this.get(rmaId);
371
387
  },
372
388
 
@@ -442,17 +458,26 @@ function create(opts) {
442
458
  var rejectedReason = _boundedString(input.rejected_reason, "rejected_reason", MAX_DETAIL_LEN);
443
459
  var operatorNotes = _boundedString(input.operator_notes, "operator_notes", MAX_NOTE_LEN);
444
460
 
445
- var current = await this._currentStatus(rmaId);
446
- _assertTransition(current, "reject");
447
-
448
461
  var ts = _now();
449
- await query(
462
+ // Atomic claim — reject is legal from 'pending' OR 'approved', so the
463
+ // predicate is `AND status IN ('pending','approved')`; it serializes
464
+ // against a concurrent approve/markReceived/reject so they can't both
465
+ // write. Zero rows → re-read to surface RMA_NOT_FOUND vs
466
+ // RMA_TRANSITION_REFUSED unchanged.
467
+ var upd = await query(
450
468
  "UPDATE return_authorizations SET status = 'rejected', " +
451
469
  "rejected_at = ?1, rejected_reason = ?2, " +
452
470
  "operator_notes = CASE WHEN ?3 = '' THEN operator_notes ELSE ?3 END, " +
453
- "updated_at = ?1 WHERE id = ?4",
471
+ "updated_at = ?1 WHERE id = ?4 AND status IN ('pending', 'approved')",
454
472
  [ts, rejectedReason, operatorNotes, rmaId],
455
473
  );
474
+ if (Number(upd.rowCount || 0) === 0) {
475
+ var current = await this._currentStatus(rmaId);
476
+ _assertTransition(current, "reject");
477
+ var raced = new Error("returns: reject already claimed for rma " + rmaId);
478
+ raced.code = "RMA_TRANSITION_REFUSED";
479
+ throw raced;
480
+ }
456
481
  return await this.get(rmaId);
457
482
  },
458
483
 
@@ -651,17 +676,30 @@ function create(opts) {
651
676
  // operator sees committed-payout vs pending-payout. Currency
652
677
  // mix is reported per-bucket so a multi-currency shop doesn't
653
678
  // collapse to a meaningless single number.
679
+ //
680
+ // These rollups pin their OWN status ('refunded' / approved+received), so
681
+ // they must build off a status-free base WHERE (from/to only). Grafting
682
+ // onto the caller-filtered whereSql produced a contradictory predicate
683
+ // (e.g. `status = 'pending' AND status = 'refunded'`) that silently
684
+ // summed to zero whenever an explicit status filter was supplied.
685
+ var valueWhere = [];
686
+ var valueParams = [];
687
+ var vidx = 1;
688
+ if (from != null) { valueWhere.push("created_at >= ?" + vidx); valueParams.push(from); vidx += 1; }
689
+ if (to != null) { valueWhere.push("created_at < ?" + vidx); valueParams.push(to); vidx += 1; }
690
+ var valueWhereSql = valueWhere.length ? (" WHERE " + valueWhere.join(" AND ")) : "";
691
+
654
692
  var refundedRows = (await query(
655
693
  "SELECT refund_currency, SUM(refund_amount_minor) AS total FROM return_authorizations" +
656
- (whereSql ? whereSql + " AND status = 'refunded'" : " WHERE status = 'refunded'") +
694
+ valueWhereSql + (valueWhereSql ? " AND " : " WHERE ") + "status = 'refunded'" +
657
695
  " GROUP BY refund_currency",
658
- params,
696
+ valueParams,
659
697
  )).rows;
660
698
  var pendingRows = (await query(
661
699
  "SELECT refund_currency, SUM(refund_amount_minor) AS total FROM return_authorizations" +
662
- (whereSql ? whereSql + " AND status IN ('approved','received')" : " WHERE status IN ('approved','received')") +
700
+ valueWhereSql + (valueWhereSql ? " AND " : " WHERE ") + "status IN ('approved','received')" +
663
701
  " GROUP BY refund_currency",
664
- params,
702
+ valueParams,
665
703
  )).rows;
666
704
 
667
705
  function _byCurrency(rows) {
@@ -597,6 +597,10 @@ function create(opts) {
597
597
  " is " + claim.status + ", only filed claims can move to approved");
598
598
  }
599
599
  var ins = await _getInsuranceRow(claim.insurance_id);
600
+ if (!ins) {
601
+ throw new TypeError("shippingInsurance.markClaimApproved: parent insurance " +
602
+ claim.insurance_id + " not found for claim " + claimId);
603
+ }
600
604
  if (input.payout_minor > Number(ins.declared_value_minor)) {
601
605
  throw new TypeError("shippingInsurance.markClaimApproved: payout_minor " +
602
606
  input.payout_minor + " exceeds declared_value_minor " +
@@ -373,12 +373,22 @@ function create(opts) {
373
373
  }
374
374
 
375
375
  var ts = _now();
376
- await query(
376
+ // Atomic claim — `AND status = 'pending'` serializes the transition so
377
+ // two concurrent broker callbacks (worker-drain double-fire) can't both
378
+ // record, last-write-wins, conflicting tracking/cost. The JS check above
379
+ // gives the clean sequential not-found / wrong-status refusal; this
380
+ // rowCount guard catches the concurrent racer.
381
+ var upd = await query(
377
382
  "UPDATE shipping_labels SET status = 'purchased', tracking_number = ?1, " +
378
383
  "label_url = ?2, cost_minor = ?3, currency = ?4, purchased_via = ?5, " +
379
- "purchased_at = ?6 WHERE id = ?7",
384
+ "purchased_at = ?6 WHERE id = ?7 AND status = 'pending'",
380
385
  [tracking, url, input.cost_minor, input.currency, input.purchased_via, ts, input.label_id],
381
386
  );
387
+ if (Number(upd.rowCount || 0) === 0) {
388
+ var raced = new Error("shipping-labels.markPurchased: refused — label already transitioned (concurrent claim)");
389
+ raced.code = "SHIPPING_LABELS_TRANSITION_REFUSED";
390
+ throw raced;
391
+ }
382
392
  return await _getLabelRow(input.label_id);
383
393
  },
384
394
 
@@ -409,10 +419,18 @@ function create(opts) {
409
419
  throw werr;
410
420
  }
411
421
 
412
- await query(
413
- "UPDATE shipping_labels SET status = 'voided', void_reason = ?1, voided_at = ?2 WHERE id = ?3",
422
+ var upd = await query(
423
+ "UPDATE shipping_labels SET status = 'voided', void_reason = ?1, voided_at = ?2 WHERE id = ?3 AND status = 'purchased'",
414
424
  [reason, ts, input.label_id],
415
425
  );
426
+ if (Number(upd.rowCount || 0) === 0) {
427
+ // Lost the claim to a concurrent void/markUsed — the label already left
428
+ // 'purchased'. (Sequential not-found / wrong-status / void-window are
429
+ // refused above.)
430
+ var raced = new Error("shipping-labels.voidLabel: refused — label already transitioned (concurrent claim)");
431
+ raced.code = "SHIPPING_LABELS_TRANSITION_REFUSED";
432
+ throw raced;
433
+ }
416
434
  return await _getLabelRow(input.label_id);
417
435
  },
418
436
 
@@ -439,10 +457,17 @@ function create(opts) {
439
457
  throw err;
440
458
  }
441
459
  var ts = _now();
442
- await query(
443
- "UPDATE shipping_labels SET status = 'used', used_at = ?1 WHERE id = ?2",
460
+ // Atomic claim — `AND status = 'purchased'` serializes against a
461
+ // concurrent void/markUsed so the parcel can't be both voided and used.
462
+ var upd = await query(
463
+ "UPDATE shipping_labels SET status = 'used', used_at = ?1 WHERE id = ?2 AND status = 'purchased'",
444
464
  [ts, labelId],
445
465
  );
466
+ if (Number(upd.rowCount || 0) === 0) {
467
+ var raced = new Error("shipping-labels.markUsed: refused — label already transitioned (concurrent claim)");
468
+ raced.code = "SHIPPING_LABELS_TRANSITION_REFUSED";
469
+ throw raced;
470
+ }
446
471
  return await _getLabelRow(labelId);
447
472
  },
448
473
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.80",
3
+ "version": "0.4.82",
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": {