@blamejs/blamejs-shop 0.4.50 → 0.4.52
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/asset-manifest.json +1 -1
- package/lib/consent-ledger.js +50 -3
- package/lib/storefront.js +145 -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.4.x
|
|
10
10
|
|
|
11
|
+
- v0.4.52 (2026-06-14) — **Consent records now carry the GDPR Article 6 lawful basis they rest on.** Each decision in the durable per-customer consent ledger now records the GDPR Article 6(1) lawful basis it is processed under — consent, contract, legal obligation, vital interests, public task, or legitimate interests — alongside what was decided and when. The basis defaults from the consent kind (cookie categories, marketing email/SMS, and data-sharing opt-ins are all consent-based), and a caller may pass an explicit, validated basis for a record that rests on another. The basis is surfaced in the subject-access and supervisory-authority exports, so a consent audit now shows not just the decision but the legal ground for it. A migration adds a nullable, constrained lawful_basis column; pre-existing rows keep a null basis rather than being assigned one retroactively. Apply the pending migration on upgrade. **Added:** *Lawful basis on consent-ledger records* — The consent ledger now stamps the GDPR Article 6(1) lawful basis on every recorded decision. The basis defaults from the consent kind — every category the ledger tracks (cookie functional/analytics/marketing/preferences, marketing email, marketing SMS, partner and analytics data-sharing, and general data processing) is consent-based — and an explicit basis can be supplied and is validated against the six Article 6(1) values. Withdrawal records carry the same basis as the grant they revoke. The basis is included in the subject-access export (CSV and JSON) and the jurisdiction bulk export, so a consent audit shows the legal ground each decision rests on. **Changed:** *Migration: lawful_basis column on consent_ledger* — A new migration adds a nullable lawful_basis column to consent_ledger, constrained to the six Article 6(1) bases. The column is nullable and the constraint applies only to rows written after the migration, so existing records keep a null basis (not retroactively assigned) while every new record carries one. Apply the pending migration on upgrade.
|
|
12
|
+
|
|
13
|
+
- v0.4.51 (2026-06-14) — **The order page now shows a dated activity timeline of the order's lifecycle.** A customer viewing their order now sees a chronological activity feed of what has happened to it — placed, payment received, shipped, delivered, cancelled, and refunds — newest first, each with its date and the relevant detail. A shipped event shows the carrier and tracking number when one was recorded; a refund shows its amount (and marks a partial refund as such); a click-and-collect delivery shows the pickup location. The feed is built from the order's own transition history and is scoped to the order being viewed, so it appears only to the order's owner or a guest holding the order's access link. Internal fulfillment bookkeeping and operator-process detail are not surfaced — only customer-meaningful milestones. The order page renders this server-side with no client JavaScript. No migration to apply. **Added:** *Order activity timeline on the order page* — The order detail page renders a dated, newest-first timeline of the order's lifecycle events: placed, payment received, shipped (with carrier and tracking number when present), delivered (with pickup location for click-and-collect), cancelled, and refunds (with the refunded amount, labelled partial when applicable). Events are drawn from the order's transition history and filtered to customer-meaningful milestones — internal fulfillment steps, raw state transitions, operator-process reasons, and payment-provider identifiers are never shown. Every rendered value is HTML-escaped, and the feed is shown only within the order's existing access scope (the signed-in owner or a guest order's access link).
|
|
14
|
+
|
|
11
15
|
- v0.4.50 (2026-06-14) — **Refresh the vendored blamejs framework to 0.15.11.** Refreshes the vendored blamejs framework from 0.15.9 to 0.15.11 (through 0.15.10). 0.15.11 replaces a family of quadratic-time regexes that hostile input could exploit to stall a worker with linear-time scans, refuses a relocatable sealed-cell downgrade on the read side, fails closed when row-level security is enabled behind a non-native driver, and verifies the vendored crypto against a reviewed pin. 0.15.10 makes S3 Object-Lock versioned erasure reachable through the object store — a versioned delete that targets a specific object version (refused under an active retention rather than silently writing a delete-marker), plus version enumeration for right-to-erasure workflows — and pins the build toolchain's native bundler binary to a reviewed hash. This refresh carries no shop-facing API change and applies no migration; it keeps the bundled framework current and the security posture aligned with the latest release. **Changed:** *Vendored blamejs refreshed to 0.15.11* — The bundled framework is updated to blamejs 0.15.11. Across 0.15.10 and 0.15.11 it hardens a family of regular expressions against quadratic-time blow-up on hostile input (linear-time scans), refuses a relocatable sealed-cell downgrade when reading, fails closed when row-level security is enabled behind a driver that can't enforce it, adds versioned object erasure for S3 Object-Lock buckets (a versioned delete that refuses an active retention instead of leaving the data behind a delete-marker, plus version enumeration), and pins the build toolchain's native binary to a reviewed hash. Storefront and admin behaviour is unchanged by the refresh; the framework's PQC-first crypto, security middleware, and request lifecycle are carried forward as-is.
|
|
12
16
|
|
|
13
17
|
- v0.4.49 (2026-06-14) — **A stock write-off or audit adjustment can no longer strand a paid hold by dropping on-hand below what's reserved.** An operator stock write-off and a cycle-count audit adjustment both debit on-hand stock through the location adjustment, which guarded only against the shelf going below zero — not below the quantity already reserved by outstanding held allocations. So a write-off (or an audit-applied negative variance) could drop a location's on-hand below its outstanding holds, and when one of those holds later committed at fulfillment its debit would fail, stranding a paid order that could no longer be picked. Write-offs and audit adjustments now refuse a debit that would push on-hand below the held quantity for that SKU at that location (un-pinned holds count against every location), enforced inside the write so concurrent debits can't slip past it. A hold's own commit debit is unaffected — that stock is already reserved to it. No migration to apply. **Fixed:** *Stock write-offs and audit adjustments respect reserved holds* — The location stock adjustment now takes a hold-respecting mode used by write-offs and audit variance application: a debit is refused when it would drop on-hand below the outstanding held quantity for the SKU at that location, evaluated atomically as part of the write (un-pinned holds count against every location, matching the availability rule). Previously these operator adjustments only prevented on-hand from going negative, so they could shrink the shelf below what paid/committed holds had reserved — and the hold's later commit would then fail at fulfillment. Committing a hold still debits normally, since that stock is already reserved to it.
|
package/lib/asset-manifest.json
CHANGED
package/lib/consent-ledger.js
CHANGED
|
@@ -131,6 +131,7 @@ var CSV_COLUMNS = Object.freeze([
|
|
|
131
131
|
"consent_kind",
|
|
132
132
|
"state",
|
|
133
133
|
"source",
|
|
134
|
+
"lawful_basis",
|
|
134
135
|
"jurisdiction",
|
|
135
136
|
"evidence_ref",
|
|
136
137
|
"occurred_at",
|
|
@@ -138,6 +139,32 @@ var CSV_COLUMNS = Object.freeze([
|
|
|
138
139
|
|
|
139
140
|
var b = require("./vendor/blamejs");
|
|
140
141
|
|
|
142
|
+
// The GDPR Art. 6(1) lawful bases, sourced from the framework's
|
|
143
|
+
// consent primitive so this ledger's vocabulary tracks the
|
|
144
|
+
// primitive's ground truth instead of re-listing the six strings.
|
|
145
|
+
var LAWFUL_BASES = b.consent.LAWFUL_BASES;
|
|
146
|
+
|
|
147
|
+
// Every consent kind this ledger records is consent-gated: cookies
|
|
148
|
+
// are non-essential under ePrivacy Art. 5(3) (the strictly-necessary
|
|
149
|
+
// set lives in cookieConsent, never here), marketing email / SMS rest
|
|
150
|
+
// on Art. 6(1)(a), and third-party data sharing is not a contract
|
|
151
|
+
// necessity. data_processing is the opt-in catch-all — contract /
|
|
152
|
+
// legal-obligation processing is recorded by the domain table that
|
|
153
|
+
// owns it, not by this opt-in ledger. So each kind maps to "consent".
|
|
154
|
+
// A caller may still pass an explicit, validated lawful_basis to
|
|
155
|
+
// override the default for a row that genuinely rests on another basis.
|
|
156
|
+
var KIND_TO_LAWFUL_BASIS = Object.freeze({
|
|
157
|
+
cookies_functional: "consent",
|
|
158
|
+
cookies_analytics: "consent",
|
|
159
|
+
cookies_marketing: "consent",
|
|
160
|
+
cookies_preferences: "consent",
|
|
161
|
+
marketing_email: "consent",
|
|
162
|
+
marketing_sms: "consent",
|
|
163
|
+
data_sharing_partners: "consent",
|
|
164
|
+
data_sharing_analytics: "consent",
|
|
165
|
+
data_processing: "consent",
|
|
166
|
+
});
|
|
167
|
+
|
|
141
168
|
// ---- monotonic clock ----------------------------------------------------
|
|
142
169
|
//
|
|
143
170
|
// Operator-driven writes can land in the same millisecond on fast
|
|
@@ -195,6 +222,16 @@ function _source(s) {
|
|
|
195
222
|
return s;
|
|
196
223
|
}
|
|
197
224
|
|
|
225
|
+
function _lawfulBasis(s) {
|
|
226
|
+
if (typeof s !== "string" || LAWFUL_BASES.indexOf(s) === -1) {
|
|
227
|
+
throw new TypeError(
|
|
228
|
+
"consentLedger: lawful_basis must be one of " + LAWFUL_BASES.join(", ") +
|
|
229
|
+
", got " + JSON.stringify(s)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return s;
|
|
233
|
+
}
|
|
234
|
+
|
|
198
235
|
function _optJurisdiction(s) {
|
|
199
236
|
if (s == null || s === "") return null;
|
|
200
237
|
if (typeof s !== "string") {
|
|
@@ -282,6 +319,7 @@ function _rowToRecord(row) {
|
|
|
282
319
|
consent_kind: row.consent_kind,
|
|
283
320
|
state: row.state,
|
|
284
321
|
source: row.source,
|
|
322
|
+
lawful_basis: row.lawful_basis == null ? null : row.lawful_basis,
|
|
285
323
|
jurisdiction: row.jurisdiction == null ? null : row.jurisdiction,
|
|
286
324
|
evidence_ref: row.evidence_ref == null ? null : row.evidence_ref,
|
|
287
325
|
occurred_at: Number(row.occurred_at),
|
|
@@ -327,15 +365,23 @@ function create(opts) {
|
|
|
327
365
|
var source = _source(input.source);
|
|
328
366
|
var jurisdiction = _optJurisdiction(input.jurisdiction);
|
|
329
367
|
var evidenceRef = _optEvidenceRef(input.evidence_ref);
|
|
368
|
+
// Lawful basis defaults from the kind (every kind here is
|
|
369
|
+
// consent-gated; the basis is state-agnostic so a withdrawal row
|
|
370
|
+
// carries the same basis as its grant). An explicit lawful_basis is
|
|
371
|
+
// validated and overrides the default for a row that rests on a
|
|
372
|
+
// different Art. 6 basis.
|
|
373
|
+
var lawfulBasis = input.lawful_basis == null
|
|
374
|
+
? KIND_TO_LAWFUL_BASIS[consentKind]
|
|
375
|
+
: _lawfulBasis(input.lawful_basis);
|
|
330
376
|
|
|
331
377
|
var id = b.uuid.v7();
|
|
332
378
|
var ts = _now();
|
|
333
379
|
|
|
334
380
|
await query(
|
|
335
381
|
"INSERT INTO consent_ledger " +
|
|
336
|
-
"(id, customer_id, consent_kind, state, source, jurisdiction, evidence_ref, occurred_at) " +
|
|
337
|
-
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
|
338
|
-
[id, customerId, consentKind, state, source, jurisdiction, evidenceRef, ts],
|
|
382
|
+
"(id, customer_id, consent_kind, state, source, lawful_basis, jurisdiction, evidence_ref, occurred_at) " +
|
|
383
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
|
384
|
+
[id, customerId, consentKind, state, source, lawfulBasis, jurisdiction, evidenceRef, ts],
|
|
339
385
|
);
|
|
340
386
|
|
|
341
387
|
return {
|
|
@@ -344,6 +390,7 @@ function create(opts) {
|
|
|
344
390
|
consent_kind: consentKind,
|
|
345
391
|
state: state,
|
|
346
392
|
source: source,
|
|
393
|
+
lawful_basis: lawfulBasis,
|
|
347
394
|
jurisdiction: jurisdiction,
|
|
348
395
|
evidence_ref: evidenceRef,
|
|
349
396
|
occurred_at: ts,
|
package/lib/storefront.js
CHANGED
|
@@ -7589,6 +7589,7 @@ var ORDER_PAGE =
|
|
|
7589
7589
|
" </div>\n" +
|
|
7590
7590
|
" <aside class=\"order-page__totals\">\n" +
|
|
7591
7591
|
" RAW_ORDER_TIMELINE" +
|
|
7592
|
+
" RAW_ORDER_EVENTS" +
|
|
7592
7593
|
" RAW_ORDER_TRACKING" +
|
|
7593
7594
|
" <h2 class=\"pdp__variants-title\">Totals</h2>\n" +
|
|
7594
7595
|
" <dl class=\"totals-list\">\n" +
|
|
@@ -7717,6 +7718,135 @@ function _orderTimelineBlock(status) {
|
|
|
7717
7718
|
"<ol class=\"order-timeline__steps\">" + steps + "</ol></div>";
|
|
7718
7719
|
}
|
|
7719
7720
|
|
|
7721
|
+
// The customer-facing rich event timeline reads the order's own
|
|
7722
|
+
// transition ledger (order.get() hydrates `order.transitions` from
|
|
7723
|
+
// `order_transitions`, oldest-first). Each row carries an `on_event`,
|
|
7724
|
+
// an `occurred_at`, and a `metadata_json` blob; this block flattens the
|
|
7725
|
+
// ledger into a readable, dated, chronological feed — placed, paid,
|
|
7726
|
+
// shipped, delivered, refunded (with amounts), cancelled — that the
|
|
7727
|
+
// customer sees alongside the status rail.
|
|
7728
|
+
//
|
|
7729
|
+
// Customer-meaningful events only. The FSM also records mid-fulfillment
|
|
7730
|
+
// bookkeeping (`start_fulfillment` — the warehouse picking the parcel)
|
|
7731
|
+
// and the `__init__` placeholder edge; neither is something a customer
|
|
7732
|
+
// acts on, so both stay out of the feed. The raw `from_state →
|
|
7733
|
+
// to_state` string, the internal `reason` (operator-process strings
|
|
7734
|
+
// like `admin:console` / `stale-pending-reap`), and the rest of the
|
|
7735
|
+
// metadata blob are NEVER rendered — only the per-event detail the
|
|
7736
|
+
// customer cares about (a refund amount, a tracking number) is pulled
|
|
7737
|
+
// out, by key, and escaped at the sink.
|
|
7738
|
+
var ORDER_EVENT_LABELS = {
|
|
7739
|
+
"create": "Order placed",
|
|
7740
|
+
"mark_paid": "Payment received",
|
|
7741
|
+
"mark_shipped": "Order shipped",
|
|
7742
|
+
"mark_delivered": "Order delivered",
|
|
7743
|
+
"cancel": "Order cancelled",
|
|
7744
|
+
// `refund` covers both a partial refund (a same-state self-loop row)
|
|
7745
|
+
// and the terminal balance-clearing refund; the amount in metadata
|
|
7746
|
+
// distinguishes them in the detail line.
|
|
7747
|
+
"refund": "Refund issued",
|
|
7748
|
+
};
|
|
7749
|
+
|
|
7750
|
+
// Events the customer sees on their own order page. Everything not in
|
|
7751
|
+
// this set (start_fulfillment, any future internal-only FSM edge) is
|
|
7752
|
+
// dropped from the customer feed.
|
|
7753
|
+
var ORDER_EVENT_CUSTOMER_VISIBLE = {
|
|
7754
|
+
"create": true,
|
|
7755
|
+
"mark_paid": true,
|
|
7756
|
+
"mark_shipped": true,
|
|
7757
|
+
"mark_delivered": true,
|
|
7758
|
+
"cancel": true,
|
|
7759
|
+
"refund": true,
|
|
7760
|
+
};
|
|
7761
|
+
|
|
7762
|
+
// Format an order-transition `occurred_at` (epoch ms) the same way the
|
|
7763
|
+
// shipment timeline does — "YYYY-MM-DD HH:MM" UTC — so the two feeds
|
|
7764
|
+
// read consistently. Returns "" for an absent/garbage timestamp.
|
|
7765
|
+
function _orderEventWhen(occurredAt) {
|
|
7766
|
+
if (occurredAt == null) return "";
|
|
7767
|
+
var n = Number(occurredAt);
|
|
7768
|
+
if (!isFinite(n)) return "";
|
|
7769
|
+
return new Date(n).toISOString().slice(0, 16).replace("T", " ");
|
|
7770
|
+
}
|
|
7771
|
+
|
|
7772
|
+
// Build the per-event customer-facing detail line from the transition's
|
|
7773
|
+
// metadata — by key, never the raw blob. A refund row surfaces its
|
|
7774
|
+
// amount (formatted in the order currency); a shipped row surfaces a
|
|
7775
|
+
// carrier + tracking number when the metadata carried one. Everything
|
|
7776
|
+
// returned here is escaped by the caller at the render sink. Returns ""
|
|
7777
|
+
// when there's no customer-relevant detail for the event.
|
|
7778
|
+
function _orderEventDetail(onEvent, meta, currency) {
|
|
7779
|
+
if (!meta || typeof meta !== "object") return "";
|
|
7780
|
+
if (onEvent === "refund") {
|
|
7781
|
+
var amt = meta.amount_minor;
|
|
7782
|
+
if (Number.isInteger(amt) && amt > 0) {
|
|
7783
|
+
var label = meta.partial === true ? "Partial refund: " : "Amount: ";
|
|
7784
|
+
return label + pricing.format(amt, currency || "USD");
|
|
7785
|
+
}
|
|
7786
|
+
return "";
|
|
7787
|
+
}
|
|
7788
|
+
if (onEvent === "mark_shipped") {
|
|
7789
|
+
var parts = [];
|
|
7790
|
+
if (meta.carrier) parts.push(String(meta.carrier));
|
|
7791
|
+
if (meta.tracking_number) parts.push("Tracking " + String(meta.tracking_number));
|
|
7792
|
+
return parts.join(" · ");
|
|
7793
|
+
}
|
|
7794
|
+
if (onEvent === "mark_delivered") {
|
|
7795
|
+
// A click-and-collect delivery stamps a pickup location into the
|
|
7796
|
+
// metadata; surface it as the delivered-detail when present.
|
|
7797
|
+
if (meta.pickup_location_code) return "Picked up at " + String(meta.pickup_location_code);
|
|
7798
|
+
return "";
|
|
7799
|
+
}
|
|
7800
|
+
return "";
|
|
7801
|
+
}
|
|
7802
|
+
|
|
7803
|
+
// Render the rich, dated, chronological event timeline from the order's
|
|
7804
|
+
// transition ledger. Newest-first. Each event is a label + a UTC
|
|
7805
|
+
// timestamp + an optional customer-relevant detail (refund amount,
|
|
7806
|
+
// tracking number, pickup location). Internal-only transitions
|
|
7807
|
+
// (start_fulfillment, the __init__ placeholder, unknown future events)
|
|
7808
|
+
// are filtered out. Every data-derived value — label, detail, time — is
|
|
7809
|
+
// HTML-escaped at the sink, matching the escape-by-default convention of
|
|
7810
|
+
// the rest of this file. Returns "" when the order carries no
|
|
7811
|
+
// customer-visible transitions (the status rail above still renders), so
|
|
7812
|
+
// a brand-new order with a single placed event still shows that event.
|
|
7813
|
+
function _orderEventTimelineBlock(o) {
|
|
7814
|
+
if (!o || !Array.isArray(o.transitions) || !o.transitions.length) return "";
|
|
7815
|
+
var esc = b.template.escapeHtml;
|
|
7816
|
+
var currency = o.currency || "USD";
|
|
7817
|
+
var events = [];
|
|
7818
|
+
for (var i = 0; i < o.transitions.length; i += 1) {
|
|
7819
|
+
var t = o.transitions[i];
|
|
7820
|
+
if (!t || !ORDER_EVENT_CUSTOMER_VISIBLE[t.on_event]) continue;
|
|
7821
|
+
var meta = {};
|
|
7822
|
+
try { meta = JSON.parse(t.metadata_json || "{}"); }
|
|
7823
|
+
catch (_e) { meta = {}; } // drop-silent — bad JSON falls through to no-detail
|
|
7824
|
+
events.push({
|
|
7825
|
+
label: ORDER_EVENT_LABELS[t.on_event] || t.on_event,
|
|
7826
|
+
occurred_at: Number(t.occurred_at) || 0,
|
|
7827
|
+
detail: _orderEventDetail(t.on_event, meta, currency),
|
|
7828
|
+
});
|
|
7829
|
+
}
|
|
7830
|
+
if (!events.length) return "";
|
|
7831
|
+
// Newest-first for display. The ledger arrives oldest-first; sort a
|
|
7832
|
+
// copy descending by occurred_at, stable on the original order for ties
|
|
7833
|
+
// (same-ms events keep ledger sequence, reversed) so the feed is
|
|
7834
|
+
// deterministic across runs.
|
|
7835
|
+
events.reverse();
|
|
7836
|
+
events.sort(function (a, c) { return c.occurred_at - a.occurred_at; });
|
|
7837
|
+
var rows = events.map(function (e) {
|
|
7838
|
+
var when = _orderEventWhen(e.occurred_at);
|
|
7839
|
+
return "<li class=\"order-events__event\">" +
|
|
7840
|
+
"<span class=\"order-events__label\">" + esc(e.label) + "</span>" +
|
|
7841
|
+
(e.detail ? " <span class=\"order-events__detail\">" + esc(e.detail) + "</span>" : "") +
|
|
7842
|
+
(when ? " <time class=\"order-events__when\" datetime=\"" + esc(when) + "\">" + esc(when) + "</time>" : "") +
|
|
7843
|
+
"</li>";
|
|
7844
|
+
}).join("");
|
|
7845
|
+
return "<div class=\"order-events\">" +
|
|
7846
|
+
"<h2 class=\"pdp__variants-title\">Order activity</h2>" +
|
|
7847
|
+
"<ol class=\"order-events__list\">" + rows + "</ol></div>";
|
|
7848
|
+
}
|
|
7849
|
+
|
|
7720
7850
|
// Cap on how many carrier events the per-shipment timeline renders. A
|
|
7721
7851
|
// long-haul international parcel can accumulate dozens of scans; the
|
|
7722
7852
|
// customer-facing panel shows the most recent MAX_SHIPMENT_TIMELINE
|
|
@@ -8142,6 +8272,13 @@ function renderOrder(opts) {
|
|
|
8142
8272
|
// the order status alone, so they render even without tracking wired.
|
|
8143
8273
|
var shipments = opts.shipments || [];
|
|
8144
8274
|
var timelineHtml = _orderTimelineBlock(o.status);
|
|
8275
|
+
// Rich, dated, chronological event feed from the order's own
|
|
8276
|
+
// transition ledger (order.get() hydrates o.transitions). Read-only,
|
|
8277
|
+
// customer-visible events only, every value escaped at the sink. Empty
|
|
8278
|
+
// string for an order whose ledger wasn't hydrated (the status rail
|
|
8279
|
+
// above still renders), so legacy callers that pass no transitions are
|
|
8280
|
+
// unaffected.
|
|
8281
|
+
var eventsHtml = _orderEventTimelineBlock(o);
|
|
8145
8282
|
var trackingHtml = _orderTrackingBlock(shipments);
|
|
8146
8283
|
// The receipt-download link carries the guest order's ?k= access token when
|
|
8147
8284
|
// the page was opened from the emailed capability link, so the download
|
|
@@ -8174,6 +8311,7 @@ function renderOrder(opts) {
|
|
|
8174
8311
|
total: total,
|
|
8175
8312
|
ship_to: o.ship_to || null,
|
|
8176
8313
|
timeline_html: timelineHtml,
|
|
8314
|
+
events_html: eventsHtml,
|
|
8177
8315
|
tracking_html: trackingHtml,
|
|
8178
8316
|
pickup_html: _orderPickupBlock(opts.pickup),
|
|
8179
8317
|
gift_html: _orderGiftBlock(opts.gift_options),
|
|
@@ -8229,9 +8367,16 @@ function renderOrder(opts) {
|
|
|
8229
8367
|
.replace("RAW_REORDER_NOTICE", reorderNotice + cancelNotice)
|
|
8230
8368
|
.replace("RAW_ORDER_TIMELINE", timelineHtml)
|
|
8231
8369
|
.replace("RAW_ORDER_TRACKING", trackingHtml)
|
|
8370
|
+
.replace("RAW_ORDER_EVENTS", "RAW_ORDER_EVENTS_PLACEHOLDER")
|
|
8232
8371
|
.replace("RAW_ORDER_PICKUP", _orderPickupBlock(opts.pickup))
|
|
8233
8372
|
.replace("RAW_ORDER_ACTIONS", actionsHtml)
|
|
8234
8373
|
.replace("RAW_SHIP_TO", _shipToAddressBlock(o.ship_to));
|
|
8374
|
+
// The event timeline carries operator-influenced free text (carrier
|
|
8375
|
+
// names, tracking numbers — already escaped into the block, but a `$`
|
|
8376
|
+
// in that escaped text would still trip String.replace's dollar
|
|
8377
|
+
// substitution) — splice it via the replacer-function helper, never a
|
|
8378
|
+
// replacement-string .replace.
|
|
8379
|
+
body = _spliceRaw(body, "RAW_ORDER_EVENTS_PLACEHOLDER", eventsHtml);
|
|
8235
8380
|
// The gift block carries customer free text (escaped, but a `$` in it would
|
|
8236
8381
|
// still trip String.replace's dollar substitution) — splice it via the
|
|
8237
8382
|
// replacer-function helper, never a replacement-string .replace.
|
package/package.json
CHANGED