@blamejs/blamejs-shop 0.4.9 → 0.4.10

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,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - v0.4.10 (2026-06-05) — **The Promo banners console is back in the admin nav.** The promo-banners management screen introduced in 0.4.4 was fully built and mounted, but the admin nav never showed its link: the nav filters items against an availability map, and the map was missing the one key the Promo banners item checks, so the link was filtered off every page and the screen was reachable only by typing /admin/promo-banners. The key is in the map now, and a contract test pins every nav item's gate key to the map so a future screen can't ship undiscoverable the same way. **Added:** *Nav gate keys are contract-tested* — A test now parses every nav item's gate key against the availability map and fails the suite on any mismatch, in either direction of drift — a new screen whose key is forgotten can't ship with a permanently hidden link again. **Fixed:** *Promo banners nav link renders* — The admin nav gates each item on an availability map keyed by the deps the console was mounted with. The Promo banners item checked a key the map never defined, which reads as permanently unavailable — so the link was hidden on every authenticated admin page even though the screen itself mounted and worked. Operators see the link again wherever the promo-banners primitive is wired.
12
+
11
13
  - v0.4.9 (2026-06-05) — **Scheduled jobs actually run now, the cart's discount-code entry renders, and low-stock alerts reach the admin console.** Three shipped features were unreachable in a deployed store because the production composition never connected them. The worker's scheduled ticks and inventory events carry no browser headers, so the bot guard rejected every one of them before the shared-secret check ran — cart recovery, back-in-stock scans, wishlist alerts and digests, abandoned-checkout reaping, and customer-portal expiry never executed on a deployment fronted by the bundled worker. The cart page's discount-code entry and the low-stock alert pipeline were similarly disconnected: each renders or mounts only when the server hands the backing engine to the surface, and the server never did. All three are wired now, and the boot test gains a full-composition pass that fails the build when a gated surface stops reaching the wire. **Added:** *Boot test proves the full composition on the wire* — The integration boot test now boots `server.js` twice: once bare (the container health-check contract, unchanged) and once against an in-memory stand-in for the worker's SQL bridge, which mounts the full catalog + cart + storefront + admin composition exactly as a deployment runs it. The bridged pass adds to cart through the production middleware stack, applies a discount code end to end (the entry renders, the code resolves, the applied chip persists), proves the worker-shaped scheduled tick reaches its handler, and drives a real stock mutation — an admin restock on a SKU under its threshold — into a persisted low-stock alert row without ever touching the intake endpoint directly. A feature gated on a dependency the server forgets to hand over now fails this gate instead of shipping invisible — the failure mode behind all three fixes above. **Fixed:** *Internal scheduled and event endpoints are no longer rejected by the bot guard* — The worker calls the container's internal endpoints (`/_/cart-recovery-tick`, `/_/stock-alert-sweep`, `/_/wishlist-alerts-sweep`, `/_/wishlist-digest-sweep`, `/_/customer-portal-expire`, `/_/stale-order-reap`, `/_/low-stock-alert`) with no browser fingerprint, and the bot guard's missing-header heuristic returned 403 before each handler's constant-time shared-secret check ever ran. Because the worker fires these calls without reading the response, the failures were invisible: on a deployment fronted by the bundled worker, none of these jobs had ever executed — abandoned checkouts kept their stock holds, recovery and wishlist passes never ran, and expired portal grants were never cleaned. These paths now skip the browser-fingerprint heuristics only; every one of them still refuses callers without the shared bridge secret, and `/_/health` keeps its bot-guard coverage unchanged. · *The cart page's discount-code entry renders* — The "Have a discount code?" block and its `POST /cart/coupon` / `POST /cart/coupon/remove` routes mount only when the storefront receives the discount engine, and the server never passed it — so the entry introduced in 0.3.69 was invisible in a deployed store even though checkout already honoured codes submitted through its own field. The engine is now threaded to the storefront: shoppers can apply and remove codes on the cart, and the applied code carries through the same quote-and-confirm path checkout always used. · *Low-stock alerts flow end to end* — Crossing a SKU's low-stock threshold produced no alert, for two independent reasons. The catalog's stock-mutating operations — the checkout hold, a release, a decrement, an admin restock — expose an observer hook for exactly this, and the server never connected it, so the threshold check simply never ran. And the worker-side path (the inventory lock posting the alert to the container) had no `/_/low-stock-alert` endpoint to receive it, so even a directly-posted alert was dropped: no `inventory_alerts` row, no `inventory.low_stock` webhook, and a `/admin/inventory/alerts` screen that never mounted. Both halves are wired now: every stock mutation that leaves a SKU under its configured threshold writes the alert row, fans out the webhook through the shared dispatcher, and emits the warning log line; the intake endpoint exists (secret-gated, like the other internal endpoints); and the admin alert-history screen is reachable. · *Stray editor artifact removed from release-notes/* — A temporary file accidentally committed alongside the 0.3.74 notes is gone from the repository.
12
14
 
13
15
  - v0.4.8 (2026-06-05) — **Concurrent order status changes are race-safe: a cancelled order can't be revived and a freed stock hold can't be settled twice.** An order's status change read the current state, decided the next one, then wrote it back. Under two writers racing the same order — a payment confirmation arriving as the stale-checkout sweeper cancels it, or an operator marking it paid while a provider webhook lands — both could act on the same starting state, so the later write silently overwrote the earlier one. The write now carries the state it was decided from, so it only lands while the order is still in that state; the writer that loses the race applies nothing and reports the order as the winner left it. This closes a window where the sweeper could free an order's stock hold and the payment could then still confirm — overselling the last unit — and where a settled hold could be released or debited a second time. The common card path was already shielded by a pre-cancel check against the payment provider; this protects the paths that aren't (express wallets and PayPal, a store running without the card provider configured, an operator marking an order paid by hand, and a shopper cancelling as a webhook arrives). **Fixed:** *Order status transitions are serialized by a conditional claim* — A status transition now writes with the condition that the order is still in the state the transition was computed from, so concurrent transitions on one order resolve to exactly one winner. The loser matches no row and becomes a no-op: it writes no status change, records no transition entry, and runs no inventory settlement — preventing a stock hold from being released or debited twice and preventing a terminal order from being moved back to an active state. An uncontended transition behaves exactly as before. This mirrors the existing claim used when attaching a payment intent to a pending order. · *Stock holds can no longer be double-settled across the cancel and capture paths* — When the abandoned-checkout sweeper cancels a pending order at the same moment its payment confirms, only one edge now settles the hold: the cancel frees it or the capture debits it, never both. Previously the two could interleave so a hold was released and the same order still reached paid, overselling the unit. The fix covers the paths the provider-side pre-cancel check does not — express-wallet and PayPal orders, deployments without the card provider configured, an operator marking an order paid from the console, and a shopper cancelling while a webhook is in flight.
package/lib/admin.js CHANGED
@@ -592,7 +592,7 @@ function mount(router, deps) {
592
592
  // `reports` is always present in the nav (read-only sales summary needs no
593
593
  // extra dep); its route mounts unconditionally and renders an unconfigured
594
594
  // notice when the salesReports primitive isn't wired.
595
- var navAvailable = { analytics: !!deps.analytics, returns: !!returns, reviews: !!reviews, productQa: !!productQa, subscriptions: !!deps.subscriptions, preorder: !!deps.preorder, webhooks: !!deps.webhooks, collections: !!deps.collections, customers: !!deps.customers, customerSegments: !!customerSegments, giftcards: !!deps.giftcards, announcementBar: !!deps.announcementBar, blog: !!deps.blog, knowledgeBase: !!deps.knowledgeBase, customerSurveys: !!deps.customerSurveys, storefrontPages: !!deps.storefrontPages, businessHours: !!deps.businessHours, taxRates: !!deps.taxRates, shippingZones: !!deps.shippingZones, deliveryEstimate: !!deps.deliveryEstimate, autoDiscount: !!deps.autoDiscount, discountAllocation: !!deps.discountAllocation, quantityDiscounts: !!deps.quantityDiscounts, loyalty: !!deps.loyalty, pickLists: !!pickLists, salesTaxFilings: !!salesTaxFilings, shippingLabels: !!shippingLabels, supportTickets: !!supportTickets, complianceExport: !!complianceExport, orderExchanges: !!orderExchanges, orderRatings: !!orderRatings, clickAndCollect: !!clickAndCollect, giftOptions: !!giftOptions, searchRanking: !!searchRanking, trustBadges: !!trustBadges, orderExport: !!orderExport, auditLog: auditLog, errorLog: !!errorLog, carts: !!cart };
595
+ var navAvailable = { analytics: !!deps.analytics, returns: !!returns, reviews: !!reviews, productQa: !!productQa, subscriptions: !!deps.subscriptions, preorder: !!deps.preorder, webhooks: !!deps.webhooks, collections: !!deps.collections, customers: !!deps.customers, customerSegments: !!customerSegments, giftcards: !!deps.giftcards, announcementBar: !!deps.announcementBar, promoBanners: !!deps.promoBanners, blog: !!deps.blog, knowledgeBase: !!deps.knowledgeBase, customerSurveys: !!deps.customerSurveys, storefrontPages: !!deps.storefrontPages, businessHours: !!deps.businessHours, taxRates: !!deps.taxRates, shippingZones: !!deps.shippingZones, deliveryEstimate: !!deps.deliveryEstimate, autoDiscount: !!deps.autoDiscount, discountAllocation: !!deps.discountAllocation, quantityDiscounts: !!deps.quantityDiscounts, loyalty: !!deps.loyalty, pickLists: !!pickLists, salesTaxFilings: !!salesTaxFilings, shippingLabels: !!shippingLabels, supportTickets: !!supportTickets, complianceExport: !!complianceExport, orderExchanges: !!orderExchanges, orderRatings: !!orderRatings, clickAndCollect: !!clickAndCollect, giftOptions: !!giftOptions, searchRanking: !!searchRanking, trustBadges: !!trustBadges, orderExport: !!orderExport, auditLog: auditLog, errorLog: !!errorLog, carts: !!cart };
596
596
 
597
597
  try { b.audit.registerNamespace(AUDIT_NAMESPACE); } catch (_e) { /* idempotent */ }
598
598
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.9",
2
+ "version": "0.4.10",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-6k53cvkRrxMgmeStLIoLjVXZQHqIJgTmv1Izd8TYhh1HOC4POgE6GCvx1bsalyEP",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
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": {