@blamejs/blamejs-shop 0.0.57 → 0.0.59
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 +1025 -0
- package/lib/collections.js +916 -0
- package/lib/customer-segments.js +817 -0
- package/lib/gift-options.js +596 -0
- package/lib/index.js +16 -0
- package/lib/mailing-audiences.js +855 -0
- package/lib/order-timeline.js +1073 -0
- package/lib/promo-banners.js +726 -0
- package/lib/quantity-discounts.js +781 -0
- package/lib/recently-viewed.js +511 -0
- package/lib/return-labels.js +477 -0
- package/lib/sales-reports.js +843 -0
- package/lib/search-synonyms.js +792 -0
- package/lib/shipping-labels.js +603 -0
- package/lib/stock-alerts.js +563 -0
- package/lib/subscription-controls.js +723 -0
- package/lib/support-tickets.js +898 -0
- 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.0.x
|
|
10
10
|
|
|
11
|
+
- v0.0.59 (2026-05-22) — **Eleven new primitives: collections, customer segments, recently-viewed, stock alerts, shipping + return labels, promo banners, search synonyms, affiliates, mailing audiences, order timeline.** Eleven primitives ship in one release. `collections` registers manual + smart product groupings. `customerSegments` evaluates RFM-style rules over order history. `recentlyViewed` tracks per-customer / per-session product browse history. `stockAlerts` issues back-in-stock subscriptions with confirmation tokens. `shippingLabels` + `returnLabels` are the carrier-agnostic label-management primitives. `promoBanners` is the storefront CMS for placement-targeted marketing. `searchSynonyms` rewrites and corrects customer search queries. `affiliates` runs the partner program with single-use codes + per-affiliate commission ledgers. `mailingAudiences` is the newsletter segmentation primitive. `orderTimeline` aggregates events from every order-touching primitive into a single chronologically-sorted feed with operator-friendly summary memoization. **Added:** *`collections` primitive — manual + smart product collections* — `bShop.collections.create({ query?, catalog, cursorSecret? })` returns `{ defineManual, defineSmart, get, list, update, archive, addProduct, removeProduct, reorderProducts, productsIn, collectionsForProduct, evaluateRules }`. Manual collections store an explicit ordered membership; smart collections evaluate rules against the catalog at read time. Rules support `eq / neq / contains / gt / gte / lt / lte / in / not_in / between` with field-class guards. `collectionsForProduct(product_id)` unions manual membership with active-smart-rule matches. `update` uses `b.safeSql.assertOneOf` against an `ALLOWED_COLUMNS` allowlist. Migration `0043_collections.sql`. · *`customerSegments` primitive — RFM-style segmentation with cached membership* — `bShop.customerSegments.create({ query?, cursorSecret? })` returns `{ defineSegment, evaluate, recompute, segmentsForCustomer, listSegments, update, archive, unarchive, stats }`. Rules ANDed across `recency_days_max/min`, `frequency_orders_min`, `lifetime_orders_min/max`, `monetary_minor_min/max`, `aov_minor_min`, `refund_rate_bps_min/max`, `last_order_status_in`, `country_in`, `currency_in`. Cancelled orders excluded from aggregates. `recompute()` refreshes the membership cache; subsequent `evaluate(slug)` reads off the cache for paginated iteration. Migration `0049_customer_segments.sql`. · *`recentlyViewed` primitive — per-customer / per-session browse history* — `bShop.recentlyViewed.create({ query?, catalog })` returns `{ recordView, forCustomer, forSession, merge, recommend, cleanupOlderThan, purgeCustomer }`. Session ids are namespace-hashed (`recently-viewed-session`) before write — the log never holds a recoverable identifier. Re-viewing a product within 5 minutes updates the existing row's `last_viewed_at` + increments `view_count` rather than writing a new one. `forCustomer({ exclude_purchased: true })` filters out products the customer has already bought. `merge({ session_id, customer_id })` rolls a guest session into the customer record at login. Sliding-window cap default 20. Migration `0050_recently_viewed.sql`. · *`stockAlerts` primitive — back-in-stock notifications with confirmation tokens* — `bShop.stockAlerts.create({ query?, catalog, notifications? })` returns `{ subscribe, unsubscribe, confirm, isSubscribed, listForSku, listForCustomer, scanAndNotify, cleanupExpired, stats, hashEmail }`. `subscribe` mints a 32-char base64url confirmation token (hash stored, plaintext returned once). `scanAndNotify({ now })` walks pending alerts where `stock_on_hand - stock_held > 0` and (when `notifications` is wired) enqueues an in-app message under event-type `stock.back-in-stock`. Email + token always hashed via `namespaceHash` before write. Migration `0048_stock_alerts.sql`. · *`shippingLabels` primitive — carrier-agnostic shipping label management* — `bShop.shippingLabels.create({ query?, cursorSecret? })` returns `{ requestLabel, markPurchased, voidLabel, markUsed, getLabel, labelsForShipment, labelsForOrder, pendingLabels, voidedInWindow, costsByPeriod, customsForLabel }`. `purchased_via` enum: `easypost / shippo / shipstation / stamps_com / manual / custom`. `label_url` gated through `b.safeUrl.parse` (https-only). `voidLabel` refuses past the carrier's 30-day window with a typed error code. `costsByPeriod({ from, to, carrier? })` groups by purchased_via + currency for cost-allocation dashboards. The actual label-generation HTTP call is the operator's worker. Migration `0051_shipping_labels.sql`. · *`returnLabels` primitive — operator-funded return-shipping flow* — `bShop.returnLabels.create({ query?, returns? })` returns `{ issueLabel, markShipped, markInTransit, markDelivered, markException, getLabel, labelForReturn, pendingPickup, inTransit, eventsForLabel }`. FSM: `issued → shipped → in_transit → delivered`, with an `exception` terminal branch. `markInTransit` accepts re-entry so a multi-hop journey produces a row-per-scan timeline. `markDelivered` calls the injected `returns.markReceived` callback so the RMA flips to `received` status automatically. `issueLabel` refuses unless the underlying return is in `approved` status. Migration `0052_return_labels.sql`. · *`promoBanners` primitive — operator-controlled marketing banners across storefront placements* — `bShop.promoBanners.create({ query?, customerSegments? })` returns `{ defineBanner, activeForPlacement, listAll, getBanner, updateBanner, archive, unarchive, renderHtml, impressionCount, clickCount, recordImpression, recordClick }`. Placements: `top_strip / homepage_hero / pdp_side / cart_side / search_empty / footer`. Audiences: `all / logged_in / guest / segment`. Themes: `info / promo / urgency / success`. `cta_url` gated through `b.safeUrl.parse` (https-only + /-rooted internal paths). `renderHtml` escapes every operator-input field via `b.template.escapeHtml` so script / img-onerror payloads render inert. `recordImpression` / `recordClick` drop-silent on bad slug (hot-path). Migration `0053_promo_banners.sql`. · *`searchSynonyms` primitive — query rewriting + typo correction + stopword removal* — `bShop.searchSynonyms.create({ query? })` returns `{ addGroup, addTypo, addStopword, removeStopword, getGroup, listGroups, updateGroup, deleteGroup, listTypos, listStopwords, rewrite, learnFromQueries }`. Group kinds: `bidirectional` (any term matches any other) + `directional` (terms[0..N-1] all map to terms[N], canonical is terms[N]). `rewrite(query, { include_corrections?, max_expansions? })` returns `{ canonical, expansions, corrections }`. Tokenise → strip control bytes (don't refuse — denial-of-search would block legitimate shoppers) → stopword drop → typo correction → conservative English stemmer (`-ies / -ing / -ed / -es / -s`, min length 4). `learnFromQueries({ from, to, min_count })` scans `search_query_log` for term co-occurrences not yet covered by an existing group. Migration `0055_search_synonyms.sql`. · *`affiliates` primitive — partner program with single-use codes + per-affiliate commission ledger* — `bShop.affiliates.create({ query?, cursorSecret? })` returns `{ registerAffiliate, getAffiliate, affiliateByCode, listAffiliates, updateAffiliate, pauseAffiliate, reinstateAffiliate, recordVisit, attributionForSession, recordCommissionEvent, commissionsForAffiliate, markCommissionPaid, markCommissionVoided, payoutsDue, topAffiliates }`. Codes are 8 chars over a confusion-resistant 32-glyph alphabet. Commission kinds: `percent_bps / amount_per_order_minor / amount_per_signup_minor`. `commission_minor` is computed at write time and stored — historical payouts don't shift when the operator later edits the affiliate's rate. `recordCommissionEvent` is idempotent on `(order_id, affiliate_id)`. Email + visitor session id always hashed via `namespaceHash` before write. Migration `0057_affiliates.sql`. · *`mailingAudiences` primitive — newsletter audience segmentation with suppression filtering* — `bShop.mailingAudiences.create({ query?, newsletter?, emailSuppressions?, cursorSecret? })` returns `{ defineAudience, resolve, count, getAudience, listAudiences, update, archive, recompute, auditDelivery, listDeliveries }`. Audience rules over `newsletter_signups`: `subscribed_after/before`, `source_in`, `tag_any/all`, `country_in`, `double_opt_in`, `language_in`, `customer_status_in`. `resolve({ slug, skip_suppressed: true })` filters out suppressions via the injected `emailSuppressions` peer (default true). Defaults: emails return hashed only; plaintext requires `include_plaintext: true` for verifier-role-gated routes. `recompute()` refreshes the per-slug membership cache. `auditDelivery` writes append-only rows for compliance export. Unsubscribed signups never match any rule. Migration `0056_mailing_audiences.sql` (+ additive columns on `newsletter_signups` for `tags_csv` / `country` / `language` / `customer_status` / `double_opt_in_at`). · *`orderTimeline` primitive — unified per-order event feed across every order-touching primitive* — `bShop.orderTimeline.create({ query?, order?, orderTracking?, orderNotes?, returns?, payment?, fraudScreen?, shippingLabels?, notifications? })` returns `{ forOrder, summarize, customerVisibleFor, compareOrders, recentActivity }`. Aggregates events from `order_transitions`, `shipment_events`, `order_notes`, `return_authorizations`, `fraud_screenings`, `shipping_labels`, and `notifications` into a chronologically-sorted feed newest-first. `customerVisibleFor({ order_id, locale })` filters to events the customer should see (en / es / de built-in, unknown locales fall back to English). `summarize(order_id)` returns `{ status, first_event_at, last_event_at, event_count, milestones: { paid_at?, fulfilled_at?, shipped_at?, delivered_at?, refunded_at? }, cache_hit }`. Cache invalidation tracks both `last_event_at` AND a per-source row count so a same-millisecond event still flushes the cache. Every injected peer is optional. Migration `0054_order_timeline_cache.sql`. **Fixed:** *`returnLabels.issueLabel` — accepts an optional `issued_at` epoch-ms for deterministic test ordering* — Synthetic-timestamp tests that mix `markShipped({ shipped_at: ... })` with the issuer's wall-clock `Date.now()` could see the issued event sort AFTER the synthetic ones. `issueLabel` now accepts `issued_at` so the full timeline can be authored from a single clock source. Production callers omit the field; `Date.now()` is the default. · *`orderTimeline.summarize` — cache freshness check covers every source the collector walks* — `_sourceStats` now joins `shipping_labels` (via `shipments.order_id`) and `notifications` (via `payload_json` `LIKE '%"order_id":"<uuid>"%'`) so the per-source row count matches `_collectAll`. Previously, an order with any shipping-label or notification rows had a mismatched count between cache-write and cache-check, and the cache never registered as fresh. The second `summarize(order_id)` call now correctly hits the cache.
|
|
12
|
+
|
|
13
|
+
- v0.0.58 (2026-05-22) — **Five new primitives: sales reports, quantity discounts, subscription controls, gift options, support tickets.** Five primitives ship in one release. `salesReports` aggregates orders for operator dashboards with cursor-paginated detail views + opt-in memoization. `quantityDiscounts` adds automatic tier pricing (buy N for X% off) scoped per SKU / product / collection / vendor / category / global, distinct from coupon codes. `subscriptionControls` layers pause / resume / skip-next / change-quantity / change-frequency / cancel / reactivate on top of the existing `subscriptions` surface with a full audit ledger. `giftOptions` adds gift wrapping (operator-configured wrap SKUs with fees) + gift message + recipient-name + gift-receipt toggle, with HTML-escape at every rendered field. `supportTickets` is a broader customer-service surface than `orderNotes` — pre-sale questions, account problems, complaints — with threaded messages, priority FSM, SLA timer, and per-operator assignment. **Added:** *`salesReports` primitive — order aggregations for operator dashboards* — `bShop.salesReports.create({ query?, cursorSecret? })` returns `{ revenueByDay, revenueByWeek, revenueByMonth, topProducts, topCustomers, revenueByCountry, revenueByPaymentMethod, customerCohort, aov, refundRate, funnel, purgeExpired }`. Each surface accepts `{ from, to, currency? }`; top-N surfaces add `{ limit, cursor? }` for HMAC-paginated detail. Opt-in memoization (`cache: true`) writes to a `sales_report_cache` row keyed on `(report_key, params_hash)` with operator-tuned TTL; default is fresh recompute so first-load dashboards never serve stale data. `customerCohort(cohort_month)` returns per-month retention for the cohort that first ordered in `cohort_month`. `funnel({ from, to })` counts checkout-started / payment-intent-created / paid / fulfilled / refunded. Migration `0042_sales_report_cache.sql` (id, report_key, params_hash, result_json, computed_at, expires_at + indexes on (report_key, expires_at), (params_hash)). · *`quantityDiscounts` primitive — automatic tier pricing per SKU / product / collection / vendor / category / global* — `bShop.quantityDiscounts.create({ query?, catalog })` returns `{ defineTier, getTiersForLine, applyToLine, applyToCart, update, archive, unarchive, list, tierBreakdown }`. Discount kinds: `percent_off (bps)`, `amount_off_each (minor)`, `amount_off_total (minor)`, `fixed_each_price (minor)`. `applyToLine` returns `{ original_unit_minor, discounted_unit_minor, line_subtotal_minor, line_discount_minor, applied_tier_id }` after walking applicable rules ordered by scope-specificity (sku > product > collection > vendor > category > global) and picking the best one. `exclusive: true` on a tier set short-circuits stacking. `defineTier` refuses overlapping `min_quantity` values within a single tier set so the schedule is unambiguous. Per-unit math composes `b.money.fromMinorUnits(...).multiply([num, den])` for half-even rounding consistent with the framework Money class. Migration `0044_quantity_discounts.sql` (`qd_tier_sets` + `qd_tiers` tables). · *`subscriptionControls` primitive — pause / resume / skip / change-quantity / change-frequency / cancel / reactivate* — `bShop.subscriptionControls.create({ query?, subscriptions })` returns `{ pause, resume, skipNext, changeQuantity, changeFrequency, cancel, reactivate, historyForSubscription, actorReport, scanAutoResume }`. `pause({ until?, reason })` flips active → paused; an `until` value schedules auto-resume via `scanAutoResume(now)`. `skipNext({ count, reason })` bumps `next_billing_at` by N periods without changing status; `changeFrequency({ new_frequency, reason })` recomputes `next_billing_at` on the new schedule (weekly / biweekly / monthly / quarterly / semiannual / annual). `cancel({ immediate? })` defaults to end-of-period (customer keeps access through the paid period); `immediate: true` ends now. `reactivate` allows cancelled → active within a 90-day grace window; refuses past. Every state change writes a `subscription_control_events` row with `actor_type`, `actor_id`, `before_json`, `after_json`, and a 280-char operator-prose `reason` so customer service can replay the full state arc. Migration `0045_subscription_controls.sql` (new event table + ALTER TABLE additions to `subscriptions` for `paused_at` / `paused_until` / `cancelled_at` / `quantity` / `frequency` / `next_billing_at`). · *`giftOptions` primitive — gift wrapping, gift message, gift receipt* — `bShop.giftOptions.create({ query?, catalog })` returns `{ defineWrap, listWraps, getWrap, updateWrap, archiveWrap, setForOrder, getForOrder, clearForOrder, feeForOrder, renderPackingSlipLine, analytics }`. `wrap_sku` references a real catalog SKU so wrap inventory + cost flow through normal channels. `setForOrder({ order_id, wrap_sku?, gift_message?, recipient_name?, hide_prices? })` validates `gift_message` (≤ 500 chars, control-byte-free, zero-width-char-free) + `recipient_name` (≤ 120 chars) + refuses unknown / archived wrap_sku. `renderPackingSlipLine({ order_id, locale })` returns `{ message_lines, recipient_name, hide_prices }` with every operator-input field HTML-escaped via `b.template.escapeHtml` (script-in-message / img-onerror payloads are rendered inert). `analytics({ from, to })` powers the operator dashboard (gift-option order count, top wrap_skus, gift-message rate). Migration `0046_gift_options.sql` (`gift_wraps` + per-order `gift_options` rows). · *`supportTickets` primitive — threaded customer-service surface with SLA timers* — `bShop.supportTickets.create({ query?, cursorSecret? })` returns `{ open, reply, transition, assign, unassign, addTag, removeTag, get, listForCustomer, listOpenAssignedTo, listUnassigned, thread, slaCheck, metrics }`. Distinct from `orderNotes` — a ticket isn't necessarily tied to an order. Categories: `pre_sale / order_issue / shipping / billing / refund / account / complaint / feature_request / other`. Priority: `low / normal / high / urgent`. Status FSM: `new → in_progress → waiting_customer → in_progress → resolved → closed`, with `reopened` re-entering `in_progress` and `closed` reachable from any state. Operator reply auto-flips `new → in_progress`. Email always hashed via `namespaceHash('support-ticket-email', ...)` before write. `slaCheck()` returns tickets breaching SLA (urgent 1h / high 4h / normal 24h / low 72h since last operator action). `metrics({ from, to })` covers per-category counts, average first-response time, resolution rate, escalation rate. Migration `0047_support_tickets.sql` (`support_tickets` + `support_messages` + `support_status_history` tables).
|
|
14
|
+
|
|
11
15
|
- v0.0.57 (2026-05-22) — **Ten new primitives: bundles, variants, multi-warehouse inventory, backorder, payment methods, order notes, fraud screening, order export, print-on-demand, save-for-later.** Ten primitives ship in one release. `bundles` registers kit-products that expand at cart resolution time. `variants` is the option-axis layer that lets a single product carry the cartesian product of color × size × style (etc.) with per-variant overrides. `inventoryLocations` splits stock across N warehouses + retail stores + drop-ship suppliers with a routing strategy. `backorder` lets out-of-stock SKUs still be ordered with an expected-ship date carried to the PDP. `paymentMethods` stores per-customer processor tokens (Stripe payment-method ids, PayPal billing-agreement ids, etc.) with strict refuses on PAN / CVV-shaped fields. `orderNotes` adds a threaded customer-service surface attached to each order with `internal` and `customer_visible` visibility. `fraudScreen` returns a `{score, decision, signals}` triage at checkout from a 13-signal heuristic. `orderExport` streams CSV / NDJSON / scheduled-export rows with CSV-injection refusal at every cell. `printOnDemand` is the supplier-agnostic POD passthrough (Printful / Printify / Gelato / Lulu / Gooten / Cloudprinter + `custom`). `saveForLater` parks a cart line on the customer's account without losing the snapshot price. **Added:** *`bundles` primitive — kit / multi-pack products that expand at cart resolution* — `bShop.bundles.create({ query?, catalog })` returns `{ defineBundle, getBundle, listBundles, expand, priceBundle, updateBundle, deleteBundle }`. Bundle SKUs reference real catalog SKUs (or nested bundle SKUs up to 2 levels). `expand({ bundle_sku, quantity })` returns the flattened component list with multipliers applied. `priceBundle` sums child priceMinor and applies an optional `bundle_discount_bps` override. Cycle detection refuses bundles that eventually point back to themselves. `deleteBundle` refuses while any active cart line references the bundle. Migration `0032_bundles.sql` (`bundles` + `bundle_components` tables with UNIQUE(bundle_sku, sku) + FK CASCADE). · *`variants` primitive — product option matrix with per-variant overrides* — `bShop.variants.create({ query?, catalog })` returns `{ defineAxis, generateMatrix, materializeMatrix, getVariant, variantsForProduct, findVariant, updateVariant, archiveVariant, unarchiveVariant, archiveAxisOption }`. Operators register axes (`color`, `size`, etc.) with positions and option labels. `generateMatrix` previews the cartesian product without writing — the operator validates before `materializeMatrix` commits. SKU shape: `<prefix>-<axis1-opt>-<axis2-opt>-...` lowercase ASCII slug. `findVariant({ product_id, axis_values: { color: 'red', size: 'L' } })` resolves a specific cell. `image_url` validates via `b.safeUrl` so `javascript:` / `data:` schemes are refused at write. Migration `0033_product_variants.sql` (`product_variant_axes` + `product_variant_axis_options` + `product_variants` tables). · *`inventoryLocations` primitive — multi-warehouse stock + order routing* — `bShop.inventoryLocations.create({ query?, catalog })` returns `{ defineLocation, listLocations, getLocation, updateLocation, deactivateLocation, setStock, adjustStock, transferStock, stockForSku, totalForSku, availableLocations, routeOrder }`. Each location has a type (`warehouse` / `retail` / `dropship`) + priority + active flag. `transferStock` is an atomic two-row write so no stock is created or lost. `routeOrder` strategies: `priority-fill` (default — walks active locations in priority order), `single-location-cheapest`, `nearest-by-postal-prefix`. Returns `{ allocation: [{ location_code, lines }], unfulfillable }`. `inventory_adjustments` ledger is append-only — every `adjustStock` writes a reason-tagged row for audit replay. Migration `0034_inventory_locations.sql`. · *`backorder` primitive — orderable when out-of-stock with expected-ship dates* — `bShop.backorder.create({ query?, catalog })` returns `{ markBackorderable, markNotBackorderable, getStatus, listBackorderable, recordBackorder, fulfillBackorder, cancelBackorder, availabilityFor, customerBackorders, pendingForSku, arrivalsThisWeek }`. `availabilityFor(sku)` returns `{ status: 'in_stock' | 'backorderable' | 'out_of_stock', expected_ship_date?, message? }` for the PDP. `recordBackorder` enforces the per-sku `max_backorder_quantity` cap (NULL = unlimited). `arrivalsThisWeek` returns backorders with `expected_ship_date <= now + 7d` for operator-facing dashboards. Migration `0035_backorder.sql` (`backorder_skus` config table + `backorder_lines` per-order rows with status FSM (pending / fulfilled / cancelled)). · *`paymentMethods` primitive — saved processor tokens with PAN / CVV refusal* — `bShop.paymentMethods.create({ query? })` returns `{ add, get, listForCustomer, setDefault, archive, markExpired, defaultForCustomer, byProcessorToken, audit }`. Add refuses any field that looks like a raw PAN (regex: 13-19 digits in a row, including hyphen / space-collapsed forms), refuses CVV-shaped fields, validates processor enum (stripe / paypal / square / braintree / authorize_net), validates last4 (exactly 4 digits), validates expiry not in the past. `setDefault` enforces write-side default-uniqueness — promoting clears sibling default in the same transaction. `markExpired` is operator-scheduler-callable and writes an audit row per archived method. `audit(payment_method_id)` returns the row's full state-change history. Migration `0036_payment_methods.sql` (`payment_methods` + `payment_method_audit` tables with partial UNIQUE index on `(customer_id) WHERE is_default = 1 AND archived_at IS NULL`). · *`orderNotes` primitive — threaded customer-service notes per order* — `bShop.orderNotes.create({ query?, cursorSecret? })` returns `{ add, get, listForOrder, thread, markRead, pin, unpin, customerVisibleForOrder, internalForOrder, resolve, reopen }`. Visibility enum: `internal` (operator-only) / `customer_visible` (rendered on the customer's order page + emailed). Body cap 8000 chars, refuses control bytes + zero-width chars + empty-after-trim. `thread` returns a tree-shaped reply graph. `pin` floats a note to the top of the listing; resolve / reopen runs an internal-thread workflow with a 280-char resolution summary. Migration `0037_order_notes.sql` (`order_notes` + `order_note_reads` tables with indexes on (order_id, visibility, created_at desc) and (order_id, pinned desc, created_at desc)). · *`fraudScreen` primitive — heuristic risk score + decision at checkout* — `bShop.fraudScreen.create({ query?, customers?, addresses?, paymentMethods?, emailSuppressions? })` returns `{ screen, recordOutcome, recordChargeback, flagEmail, unflagEmail, customerRiskHistory, recentScreenings, hashEmail }`. `screen({ order_draft })` returns `{ score: 0..100, decision: 'approve' | 'review' | 'step_up' | 'refuse', signals }`. Decision thresholds: 0-39 approve, 40-69 review, 70-89 step_up, 90+ refuse. 13 signals fire independently and stack with operator-tunable weights: velocity (>N orders/24h from same email hash), high-value-new-customer, address-mismatch, free-email-domain, disposable-email-domain, session-too-fast (< 15s), session-too-old (> 24h), ua-curl-class, large-line-count, mismatched-bin-country (operator-supplied BIN → country map), prior-chargeback (looks up chargebacks ledger by email hash), suppressed-email, manually-flagged. Emails always hashed via `namespaceHash('fraud-email', ...)` before write. Migration `0038_fraud_screenings.sql` (`fraud_screenings` + `fraud_chargebacks` + `fraud_email_flags` tables). · *`orderExport` primitive — CSV / NDJSON streaming export with CSV-injection refusal* — `bShop.orderExport.create({ query?, order, cursorSecret? })` returns `{ csvForRange, ndjsonForRange, summaryForRange, scheduleExport, cancelExport, getExport, listExports, markExportRunning, markExportComplete, markExportFailed }`. Built-in 24-column schema covering order id / status / customer hash / addresses / line counts / monetary totals / payment method / fulfillment status / refund / return / chargeback flags. RFC-4180 quoting with CRLF; cell content starting with `=`, `+`, `-`, `@` gets prefixed with `'` per OWASP guidance (signed numerics like `+15.00` are exempt). Email is hashed via `namespaceHash('order-export-email', ...)` — raw email never reaches the export. `scheduleExport` writes a queued row + the operator's worker walks the `pod_fulfillments`-style FSM (queued → running → complete | failed | cancelled). Migration `0039_scheduled_exports.sql`. · *`printOnDemand` primitive — supplier-agnostic POD passthrough* — `bShop.printOnDemand.create({ query?, catalog })` returns `{ bindSku, unbindSku, getBinding, listBindings, updateBinding, costForOrder, forwardOrder, markFulfillmentSubmitted, markFulfillmentShipped, markFulfillmentFailed, markFulfillmentCancelled, getFulfillment, fulfillmentsForOrder, pendingFulfillments }`. Supplier enum: `printful` / `printify` / `gelato` / `lulu` / `gooten` / `cloudprinter` / `custom`. The primitive stores the SKU → supplier-product binding (artwork URL + variant id + position config + colorway + operator's wholesale `cost_minor`) and the per-order pending-fulfillment row. The actual HTTP push to each supplier is the operator's worker (each has its own auth + endpoint). `costForOrder(order_lines)` returns `{ total_cost_minor, by_supplier, unbound_skus }` for margin reporting. Migration `0040_print_on_demand.sql` (`pod_bindings` + `pod_fulfillments` tables with FSM-driven status column). · *`saveForLater` primitive — park a cart line on the customer account* — `bShop.saveForLater.create({ query?, catalog, currency?, cursorSecret? })` returns `{ moveFromCart, moveToCart, add, remove, clear, listForCustomer, countForCustomer, staleCheck, repriceAll, expireOlderThan }`. Distinct from `wishlist` — `wishlist` is "I'm interested", `saveForLater` is "I was about to buy this but not now". Lines carry their original cart context (price-at-save, variant, quantity, notes) so the customer can re-add with the snapshot price or the current price (`moveToCart` `use_price` parameter). `staleCheck` flags `is_stale` (price changed), `is_unavailable` (SKU gone), `is_low_stock` (catalog quantity < quantity-saved) per row. Refuses re-add to cart when the SKU is now out-of-stock and not backorderable. `expireOlderThan(days)` is the operator-scheduler sweep. Migration `0041_save_for_later.sql` with UNIQUE(customer_id, sku, COALESCE(variant_id, '')). **Fixed:** *`orderNotes` — monotonic `created_at` so back-to-back inserts always sort newest-first* — `Date.now()` on Windows resolves at ~15ms — three notes inserted in the same millisecond shared a `created_at` value, and the secondary `id DESC` tiebreaker fell back to UUID v7's random suffix (unstable since the time prefix collapsed). The primitive now stamps `created_at` with a process-local monotonic clock that increments by 1ms when `Date.now()` returns a stale value, so the listing's `pinned DESC, created_at DESC, id DESC` ordering reliably places the most recent note first regardless of insert burstiness.
|
|
12
16
|
|
|
13
17
|
- v0.0.56 (2026-05-22) — **Republish of the eleven-primitive batch to npm.** 0.0.55 source landed on `main` but the npm publish workflow ran against the prior commit and didn't reach the registry. 0.0.56 republishes the same eleven primitives (`orderTracking`, `returns`, `loyalty`, `referrals`, `notifications`, `addresses`, `taxExempt`, `emailSuppressions`, `currencyDisplay`, `searchSuggestions`, `cartAbandonment`) plus migrations `0021`-`0031` so `npm install @blamejs/blamejs-shop@0.0.56` pulls the full surface. No code changes from 0.0.55 — only the version bump + this changelog entry. **Fixed:** *Republish to npm — 0.0.55 source on `main` reached operators via `git clone` but not via `npm install`* — `npm install @blamejs/blamejs-shop@0.0.55` would resolve to a missing version. 0.0.56 ships the identical surface so operators upgrading by version number get the full eleven-primitive batch + the eleven new migrations. Tag `v0.0.55` remains on the repo; operators referring to either version see equivalent code.
|