@blamejs/blamejs-shop 0.1.19 → 0.1.20
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/README.md +2 -0
- package/lib/storefront.js +62 -17
- 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.1.x
|
|
10
10
|
|
|
11
|
+
- v0.1.20 (2026-05-25) — **A "Customers also bought" rail on the order confirmation.** The order confirmation page now shows a recommendation rail. After a purchase, `/orders/:id` surfaces up to four products drawn from operator-curated pins first, then co-purchase signals from the order's own items (what other shoppers bought alongside them), then category-popular and in-stock fallbacks — always excluding what was just bought. The rail is best-effort: if the engine isn't wired it simply doesn't render. **Added:** *Order-confirmation recommendation rail* — The post-purchase page renders a "Customers also bought" rail beneath the order summary, anchored on the order's items. Picks come from the recommendations engine: operator-curated overrides first, then a co-purchase signal (products bought in the same orders), then category-popular and in-stock fallbacks to fill the rail; the order's own products are always excluded. Each pick reuses the storefront product-card markup (image, title, price) and links to the product page. The rail renders in both the default and file-backed theme layouts; a store without the recommendations primitive wired renders no rail (and a read failure degrades to none rather than erroring the confirmation page).
|
|
12
|
+
|
|
11
13
|
- v0.1.19 (2026-05-25) — **Admin console — a webhooks screen, and order events now fan out.** Webhooks join the admin console. `/admin/webhooks` registers an endpoint (with a one-time signing-secret reveal), lists endpoints, enables / disables and deletes one, and opens an endpoint's delivery feed to retry a failed delivery. Order lifecycle transitions now fan out to registered endpoints — the order primitive is wired to the webhook dispatcher so a paid / fulfilled / shipped / delivered / cancelled / refunded transition produces a signed delivery. **Added:** *Webhooks management screen* — `/admin/webhooks` registers an outbound endpoint — an https:// URL plus the events to subscribe (or all) — and shows the HMAC-SHA3-512 signing secret once on creation; the secret is never rendered in the endpoint list afterward. The list shows each endpoint's URL, events, status, and per-minute rate, with actions to enable / disable, delete, and open its deliveries. The delivery feed lists each attempt (event, status, response code, attempt count, last error, time) and retries a failed delivery. The JSON API a bearer-token client already used — create / list / update / delete / deliveries / retry — is unchanged; the browser screen content-negotiates on the same paths. · *Order events fan out to webhooks* — The order primitive is now wired to the webhook dispatcher, so an order transition (`order.mark_paid`, `order.start_fulfillment`, `order.mark_shipped`, `order.mark_delivered`, `order.cancel`, `order.refund`) produces a signed delivery to every subscribed endpoint. Dispatch is post-persist — a delivery failure can never roll back the transition that landed — and a failed delivery is recorded for retry from the console rather than surfaced to the request that triggered it. **Changed:** *Console nav gains Webhooks* — The signed-in admin nav now includes Webhooks, shown when the webhooks primitive is wired. Endpoint create, list, enable / disable, delete, the delivery feed, and retry content-negotiate like the other console screens: a bearer-token client gets the JSON API, a signed-in browser gets HTML. A request without the bearer token returns the sign-in form on a GET and redirects on a write. An https-only URL or empty event set re-renders the form with the validator's message, and an unknown endpoint or delivery is a no-op notice rather than a 500.
|
|
12
14
|
|
|
13
15
|
- v0.1.18 (2026-05-25) — **Admin console — a subscription-plans screen.** Subscription plans join the admin console. `/admin/subscription-plans` lists the recurring-offer catalog — price, interval, trial, and Stripe price id — with an active / archived filter, creates a plan from a signed-in browser, and archives one. As with the other console screens, the same path serves the existing JSON API to a bearer-token client unchanged. **Added:** *Subscription-plans management screen* — `/admin/subscription-plans` renders the plan catalog (price and interval, trial length, Stripe price id, linked variant, status) with an active / archived filter, when opened in a signed-in browser; the same path serves the JSON list to a bearer-token client. A form creates a plan — Stripe price id, interval, interval count, currency, amount, trial days, and an optional variant link — and each active row archives in one submit. A bad-shape create re-renders the form with the validator's message rather than a 500, and archiving an unknown plan is a no-op notice. Archiving is terminal from the console: because a plan mirrors a Stripe price id that can go stale, a retired plan is re-offered by creating a new one against a fresh price id. **Changed:** *Console nav gains Subscriptions* — The signed-in admin nav now includes Subscriptions alongside Products, Inventory, Orders, Returns, and Reviews, shown when the subscriptions primitive is wired. The `/admin/subscription-plans` list, create, and archive endpoints content-negotiate like the other screens: a bearer-token client gets the JSON API, a signed-in browser gets HTML. A request without the bearer token returns the sign-in form on a GET and redirects on a write.
|
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
|
|
|
69
69
|
| **`lib/addresses.js`** | Per-customer address book at `/account/addresses` — add / edit / set default shipping or billing / remove. One-default-per-role invariant (promoting clears the prior). Every by-id route confirms the address belongs to the signed-in customer before acting (a guessed id returns 404). `b.guardUuid` ids, 2-char ISO country. |
|
|
70
70
|
| **`lib/returns.js`** | Self-serve RMAs. Customer requests a return against their own order at `/account/orders/:id/return` (items + reason, ownership-checked, lines built from the order's own records) and tracks status at `/account/returns`. Operators work `/admin/returns` — approve (refund amount) / mark received / refund / reject — over the pending → approved → received → refunded FSM; illegal transitions are 409, bad ids 404. |
|
|
71
71
|
| **`lib/recently-viewed.js`** | Signed-in customer browse history. A product-page visit records the view server-side against the customer's account (drop-silent — never blocks the page); `/account/recently-viewed` lists them newest-first as a grid with a Clear-history control. De-duped + capped per customer, archived products drop out, login-gated. Guest/session history is opt-in (a client beacon) and not shipped — the lib's `forSession` + `merge` support it. |
|
|
72
|
+
| **`lib/recommendations.js`** | Product-recommendation engine. Operator-curated override pins first (`setOverride` — "when viewing A, show B", kind-scoped + weight-ordered), then a signal-based fallback: co-purchase (products bought in the same orders), category-popular, and in-stock-random filler. `recommendForProduct` / `recommendForCart` / `recommendForCustomer` / `recommendForCategory` each return renderable picks (active + in-stock, source product excluded). The order confirmation page (`/orders/:id`) renders a "Customers also bought" rail from it — best-effort, anchored on the order's items, excluding what was just bought. |
|
|
72
73
|
| **`lib/collections.js`** | Curated + smart product groupings. `GET /collections` lists the shop's active collections; `GET /collections/:slug` renders the grid — manual collections list hand-picked members, smart collections evaluate stored rules against the active catalog and apply the collection's sort strategy. Each product resolves fresh, so archived products drop out. Public, no sign-in; a bad or unknown slug is a 404 (never a 500). Linked from the footer on every page. |
|
|
73
74
|
| **`lib/subscriptions.js`** | Stripe-backed recurring billing — `subscription_plans` (interval / amount / trial) + `subscriptions` (mirrors Stripe's object byte-for-byte). `subscriptions.create` POSTs to Stripe via the payment dep, then persists the returned object locally. `handleStripeEvent` replays `customer.subscription.*` events into the local row so the shop has an authoritative view without round-tripping. |
|
|
74
75
|
| **`lib/newsletter.js`** | Operator-collected email broadcast list — `signup({ email, source })` composes `b.guardEmail` for shape validation, `b.crypto.namespaceHash` for the dedup key, and `INSERT OR IGNORE` for idempotency. Storefront POST `/newsletter` route renders a designed thank-you card with separate copy for the `new` vs `dedup` branches. |
|
|
@@ -96,6 +97,7 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
|
|
|
96
97
|
- `migrations-d1/0206_orders_email_hash.sql` — queryable buyer-email hash on orders (guest-order reconciliation key)
|
|
97
98
|
- `migrations-d1/0043_collections.sql` — manual + smart product collections (members + rules + sort strategy)
|
|
98
99
|
- `migrations-d1/0050_recently_viewed.sql` — per-customer / per-session product browse history (dedup + per-subject cap)
|
|
100
|
+
- `migrations-d1/0105_recommendations.sql` — operator-curated recommendation overrides (kind-scoped, weight-ordered)
|
|
99
101
|
|
|
100
102
|
### Demo seed
|
|
101
103
|
|
package/lib/storefront.js
CHANGED
|
@@ -1818,20 +1818,23 @@ function renderOrder(opts) {
|
|
|
1818
1818
|
var tax = pricing.format(o.tax_minor, o.currency);
|
|
1819
1819
|
var shipping = pricing.format(o.shipping_minor, o.currency);
|
|
1820
1820
|
var total = pricing.format(o.grand_total_minor, o.currency);
|
|
1821
|
+
var recs = opts.recommendations || [];
|
|
1821
1822
|
if (opts.theme) {
|
|
1822
1823
|
return opts.theme.render("order", {
|
|
1823
|
-
title:
|
|
1824
|
-
shop_name:
|
|
1825
|
-
cart_count:
|
|
1826
|
-
order_id:
|
|
1827
|
-
status:
|
|
1828
|
-
lines:
|
|
1829
|
-
has_lines:
|
|
1830
|
-
subtotal:
|
|
1831
|
-
tax:
|
|
1832
|
-
shipping:
|
|
1833
|
-
total:
|
|
1834
|
-
|
|
1824
|
+
title: "Order " + o.id,
|
|
1825
|
+
shop_name: shopName,
|
|
1826
|
+
cart_count: cartCount,
|
|
1827
|
+
order_id: o.id,
|
|
1828
|
+
status: o.status,
|
|
1829
|
+
lines: rendered,
|
|
1830
|
+
has_lines: rendered.length > 0,
|
|
1831
|
+
subtotal: subtotal,
|
|
1832
|
+
tax: tax,
|
|
1833
|
+
shipping: shipping,
|
|
1834
|
+
total: total,
|
|
1835
|
+
recommendations: recs,
|
|
1836
|
+
has_recommendations: recs.length > 0,
|
|
1837
|
+
asset_css_main: opts.theme.assetUrl("css/main.css"),
|
|
1835
1838
|
});
|
|
1836
1839
|
}
|
|
1837
1840
|
function _orderEsc(s) { return b.template.escapeHtml(s); }
|
|
@@ -1858,12 +1861,24 @@ function renderOrder(opts) {
|
|
|
1858
1861
|
shipping: shipping,
|
|
1859
1862
|
total: total,
|
|
1860
1863
|
}).replace("RAW_LINES", rows);
|
|
1864
|
+
// Post-purchase cross-sell rail — reuses the catalog grid + product-card
|
|
1865
|
+
// markup (so it inherits the storefront's card styling), rendered only
|
|
1866
|
+
// when the picker returned something.
|
|
1867
|
+
var railHtml = "";
|
|
1868
|
+
if (recs.length) {
|
|
1869
|
+
var railCards = recs.map(function (p) { return _buildProductCard(p); }).join("");
|
|
1870
|
+
railHtml =
|
|
1871
|
+
"<section class=\"catalog-section order-recommendations\">" +
|
|
1872
|
+
"<header class=\"section-head\"><h2 class=\"section-head__title\">Customers also bought</h2></header>" +
|
|
1873
|
+
"<div class=\"grid\">" + railCards + "</div>" +
|
|
1874
|
+
"</section>";
|
|
1875
|
+
}
|
|
1861
1876
|
return _wrap({
|
|
1862
1877
|
title: "Order " + o.id,
|
|
1863
1878
|
shop_name: shopName,
|
|
1864
1879
|
cart_count: cartCount,
|
|
1865
1880
|
theme_css: opts.theme_css,
|
|
1866
|
-
body: body,
|
|
1881
|
+
body: body + railHtml,
|
|
1867
1882
|
});
|
|
1868
1883
|
}
|
|
1869
1884
|
|
|
@@ -3019,11 +3034,41 @@ function mount(router, deps) {
|
|
|
3019
3034
|
hero_media: media.length ? media[0] : null,
|
|
3020
3035
|
};
|
|
3021
3036
|
}
|
|
3037
|
+
// "Customers also bought" rail — co-purchase signals anchored on the
|
|
3038
|
+
// order's own items (and excluding them, so we never recommend what
|
|
3039
|
+
// was just bought). Best-effort: a read failure (engine not wired /
|
|
3040
|
+
// tables not migrated) degrades to no rail rather than 500-ing the
|
|
3041
|
+
// confirmation page.
|
|
3042
|
+
var recommendations = [];
|
|
3043
|
+
if (deps.recommendations) {
|
|
3044
|
+
try {
|
|
3045
|
+
var orderProductIds = [];
|
|
3046
|
+
for (var li = 0; li < (o.lines || []).length; li += 1) {
|
|
3047
|
+
var look = productLookup[o.lines[li].variant_id];
|
|
3048
|
+
if (look && look.product && orderProductIds.indexOf(look.product.id) === -1) {
|
|
3049
|
+
orderProductIds.push(look.product.id);
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
if (orderProductIds.length) {
|
|
3053
|
+
// recommendForCart aggregates the co-purchase signal across
|
|
3054
|
+
// EVERY purchased product (not just the first) and pivots the
|
|
3055
|
+
// category-popular fallback off the order's dominant
|
|
3056
|
+
// collection; it also self-excludes the order's own products,
|
|
3057
|
+
// so a multi-item order's rail reflects the whole order.
|
|
3058
|
+
var picks = await deps.recommendations.recommendForCart(orderProductIds, { limit: 4 });
|
|
3059
|
+
for (var pi = 0; pi < picks.length; pi += 1) {
|
|
3060
|
+
var card = await _decorateProductCard(picks[pi].product_id);
|
|
3061
|
+
if (card) recommendations.push(card);
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
} catch (_e) { recommendations = []; }
|
|
3065
|
+
}
|
|
3022
3066
|
_send(res, 200, renderOrder({
|
|
3023
|
-
order:
|
|
3024
|
-
product_lookup:
|
|
3025
|
-
|
|
3026
|
-
|
|
3067
|
+
order: o,
|
|
3068
|
+
product_lookup: productLookup,
|
|
3069
|
+
recommendations: recommendations,
|
|
3070
|
+
shop_name: shopName,
|
|
3071
|
+
theme: theme,
|
|
3027
3072
|
}));
|
|
3028
3073
|
});
|
|
3029
3074
|
|
package/package.json
CHANGED