@blamejs/blamejs-shop 0.3.3 → 0.3.5
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 +950 -6
- package/lib/asset-manifest.json +1 -1
- 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.5 (2026-05-30) — **Shipping-label management in the admin console.** The admin console gains shipping-label tools. On an order, void a purchased label — with a reason — when it needs reprinting, alongside the existing mark-as-used action. A new Shipping labels section adds cross-order visibility: a status-filtered list, the mint queue of labels awaiting a broker, and a broker-spend report that totals label costs by carrier and currency over a date range. This is bookkeeping and reporting only — nothing in the storefront, cart, or checkout changes. **Added:** *Void a shipping label from the order* — Each purchased label on an order now offers a Void action with a reason, for when a label is reprinted at a different weight or carrier. Voiding is refused once a label has been used or already voided, or once it is past the carrier's void window. · *Shipping labels admin section* — A new Shipping labels section gives cross-order visibility: a status-filtered label list, the mint queue of labels awaiting a broker, and a broker-spend report totalling label cost grouped by carrier and currency over a date range.
|
|
12
|
+
|
|
13
|
+
- v0.3.4 (2026-05-30) — **Sales-tax filings and remittance tracking in the admin console.** The admin console gains a Tax filings section for tracking sales-tax remittance across reporting periods. Open a filing period for a jurisdiction, compute the tax collected from completed orders in that window, then record the submission and the payment as you remit. Each filing carries a per-rate breakdown and an audit trail, and the list surfaces what's due soon or already overdue. A per-jurisdiction remittance report totals the tax owed across a date range. Every figure derives from completed orders — nothing in the storefront, cart, or checkout changes. **Added:** *Tax filings admin console* — A new Tax filings section tracks sales-tax remittance: open a filing period for a jurisdiction and reporting kind, compute the tax collected from completed orders in the window with a per-rate breakdown, then record submission and payment through the filing lifecycle. The list surfaces filings due soon or overdue, and a remittance report totals tax owed per jurisdiction across a date range. This is reporting only — it reads completed orders and changes nothing in checkout.
|
|
14
|
+
|
|
11
15
|
- v0.3.3 (2026-05-30) — **Manage quantity-break (volume) pricing from the admin console.** The admin console gains a Quantity breaks section for creating and managing volume-pricing tier sets — buy-more-save-more rules scoped to a SKU, product, collection, vendor, category, or the whole store. Each set defines one or more quantity thresholds (e.g. buy 5, buy 10) with a percentage off, an amount off each unit, an amount off the line, or a fixed per-unit price, and can be marked exclusive or left to stack with others. A sample-price preview shows the discounted unit at each threshold before you commit, and sets archive and restore. The tiers take effect immediately on the product page, in the cart, and at checkout. **Added:** *Quantity Breaks admin console* — A new Quantity breaks section manages volume-pricing tier sets: create a set scoped to a SKU, product, collection, vendor, category, or globally; define one or more quantity thresholds with a percent-off, amount-off-each, amount-off-line, or fixed-per-unit discount; mark a set exclusive or let it stack. A sample-price preview shows the discounted unit at each threshold, and sets archive and restore. The tiers apply automatically on the product page, the cart, and at checkout.
|
|
12
16
|
|
|
13
17
|
- v0.3.2 (2026-05-30) — **Creating a product lands you on its detail screen to finish setup.** Creating a product from the admin console now takes you straight to that product's detail screen — where you add a variant, set its price, and add stock — instead of returning you to the product list. A banner on the new product names the remaining steps to make it sellable. This removes the hunt-for-the-product-you-just-made step from the catalog setup flow. **Changed:** *Product create flows straight into editing* — Submitting the new-product form now redirects to the product's detail screen with a banner naming the next steps — add a variant with a SKU, set its price, and add stock — rather than bouncing back to the product list. Configuring a just-created product no longer requires finding it again first.
|
package/lib/admin.js
CHANGED
|
@@ -145,6 +145,11 @@ var _UPLOAD_IMAGE_CT = {
|
|
|
145
145
|
// generic 413.
|
|
146
146
|
var _UPLOAD_MAX_BYTES = b.constants.BYTES.mib(10);
|
|
147
147
|
|
|
148
|
+
// Cap on the standalone shipping-labels list / pending-queue reads. Matches
|
|
149
|
+
// the labels primitive's own MAX_PENDING_LIMIT so a console drain can never
|
|
150
|
+
// ask for more than the primitive will return.
|
|
151
|
+
var MAX_LABEL_LIST_LIMIT = 200;
|
|
152
|
+
|
|
148
153
|
// ---- shared helpers -----------------------------------------------------
|
|
149
154
|
|
|
150
155
|
function _parseEpochMs(str, label) {
|
|
@@ -438,6 +443,7 @@ function mount(router, deps) {
|
|
|
438
443
|
var pickLists = deps.pickLists || null; // warehouse pick-list console disabled when absent
|
|
439
444
|
var shippingLabels = deps.shippingLabels || null; // per-shipment carrier-label record disabled when absent
|
|
440
445
|
var splitShipments = deps.splitShipments || null; // order split-shipment planner disabled when absent
|
|
446
|
+
var salesTaxFilings = deps.salesTaxFilings || null; // sales-tax-filing remittance console disabled when absent
|
|
441
447
|
|
|
442
448
|
// Which optional console sections are wired — gates their nav links so a
|
|
443
449
|
// signed-in admin is never sent to a route that wasn't mounted. Passed
|
|
@@ -445,7 +451,7 @@ function mount(router, deps) {
|
|
|
445
451
|
// `reports` is always present in the nav (read-only sales summary needs no
|
|
446
452
|
// extra dep); its route mounts unconditionally and renders an unconfigured
|
|
447
453
|
// notice when the salesReports primitive isn't wired.
|
|
448
|
-
var navAvailable = { returns: !!returns, reviews: !!reviews, productQa: !!productQa, subscriptions: !!deps.subscriptions, webhooks: !!deps.webhooks, collections: !!deps.collections, customers: !!deps.customers, giftcards: !!deps.giftcards, announcementBar: !!deps.announcementBar, customerSurveys: !!deps.customerSurveys, businessHours: !!deps.businessHours, taxRates: !!deps.taxRates, shippingZones: !!deps.shippingZones, autoDiscount: !!deps.autoDiscount, quantityDiscounts: !!deps.quantityDiscounts, pickLists: !!pickLists };
|
|
454
|
+
var navAvailable = { returns: !!returns, reviews: !!reviews, productQa: !!productQa, subscriptions: !!deps.subscriptions, webhooks: !!deps.webhooks, collections: !!deps.collections, customers: !!deps.customers, giftcards: !!deps.giftcards, announcementBar: !!deps.announcementBar, customerSurveys: !!deps.customerSurveys, businessHours: !!deps.businessHours, taxRates: !!deps.taxRates, shippingZones: !!deps.shippingZones, autoDiscount: !!deps.autoDiscount, quantityDiscounts: !!deps.quantityDiscounts, pickLists: !!pickLists, salesTaxFilings: !!salesTaxFilings, shippingLabels: !!shippingLabels };
|
|
449
455
|
|
|
450
456
|
try { b.audit.registerNamespace(AUDIT_NAMESPACE); } catch (_e) { /* idempotent */ }
|
|
451
457
|
|
|
@@ -1670,6 +1676,46 @@ function mount(router, deps) {
|
|
|
1670
1676
|
_redirect(res, "/admin/orders/" + enc + "?label=1");
|
|
1671
1677
|
},
|
|
1672
1678
|
));
|
|
1679
|
+
|
|
1680
|
+
// Void a recorded label (broker void, within the 30-day window).
|
|
1681
|
+
// purchased → voided. Mirrors the /used route: the primitive enforces
|
|
1682
|
+
// the transition + window, a TypeError (bad id / missing reason) or a
|
|
1683
|
+
// SHIPPING_LABELS_* coded error (wrong status / expired window) surfaces
|
|
1684
|
+
// as a 400 on the JSON path and an `?err` notice on the browser path.
|
|
1685
|
+
router.post("/admin/orders/:id/labels/:labelId/void", _pageOrApi(false,
|
|
1686
|
+
W("order.shipment.label.void", async function (req, res) {
|
|
1687
|
+
var body = req.body || {};
|
|
1688
|
+
var out;
|
|
1689
|
+
try {
|
|
1690
|
+
out = await shippingLabels.voidLabel({ label_id: req.params.labelId, reason: body.reason });
|
|
1691
|
+
} catch (e) {
|
|
1692
|
+
if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message);
|
|
1693
|
+
if (e && e.code && /SHIPPING_LABELS/.test(e.code)) return _problem(res, 400, "bad-request", e.message);
|
|
1694
|
+
throw e;
|
|
1695
|
+
}
|
|
1696
|
+
_json(res, 200, out);
|
|
1697
|
+
return { id: req.params.id };
|
|
1698
|
+
}),
|
|
1699
|
+
async function (req, res) {
|
|
1700
|
+
var id = req.params.id;
|
|
1701
|
+
var enc = encodeURIComponent(id);
|
|
1702
|
+
var body = req.body || {};
|
|
1703
|
+
try {
|
|
1704
|
+
await shippingLabels.voidLabel({
|
|
1705
|
+
label_id: req.params.labelId,
|
|
1706
|
+
// A blank reason field reaches the primitive as undefined so the
|
|
1707
|
+
// operator-facing "reason required" message surfaces, rather than
|
|
1708
|
+
// the empty string slipping past the non-empty check.
|
|
1709
|
+
reason: (body.reason && body.reason.length) ? body.reason : undefined,
|
|
1710
|
+
});
|
|
1711
|
+
} catch (e) {
|
|
1712
|
+
if (!(e instanceof TypeError) && !(e && e.code && /SHIPPING_LABELS/.test(e.code))) throw e;
|
|
1713
|
+
return _redirect(res, "/admin/orders/" + enc + "?err=1");
|
|
1714
|
+
}
|
|
1715
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".order.shipment.label.void", outcome: "success", metadata: { id: id, label_id: req.params.labelId } });
|
|
1716
|
+
_redirect(res, "/admin/orders/" + enc + "?label=1");
|
|
1717
|
+
},
|
|
1718
|
+
));
|
|
1673
1719
|
}
|
|
1674
1720
|
|
|
1675
1721
|
// Compose requestLabel + markPurchased into one recorded label. The
|
|
@@ -1712,6 +1758,206 @@ function mount(router, deps) {
|
|
|
1712
1758
|
});
|
|
1713
1759
|
}
|
|
1714
1760
|
|
|
1761
|
+
// ---- shipping labels (standalone back-office console) ---------------
|
|
1762
|
+
//
|
|
1763
|
+
// A cross-order view of the carrier-label store, separate from the
|
|
1764
|
+
// per-shipment panel on the order detail. Three screens, each composing
|
|
1765
|
+
// a labels-primitive read:
|
|
1766
|
+
// - /admin/shipping-labels — a status-filtered list. The
|
|
1767
|
+
// primitive exposes a list read per backable status: `pending`
|
|
1768
|
+
// (pendingLabels) and `voided` (voidedInWindow over a date range).
|
|
1769
|
+
// There is no cross-order list for purchased/used labels — those
|
|
1770
|
+
// are reached from their order detail — so the filter offers the
|
|
1771
|
+
// two engine-backed views, defaulting to pending.
|
|
1772
|
+
// - /admin/shipping-labels/costs — broker-spend report grouped by
|
|
1773
|
+
// broker + currency over a date range (costsByPeriod).
|
|
1774
|
+
// - /admin/shipping-labels/pending — the mint queue (pendingLabels,
|
|
1775
|
+
// capped at 200) the operator's broker worker drains.
|
|
1776
|
+
// Mounted only when the labels primitive is wired. Every HTML handler
|
|
1777
|
+
// parses its from/to/carrier query params defensively: a malformed value
|
|
1778
|
+
// re-renders the page with a correction notice (never a 500). The bearer
|
|
1779
|
+
// JSON contract returns the same composed payload.
|
|
1780
|
+
if (shippingLabels) {
|
|
1781
|
+
// Resolve a {from, to} epoch-ms window from the query string, accepting
|
|
1782
|
+
// either raw epoch-ms (from/to — machine clients) or calendar dates
|
|
1783
|
+
// (from-date/to-date — the browser date inputs). `to-date` is the
|
|
1784
|
+
// inclusive end day, advanced to the next UTC midnight. Throws TypeError
|
|
1785
|
+
// on a malformed value; defaults to the trailing 30 days. Mirrors the
|
|
1786
|
+
// reports window resolver so both report surfaces parse identically.
|
|
1787
|
+
function _labelWindow(url) {
|
|
1788
|
+
var from = _parseEpochMs(url && url.searchParams.get("from"), "from");
|
|
1789
|
+
var to = _parseEpochMs(url && url.searchParams.get("to"), "to");
|
|
1790
|
+
if (from == null) from = _parseDateParam(url && url.searchParams.get("from-date"), "from");
|
|
1791
|
+
if (to == null) {
|
|
1792
|
+
var toDate = _parseDateParam(url && url.searchParams.get("to-date"), "to");
|
|
1793
|
+
if (toDate != null) to = toDate + b.constants.TIME.days(1);
|
|
1794
|
+
}
|
|
1795
|
+
var now = Date.now();
|
|
1796
|
+
return {
|
|
1797
|
+
to: to == null ? now : to,
|
|
1798
|
+
from: from == null ? (now - b.constants.TIME.days(30)) : from,
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// The status filter on the standalone list — only the values the
|
|
1803
|
+
// primitive exposes a cross-order list read for.
|
|
1804
|
+
function _labelStatusParam(url) {
|
|
1805
|
+
var raw = url && url.searchParams.get("status");
|
|
1806
|
+
if (raw == null || raw === "") return "pending";
|
|
1807
|
+
if (raw !== "pending" && raw !== "voided") {
|
|
1808
|
+
throw new TypeError("admin: status must be one of pending, voided");
|
|
1809
|
+
}
|
|
1810
|
+
return raw;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// Optional carrier filter for the costs report. Validated against the
|
|
1814
|
+
// primitive's frozen enum so a bad value is a notice, not a 500.
|
|
1815
|
+
function _labelCarrierParam(url) {
|
|
1816
|
+
var raw = url && url.searchParams.get("carrier");
|
|
1817
|
+
if (raw == null || raw === "") return null;
|
|
1818
|
+
if (shippingLabels.CARRIERS.indexOf(raw) === -1) {
|
|
1819
|
+
throw new TypeError("admin: carrier must be one of " + shippingLabels.CARRIERS.join(", "));
|
|
1820
|
+
}
|
|
1821
|
+
return raw;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// Compose the list payload for the chosen status. `pending` drains the
|
|
1825
|
+
// queue (limit 200, ignoring the window — a pending label has no
|
|
1826
|
+
// purchased/voided timestamp to range on); `voided` ranges voided_at
|
|
1827
|
+
// over the window.
|
|
1828
|
+
async function _labelListPayload(status, win) {
|
|
1829
|
+
if (status === "voided") {
|
|
1830
|
+
var voided = await shippingLabels.voidedInWindow({ from: win.from, to: win.to });
|
|
1831
|
+
return { status: "voided", from: win.from, to: win.to, labels: voided };
|
|
1832
|
+
}
|
|
1833
|
+
var pending = await shippingLabels.pendingLabels({ limit: MAX_LABEL_LIST_LIMIT });
|
|
1834
|
+
return { status: "pending", from: win.from, to: win.to, labels: pending };
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
router.get("/admin/shipping-labels", _pageOrApi(true,
|
|
1838
|
+
R(async function (req, res) {
|
|
1839
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
1840
|
+
var status, win;
|
|
1841
|
+
try { status = _labelStatusParam(url); win = _labelWindow(url); }
|
|
1842
|
+
catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
|
|
1843
|
+
var payload;
|
|
1844
|
+
try { payload = await _labelListPayload(status, win); }
|
|
1845
|
+
catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
|
|
1846
|
+
_json(res, 200, payload);
|
|
1847
|
+
}),
|
|
1848
|
+
async function (req, res) {
|
|
1849
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
1850
|
+
var status = "pending", notice = null;
|
|
1851
|
+
var win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
|
|
1852
|
+
try { status = _labelStatusParam(url); win = _labelWindow(url); }
|
|
1853
|
+
catch (e) {
|
|
1854
|
+
if (!(e instanceof TypeError)) throw e;
|
|
1855
|
+
notice = e.message.replace(/^admin:\s*/, "");
|
|
1856
|
+
}
|
|
1857
|
+
var payload;
|
|
1858
|
+
try { payload = await _labelListPayload(status, win); }
|
|
1859
|
+
catch (e2) {
|
|
1860
|
+
if (!(e2 instanceof TypeError)) throw e2;
|
|
1861
|
+
// A bad range that slipped past the param parse (primitive
|
|
1862
|
+
// re-validates from < to) → fall back to the default window.
|
|
1863
|
+
win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
|
|
1864
|
+
notice = e2.message.replace(/^shipping-labels:\s*/, "");
|
|
1865
|
+
payload = await _labelListPayload(status, win);
|
|
1866
|
+
}
|
|
1867
|
+
_sendHtml(res, 200, renderAdminShippingLabels({
|
|
1868
|
+
shop_name: deps.shop_name, nav_available: navAvailable,
|
|
1869
|
+
payload: payload, notice: notice,
|
|
1870
|
+
}));
|
|
1871
|
+
},
|
|
1872
|
+
));
|
|
1873
|
+
|
|
1874
|
+
router.get("/admin/shipping-labels/pending", _pageOrApi(true,
|
|
1875
|
+
R(async function (req, res) {
|
|
1876
|
+
var rows;
|
|
1877
|
+
try { rows = await shippingLabels.pendingLabels({ limit: MAX_LABEL_LIST_LIMIT }); }
|
|
1878
|
+
catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
|
|
1879
|
+
_json(res, 200, { limit: MAX_LABEL_LIST_LIMIT, labels: rows });
|
|
1880
|
+
}),
|
|
1881
|
+
async function (req, res) {
|
|
1882
|
+
var rows = [], notice = null;
|
|
1883
|
+
try { rows = await shippingLabels.pendingLabels({ limit: MAX_LABEL_LIST_LIMIT }); }
|
|
1884
|
+
catch (e) {
|
|
1885
|
+
if (!(e instanceof TypeError)) throw e;
|
|
1886
|
+
notice = e.message.replace(/^shipping-labels:\s*/, "");
|
|
1887
|
+
}
|
|
1888
|
+
_sendHtml(res, 200, renderAdminShippingLabelsPending({
|
|
1889
|
+
shop_name: deps.shop_name, nav_available: navAvailable,
|
|
1890
|
+
labels: rows, notice: notice,
|
|
1891
|
+
}));
|
|
1892
|
+
},
|
|
1893
|
+
));
|
|
1894
|
+
|
|
1895
|
+
router.get("/admin/shipping-labels/costs", _pageOrApi(true,
|
|
1896
|
+
R(async function (req, res) {
|
|
1897
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
1898
|
+
var win, carrier;
|
|
1899
|
+
try { win = _labelWindow(url); carrier = _labelCarrierParam(url); }
|
|
1900
|
+
catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
|
|
1901
|
+
var report;
|
|
1902
|
+
try { report = await _labelCostsReport(win, carrier); }
|
|
1903
|
+
catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
|
|
1904
|
+
_json(res, 200, report);
|
|
1905
|
+
}),
|
|
1906
|
+
async function (req, res) {
|
|
1907
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
1908
|
+
var notice = null, carrier = null;
|
|
1909
|
+
var win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
|
|
1910
|
+
try { win = _labelWindow(url); carrier = _labelCarrierParam(url); }
|
|
1911
|
+
catch (e) {
|
|
1912
|
+
if (!(e instanceof TypeError)) throw e;
|
|
1913
|
+
notice = e.message.replace(/^admin:\s*/, "");
|
|
1914
|
+
}
|
|
1915
|
+
var report;
|
|
1916
|
+
try { report = await _labelCostsReport(win, carrier); }
|
|
1917
|
+
catch (e2) {
|
|
1918
|
+
if (!(e2 instanceof TypeError)) throw e2;
|
|
1919
|
+
win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
|
|
1920
|
+
carrier = null;
|
|
1921
|
+
notice = e2.message.replace(/^shipping-labels:\s*/, "");
|
|
1922
|
+
report = await _labelCostsReport(win, carrier);
|
|
1923
|
+
}
|
|
1924
|
+
_sendHtml(res, 200, renderAdminShippingLabelCosts({
|
|
1925
|
+
shop_name: deps.shop_name, nav_available: navAvailable,
|
|
1926
|
+
report: report, carriers: shippingLabels.CARRIERS, notice: notice,
|
|
1927
|
+
}));
|
|
1928
|
+
},
|
|
1929
|
+
));
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// Compose the broker-spend report: the per-(broker, currency) aggregate
|
|
1933
|
+
// from costsByPeriod plus a per-currency rollup of the gross spend +
|
|
1934
|
+
// label count across the window. `_strictMinorInt`-safe — the primitive
|
|
1935
|
+
// already returns integer minor units; the rollup only sums them.
|
|
1936
|
+
async function _labelCostsReport(win, carrier) {
|
|
1937
|
+
var opts2 = { from: win.from, to: win.to };
|
|
1938
|
+
if (carrier) opts2.carrier = carrier;
|
|
1939
|
+
var rows = await shippingLabels.costsByPeriod(opts2);
|
|
1940
|
+
var byCurrency = {};
|
|
1941
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
1942
|
+
var r = rows[i];
|
|
1943
|
+
var cur = r.currency || "USD";
|
|
1944
|
+
if (!byCurrency[cur]) byCurrency[cur] = { currency: cur, total_minor: 0, label_count: 0 };
|
|
1945
|
+
// costsByPeriod coerces these to plain numbers already; treat them as
|
|
1946
|
+
// integer minor units (admin never renders raw broker prose here).
|
|
1947
|
+
byCurrency[cur].total_minor += _strictMinorInt(r.total_minor, "shippingLabels", "total_minor");
|
|
1948
|
+
byCurrency[cur].label_count += _strictMinorInt(r.label_count, "shippingLabels", "label_count");
|
|
1949
|
+
}
|
|
1950
|
+
var totals = Object.keys(byCurrency).map(function (k) { return byCurrency[k]; });
|
|
1951
|
+
totals.sort(function (a, c) { return a.currency < c.currency ? -1 : a.currency > c.currency ? 1 : 0; });
|
|
1952
|
+
return {
|
|
1953
|
+
from: win.from,
|
|
1954
|
+
to: win.to,
|
|
1955
|
+
carrier: carrier || null,
|
|
1956
|
+
by_broker: rows,
|
|
1957
|
+
totals: totals,
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1715
1961
|
// ---- split shipments (order-detail planner) -------------------------
|
|
1716
1962
|
//
|
|
1717
1963
|
// Splits one order into N parcels: ship some lines now, the rest later.
|
|
@@ -4582,6 +4828,252 @@ function mount(router, deps) {
|
|
|
4582
4828
|
));
|
|
4583
4829
|
}
|
|
4584
4830
|
|
|
4831
|
+
// ---- sales tax filings ----------------------------------------------
|
|
4832
|
+
//
|
|
4833
|
+
// Post-checkout remittance bookkeeping. Each row aggregates completed
|
|
4834
|
+
// orders that fell inside a filing window for one jurisdiction, then
|
|
4835
|
+
// walks the lifecycle the authority expects: draft → computed (the
|
|
4836
|
+
// aggregation snapshot) → submitted (filed) → paid (remittance cleared),
|
|
4837
|
+
// with an amend path for a correction. The console never touches cart /
|
|
4838
|
+
// checkout / order pricing — it reads orders the storefront already
|
|
4839
|
+
// wrote. A missing sales_tax_filings table only surfaces when a route
|
|
4840
|
+
// reads it (degrades to a notice), never at boot.
|
|
4841
|
+
if (salesTaxFilings) {
|
|
4842
|
+
// Translate a create form / JSON body into a defineFilingPeriod input.
|
|
4843
|
+
// The four window fields are epoch-ms; strict integer parsing so a
|
|
4844
|
+
// typo is a 400, never a silent zero.
|
|
4845
|
+
function _filingPeriodInput(body) {
|
|
4846
|
+
return {
|
|
4847
|
+
jurisdiction: typeof body.jurisdiction === "string" ? body.jurisdiction.trim().toUpperCase() : body.jurisdiction,
|
|
4848
|
+
kind: typeof body.kind === "string" ? body.kind.trim() : body.kind,
|
|
4849
|
+
period_start: _strictMinorInt(body.period_start, "salesTaxFilings", "period_start (epoch-ms)"),
|
|
4850
|
+
period_end: _strictMinorInt(body.period_end, "salesTaxFilings", "period_end (epoch-ms)"),
|
|
4851
|
+
due_date: _strictMinorInt(body.due_date, "salesTaxFilings", "due_date (epoch-ms)"),
|
|
4852
|
+
};
|
|
4853
|
+
}
|
|
4854
|
+
|
|
4855
|
+
// Build the list-screen model: the filtered filings, the upcoming-due
|
|
4856
|
+
// strip, and the filter values echoed back into the form. A malformed
|
|
4857
|
+
// filter (bad jurisdiction / status) throws TypeError in the primitive
|
|
4858
|
+
// — map it to an empty list so the page renders the notice, never 500.
|
|
4859
|
+
async function _filingsForBrowser(filter) {
|
|
4860
|
+
try { return await salesTaxFilings.listFilings(filter); }
|
|
4861
|
+
catch (e) { if (e instanceof TypeError) return []; throw e; }
|
|
4862
|
+
}
|
|
4863
|
+
async function _upcomingForBrowser() {
|
|
4864
|
+
try { return await salesTaxFilings.upcomingDue({ days_ahead: 30 }); }
|
|
4865
|
+
catch (e) { if (e instanceof TypeError) return []; throw e; }
|
|
4866
|
+
}
|
|
4867
|
+
|
|
4868
|
+
router.get("/admin/tax-filings", _pageOrApi(true,
|
|
4869
|
+
R(async function (req, res) {
|
|
4870
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
4871
|
+
var filter = {};
|
|
4872
|
+
var j = url && url.searchParams.get("jurisdiction");
|
|
4873
|
+
var s = url && url.searchParams.get("status");
|
|
4874
|
+
if (j) filter.jurisdiction = String(j).trim().toUpperCase();
|
|
4875
|
+
if (s) filter.status = String(s).trim();
|
|
4876
|
+
var rows;
|
|
4877
|
+
try { rows = await salesTaxFilings.listFilings(filter); }
|
|
4878
|
+
catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
|
|
4879
|
+
_json(res, 200, { rows: rows });
|
|
4880
|
+
}),
|
|
4881
|
+
async function (req, res) {
|
|
4882
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
4883
|
+
var j = url && url.searchParams.get("jurisdiction");
|
|
4884
|
+
var s = url && url.searchParams.get("status");
|
|
4885
|
+
var jurisdiction = j ? String(j).trim().toUpperCase() : null;
|
|
4886
|
+
var status = s ? String(s).trim() : null;
|
|
4887
|
+
var filter = {};
|
|
4888
|
+
if (jurisdiction) filter.jurisdiction = jurisdiction;
|
|
4889
|
+
if (status) filter.status = status;
|
|
4890
|
+
var rows = await _filingsForBrowser(filter);
|
|
4891
|
+
var upcoming = await _upcomingForBrowser();
|
|
4892
|
+
_sendHtml(res, 200, renderAdminTaxFilings({
|
|
4893
|
+
shop_name: deps.shop_name, nav_available: navAvailable,
|
|
4894
|
+
filings: rows, upcoming: upcoming,
|
|
4895
|
+
jurisdiction: jurisdiction, status_filter: status,
|
|
4896
|
+
kinds: salesTaxFilings.KINDS, statuses: salesTaxFilings.STATUSES,
|
|
4897
|
+
created: url && url.searchParams.get("created"),
|
|
4898
|
+
notice: (url && url.searchParams.get("err")) ? "That action couldn't be completed for the filing." : null,
|
|
4899
|
+
}));
|
|
4900
|
+
},
|
|
4901
|
+
));
|
|
4902
|
+
|
|
4903
|
+
// Open a filing period → draft row. A duplicate (jurisdiction, kind,
|
|
4904
|
+
// period_start) is refused by the primitive's UNIQUE index; surface it
|
|
4905
|
+
// as a 400 notice rather than a 500.
|
|
4906
|
+
router.post("/admin/tax-filings", _pageOrApi(false,
|
|
4907
|
+
W("tax_filing.create", async function (req, res) {
|
|
4908
|
+
var filing;
|
|
4909
|
+
try { filing = await salesTaxFilings.defineFilingPeriod(_filingPeriodInput(req.body || {})); }
|
|
4910
|
+
catch (e) {
|
|
4911
|
+
if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message);
|
|
4912
|
+
if (e.code === "SALES_TAX_FILING_DUPLICATE") return _problem(res, 409, "filing-duplicate", e.message);
|
|
4913
|
+
throw e;
|
|
4914
|
+
}
|
|
4915
|
+
_json(res, 201, filing);
|
|
4916
|
+
return { id: filing.id };
|
|
4917
|
+
}),
|
|
4918
|
+
async function (req, res) {
|
|
4919
|
+
var filing;
|
|
4920
|
+
try { filing = await salesTaxFilings.defineFilingPeriod(_filingPeriodInput(req.body || {})); }
|
|
4921
|
+
catch (e) {
|
|
4922
|
+
if (!(e instanceof TypeError) && e.code !== "SALES_TAX_FILING_DUPLICATE") throw e;
|
|
4923
|
+
var rows = await _filingsForBrowser({});
|
|
4924
|
+
var upcoming = await _upcomingForBrowser();
|
|
4925
|
+
return _sendHtml(res, 400, renderAdminTaxFilings({
|
|
4926
|
+
shop_name: deps.shop_name, nav_available: navAvailable,
|
|
4927
|
+
filings: rows, upcoming: upcoming,
|
|
4928
|
+
kinds: salesTaxFilings.KINDS, statuses: salesTaxFilings.STATUSES,
|
|
4929
|
+
notice: (e && e.message || "").replace(/^salesTaxFilings[.:]\s*/, ""),
|
|
4930
|
+
}));
|
|
4931
|
+
}
|
|
4932
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".tax_filing.create", outcome: "success", metadata: { id: filing.id } });
|
|
4933
|
+
_redirect(res, "/admin/tax-filings/" + encodeURIComponent(filing.id) + "?created=1");
|
|
4934
|
+
},
|
|
4935
|
+
));
|
|
4936
|
+
|
|
4937
|
+
// Per-jurisdiction remittance report over a [from, to] window. GET so
|
|
4938
|
+
// the window lives in the URL (bookmarkable). A bad / missing range is
|
|
4939
|
+
// a notice on the list page, not a 500.
|
|
4940
|
+
router.get("/admin/tax-filings/report", _pageOrApi(true,
|
|
4941
|
+
R(async function (req, res) {
|
|
4942
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
4943
|
+
var j = url && url.searchParams.get("jurisdiction");
|
|
4944
|
+
if (!j) return _problem(res, 400, "bad-request", "jurisdiction required");
|
|
4945
|
+
var from = _parseEpochMs(url && url.searchParams.get("from"), "from");
|
|
4946
|
+
var to = _parseEpochMs(url && url.searchParams.get("to"), "to");
|
|
4947
|
+
if (from == null || to == null) return _problem(res, 400, "bad-request", "from and to (epoch-ms) required");
|
|
4948
|
+
var report;
|
|
4949
|
+
try { report = await salesTaxFilings.auditReportForJurisdiction({ jurisdiction: String(j).trim().toUpperCase(), from: from, to: to }); }
|
|
4950
|
+
catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
|
|
4951
|
+
_json(res, 200, report);
|
|
4952
|
+
}),
|
|
4953
|
+
async function (req, res) {
|
|
4954
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
4955
|
+
var j = url && url.searchParams.get("jurisdiction");
|
|
4956
|
+
var jurisdiction = j ? String(j).trim().toUpperCase() : null;
|
|
4957
|
+
// Parse the window defensively — a present-but-malformed epoch throws
|
|
4958
|
+
// TypeError; treat it as unset so the window notice guides the operator
|
|
4959
|
+
// (a bad range is a notice on this page, never a 500).
|
|
4960
|
+
var from = null, to = null;
|
|
4961
|
+
try {
|
|
4962
|
+
from = _parseEpochMs(url && url.searchParams.get("from"), "from");
|
|
4963
|
+
to = _parseEpochMs(url && url.searchParams.get("to"), "to");
|
|
4964
|
+
} catch (e) { if (!(e instanceof TypeError)) throw e; }
|
|
4965
|
+
var report = null, notice = null;
|
|
4966
|
+
if (!jurisdiction) {
|
|
4967
|
+
notice = "Enter a jurisdiction and a window to run a remittance report.";
|
|
4968
|
+
} else if (from == null || to == null) {
|
|
4969
|
+
notice = "Enter both a from and a to date (epoch-ms) for the report window.";
|
|
4970
|
+
} else {
|
|
4971
|
+
try { report = await salesTaxFilings.auditReportForJurisdiction({ jurisdiction: jurisdiction, from: from, to: to }); }
|
|
4972
|
+
catch (e) { if (!(e instanceof TypeError)) throw e; notice = (e.message || "").replace(/^salesTaxFilings[.:]\s*/, ""); }
|
|
4973
|
+
}
|
|
4974
|
+
_sendHtml(res, 200, renderAdminTaxFilingReport({
|
|
4975
|
+
shop_name: deps.shop_name, nav_available: navAvailable,
|
|
4976
|
+
report: report, jurisdiction: jurisdiction, notice: notice,
|
|
4977
|
+
}));
|
|
4978
|
+
},
|
|
4979
|
+
));
|
|
4980
|
+
|
|
4981
|
+
async function _filingDetailModel(id) {
|
|
4982
|
+
return await salesTaxFilings.getFiling(id);
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4985
|
+
router.get("/admin/tax-filings/:id", _pageOrApi(true,
|
|
4986
|
+
R(async function (req, res) {
|
|
4987
|
+
var filing;
|
|
4988
|
+
try { filing = await _filingDetailModel(req.params.id); }
|
|
4989
|
+
catch (e) { if (e instanceof TypeError) return _problem(res, 404, "filing-not-found", e.message); throw e; }
|
|
4990
|
+
if (!filing) return _problem(res, 404, "filing-not-found");
|
|
4991
|
+
_json(res, 200, filing);
|
|
4992
|
+
}),
|
|
4993
|
+
async function (req, res) {
|
|
4994
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
4995
|
+
var filing;
|
|
4996
|
+
try { filing = await _filingDetailModel(req.params.id); }
|
|
4997
|
+
catch (e) { if (!(e instanceof TypeError)) throw e; filing = null; }
|
|
4998
|
+
if (!filing) return _sendHtml(res, 404, renderAdminTaxFilings({
|
|
4999
|
+
shop_name: deps.shop_name, nav_available: navAvailable,
|
|
5000
|
+
filings: [], upcoming: [], kinds: salesTaxFilings.KINDS, statuses: salesTaxFilings.STATUSES,
|
|
5001
|
+
notice: "Filing not found.",
|
|
5002
|
+
}));
|
|
5003
|
+
_sendHtml(res, 200, renderAdminTaxFiling({
|
|
5004
|
+
shop_name: deps.shop_name, nav_available: navAvailable, filing: filing,
|
|
5005
|
+
computed: url && url.searchParams.get("computed"),
|
|
5006
|
+
created: url && url.searchParams.get("created"),
|
|
5007
|
+
submitted: url && url.searchParams.get("submitted"),
|
|
5008
|
+
paid: url && url.searchParams.get("paid"),
|
|
5009
|
+
amended: url && url.searchParams.get("amended"),
|
|
5010
|
+
notice: (url && url.searchParams.get("err"))
|
|
5011
|
+
? String(url.searchParams.get("err_msg") || "That action couldn't be completed for the filing.")
|
|
5012
|
+
: null,
|
|
5013
|
+
}));
|
|
5014
|
+
},
|
|
5015
|
+
));
|
|
5016
|
+
|
|
5017
|
+
// Lifecycle transitions — each is a browser POST that PRGs back to the
|
|
5018
|
+
// detail. A bad transition (wrong status) or bad input is a ?err notice
|
|
5019
|
+
// on the detail page, never a 500. The bearer JSON contract returns the
|
|
5020
|
+
// updated filing (or a problem document).
|
|
5021
|
+
function _filingActionRoute(suffix, audit, okParam, run) {
|
|
5022
|
+
router.post("/admin/tax-filings/:id/" + suffix, _pageOrApi(false,
|
|
5023
|
+
W("tax_filing." + audit, async function (req, res) {
|
|
5024
|
+
var filing;
|
|
5025
|
+
try { filing = await run(req.params.id, req.body || {}); }
|
|
5026
|
+
catch (e) {
|
|
5027
|
+
if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message);
|
|
5028
|
+
if (e.code === "SALES_TAX_FILING_NOT_FOUND") return _problem(res, 404, "filing-not-found", e.message);
|
|
5029
|
+
if (e.code === "SALES_TAX_FILING_BAD_TRANSITION") return _problem(res, 409, "filing-bad-transition", e.message);
|
|
5030
|
+
throw e;
|
|
5031
|
+
}
|
|
5032
|
+
_json(res, 200, filing);
|
|
5033
|
+
return { id: filing.id };
|
|
5034
|
+
}),
|
|
5035
|
+
async function (req, res) {
|
|
5036
|
+
var enc = encodeURIComponent(req.params.id);
|
|
5037
|
+
try {
|
|
5038
|
+
await run(req.params.id, req.body || {});
|
|
5039
|
+
} catch (e) {
|
|
5040
|
+
if (!(e instanceof TypeError) &&
|
|
5041
|
+
e.code !== "SALES_TAX_FILING_NOT_FOUND" &&
|
|
5042
|
+
e.code !== "SALES_TAX_FILING_BAD_TRANSITION") throw e;
|
|
5043
|
+
var msg = (e && e.message || "").replace(/^salesTaxFilings[.:]\s*/, "");
|
|
5044
|
+
return _redirect(res, "/admin/tax-filings/" + enc + "?err=1&err_msg=" + encodeURIComponent(msg));
|
|
5045
|
+
}
|
|
5046
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".tax_filing." + audit, outcome: "success", metadata: { id: req.params.id } });
|
|
5047
|
+
_redirect(res, "/admin/tax-filings/" + enc + "?" + okParam + "=1");
|
|
5048
|
+
},
|
|
5049
|
+
));
|
|
5050
|
+
}
|
|
5051
|
+
|
|
5052
|
+
_filingActionRoute("compute", "compute", "computed", function (id) {
|
|
5053
|
+
return salesTaxFilings.computeFiling({ filing_id: id });
|
|
5054
|
+
});
|
|
5055
|
+
_filingActionRoute("submit", "submit", "submitted", function (id, body) {
|
|
5056
|
+
return salesTaxFilings.recordSubmission({
|
|
5057
|
+
filing_id: id,
|
|
5058
|
+
submission_ref: typeof body.submission_ref === "string" ? body.submission_ref.trim() : body.submission_ref,
|
|
5059
|
+
submitted_by: typeof body.submitted_by === "string" ? body.submitted_by.trim() : body.submitted_by,
|
|
5060
|
+
});
|
|
5061
|
+
});
|
|
5062
|
+
_filingActionRoute("pay", "pay", "paid", function (id, body) {
|
|
5063
|
+
return salesTaxFilings.recordPayment({
|
|
5064
|
+
filing_id: id,
|
|
5065
|
+
payment_minor: _strictMinorInt(body.payment_minor, "salesTaxFilings", "payment_minor"),
|
|
5066
|
+
payment_ref: typeof body.payment_ref === "string" ? body.payment_ref.trim() : body.payment_ref,
|
|
5067
|
+
});
|
|
5068
|
+
});
|
|
5069
|
+
_filingActionRoute("amend", "amend", "amended", function (id, body) {
|
|
5070
|
+
return salesTaxFilings.markAmended({
|
|
5071
|
+
filing_id: id,
|
|
5072
|
+
reason: typeof body.reason === "string" ? body.reason.trim() : body.reason,
|
|
5073
|
+
});
|
|
5074
|
+
});
|
|
5075
|
+
}
|
|
5076
|
+
|
|
4585
5077
|
// Read a jurisdiction's rate history for the browser list. A null /
|
|
4586
5078
|
// unset jurisdiction yields an empty list (the page shows the picker
|
|
4587
5079
|
// form). A malformed jurisdiction throws TypeError in the primitive —
|
|
@@ -5645,7 +6137,9 @@ var ADMIN_NAV_ITEMS = [
|
|
|
5645
6137
|
{ key: "discounts", href: "/admin/discounts", label: "Discounts", requires: "autoDiscount" },
|
|
5646
6138
|
{ key: "quantity-discounts", href: "/admin/quantity-discounts", label: "Quantity breaks", requires: "quantityDiscounts" },
|
|
5647
6139
|
{ key: "tax", href: "/admin/tax-rates", label: "Tax", requires: "taxRates" },
|
|
6140
|
+
{ key: "tax-filings", href: "/admin/tax-filings", label: "Tax filings", requires: "salesTaxFilings" },
|
|
5648
6141
|
{ key: "shipping", href: "/admin/shipping", label: "Shipping", requires: "shippingZones" },
|
|
6142
|
+
{ key: "shipping-labels", href: "/admin/shipping-labels", label: "Shipping labels", requires: "shippingLabels" },
|
|
5649
6143
|
{ key: "pick-lists", href: "/admin/pick-lists", label: "Pick lists", requires: "pickLists" },
|
|
5650
6144
|
{ key: "announcements", href: "/admin/announcements", label: "Announcements", requires: "announcementBar" },
|
|
5651
6145
|
{ key: "surveys", href: "/admin/surveys", label: "Surveys", requires: "customerSurveys" },
|
|
@@ -6048,6 +6542,173 @@ function renderAdminReports(opts) {
|
|
|
6048
6542
|
return _renderAdminShell(opts.shop_name, "Reports", body, "reports", opts.nav_available);
|
|
6049
6543
|
}
|
|
6050
6544
|
|
|
6545
|
+
// A single shipping-label row for the standalone cross-order tables. Shows
|
|
6546
|
+
// carrier + service, tracking number, cost (via pricing.format from the
|
|
6547
|
+
// label's own minor units + currency), the broker it came from, its status
|
|
6548
|
+
// pill, and a link to the broker's label URL. Read-only — lifecycle actions
|
|
6549
|
+
// (mark used / void) live on the order detail where the shipment context is.
|
|
6550
|
+
function _shippingLabelRow(l) {
|
|
6551
|
+
var costCell = (l.cost_minor != null && l.currency)
|
|
6552
|
+
? _htmlEscape(pricing.format(l.cost_minor, l.currency))
|
|
6553
|
+
: "<span class=\"meta\">—</span>";
|
|
6554
|
+
var trackCell = l.tracking_number
|
|
6555
|
+
? "<code class=\"order-id\">" + _htmlEscape(String(l.tracking_number)) + "</code>"
|
|
6556
|
+
: "<span class=\"meta\">—</span>";
|
|
6557
|
+
var urlCell = l.label_url
|
|
6558
|
+
? "<a class=\"order-id\" href=\"" + _htmlEscape(String(l.label_url)) + "\" rel=\"noopener nofollow\" target=\"_blank\">Open ↗</a>"
|
|
6559
|
+
: "<span class=\"meta\">—</span>";
|
|
6560
|
+
var viaCell = l.purchased_via
|
|
6561
|
+
? _htmlEscape(String(l.purchased_via))
|
|
6562
|
+
: "<span class=\"meta\">—</span>";
|
|
6563
|
+
return "<tr>" +
|
|
6564
|
+
"<td>" + _htmlEscape(String(l.carrier)) + " <span class=\"meta\">" + _htmlEscape(String(l.service_level || "")) + "</span></td>" +
|
|
6565
|
+
"<td>" + trackCell + "</td>" +
|
|
6566
|
+
"<td class=\"num\">" + costCell + "</td>" +
|
|
6567
|
+
"<td>" + viaCell + "</td>" +
|
|
6568
|
+
"<td><span class=\"status-pill " + _htmlEscape(String(l.status)) + "\">" + _htmlEscape(String(l.status)) + "</span></td>" +
|
|
6569
|
+
"<td>" + urlCell + "</td>" +
|
|
6570
|
+
"<td>" + _htmlEscape(_fmtDate(l.created_at)) + "</td>" +
|
|
6571
|
+
"</tr>";
|
|
6572
|
+
}
|
|
6573
|
+
|
|
6574
|
+
// Sub-nav shared by the three standalone shipping-label screens — the
|
|
6575
|
+
// list, the broker-spend report, and the mint queue.
|
|
6576
|
+
function _shippingLabelSubnav(active) {
|
|
6577
|
+
var tabs = [
|
|
6578
|
+
["list", "/admin/shipping-labels", "All labels"],
|
|
6579
|
+
["pending", "/admin/shipping-labels/pending", "Mint queue"],
|
|
6580
|
+
["costs", "/admin/shipping-labels/costs", "Broker spend"],
|
|
6581
|
+
];
|
|
6582
|
+
return "<div class=\"order-filters\">" + tabs.map(function (t) {
|
|
6583
|
+
return "<a class=\"chip" + (active === t[0] ? " chip--on" : "") + "\" href=\"" + t[1] + "\">" + _htmlEscape(t[2]) + "</a>";
|
|
6584
|
+
}).join("") + "</div>";
|
|
6585
|
+
}
|
|
6586
|
+
|
|
6587
|
+
// Standalone cross-order label list. The primitive exposes a list read for
|
|
6588
|
+
// two statuses — pending (the mint queue) and voided (over a date range) —
|
|
6589
|
+
// so the status filter offers those two. Purchased + used labels are
|
|
6590
|
+
// reached from their order's detail panel, not browsed cross-order. A date
|
|
6591
|
+
// range narrows the voided view (ignored for pending, which has no
|
|
6592
|
+
// purchased/voided timestamp to range on).
|
|
6593
|
+
function renderAdminShippingLabels(opts) {
|
|
6594
|
+
opts = opts || {};
|
|
6595
|
+
var payload = opts.payload || { status: "pending", labels: [], from: Date.now(), to: Date.now() };
|
|
6596
|
+
var notice = opts.notice ? "<div class=\"banner banner--warn\">" + _htmlEscape(opts.notice) + "</div>" : "";
|
|
6597
|
+
var labels = payload.labels || [];
|
|
6598
|
+
var status = payload.status || "pending";
|
|
6599
|
+
|
|
6600
|
+
var statuses = [["pending", "Pending"], ["voided", "Voided"]];
|
|
6601
|
+
var chips = "<div class=\"order-filters\">" + statuses.map(function (s) {
|
|
6602
|
+
return "<a class=\"chip" + (status === s[0] ? " chip--on" : "") + "\" href=\"/admin/shipping-labels?status=" + encodeURIComponent(s[0]) + "\">" + _htmlEscape(s[1]) + "</a>";
|
|
6603
|
+
}).join("") + "</div>";
|
|
6604
|
+
|
|
6605
|
+
// Date-range form only narrows the voided view (GET so the window lives in
|
|
6606
|
+
// the URL). For pending it's still shown for consistency but the window is
|
|
6607
|
+
// not applied.
|
|
6608
|
+
var fromVal = _dateInputValue(payload.from);
|
|
6609
|
+
var toVal = _dateInputValue((payload.to || Date.now()) - b.constants.TIME.days(1));
|
|
6610
|
+
var rangeForm =
|
|
6611
|
+
"<form method=\"get\" action=\"/admin/shipping-labels\" class=\"order-filters\">" +
|
|
6612
|
+
"<input type=\"hidden\" name=\"status\" value=\"" + _htmlEscape(status) + "\">" +
|
|
6613
|
+
"<label class=\"form-field\"><span>From</span><input type=\"date\" name=\"from-date\" value=\"" + _htmlEscape(fromVal) + "\"></label>" +
|
|
6614
|
+
"<label class=\"form-field\"><span>To</span><input type=\"date\" name=\"to-date\" value=\"" + _htmlEscape(toVal) + "\"></label>" +
|
|
6615
|
+
"<button class=\"btn\" type=\"submit\">Apply</button>" +
|
|
6616
|
+
"</form>";
|
|
6617
|
+
|
|
6618
|
+
var rows = labels.map(_shippingLabelRow).join("");
|
|
6619
|
+
var table = labels.length
|
|
6620
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Carrier</th><th scope=\"col\">Tracking</th><th scope=\"col\" class=\"num\">Cost</th><th scope=\"col\">Broker</th><th scope=\"col\">Status</th><th scope=\"col\">Label</th><th scope=\"col\">Created</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
|
|
6621
|
+
: "<p class=\"empty\">No " + _htmlEscape(status) + " labels" + (status === "voided" ? " in this window" : "") + ".</p>";
|
|
6622
|
+
|
|
6623
|
+
var body =
|
|
6624
|
+
"<section><h2>Shipping labels</h2>" + notice +
|
|
6625
|
+
_shippingLabelSubnav("list") +
|
|
6626
|
+
chips +
|
|
6627
|
+
(status === "voided" ? rangeForm : "") +
|
|
6628
|
+
table +
|
|
6629
|
+
"</section>";
|
|
6630
|
+
return _renderAdminShell(opts.shop_name, "Shipping labels", body, "shipping-labels", opts.nav_available);
|
|
6631
|
+
}
|
|
6632
|
+
|
|
6633
|
+
// The mint queue: pending labels awaiting a broker mint, oldest first. The
|
|
6634
|
+
// operator's broker worker drains this (requestLabel wrote each row;
|
|
6635
|
+
// markPurchased clears it). Read-only console view of the same queue.
|
|
6636
|
+
function renderAdminShippingLabelsPending(opts) {
|
|
6637
|
+
opts = opts || {};
|
|
6638
|
+
var labels = opts.labels || [];
|
|
6639
|
+
var notice = opts.notice ? "<div class=\"banner banner--warn\">" + _htmlEscape(opts.notice) + "</div>" : "";
|
|
6640
|
+
var rows = labels.map(_shippingLabelRow).join("");
|
|
6641
|
+
var table = labels.length
|
|
6642
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Carrier</th><th scope=\"col\">Tracking</th><th scope=\"col\" class=\"num\">Cost</th><th scope=\"col\">Broker</th><th scope=\"col\">Status</th><th scope=\"col\">Label</th><th scope=\"col\">Created</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
|
|
6643
|
+
: "<p class=\"empty\">No labels awaiting a broker mint.</p>";
|
|
6644
|
+
var body =
|
|
6645
|
+
"<section><h2>Shipping labels</h2>" + notice +
|
|
6646
|
+
_shippingLabelSubnav("pending") +
|
|
6647
|
+
"<p class=\"meta\">Pending labels awaiting a broker mint, oldest first (up to " + String(MAX_LABEL_LIST_LIMIT) + ").</p>" +
|
|
6648
|
+
table +
|
|
6649
|
+
"</section>";
|
|
6650
|
+
return _renderAdminShell(opts.shop_name, "Shipping labels", body, "shipping-labels", opts.nav_available);
|
|
6651
|
+
}
|
|
6652
|
+
|
|
6653
|
+
// Broker-spend report: gross label spend + label count grouped by broker +
|
|
6654
|
+
// currency over a date range, with an optional carrier filter and a
|
|
6655
|
+
// per-currency headline rollup. Money formats through the same pricing
|
|
6656
|
+
// helper the order + reports pages use.
|
|
6657
|
+
function renderAdminShippingLabelCosts(opts) {
|
|
6658
|
+
opts = opts || {};
|
|
6659
|
+
var report = opts.report || { from: Date.now(), to: Date.now(), by_broker: [], totals: [], carrier: null };
|
|
6660
|
+
var carriers = opts.carriers || [];
|
|
6661
|
+
var notice = opts.notice ? "<div class=\"banner banner--warn\">" + _htmlEscape(opts.notice) + "</div>" : "";
|
|
6662
|
+
|
|
6663
|
+
var fromVal = _dateInputValue(report.from);
|
|
6664
|
+
var toVal = _dateInputValue(report.to - b.constants.TIME.days(1));
|
|
6665
|
+
var carrierOpts = "<option value=\"\">All carriers</option>" + carriers.map(function (c) {
|
|
6666
|
+
return "<option value=\"" + _htmlEscape(c) + "\"" + (report.carrier === c ? " selected" : "") + ">" + _htmlEscape(c) + "</option>";
|
|
6667
|
+
}).join("");
|
|
6668
|
+
var rangeForm =
|
|
6669
|
+
"<form method=\"get\" action=\"/admin/shipping-labels/costs\" class=\"order-filters\">" +
|
|
6670
|
+
"<label class=\"form-field\"><span>From</span><input type=\"date\" name=\"from-date\" value=\"" + _htmlEscape(fromVal) + "\"></label>" +
|
|
6671
|
+
"<label class=\"form-field\"><span>To</span><input type=\"date\" name=\"to-date\" value=\"" + _htmlEscape(toVal) + "\"></label>" +
|
|
6672
|
+
"<label class=\"form-field\"><span>Carrier</span><select name=\"carrier\">" + carrierOpts + "</select></label>" +
|
|
6673
|
+
"<button class=\"btn\" type=\"submit\">Apply</button>" +
|
|
6674
|
+
"</form>";
|
|
6675
|
+
|
|
6676
|
+
// Per-currency headline cards — total gross spend + label count.
|
|
6677
|
+
var totals = report.totals || [];
|
|
6678
|
+
var statsBlock = totals.length
|
|
6679
|
+
? "<section><h2>Total spend</h2><div class=\"stat-grid\">" + totals.map(function (t) {
|
|
6680
|
+
return _statCard("Spend (" + t.currency + ")", pricing.format(t.total_minor, t.currency)) +
|
|
6681
|
+
_statCard("Labels (" + t.currency + ")", String(t.label_count));
|
|
6682
|
+
}).join("") + "</div></section>"
|
|
6683
|
+
: "<section><h2>Total spend</h2><p class=\"empty\">No labels purchased in this window.</p></section>";
|
|
6684
|
+
|
|
6685
|
+
var byBroker = report.by_broker || [];
|
|
6686
|
+
var brokerRows = byBroker.length
|
|
6687
|
+
? byBroker.map(function (r) {
|
|
6688
|
+
return "<tr>" +
|
|
6689
|
+
"<td>" + _htmlEscape(String(r.purchased_via)) + "</td>" +
|
|
6690
|
+
"<td>" + _htmlEscape(String(r.currency)) + "</td>" +
|
|
6691
|
+
"<td class=\"num\">" + _htmlEscape(String(r.label_count)) + "</td>" +
|
|
6692
|
+
"<td class=\"num\">" + _htmlEscape(pricing.format(r.total_minor, r.currency)) + "</td>" +
|
|
6693
|
+
"</tr>";
|
|
6694
|
+
}).join("")
|
|
6695
|
+
: "<tr><td colspan=\"4\" class=\"empty\">No labels purchased in this window.</td></tr>";
|
|
6696
|
+
var brokerBlock =
|
|
6697
|
+
"<section><h2>By broker</h2><div class=\"panel\">" +
|
|
6698
|
+
"<table><thead><tr><th scope=\"col\">Broker</th><th scope=\"col\">Currency</th><th scope=\"col\" class=\"num\">Labels</th><th scope=\"col\" class=\"num\">Spend</th></tr></thead><tbody>" + brokerRows + "</tbody></table>" +
|
|
6699
|
+
"</div></section>";
|
|
6700
|
+
|
|
6701
|
+
var body =
|
|
6702
|
+
"<section><h2>Shipping labels</h2>" + notice +
|
|
6703
|
+
_shippingLabelSubnav("costs") +
|
|
6704
|
+
"<p class=\"meta\">Window: " + _htmlEscape(_fmtDate(report.from)) + " → " + _htmlEscape(_fmtDate(report.to)) +
|
|
6705
|
+
(report.carrier ? " · carrier " + _htmlEscape(String(report.carrier)) : "") + "</p>" +
|
|
6706
|
+
rangeForm +
|
|
6707
|
+
"</section>" +
|
|
6708
|
+
statsBlock + brokerBlock;
|
|
6709
|
+
return _renderAdminShell(opts.shop_name, "Shipping labels", body, "shipping-labels", opts.nav_available);
|
|
6710
|
+
}
|
|
6711
|
+
|
|
6051
6712
|
function renderAdminOrder(opts) {
|
|
6052
6713
|
opts = opts || {};
|
|
6053
6714
|
var o = opts.order;
|
|
@@ -6258,17 +6919,26 @@ function _orderLabelPanel(orderId, shipment, carriers, packageTypes, purchasedVi
|
|
|
6258
6919
|
var urlCell = l.label_url
|
|
6259
6920
|
? "<a class=\"order-id\" href=\"" + _htmlEscape(String(l.label_url)) + "\" rel=\"noopener nofollow\" target=\"_blank\">Open ↗</a>"
|
|
6260
6921
|
: "<span class=\"meta\">—</span>";
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6922
|
+
// A purchased label can be marked used (handed to the carrier) OR voided
|
|
6923
|
+
// (broker void, within the 30-day window). The void form carries a
|
|
6924
|
+
// required reason input so the operator records WHY before the POST; the
|
|
6925
|
+
// primitive refuses a blank/expired void with a 400 → notice.
|
|
6926
|
+
var actionCell = "<span class=\"meta\">—</span>";
|
|
6927
|
+
if (l.status === "purchased") {
|
|
6928
|
+
actionCell =
|
|
6929
|
+
"<form method=\"post\" action=\"/admin/orders/" + _htmlEscape(orderId) + "/labels/" + _htmlEscape(l.id) + "/used\" class=\"form-inline\">" +
|
|
6930
|
+
"<button class=\"btn btn--ghost\" type=\"submit\">Mark used</button></form> " +
|
|
6931
|
+
"<form method=\"post\" action=\"/admin/orders/" + _htmlEscape(orderId) + "/labels/" + _htmlEscape(l.id) + "/void\" class=\"form-inline\">" +
|
|
6932
|
+
"<input type=\"text\" name=\"reason\" placeholder=\"Void reason\" maxlength=\"512\" aria-label=\"Void reason\" required>" +
|
|
6933
|
+
"<button class=\"btn btn--danger\" type=\"submit\">Void</button></form>";
|
|
6934
|
+
}
|
|
6265
6935
|
return "<tr>" +
|
|
6266
6936
|
"<td>" + _htmlEscape(String(l.carrier)) + " <span class=\"meta\">" + _htmlEscape(String(l.service_level || "")) + "</span></td>" +
|
|
6267
6937
|
"<td>" + trackCell + "</td>" +
|
|
6268
6938
|
"<td class=\"num\">" + costCell + "</td>" +
|
|
6269
6939
|
"<td><span class=\"status-pill " + _htmlEscape(l.status) + "\">" + _htmlEscape(l.status) + "</span></td>" +
|
|
6270
6940
|
"<td>" + urlCell + "</td>" +
|
|
6271
|
-
"<td>" +
|
|
6941
|
+
"<td>" + actionCell + "</td>" +
|
|
6272
6942
|
"</tr>";
|
|
6273
6943
|
}).join("");
|
|
6274
6944
|
var labelsTable = labels.length
|
|
@@ -7166,6 +7836,280 @@ function renderAdminTaxRates(opts) {
|
|
|
7166
7836
|
return _renderAdminShell(opts.shop_name, "Tax rates", body, "tax", opts.nav_available);
|
|
7167
7837
|
}
|
|
7168
7838
|
|
|
7839
|
+
// Money for the filings screens. A filing aggregates orders that may span
|
|
7840
|
+
// currencies, so the row stores raw minor-unit totals with no single
|
|
7841
|
+
// currency column. Display them through the same pricing formatter the rest
|
|
7842
|
+
// of the console uses, in the operator's display currency (defaults to USD)
|
|
7843
|
+
// — the number is the reconciliation figure regardless of the symbol.
|
|
7844
|
+
var TAX_FILING_DISPLAY_CURRENCY = "USD";
|
|
7845
|
+
function _filingMoney(minor) {
|
|
7846
|
+
return pricing.format(Number(minor) || 0, TAX_FILING_DISPLAY_CURRENCY);
|
|
7847
|
+
}
|
|
7848
|
+
|
|
7849
|
+
// Sales-tax-filings list screen — open filings (filterable by jurisdiction
|
|
7850
|
+
// + status), an upcoming-due strip, and the open-a-period form. Read-heavy:
|
|
7851
|
+
// the lifecycle actions live on each filing's detail page.
|
|
7852
|
+
function renderAdminTaxFilings(opts) {
|
|
7853
|
+
opts = opts || {};
|
|
7854
|
+
var filings = opts.filings || [];
|
|
7855
|
+
var upcoming = opts.upcoming || [];
|
|
7856
|
+
var kinds = opts.kinds || ["monthly", "quarterly", "annual"];
|
|
7857
|
+
var statuses = opts.statuses || ["draft", "computed", "submitted", "paid", "amended"];
|
|
7858
|
+
var jurisdiction = opts.jurisdiction || "";
|
|
7859
|
+
var statusFilter = opts.status_filter || "";
|
|
7860
|
+
var created = opts.created ? "<div class=\"banner banner--ok\">Filing period opened.</div>" : "";
|
|
7861
|
+
var notice = opts.notice ? "<div class=\"banner banner--err\">" + _htmlEscape(opts.notice) + "</div>" : "";
|
|
7862
|
+
|
|
7863
|
+
// Status pill class reuse: map filing status onto the order pill palette so
|
|
7864
|
+
// the visual language matches the rest of the console.
|
|
7865
|
+
function _statusPill(s) {
|
|
7866
|
+
var cls = s === "paid" ? "paid"
|
|
7867
|
+
: s === "submitted" ? "shipped"
|
|
7868
|
+
: s === "computed" ? "fulfilling"
|
|
7869
|
+
: s === "amended" ? "cancelled"
|
|
7870
|
+
: "pending";
|
|
7871
|
+
return "<span class=\"status-pill " + cls + "\">" + _htmlEscape(s) + "</span>";
|
|
7872
|
+
}
|
|
7873
|
+
|
|
7874
|
+
// Filter form — jurisdiction text + status select. GET so the filter
|
|
7875
|
+
// lives in the URL.
|
|
7876
|
+
var statusOpts = [{ value: "", label: "All statuses" }].concat(statuses.map(function (s) { return { value: s, label: s }; }));
|
|
7877
|
+
var filterForm =
|
|
7878
|
+
"<form method=\"get\" action=\"/admin/tax-filings\" class=\"order-filters\">" +
|
|
7879
|
+
"<label class=\"form-field\"><span>Jurisdiction</span>" +
|
|
7880
|
+
"<input type=\"text\" name=\"jurisdiction\" value=\"" + _htmlEscape(jurisdiction) + "\" placeholder=\"US or US-CA\" maxlength=\"6\" class=\"input-code\"></label>" +
|
|
7881
|
+
_selectField("Status", "status", statusOpts, statusFilter, "", "") +
|
|
7882
|
+
"<button class=\"btn\" type=\"submit\">Filter</button>" +
|
|
7883
|
+
"<a class=\"btn btn--ghost\" href=\"/admin/tax-filings\">Clear</a>" +
|
|
7884
|
+
"</form>";
|
|
7885
|
+
|
|
7886
|
+
// Due/overdue strip — open filings due within the next 30 days OR already
|
|
7887
|
+
// past their due date (upcomingDue filters due_date <= now+30d, no lower bound).
|
|
7888
|
+
var dueRows = upcoming.map(function (f) {
|
|
7889
|
+
return "<tr>" +
|
|
7890
|
+
"<td><a class=\"order-id\" href=\"/admin/tax-filings/" + _htmlEscape(f.id) + "\">" + _htmlEscape(f.jurisdiction) + "</a></td>" +
|
|
7891
|
+
"<td>" + _htmlEscape(f.kind) + "</td>" +
|
|
7892
|
+
"<td>" + _statusPill(f.status) + "</td>" +
|
|
7893
|
+
"<td>" + _htmlEscape(_fmtDate(f.due_date)) + "</td>" +
|
|
7894
|
+
"</tr>";
|
|
7895
|
+
}).join("");
|
|
7896
|
+
var dueBlock = upcoming.length
|
|
7897
|
+
? "<section><h2>Due soon or overdue</h2><div class=\"panel\">" +
|
|
7898
|
+
"<table><thead><tr><th scope=\"col\">Jurisdiction</th><th scope=\"col\">Period</th><th scope=\"col\">Status</th><th scope=\"col\">Due</th></tr></thead><tbody>" + dueRows + "</tbody></table>" +
|
|
7899
|
+
"</div></section>"
|
|
7900
|
+
: "";
|
|
7901
|
+
|
|
7902
|
+
// Filings table.
|
|
7903
|
+
var rows = filings.map(function (f) {
|
|
7904
|
+
return "<tr>" +
|
|
7905
|
+
"<td><a class=\"order-id\" href=\"/admin/tax-filings/" + _htmlEscape(f.id) + "\">" + _htmlEscape(f.jurisdiction) + "</a></td>" +
|
|
7906
|
+
"<td>" + _htmlEscape(f.kind) + "</td>" +
|
|
7907
|
+
"<td>" + _htmlEscape(_fmtDate(f.period_start)) + " → " + _htmlEscape(_fmtDate(f.period_end)) + "</td>" +
|
|
7908
|
+
"<td>" + _htmlEscape(_fmtDate(f.due_date)) + "</td>" +
|
|
7909
|
+
"<td>" + _statusPill(f.status) + "</td>" +
|
|
7910
|
+
"<td class=\"num\">" + _htmlEscape(_filingMoney(f.tax_collected_minor)) + "</td>" +
|
|
7911
|
+
"<td class=\"num\">" + _htmlEscape(_filingMoney(f.tax_owed_minor)) + "</td>" +
|
|
7912
|
+
"</tr>";
|
|
7913
|
+
}).join("");
|
|
7914
|
+
var table = filings.length
|
|
7915
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Jurisdiction</th><th scope=\"col\">Kind</th><th scope=\"col\">Period</th><th scope=\"col\">Due</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Collected</th><th scope=\"col\" class=\"num\">Owed</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
|
|
7916
|
+
: "<p class=\"empty\">No filings" + (jurisdiction ? " for " + _htmlEscape(jurisdiction) : "") + " yet. Open a period below.</p>";
|
|
7917
|
+
|
|
7918
|
+
// Open-a-period form.
|
|
7919
|
+
var kindOpts = kinds.map(function (k) { return { value: k, label: k }; });
|
|
7920
|
+
var createForm =
|
|
7921
|
+
"<div class=\"panel mt mw-34\">" +
|
|
7922
|
+
"<h3 class=\"subhead\">Open a filing period</h3>" +
|
|
7923
|
+
"<form method=\"post\" action=\"/admin/tax-filings\">" +
|
|
7924
|
+
_setupField("Jurisdiction", "jurisdiction", "", "text", "ISO 3166-1 country, optionally -subdivision (e.g. US, US-CA, DE-BY).", " maxlength=\"6\" class=\"input-code\" required") +
|
|
7925
|
+
_selectField("Kind", "kind", kindOpts, "quarterly", "Filing cadence the authority expects.", " required") +
|
|
7926
|
+
_setupField("Period start (epoch-ms)", "period_start", "", "number", "First instant of the filing window.", " min=\"0\" required") +
|
|
7927
|
+
_setupField("Period end (epoch-ms)", "period_end", "", "number", "Exclusive end of the window (must be after start).", " min=\"0\" required") +
|
|
7928
|
+
_setupField("Due date (epoch-ms)", "due_date", "", "number", "When the filing is due (must be on or after the period end).", " min=\"0\" required") +
|
|
7929
|
+
"<div class=\"actions-row\"><button class=\"btn\" type=\"submit\">Open period</button></div>" +
|
|
7930
|
+
"</form>" +
|
|
7931
|
+
"</div>";
|
|
7932
|
+
|
|
7933
|
+
// Report shortcut.
|
|
7934
|
+
var reportForm =
|
|
7935
|
+
"<div class=\"panel mt mw-34\">" +
|
|
7936
|
+
"<h3 class=\"subhead\">Remittance report</h3>" +
|
|
7937
|
+
"<form method=\"get\" action=\"/admin/tax-filings/report\">" +
|
|
7938
|
+
_setupField("Jurisdiction", "jurisdiction", jurisdiction, "text", "Roll up every filing whose period intersects the window.", " maxlength=\"6\" class=\"input-code\" required") +
|
|
7939
|
+
_setupField("From (epoch-ms)", "from", "", "number", "Window start.", " min=\"0\" required") +
|
|
7940
|
+
_setupField("To (epoch-ms)", "to", "", "number", "Window end.", " min=\"0\" required") +
|
|
7941
|
+
"<div class=\"actions-row\"><button class=\"btn btn--ghost\" type=\"submit\">Run report</button></div>" +
|
|
7942
|
+
"</form>" +
|
|
7943
|
+
"</div>";
|
|
7944
|
+
|
|
7945
|
+
var body =
|
|
7946
|
+
"<section><h2>Sales tax filings</h2>" + created + notice + filterForm + table + "</section>" +
|
|
7947
|
+
dueBlock +
|
|
7948
|
+
"<section><div class=\"two-col\">" + createForm + reportForm + "</div></section>";
|
|
7949
|
+
return _renderAdminShell(opts.shop_name, "Tax filings", body, "tax-filings", opts.nav_available);
|
|
7950
|
+
}
|
|
7951
|
+
|
|
7952
|
+
// Sales-tax-filing detail — the snapshot totals, the per-rate breakdown, the
|
|
7953
|
+
// audit trail, and the lifecycle action forms gated on the filing's status.
|
|
7954
|
+
function renderAdminTaxFiling(opts) {
|
|
7955
|
+
opts = opts || {};
|
|
7956
|
+
var f = opts.filing;
|
|
7957
|
+
var banners =
|
|
7958
|
+
(opts.created ? "<div class=\"banner banner--ok\">Filing period opened.</div>" : "") +
|
|
7959
|
+
(opts.computed ? "<div class=\"banner banner--ok\">Snapshot computed.</div>" : "") +
|
|
7960
|
+
(opts.submitted ? "<div class=\"banner banner--ok\">Submission recorded.</div>" : "") +
|
|
7961
|
+
(opts.paid ? "<div class=\"banner banner--ok\">Payment recorded.</div>" : "") +
|
|
7962
|
+
(opts.amended ? "<div class=\"banner banner--ok\">Filing amended.</div>" : "") +
|
|
7963
|
+
(opts.notice ? "<div class=\"banner banner--err\">" + _htmlEscape(opts.notice) + "</div>" : "");
|
|
7964
|
+
if (!f) {
|
|
7965
|
+
return _renderAdminShell(opts.shop_name, "Filing", "<section><h2>Filing</h2>" + banners + "<p class=\"empty\">Filing not found.</p></section>", "tax-filings", opts.nav_available);
|
|
7966
|
+
}
|
|
7967
|
+
var enc = encodeURIComponent(f.id);
|
|
7968
|
+
|
|
7969
|
+
var meta =
|
|
7970
|
+
"<section><h2>" + _htmlEscape(f.jurisdiction) + " · " + _htmlEscape(f.kind) + "</h2>" + banners +
|
|
7971
|
+
"<p class=\"meta\">Window: " + _htmlEscape(_fmtDate(f.period_start)) + " → " + _htmlEscape(_fmtDate(f.period_end)) +
|
|
7972
|
+
" · Due " + _htmlEscape(_fmtDate(f.due_date)) +
|
|
7973
|
+
" · Status <span class=\"status-pill " + _htmlEscape(f.status) + "\">" + _htmlEscape(f.status) + "</span></p>" +
|
|
7974
|
+
"</section>";
|
|
7975
|
+
|
|
7976
|
+
var stats =
|
|
7977
|
+
"<section><h2>Snapshot</h2><div class=\"stat-grid\">" +
|
|
7978
|
+
_statCard("Gross revenue", _filingMoney(f.gross_revenue_minor)) +
|
|
7979
|
+
_statCard("Taxable revenue", _filingMoney(f.taxable_revenue_minor)) +
|
|
7980
|
+
_statCard("Exempt revenue", _filingMoney(f.exempt_revenue_minor)) +
|
|
7981
|
+
_statCard("Tax collected", _filingMoney(f.tax_collected_minor)) +
|
|
7982
|
+
_statCard("Tax owed", _filingMoney(f.tax_owed_minor), true) +
|
|
7983
|
+
"</div>" +
|
|
7984
|
+
(f.computed_at == null ? "<p class=\"meta\">Not computed yet — run the snapshot to aggregate orders in the window.</p>"
|
|
7985
|
+
: "<p class=\"meta\">Computed " + _htmlEscape(_fmtDate(f.computed_at)) + ".</p>") +
|
|
7986
|
+
"</section>";
|
|
7987
|
+
|
|
7988
|
+
// Per-rate breakdown table — one row per rate bucket the snapshot produced.
|
|
7989
|
+
var breakdown = f.by_rate_breakdown || {};
|
|
7990
|
+
var bkeys = Object.keys(breakdown);
|
|
7991
|
+
var breakdownRows = bkeys.map(function (k) {
|
|
7992
|
+
var bkt = breakdown[k] || {};
|
|
7993
|
+
var label = k === "__none__" ? "no rate matched"
|
|
7994
|
+
: k === "__exempt__" ? "exempt"
|
|
7995
|
+
: k === "__unknown__" ? "unknown rate"
|
|
7996
|
+
: _fmtBps(k);
|
|
7997
|
+
return "<tr>" +
|
|
7998
|
+
"<td>" + _htmlEscape(label) + (k === "__none__" || k === "__exempt__" || k === "__unknown__" ? "" : " <span class=\"meta\">(" + _htmlEscape(String(k)) + " bps)</span>") + "</td>" +
|
|
7999
|
+
"<td class=\"num\">" + _htmlEscape(String(bkt.order_count == null ? 0 : bkt.order_count)) + "</td>" +
|
|
8000
|
+
"<td class=\"num\">" + _htmlEscape(_filingMoney(bkt.taxable_minor)) + "</td>" +
|
|
8001
|
+
"<td class=\"num\">" + _htmlEscape(_filingMoney(bkt.tax_minor)) + "</td>" +
|
|
8002
|
+
"</tr>";
|
|
8003
|
+
}).join("");
|
|
8004
|
+
var breakdownBlock = bkeys.length
|
|
8005
|
+
? "<section><h2>By rate</h2><div class=\"panel\"><table><thead><tr><th scope=\"col\">Rate</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\" class=\"num\">Taxable</th><th scope=\"col\" class=\"num\">Tax</th></tr></thead><tbody>" + breakdownRows + "</tbody></table></div></section>"
|
|
8006
|
+
: "";
|
|
8007
|
+
|
|
8008
|
+
// Audit trail — submission + payment + amendment columns once recorded.
|
|
8009
|
+
var trail = "";
|
|
8010
|
+
if (f.submission_ref || f.payment_ref || f.amended_reason) {
|
|
8011
|
+
var trailRows = "";
|
|
8012
|
+
if (f.submission_ref) {
|
|
8013
|
+
trailRows += "<tr><td>Submitted</td><td>" + _htmlEscape(f.submission_ref) +
|
|
8014
|
+
(f.submitted_by ? " <span class=\"meta\">by " + _htmlEscape(f.submitted_by) + "</span>" : "") +
|
|
8015
|
+
(f.submitted_at != null ? " <span class=\"meta\">" + _htmlEscape(_fmtDate(f.submitted_at)) + "</span>" : "") + "</td></tr>";
|
|
8016
|
+
}
|
|
8017
|
+
if (f.payment_ref || f.payment_minor != null) {
|
|
8018
|
+
trailRows += "<tr><td>Paid</td><td>" + _htmlEscape(_filingMoney(f.payment_minor)) +
|
|
8019
|
+
(f.payment_ref ? " <span class=\"meta\">ref " + _htmlEscape(f.payment_ref) + "</span>" : "") +
|
|
8020
|
+
(f.paid_at != null ? " <span class=\"meta\">" + _htmlEscape(_fmtDate(f.paid_at)) + "</span>" : "") + "</td></tr>";
|
|
8021
|
+
}
|
|
8022
|
+
if (f.amended_reason) {
|
|
8023
|
+
trailRows += "<tr><td>Amended</td><td>" + _htmlEscape(f.amended_reason) +
|
|
8024
|
+
(f.amended_at != null ? " <span class=\"meta\">" + _htmlEscape(_fmtDate(f.amended_at)) + "</span>" : "") + "</td></tr>";
|
|
8025
|
+
}
|
|
8026
|
+
trail = "<section><h2>Audit trail</h2><div class=\"panel\"><table><tbody>" + trailRows + "</tbody></table></div></section>";
|
|
8027
|
+
}
|
|
8028
|
+
|
|
8029
|
+
// Lifecycle actions — gated on the FSM. draft → compute; computed →
|
|
8030
|
+
// submit or amend; submitted → pay or amend; paid → amend.
|
|
8031
|
+
var actions = "";
|
|
8032
|
+
if (f.status === "draft" || f.status === "computed") {
|
|
8033
|
+
actions +=
|
|
8034
|
+
"<form method=\"post\" action=\"/admin/tax-filings/" + enc + "/compute\" class=\"form-inline\">" +
|
|
8035
|
+
"<button class=\"btn\" type=\"submit\">" + (f.status === "computed" ? "Recompute snapshot" : "Compute snapshot") + "</button>" +
|
|
8036
|
+
"</form>";
|
|
8037
|
+
}
|
|
8038
|
+
if (f.status === "computed") {
|
|
8039
|
+
actions +=
|
|
8040
|
+
"<form method=\"post\" action=\"/admin/tax-filings/" + enc + "/submit\" class=\"stack\">" +
|
|
8041
|
+
_setupField("Submission reference", "submission_ref", "", "text", "The authority's confirmation number (e.g. DR-123-456).", " maxlength=\"200\" required") +
|
|
8042
|
+
_setupField("Submitted by", "submitted_by", "", "text", "Who filed it.", " maxlength=\"200\" required") +
|
|
8043
|
+
"<div class=\"actions-row\"><button class=\"btn\" type=\"submit\">Record submission</button></div>" +
|
|
8044
|
+
"</form>";
|
|
8045
|
+
}
|
|
8046
|
+
if (f.status === "submitted") {
|
|
8047
|
+
actions +=
|
|
8048
|
+
"<form method=\"post\" action=\"/admin/tax-filings/" + enc + "/pay\" class=\"stack\">" +
|
|
8049
|
+
_setupField("Payment (minor units)", "payment_minor", "", "number", "Remitted amount in minor units — may be a partial / installment payment.", " min=\"0\" required") +
|
|
8050
|
+
_setupField("Payment reference", "payment_ref", "", "text", "The authority's payment confirmation.", " maxlength=\"200\" required") +
|
|
8051
|
+
"<div class=\"actions-row\"><button class=\"btn\" type=\"submit\">Record payment</button></div>" +
|
|
8052
|
+
"</form>";
|
|
8053
|
+
}
|
|
8054
|
+
if (f.status === "computed" || f.status === "submitted" || f.status === "paid") {
|
|
8055
|
+
actions +=
|
|
8056
|
+
"<form method=\"post\" action=\"/admin/tax-filings/" + enc + "/amend\" class=\"stack\">" +
|
|
8057
|
+
_setupField("Amendment reason", "reason", "", "text", "Why this filing is being corrected. The snapshot stays for the audit trail.", " maxlength=\"1000\" required") +
|
|
8058
|
+
"<div class=\"actions-row\"><button class=\"btn btn--danger\" type=\"submit\">Mark amended</button></div>" +
|
|
8059
|
+
"</form>";
|
|
8060
|
+
}
|
|
8061
|
+
var actionsBlock = actions
|
|
8062
|
+
? "<section><h2>Actions</h2><div class=\"panel\">" + actions + "</div></section>"
|
|
8063
|
+
: "<section><h2>Actions</h2><p class=\"meta\">This filing is in a terminal state; re-open the period after amending to file again.</p></section>";
|
|
8064
|
+
|
|
8065
|
+
var back = "<p class=\"mt\"><a class=\"btn btn--ghost\" href=\"/admin/tax-filings\">Back to filings</a></p>";
|
|
8066
|
+
return _renderAdminShell(opts.shop_name, "Filing", meta + stats + breakdownBlock + trail + actionsBlock + back, "tax-filings", opts.nav_available);
|
|
8067
|
+
}
|
|
8068
|
+
|
|
8069
|
+
// Per-jurisdiction remittance report — the totals across every filing whose
|
|
8070
|
+
// period intersects the window. Read-only.
|
|
8071
|
+
function renderAdminTaxFilingReport(opts) {
|
|
8072
|
+
opts = opts || {};
|
|
8073
|
+
var report = opts.report;
|
|
8074
|
+
var notice = opts.notice ? "<div class=\"banner banner--warn\">" + _htmlEscape(opts.notice) + "</div>" : "";
|
|
8075
|
+
if (!report) {
|
|
8076
|
+
return _renderAdminShell(opts.shop_name, "Remittance report",
|
|
8077
|
+
"<section><h2>Remittance report</h2>" + notice +
|
|
8078
|
+
"<p class=\"mt\"><a class=\"btn btn--ghost\" href=\"/admin/tax-filings\">Back to filings</a></p></section>",
|
|
8079
|
+
"tax-filings", opts.nav_available);
|
|
8080
|
+
}
|
|
8081
|
+
var stats =
|
|
8082
|
+
"<div class=\"stat-grid\">" +
|
|
8083
|
+
_statCard("Filings", String(report.filing_count)) +
|
|
8084
|
+
_statCard("Gross revenue", _filingMoney(report.total_gross_revenue_minor)) +
|
|
8085
|
+
_statCard("Taxable revenue", _filingMoney(report.total_taxable_revenue_minor)) +
|
|
8086
|
+
_statCard("Exempt revenue", _filingMoney(report.total_exempt_revenue_minor)) +
|
|
8087
|
+
_statCard("Tax collected", _filingMoney(report.total_tax_collected_minor)) +
|
|
8088
|
+
_statCard("Tax owed", _filingMoney(report.total_tax_owed_minor), true) +
|
|
8089
|
+
_statCard("Tax paid", _filingMoney(report.total_tax_paid_minor)) +
|
|
8090
|
+
"</div>";
|
|
8091
|
+
var rows = (report.filings || []).map(function (f) {
|
|
8092
|
+
return "<tr>" +
|
|
8093
|
+
"<td><a class=\"order-id\" href=\"/admin/tax-filings/" + _htmlEscape(f.id) + "\">" + _htmlEscape(f.kind) + "</a></td>" +
|
|
8094
|
+
"<td>" + _htmlEscape(_fmtDate(f.period_start)) + " → " + _htmlEscape(_fmtDate(f.period_end)) + "</td>" +
|
|
8095
|
+
"<td><span class=\"status-pill " + _htmlEscape(f.status) + "\">" + _htmlEscape(f.status) + "</span></td>" +
|
|
8096
|
+
"<td class=\"num\">" + _htmlEscape(_filingMoney(f.tax_collected_minor)) + "</td>" +
|
|
8097
|
+
"<td class=\"num\">" + _htmlEscape(_filingMoney(f.tax_owed_minor)) + "</td>" +
|
|
8098
|
+
"</tr>";
|
|
8099
|
+
}).join("");
|
|
8100
|
+
var table = (report.filings || []).length
|
|
8101
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Period</th><th scope=\"col\">Window</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Collected</th><th scope=\"col\" class=\"num\">Owed</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
|
|
8102
|
+
: "<p class=\"empty\">No filings intersect this window.</p>";
|
|
8103
|
+
|
|
8104
|
+
var body =
|
|
8105
|
+
"<section><h2>Remittance report · " + _htmlEscape(report.jurisdiction) + "</h2>" + notice +
|
|
8106
|
+
"<p class=\"meta\">Window: " + _htmlEscape(_fmtDate(report.from)) + " → " + _htmlEscape(_fmtDate(report.to)) + "</p>" +
|
|
8107
|
+
stats + table +
|
|
8108
|
+
"<p class=\"mt\"><a class=\"btn btn--ghost\" href=\"/admin/tax-filings\">Back to filings</a></p>" +
|
|
8109
|
+
"</section>";
|
|
8110
|
+
return _renderAdminShell(opts.shop_name, "Remittance report", body, "tax-filings", opts.nav_available);
|
|
8111
|
+
}
|
|
8112
|
+
|
|
7169
8113
|
function renderAdminShipping(opts) {
|
|
7170
8114
|
opts = opts || {};
|
|
7171
8115
|
var rows = opts.zones || [];
|
package/lib/asset-manifest.json
CHANGED
package/package.json
CHANGED