@blamejs/blamejs-shop 0.3.39 → 0.3.41
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/admin.js +331 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/storefront.js +244 -4
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +8 -0
- package/lib/vendor/blamejs/README.md +8 -5
- package/lib/vendor/blamejs/SECURITY.md +7 -0
- package/lib/vendor/blamejs/api-snapshot.json +266 -2
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +1 -0
- package/lib/vendor/blamejs/index.js +7 -1
- package/lib/vendor/blamejs/lib/agent-idempotency.js +113 -0
- package/lib/vendor/blamejs/lib/agent-orchestrator.js +108 -0
- package/lib/vendor/blamejs/lib/agent-snapshot.js +137 -0
- package/lib/vendor/blamejs/lib/agent-tenant.js +193 -17
- package/lib/vendor/blamejs/lib/ai-input.js +167 -3
- package/lib/vendor/blamejs/lib/ai-output.js +463 -0
- package/lib/vendor/blamejs/lib/ai-prompt.js +304 -0
- package/lib/vendor/blamejs/lib/archive-wrap.js +234 -1
- package/lib/vendor/blamejs/lib/archive.js +1 -0
- package/lib/vendor/blamejs/lib/audit.js +3 -0
- package/lib/vendor/blamejs/lib/auth/oid4vp.js +47 -28
- package/lib/vendor/blamejs/lib/cluster.js +186 -14
- package/lib/vendor/blamejs/lib/codepoint-class.js +18 -0
- package/lib/vendor/blamejs/lib/compliance-ai-act.js +446 -0
- package/lib/vendor/blamejs/lib/consent.js +104 -8
- package/lib/vendor/blamejs/lib/content-credentials.js +851 -41
- package/lib/vendor/blamejs/lib/crypto-field.js +5 -0
- package/lib/vendor/blamejs/lib/db.js +15 -0
- package/lib/vendor/blamejs/lib/framework-error.js +21 -0
- package/lib/vendor/blamejs/lib/mail-srs.js +122 -19
- package/lib/vendor/blamejs/lib/privacy.js +168 -0
- package/lib/vendor/blamejs/lib/safe-archive.js +196 -136
- package/lib/vendor/blamejs/lib/validate-opts.js +24 -0
- package/lib/vendor/blamejs/lib/vault/rotate.js +175 -15
- package/lib/vendor/blamejs/lib/vault-aad.js +84 -33
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.11.json +72 -0
- package/lib/vendor/blamejs/release-notes/v0.14.12.json +95 -0
- package/lib/vendor/blamejs/release-notes/v0.14.13.json +52 -0
- package/lib/vendor/blamejs/release-notes/v0.14.14.json +31 -0
- package/lib/vendor/blamejs/test/00-primitives.js +9 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +103 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +91 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-snapshot.test.js +186 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-tenant.test.js +140 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-input.test.js +59 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-output.test.js +125 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-prompt.test.js +133 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +6 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +94 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-wrap.test.js +176 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cluster-vault-rotation.test.js +243 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +250 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +130 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/consent-purposes.test.js +70 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js +289 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-srs.test.js +61 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/privacy-vendor-review.test.js +69 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vault-rotate-aad.test.js +158 -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.3.x
|
|
10
10
|
|
|
11
|
+
- v0.3.41 (2026-05-31) — **The public product API no longer leaks internal error details, and bad order links return Not Found.** A server-side error on the public product API could include the raw internal error message — potentially database or query details — in its 500 response to anonymous callers. Those responses now return a generic message, with the real error recorded server-side only, matching how the admin side already behaves. Separately, opening an order or payment page with a malformed link now returns a clean Not Found instead of a server error. This release also updates the vendored blamejs runtime to v0.14.15. **Changed:** *Vendored blamejs runtime updated to v0.14.15* — The bundled blamejs runtime is updated to v0.14.15, which adds a recognized-purpose vocabulary to its consent ledger (with lawful-basis enforcement for regulated purposes) and a new privacy namespace for periodic third-party vendor-review attestations. These are additive, opt-in capabilities; existing behavior is unchanged and no operator action is required. **Fixed:** *Public product API hides internal error details* — When the public product API (the catalog endpoints) hit a server-side error, its 500 response could carry the raw internal error message — including database or constraint text — to any caller. It now returns a generic "something went wrong" message and records the real error server-side only, the same way the admin console already scrubs its errors. Validation errors still return their helpful 400 message unchanged. · *Malformed order and payment links return Not Found* — Opening an order confirmation or payment page with a malformed (non-identifier) link returned a server error instead of a Not Found. Both pages now return a clean 404 for a malformed or unknown link, matching the order reorder and cancel pages.
|
|
12
|
+
|
|
13
|
+
- v0.3.40 (2026-05-31) — **Return shipping labels — operators issue one, customers download and track it.** After a return was approved there was no way to give the customer a shipping label or for them to follow the parcel back. Operators can now issue a return label on an approved return from the admin console — entering the carrier, tracking number, and the label link generated in their carrier account — and post tracking updates as the parcel moves. The customer sees the label on their return: they can open or print it, see the carrier and tracking number, and follow the scan history, with the return marked received once it arrives back. A customer can only see and download the label for their own return. **Added:** *Return shipping labels and tracking* — Opening a return in the account area now shows its detail, and once a label has been issued the customer can open or print the return label, see the carrier and tracking number, and follow the parcel's scan history — all scoped so a customer only ever sees their own return. On the operator side, an approved return gains an Issue return label form (carrier, service level, weight, the carrier label link, tracking number, and cost) and controls to mark the parcel shipped, in transit, delivered, or held with an exception; marking it delivered records the return as received. The label link is required to be a secure URL, and the customer download is served without exposing that link in the page.
|
|
14
|
+
|
|
11
15
|
- v0.3.39 (2026-05-31) — **Paginated lists no longer show an empty extra page at exact page boundaries.** Several paginated lists offered a next page even when the current page was the last one, if the list's length happened to be an exact multiple of the page size — following that next link landed on an empty page. This affected a customer's order history, loyalty history, and store-credit history, and the admin customer list. Each now checks whether a further item actually exists before offering a next page, so the next link appears only when there is genuinely more to show. **Fixed:** *No phantom next page on exactly-full lists* — A customer's order history, loyalty history, and store-credit history, and the admin customer list, offered a next page whenever the current page was full — so a list whose total was an exact multiple of the page size showed a next link that led to an empty page. These lists now look one item past the page before offering a next link, so it appears only when a further item exists. This matches the fix already in place for collection pages.
|
|
12
16
|
|
|
13
17
|
- v0.3.38 (2026-05-31) — **Pre-order campaigns — reserve a product before it is released.** Products that are not yet released could not be sold or reserved. Operators can now run a pre-order campaign for a product from the admin console — setting its release date, price, and an optional reservation cap. While the campaign is open, the product page shows a Pre-order control with the release date and how many reservations remain, and a signed-in customer can reserve their quantity without paying upfront. Customers see and can cancel their reservations from their account, and when the operator launches the campaign on its release date, each reservation is turned into a pending order the customer pays for through normal checkout. **Added:** *Pre-order campaigns* — A Pre-orders screen in the admin console runs a campaign for a product's SKU — release date, price, an optional deposit, and a reservation cap — with launch and close controls (launch is available once the release date arrives). While a campaign is open, the product page shows a Pre-order control with the release date and remaining availability instead of the usual add-to-cart, and a signed-in customer can reserve their quantity (no charge at reservation time; reserving past the cap or on a closed campaign is refused). Customers manage their reservations under Pre-orders in their account and can cancel one, which frees that capacity. Launching the campaign converts each open reservation into a pending order the customer completes through normal checkout. A customer can only see and cancel their own reservations.
|
package/lib/admin.js
CHANGED
|
@@ -496,6 +496,7 @@ function mount(router, deps) {
|
|
|
496
496
|
var reviews = deps.reviews || null; // moderation endpoints disabled when absent
|
|
497
497
|
var productQa = deps.productQa || null; // Q&A moderation endpoints disabled when absent
|
|
498
498
|
var returns = deps.returns || null; // RMA moderation endpoints disabled when absent
|
|
499
|
+
var returnLabels = deps.returnLabels || null; // return-label issuance + tracking-update endpoints disabled when absent
|
|
499
500
|
var customers = deps.customers || null; // read-only customers console disabled when absent
|
|
500
501
|
var storeCredit = deps.storeCredit || null; // per-customer store-credit panel + grant/deduct disabled when absent
|
|
501
502
|
var customerNotes = deps.customerNotes || null; // per-customer CRM notes panel disabled when absent
|
|
@@ -3447,6 +3448,7 @@ function mount(router, deps) {
|
|
|
3447
3448
|
}));
|
|
3448
3449
|
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
3449
3450
|
var rmaCtx = await _rmaProviderContext(rma);
|
|
3451
|
+
var labelCtx = await _returnLabelContext(rma);
|
|
3450
3452
|
_sendHtml(res, 200, renderAdminReturn({
|
|
3451
3453
|
shop_name: deps.shop_name,
|
|
3452
3454
|
nav_available: navAvailable,
|
|
@@ -3457,6 +3459,15 @@ function mount(router, deps) {
|
|
|
3457
3459
|
// provider via a confirm interstitial. Absent either, it stays
|
|
3458
3460
|
// record-only with a pointer to the order page for the money side.
|
|
3459
3461
|
can_provider_refund: rmaCtx.canProviderRefund,
|
|
3462
|
+
// The return-shipping label surface: the issue-label form mounts on
|
|
3463
|
+
// an approved return with no label; once a label is issued, the
|
|
3464
|
+
// label + its carrier-scan timeline + the legal tracking-update
|
|
3465
|
+
// actions take its place. Absent the returnLabels dep this whole
|
|
3466
|
+
// panel is suppressed.
|
|
3467
|
+
labels_available: !!returnLabels,
|
|
3468
|
+
label: labelCtx.label,
|
|
3469
|
+
label_events: labelCtx.events,
|
|
3470
|
+
label_currency: rmaCtx.order ? rmaCtx.order.currency : (rma.refund_currency || "USD"),
|
|
3460
3471
|
moved: url && url.searchParams.get("moved"),
|
|
3461
3472
|
notice: url && url.searchParams.get("err") ? "That action couldn't be completed for this return." : null,
|
|
3462
3473
|
}));
|
|
@@ -3477,6 +3488,190 @@ function mount(router, deps) {
|
|
|
3477
3488
|
return ctx;
|
|
3478
3489
|
}
|
|
3479
3490
|
|
|
3491
|
+
// Resolve the return's current (most-recently-issued) shipping label plus
|
|
3492
|
+
// its ordered carrier-scan timeline for the detail screen. Best-effort —
|
|
3493
|
+
// the labels primitive may be unwired, the tables may not be migrated, or
|
|
3494
|
+
// a legacy return id may not parse; any of those degrades to "no label"
|
|
3495
|
+
// rather than throwing, so the detail never 500s on the label read.
|
|
3496
|
+
async function _returnLabelContext(rma) {
|
|
3497
|
+
var ctx = { label: null, events: [] };
|
|
3498
|
+
if (!returnLabels || !rma || !rma.id) return ctx;
|
|
3499
|
+
try { ctx.label = await returnLabels.labelForReturn(rma.id); }
|
|
3500
|
+
catch (e) { if (!(e instanceof TypeError)) throw e; ctx.label = null; }
|
|
3501
|
+
if (ctx.label) {
|
|
3502
|
+
try { ctx.events = await returnLabels.eventsForLabel(ctx.label.id); }
|
|
3503
|
+
catch (e) { if (!(e instanceof TypeError)) throw e; ctx.events = []; }
|
|
3504
|
+
}
|
|
3505
|
+
return ctx;
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
// Single classifier for a thrown return-label error → an operator-safe
|
|
3509
|
+
// status, shared by the bearer-JSON path and the cookie/HTML form path.
|
|
3510
|
+
// This MUST run BEFORE a generic `instanceof TypeError → 400` branch: the
|
|
3511
|
+
// labels primitive raises its not-found misses as *coded TypeErrors*
|
|
3512
|
+
// (issueLabel's RETURN_NOT_FOUND, _currentStatus's RETURN_LABEL_NOT_FOUND),
|
|
3513
|
+
// so checking `instanceof TypeError` first would map a genuine 404 to a
|
|
3514
|
+
// 400. Order is: coded-error map (by `.code`) first, then the generic
|
|
3515
|
+
// bad-shape TypeError fall-through the caller applies.
|
|
3516
|
+
// - a missing return / a missing label → 404 (the id named nothing)
|
|
3517
|
+
// - an issue refused (RMA not approved) → 409 (state conflict)
|
|
3518
|
+
// - a transition refused (illegal FSM hop) → 409 (state conflict)
|
|
3519
|
+
// - anything else → null (caller's default map)
|
|
3520
|
+
function _returnLabelsClientError(e) {
|
|
3521
|
+
if (!e) return null;
|
|
3522
|
+
if (e.code === "RETURN_NOT_FOUND" || e.code === "RETURN_LABEL_NOT_FOUND") {
|
|
3523
|
+
return { status: 404, slug: "return-label-not-found" };
|
|
3524
|
+
}
|
|
3525
|
+
if (e.code === "RETURN_LABEL_ISSUE_REFUSED" || e.code === "RETURN_LABEL_TRANSITION_REFUSED") {
|
|
3526
|
+
return { status: 409, slug: "return-label-action-refused" };
|
|
3527
|
+
}
|
|
3528
|
+
return null;
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
// Coerce a browser form's string fields to the numeric shapes
|
|
3532
|
+
// returnLabels.issueLabel demands. ONLY a clean unsigned-integer string
|
|
3533
|
+
// becomes a number; anything else (e.g. "340g", "", "12.5") passes through
|
|
3534
|
+
// unchanged so the primitive's _positiveInt / _nonNegInt rejects it as a
|
|
3535
|
+
// clean 400, never silently truncated by parseInt.
|
|
3536
|
+
function _intField(raw) {
|
|
3537
|
+
return (typeof raw === "string" && /^\d+$/.test(raw.trim())) ? Number(raw.trim()) : raw;
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
// The return-label issuance + tracking-update routes mount only when the
|
|
3541
|
+
// returnLabels primitive is wired — without it the customer download
|
|
3542
|
+
// surface has nothing to show, and these operator routes have no backing
|
|
3543
|
+
// store.
|
|
3544
|
+
if (returnLabels) {
|
|
3545
|
+
|
|
3546
|
+
// POST /admin/returns/:id/label — issue an operator-funded return label.
|
|
3547
|
+
// The route forwards the form/JSON straight to returnLabels.issueLabel,
|
|
3548
|
+
// which owns ALL validation: the carrier / service_level / tracking
|
|
3549
|
+
// bounds, the weight + cost integer shapes, the currency ISO check, the
|
|
3550
|
+
// HTTPS-only label_url (b.safeUrl), and the refusal unless the RMA is
|
|
3551
|
+
// `approved`. The route never hand-rolls an INSERT — composing the
|
|
3552
|
+
// primitive keeps the label_url SSRF/scheme gate and the approved-only
|
|
3553
|
+
// rule in one place. Coded errors map via _returnLabelsClientError BEFORE
|
|
3554
|
+
// the generic TypeError→400, so a missing return 404s (not 400).
|
|
3555
|
+
router.post("/admin/returns/:id/label", _pageOrApi(false,
|
|
3556
|
+
W("return.label.issue", async function (req, res) {
|
|
3557
|
+
var body = req.body || {};
|
|
3558
|
+
var label;
|
|
3559
|
+
try {
|
|
3560
|
+
label = await returnLabels.issueLabel({
|
|
3561
|
+
return_id: req.params.id,
|
|
3562
|
+
carrier: body.carrier,
|
|
3563
|
+
service_level: body.service_level,
|
|
3564
|
+
weight_grams: _intField(body.weight_grams),
|
|
3565
|
+
label_url: body.label_url,
|
|
3566
|
+
tracking_number: body.tracking_number,
|
|
3567
|
+
cost_minor: _intField(body.cost_minor),
|
|
3568
|
+
currency: body.currency,
|
|
3569
|
+
});
|
|
3570
|
+
} catch (e) {
|
|
3571
|
+
var ce = _returnLabelsClientError(e);
|
|
3572
|
+
if (ce) return _problem(res, ce.status, ce.slug, e.message);
|
|
3573
|
+
throw e; // bad-shape TypeError → _wrap maps to a clean 400
|
|
3574
|
+
}
|
|
3575
|
+
_json(res, 200, label);
|
|
3576
|
+
return label;
|
|
3577
|
+
}),
|
|
3578
|
+
async function (req, res) {
|
|
3579
|
+
var id = req.params.id;
|
|
3580
|
+
var enc = encodeURIComponent(id);
|
|
3581
|
+
var body = req.body || {};
|
|
3582
|
+
try {
|
|
3583
|
+
await returnLabels.issueLabel({
|
|
3584
|
+
return_id: id,
|
|
3585
|
+
carrier: body.carrier,
|
|
3586
|
+
service_level: body.service_level,
|
|
3587
|
+
weight_grams: _intField(body.weight_grams),
|
|
3588
|
+
label_url: body.label_url,
|
|
3589
|
+
tracking_number: body.tracking_number,
|
|
3590
|
+
cost_minor: _intField(body.cost_minor),
|
|
3591
|
+
currency: body.currency,
|
|
3592
|
+
});
|
|
3593
|
+
} catch (e) {
|
|
3594
|
+
// A coded miss/refusal OR a bad-shape TypeError both become a notice
|
|
3595
|
+
// on the detail (PRG), never a 500; anything else propagates.
|
|
3596
|
+
if (_returnLabelsClientError(e) || e instanceof TypeError) {
|
|
3597
|
+
return _redirect(res, "/admin/returns/" + enc + "?err=1");
|
|
3598
|
+
}
|
|
3599
|
+
throw e;
|
|
3600
|
+
}
|
|
3601
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".return.label.issue", outcome: "success", metadata: { id: id } });
|
|
3602
|
+
_redirect(res, "/admin/returns/" + enc + "?moved=1");
|
|
3603
|
+
},
|
|
3604
|
+
));
|
|
3605
|
+
|
|
3606
|
+
// The four tracking-update actions on a return that HAS a label. Each
|
|
3607
|
+
// appends one carrier-scan event via the matching mark-* primitive (which
|
|
3608
|
+
// owns the FSM-legality check + the event write); mark-exception carries a
|
|
3609
|
+
// short operator-supplied detail. The :id here is the RETURN id — the
|
|
3610
|
+
// route resolves its current label first (labelForReturn), so a return
|
|
3611
|
+
// with no label 404s before any mark-* call. `field` (when set) names the
|
|
3612
|
+
// body key forwarded to the primitive.
|
|
3613
|
+
function _returnLabelMark(auditEvent, markFn, field) {
|
|
3614
|
+
return _pageOrApi(false,
|
|
3615
|
+
W("return.label." + auditEvent, async function (req, res) {
|
|
3616
|
+
var body = req.body || {};
|
|
3617
|
+
var label;
|
|
3618
|
+
try {
|
|
3619
|
+
var rma = await returns.get(req.params.id);
|
|
3620
|
+
if (!rma) return _problem(res, 404, "return-label-not-found");
|
|
3621
|
+
label = await returnLabels.labelForReturn(rma.id);
|
|
3622
|
+
if (!label) return _problem(res, 404, "return-label-not-found");
|
|
3623
|
+
var updated = await markFn(label.id, body);
|
|
3624
|
+
_json(res, 200, updated);
|
|
3625
|
+
return updated;
|
|
3626
|
+
} catch (e) {
|
|
3627
|
+
var ce = _returnLabelsClientError(e);
|
|
3628
|
+
if (ce) return _problem(res, ce.status, ce.slug, e.message);
|
|
3629
|
+
throw e;
|
|
3630
|
+
}
|
|
3631
|
+
}),
|
|
3632
|
+
async function (req, res) {
|
|
3633
|
+
var id = req.params.id;
|
|
3634
|
+
var enc = encodeURIComponent(id);
|
|
3635
|
+
try {
|
|
3636
|
+
var rma = await returns.get(id);
|
|
3637
|
+
if (!rma) return _redirect(res, "/admin/returns/" + enc + "?err=1");
|
|
3638
|
+
var label = await returnLabels.labelForReturn(rma.id);
|
|
3639
|
+
if (!label) return _redirect(res, "/admin/returns/" + enc + "?err=1");
|
|
3640
|
+
await markFn(label.id, req.body || {});
|
|
3641
|
+
} catch (e) {
|
|
3642
|
+
if (_returnLabelsClientError(e) || e instanceof TypeError) {
|
|
3643
|
+
return _redirect(res, "/admin/returns/" + enc + "?err=1");
|
|
3644
|
+
}
|
|
3645
|
+
throw e;
|
|
3646
|
+
}
|
|
3647
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".return.label." + auditEvent, outcome: "success", metadata: { id: id } });
|
|
3648
|
+
_redirect(res, "/admin/returns/" + enc + "?moved=1");
|
|
3649
|
+
},
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
router.post("/admin/returns/:id/label/shipped",
|
|
3654
|
+
_returnLabelMark("shipped", function (labelId) {
|
|
3655
|
+
return returnLabels.markShipped({ label_id: labelId });
|
|
3656
|
+
}));
|
|
3657
|
+
router.post("/admin/returns/:id/label/in-transit",
|
|
3658
|
+
_returnLabelMark("in_transit", function (labelId, body) {
|
|
3659
|
+
// location is optional — a bare scan with no place is legal.
|
|
3660
|
+
return returnLabels.markInTransit({ label_id: labelId, location: body.location || undefined });
|
|
3661
|
+
}));
|
|
3662
|
+
router.post("/admin/returns/:id/label/delivered",
|
|
3663
|
+
_returnLabelMark("delivered", function (labelId) {
|
|
3664
|
+
return returnLabels.markDelivered({ label_id: labelId });
|
|
3665
|
+
}));
|
|
3666
|
+
router.post("/admin/returns/:id/label/exception",
|
|
3667
|
+
_returnLabelMark("exception", function (labelId, body) {
|
|
3668
|
+
// exception detail is REQUIRED by the primitive — a blank one throws a
|
|
3669
|
+
// TypeError the wrapper maps to a clean 400.
|
|
3670
|
+
return returnLabels.markException({ label_id: labelId, exception: body.exception });
|
|
3671
|
+
}));
|
|
3672
|
+
|
|
3673
|
+
} // end if (returnLabels)
|
|
3674
|
+
|
|
3480
3675
|
// The browser side of an RMA action: run `opFn(id, body)`, then PRG
|
|
3481
3676
|
// back to the detail. A bad id / shape (TypeError) or an FSM refusal /
|
|
3482
3677
|
// not-found (mapped by _returnsClientError) becomes a notice on the
|
|
@@ -10458,6 +10653,141 @@ function _returnPillClass(status) {
|
|
|
10458
10653
|
return "pending";
|
|
10459
10654
|
}
|
|
10460
10655
|
|
|
10656
|
+
// Operator-facing phrasing for a return_labels FSM state + a label-event
|
|
10657
|
+
// row's event_type. The customer storefront has its own shopper-facing
|
|
10658
|
+
// phrasings; the console states it plainly for the operator.
|
|
10659
|
+
var RETURN_LABEL_STATUS_TEXT = {
|
|
10660
|
+
issued: "Issued",
|
|
10661
|
+
shipped: "Shipped",
|
|
10662
|
+
in_transit: "In transit",
|
|
10663
|
+
delivered: "Delivered",
|
|
10664
|
+
exception: "Exception",
|
|
10665
|
+
};
|
|
10666
|
+
var RETURN_LABEL_EVENT_TEXT = {
|
|
10667
|
+
issued: "Label issued",
|
|
10668
|
+
shipped: "Marked shipped",
|
|
10669
|
+
in_transit: "In transit",
|
|
10670
|
+
delivered: "Delivered to merchant",
|
|
10671
|
+
exception: "Exception",
|
|
10672
|
+
};
|
|
10673
|
+
function _returnLabelPillClass(status) {
|
|
10674
|
+
if (status === "delivered") return "delivered";
|
|
10675
|
+
if (status === "exception") return "cancelled";
|
|
10676
|
+
if (status === "shipped" || status === "in_transit") return "shipped";
|
|
10677
|
+
return "pending"; // issued
|
|
10678
|
+
}
|
|
10679
|
+
// The legal next tracking actions for a label in `status`, mirroring the
|
|
10680
|
+
// return-labels FSM transition graph. Each entry is the route suffix + the
|
|
10681
|
+
// button copy. Terminal states (delivered / exception) yield none.
|
|
10682
|
+
var RETURN_LABEL_NEXT_ACTIONS = {
|
|
10683
|
+
issued: [{ suffix: "shipped", label: "Mark shipped" }, { suffix: "exception", label: "Flag exception" }],
|
|
10684
|
+
shipped: [{ suffix: "in-transit", label: "Add transit scan" }, { suffix: "delivered", label: "Mark delivered" }, { suffix: "exception", label: "Flag exception" }],
|
|
10685
|
+
in_transit: [{ suffix: "in-transit", label: "Add transit scan" }, { suffix: "delivered", label: "Mark delivered" }, { suffix: "exception", label: "Flag exception" }],
|
|
10686
|
+
delivered: [],
|
|
10687
|
+
exception: [],
|
|
10688
|
+
};
|
|
10689
|
+
|
|
10690
|
+
// The return-shipping-label panel on the return detail. Three states:
|
|
10691
|
+
// - labels primitive unwired → suppressed entirely (empty string).
|
|
10692
|
+
// - no label yet, return is `approved` → the issue-label form (the only
|
|
10693
|
+
// status issueLabel accepts; on any other status we explain why).
|
|
10694
|
+
// - a label exists → the label summary + carrier-scan timeline + the legal
|
|
10695
|
+
// tracking-update actions.
|
|
10696
|
+
// Every form renders through _renderAdminShell, which injects the _csrf
|
|
10697
|
+
// field into each POST form, so no per-form token handling is needed here.
|
|
10698
|
+
function _returnLabelPanel(opts, r) {
|
|
10699
|
+
if (!opts.labels_available) return "";
|
|
10700
|
+
var label = opts.label || null;
|
|
10701
|
+
var events = opts.label_events || [];
|
|
10702
|
+
|
|
10703
|
+
if (!label) {
|
|
10704
|
+
// No label issued yet. issueLabel accepts ONLY an `approved` return, so
|
|
10705
|
+
// the form shows only there; on any other status we say why instead of
|
|
10706
|
+
// offering a form that would 4xx.
|
|
10707
|
+
if (r.status !== "approved") {
|
|
10708
|
+
return "<div class=\"panel mt\"><h3 class=\"subhead\">Return shipping label</h3>" +
|
|
10709
|
+
"<p class=\"meta\">A prepaid return label can be issued once this return is approved.</p></div>";
|
|
10710
|
+
}
|
|
10711
|
+
var ccy = opts.label_currency || r.refund_currency || "USD";
|
|
10712
|
+
var form =
|
|
10713
|
+
"<form method=\"post\" action=\"/admin/returns/" + _htmlEscape(r.id) + "/label\" class=\"return-action\">" +
|
|
10714
|
+
"<h4>Issue return label</h4>" +
|
|
10715
|
+
"<p class=\"meta\">Generate the label in your carrier account, then record it here. The shopper downloads it from their return status page.</p>" +
|
|
10716
|
+
_setupField("Carrier", "carrier", "", "text", "e.g. USPS, UPS, FedEx.", " maxlength=\"64\" required") +
|
|
10717
|
+
_setupField("Service level", "service_level", "", "text", "e.g. Ground Advantage.", " maxlength=\"128\" required") +
|
|
10718
|
+
_setupField("Weight (grams)", "weight_grams", "", "number", "Parcel weight in grams.", " min=\"1\" required") +
|
|
10719
|
+
_setupField("Label URL", "label_url", "", "url", "The https:// link to the prepaid label PDF in your carrier account.", " maxlength=\"2048\" required") +
|
|
10720
|
+
_setupField("Tracking number", "tracking_number", "", "text", "The carrier tracking number.", " maxlength=\"128\" required") +
|
|
10721
|
+
_setupField("Cost (minor units)", "cost_minor", "", "number", "e.g. 795 for $7.95 of postage.", " min=\"0\" required") +
|
|
10722
|
+
_setupField("Currency", "currency", ccy, "text", "3-letter ISO 4217.", " maxlength=\"3\" class=\"input-code\" required") +
|
|
10723
|
+
"<button class=\"btn\" type=\"submit\">Issue return label</button>" +
|
|
10724
|
+
"</form>";
|
|
10725
|
+
return "<div class=\"panel mt\"><h3 class=\"subhead\">Return shipping label</h3>" + form + "</div>";
|
|
10726
|
+
}
|
|
10727
|
+
|
|
10728
|
+
// A label exists — summary + timeline + the legal next actions.
|
|
10729
|
+
var statusText = RETURN_LABEL_STATUS_TEXT[label.status] || label.status;
|
|
10730
|
+
var costShown = (label.cost_minor != null)
|
|
10731
|
+
? pricing.format(label.cost_minor, label.currency || "USD")
|
|
10732
|
+
: null;
|
|
10733
|
+
var summary =
|
|
10734
|
+
"<p><span class=\"status-pill " + _returnLabelPillClass(label.status) + "\">" + _htmlEscape(statusText) + "</span></p>" +
|
|
10735
|
+
"<p><span class=\"meta\">Carrier</span><br>" + _htmlEscape(label.carrier || "—") +
|
|
10736
|
+
(label.service_level ? " · " + _htmlEscape(label.service_level) : "") + "</p>" +
|
|
10737
|
+
"<p><span class=\"meta\">Tracking</span><br>" + _htmlEscape(label.tracking_number || "—") + "</p>" +
|
|
10738
|
+
(costShown ? "<p><span class=\"meta\">Postage</span><br>" + _htmlEscape(costShown) + "</p>" : "") +
|
|
10739
|
+
// The label_url is an https link the operator pasted — link it so they can
|
|
10740
|
+
// re-open the label. rel=noopener noreferrer + a literal-text check keeps
|
|
10741
|
+
// it inert beyond a navigation.
|
|
10742
|
+
(label.label_url
|
|
10743
|
+
? "<p><a class=\"btn btn--ghost\" href=\"" + _htmlEscape(label.label_url) + "\" target=\"_blank\" rel=\"noopener noreferrer\">Open label</a></p>"
|
|
10744
|
+
: "");
|
|
10745
|
+
|
|
10746
|
+
var timelineRows = events.map(function (ev) {
|
|
10747
|
+
var evText = RETURN_LABEL_EVENT_TEXT[ev.event_type] || ev.event_type;
|
|
10748
|
+
var detail = "";
|
|
10749
|
+
try {
|
|
10750
|
+
var d = ev.detail_json ? JSON.parse(ev.detail_json) : {};
|
|
10751
|
+
if (d && d.location) detail = String(d.location);
|
|
10752
|
+
else if (d && d.exception) detail = String(d.exception);
|
|
10753
|
+
} catch (_e) { detail = ""; }
|
|
10754
|
+
return "<li>" + _htmlEscape(String(evText)) +
|
|
10755
|
+
(detail ? " — " + _htmlEscape(detail) : "") +
|
|
10756
|
+
" <span class=\"meta\">" + _htmlEscape(_fmtDate(ev.occurred_at)) + "</span></li>";
|
|
10757
|
+
}).join("");
|
|
10758
|
+
var timeline = timelineRows
|
|
10759
|
+
? "<h4>Tracking timeline</h4><ul class=\"timeline\">" + timelineRows + "</ul>"
|
|
10760
|
+
: "";
|
|
10761
|
+
|
|
10762
|
+
var nextActions = (RETURN_LABEL_NEXT_ACTIONS[label.status] || []).map(function (a) {
|
|
10763
|
+
if (a.suffix === "exception") {
|
|
10764
|
+
// The exception action takes a short required detail.
|
|
10765
|
+
return "<form method=\"post\" action=\"/admin/returns/" + _htmlEscape(r.id) + "/label/exception\" class=\"return-action\">" +
|
|
10766
|
+
"<h4>" + _htmlEscape(a.label) + "</h4>" +
|
|
10767
|
+
_setupField("What happened", "exception", "", "text", "Lost / damaged / refused — shown on the timeline.", " maxlength=\"256\" required") +
|
|
10768
|
+
"<button class=\"btn btn--danger\" type=\"submit\">" + _htmlEscape(a.label) + "</button>" +
|
|
10769
|
+
"</form>";
|
|
10770
|
+
}
|
|
10771
|
+
if (a.suffix === "in-transit") {
|
|
10772
|
+
return "<form method=\"post\" action=\"/admin/returns/" + _htmlEscape(r.id) + "/label/in-transit\" class=\"return-action\">" +
|
|
10773
|
+
"<h4>" + _htmlEscape(a.label) + "</h4>" +
|
|
10774
|
+
_setupField("Location (optional)", "location", "", "text", "e.g. LAX hub.", " maxlength=\"256\"") +
|
|
10775
|
+
"<button class=\"btn\" type=\"submit\">" + _htmlEscape(a.label) + "</button>" +
|
|
10776
|
+
"</form>";
|
|
10777
|
+
}
|
|
10778
|
+
return "<form method=\"post\" action=\"/admin/returns/" + _htmlEscape(r.id) + "/label/" + _htmlEscape(a.suffix) + "\" class=\"return-action\">" +
|
|
10779
|
+
"<h4>" + _htmlEscape(a.label) + "</h4>" +
|
|
10780
|
+
"<button class=\"btn\" type=\"submit\">" + _htmlEscape(a.label) + "</button>" +
|
|
10781
|
+
"</form>";
|
|
10782
|
+
}).join("");
|
|
10783
|
+
var actionsBlock = nextActions
|
|
10784
|
+
? "<div class=\"return-actions\">" + nextActions + "</div>"
|
|
10785
|
+
: "<p class=\"meta\">This label is in a final state — no further tracking updates.</p>";
|
|
10786
|
+
|
|
10787
|
+
return "<div class=\"panel mt\"><h3 class=\"subhead\">Return shipping label</h3>" +
|
|
10788
|
+
summary + timeline + actionsBlock + "</div>";
|
|
10789
|
+
}
|
|
10790
|
+
|
|
10461
10791
|
function renderAdminReturns(opts) {
|
|
10462
10792
|
opts = opts || {};
|
|
10463
10793
|
var rmas = opts.returns || [];
|
|
@@ -10595,6 +10925,7 @@ function renderAdminReturn(opts) {
|
|
|
10595
10925
|
"<div class=\"panel mt\"><h3 class=\"subhead\">Actions</h3>" +
|
|
10596
10926
|
actions +
|
|
10597
10927
|
"</div>" +
|
|
10928
|
+
_returnLabelPanel(opts, r) +
|
|
10598
10929
|
"</section>";
|
|
10599
10930
|
return _renderAdminShell(opts.shop_name, "Return " + (r.rma_code || r.id.slice(0, 8)), body, "returns", opts.nav_available);
|
|
10600
10931
|
}
|
package/lib/asset-manifest.json
CHANGED