@blamejs/blamejs-shop 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/admin.js +523 -1
- package/lib/asset-manifest.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.3.x
|
|
10
10
|
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
11
13
|
- 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
14
|
|
|
13
15
|
- 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
|
@@ -438,6 +438,7 @@ function mount(router, deps) {
|
|
|
438
438
|
var pickLists = deps.pickLists || null; // warehouse pick-list console disabled when absent
|
|
439
439
|
var shippingLabels = deps.shippingLabels || null; // per-shipment carrier-label record disabled when absent
|
|
440
440
|
var splitShipments = deps.splitShipments || null; // order split-shipment planner disabled when absent
|
|
441
|
+
var salesTaxFilings = deps.salesTaxFilings || null; // sales-tax-filing remittance console disabled when absent
|
|
441
442
|
|
|
442
443
|
// Which optional console sections are wired — gates their nav links so a
|
|
443
444
|
// signed-in admin is never sent to a route that wasn't mounted. Passed
|
|
@@ -445,7 +446,7 @@ function mount(router, deps) {
|
|
|
445
446
|
// `reports` is always present in the nav (read-only sales summary needs no
|
|
446
447
|
// extra dep); its route mounts unconditionally and renders an unconfigured
|
|
447
448
|
// 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 };
|
|
449
|
+
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 };
|
|
449
450
|
|
|
450
451
|
try { b.audit.registerNamespace(AUDIT_NAMESPACE); } catch (_e) { /* idempotent */ }
|
|
451
452
|
|
|
@@ -4582,6 +4583,252 @@ function mount(router, deps) {
|
|
|
4582
4583
|
));
|
|
4583
4584
|
}
|
|
4584
4585
|
|
|
4586
|
+
// ---- sales tax filings ----------------------------------------------
|
|
4587
|
+
//
|
|
4588
|
+
// Post-checkout remittance bookkeeping. Each row aggregates completed
|
|
4589
|
+
// orders that fell inside a filing window for one jurisdiction, then
|
|
4590
|
+
// walks the lifecycle the authority expects: draft → computed (the
|
|
4591
|
+
// aggregation snapshot) → submitted (filed) → paid (remittance cleared),
|
|
4592
|
+
// with an amend path for a correction. The console never touches cart /
|
|
4593
|
+
// checkout / order pricing — it reads orders the storefront already
|
|
4594
|
+
// wrote. A missing sales_tax_filings table only surfaces when a route
|
|
4595
|
+
// reads it (degrades to a notice), never at boot.
|
|
4596
|
+
if (salesTaxFilings) {
|
|
4597
|
+
// Translate a create form / JSON body into a defineFilingPeriod input.
|
|
4598
|
+
// The four window fields are epoch-ms; strict integer parsing so a
|
|
4599
|
+
// typo is a 400, never a silent zero.
|
|
4600
|
+
function _filingPeriodInput(body) {
|
|
4601
|
+
return {
|
|
4602
|
+
jurisdiction: typeof body.jurisdiction === "string" ? body.jurisdiction.trim().toUpperCase() : body.jurisdiction,
|
|
4603
|
+
kind: typeof body.kind === "string" ? body.kind.trim() : body.kind,
|
|
4604
|
+
period_start: _strictMinorInt(body.period_start, "salesTaxFilings", "period_start (epoch-ms)"),
|
|
4605
|
+
period_end: _strictMinorInt(body.period_end, "salesTaxFilings", "period_end (epoch-ms)"),
|
|
4606
|
+
due_date: _strictMinorInt(body.due_date, "salesTaxFilings", "due_date (epoch-ms)"),
|
|
4607
|
+
};
|
|
4608
|
+
}
|
|
4609
|
+
|
|
4610
|
+
// Build the list-screen model: the filtered filings, the upcoming-due
|
|
4611
|
+
// strip, and the filter values echoed back into the form. A malformed
|
|
4612
|
+
// filter (bad jurisdiction / status) throws TypeError in the primitive
|
|
4613
|
+
// — map it to an empty list so the page renders the notice, never 500.
|
|
4614
|
+
async function _filingsForBrowser(filter) {
|
|
4615
|
+
try { return await salesTaxFilings.listFilings(filter); }
|
|
4616
|
+
catch (e) { if (e instanceof TypeError) return []; throw e; }
|
|
4617
|
+
}
|
|
4618
|
+
async function _upcomingForBrowser() {
|
|
4619
|
+
try { return await salesTaxFilings.upcomingDue({ days_ahead: 30 }); }
|
|
4620
|
+
catch (e) { if (e instanceof TypeError) return []; throw e; }
|
|
4621
|
+
}
|
|
4622
|
+
|
|
4623
|
+
router.get("/admin/tax-filings", _pageOrApi(true,
|
|
4624
|
+
R(async function (req, res) {
|
|
4625
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
4626
|
+
var filter = {};
|
|
4627
|
+
var j = url && url.searchParams.get("jurisdiction");
|
|
4628
|
+
var s = url && url.searchParams.get("status");
|
|
4629
|
+
if (j) filter.jurisdiction = String(j).trim().toUpperCase();
|
|
4630
|
+
if (s) filter.status = String(s).trim();
|
|
4631
|
+
var rows;
|
|
4632
|
+
try { rows = await salesTaxFilings.listFilings(filter); }
|
|
4633
|
+
catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
|
|
4634
|
+
_json(res, 200, { rows: rows });
|
|
4635
|
+
}),
|
|
4636
|
+
async function (req, res) {
|
|
4637
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
4638
|
+
var j = url && url.searchParams.get("jurisdiction");
|
|
4639
|
+
var s = url && url.searchParams.get("status");
|
|
4640
|
+
var jurisdiction = j ? String(j).trim().toUpperCase() : null;
|
|
4641
|
+
var status = s ? String(s).trim() : null;
|
|
4642
|
+
var filter = {};
|
|
4643
|
+
if (jurisdiction) filter.jurisdiction = jurisdiction;
|
|
4644
|
+
if (status) filter.status = status;
|
|
4645
|
+
var rows = await _filingsForBrowser(filter);
|
|
4646
|
+
var upcoming = await _upcomingForBrowser();
|
|
4647
|
+
_sendHtml(res, 200, renderAdminTaxFilings({
|
|
4648
|
+
shop_name: deps.shop_name, nav_available: navAvailable,
|
|
4649
|
+
filings: rows, upcoming: upcoming,
|
|
4650
|
+
jurisdiction: jurisdiction, status_filter: status,
|
|
4651
|
+
kinds: salesTaxFilings.KINDS, statuses: salesTaxFilings.STATUSES,
|
|
4652
|
+
created: url && url.searchParams.get("created"),
|
|
4653
|
+
notice: (url && url.searchParams.get("err")) ? "That action couldn't be completed for the filing." : null,
|
|
4654
|
+
}));
|
|
4655
|
+
},
|
|
4656
|
+
));
|
|
4657
|
+
|
|
4658
|
+
// Open a filing period → draft row. A duplicate (jurisdiction, kind,
|
|
4659
|
+
// period_start) is refused by the primitive's UNIQUE index; surface it
|
|
4660
|
+
// as a 400 notice rather than a 500.
|
|
4661
|
+
router.post("/admin/tax-filings", _pageOrApi(false,
|
|
4662
|
+
W("tax_filing.create", async function (req, res) {
|
|
4663
|
+
var filing;
|
|
4664
|
+
try { filing = await salesTaxFilings.defineFilingPeriod(_filingPeriodInput(req.body || {})); }
|
|
4665
|
+
catch (e) {
|
|
4666
|
+
if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message);
|
|
4667
|
+
if (e.code === "SALES_TAX_FILING_DUPLICATE") return _problem(res, 409, "filing-duplicate", e.message);
|
|
4668
|
+
throw e;
|
|
4669
|
+
}
|
|
4670
|
+
_json(res, 201, filing);
|
|
4671
|
+
return { id: filing.id };
|
|
4672
|
+
}),
|
|
4673
|
+
async function (req, res) {
|
|
4674
|
+
var filing;
|
|
4675
|
+
try { filing = await salesTaxFilings.defineFilingPeriod(_filingPeriodInput(req.body || {})); }
|
|
4676
|
+
catch (e) {
|
|
4677
|
+
if (!(e instanceof TypeError) && e.code !== "SALES_TAX_FILING_DUPLICATE") throw e;
|
|
4678
|
+
var rows = await _filingsForBrowser({});
|
|
4679
|
+
var upcoming = await _upcomingForBrowser();
|
|
4680
|
+
return _sendHtml(res, 400, renderAdminTaxFilings({
|
|
4681
|
+
shop_name: deps.shop_name, nav_available: navAvailable,
|
|
4682
|
+
filings: rows, upcoming: upcoming,
|
|
4683
|
+
kinds: salesTaxFilings.KINDS, statuses: salesTaxFilings.STATUSES,
|
|
4684
|
+
notice: (e && e.message || "").replace(/^salesTaxFilings[.:]\s*/, ""),
|
|
4685
|
+
}));
|
|
4686
|
+
}
|
|
4687
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".tax_filing.create", outcome: "success", metadata: { id: filing.id } });
|
|
4688
|
+
_redirect(res, "/admin/tax-filings/" + encodeURIComponent(filing.id) + "?created=1");
|
|
4689
|
+
},
|
|
4690
|
+
));
|
|
4691
|
+
|
|
4692
|
+
// Per-jurisdiction remittance report over a [from, to] window. GET so
|
|
4693
|
+
// the window lives in the URL (bookmarkable). A bad / missing range is
|
|
4694
|
+
// a notice on the list page, not a 500.
|
|
4695
|
+
router.get("/admin/tax-filings/report", _pageOrApi(true,
|
|
4696
|
+
R(async function (req, res) {
|
|
4697
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
4698
|
+
var j = url && url.searchParams.get("jurisdiction");
|
|
4699
|
+
if (!j) return _problem(res, 400, "bad-request", "jurisdiction required");
|
|
4700
|
+
var from = _parseEpochMs(url && url.searchParams.get("from"), "from");
|
|
4701
|
+
var to = _parseEpochMs(url && url.searchParams.get("to"), "to");
|
|
4702
|
+
if (from == null || to == null) return _problem(res, 400, "bad-request", "from and to (epoch-ms) required");
|
|
4703
|
+
var report;
|
|
4704
|
+
try { report = await salesTaxFilings.auditReportForJurisdiction({ jurisdiction: String(j).trim().toUpperCase(), from: from, to: to }); }
|
|
4705
|
+
catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
|
|
4706
|
+
_json(res, 200, report);
|
|
4707
|
+
}),
|
|
4708
|
+
async function (req, res) {
|
|
4709
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
4710
|
+
var j = url && url.searchParams.get("jurisdiction");
|
|
4711
|
+
var jurisdiction = j ? String(j).trim().toUpperCase() : null;
|
|
4712
|
+
// Parse the window defensively — a present-but-malformed epoch throws
|
|
4713
|
+
// TypeError; treat it as unset so the window notice guides the operator
|
|
4714
|
+
// (a bad range is a notice on this page, never a 500).
|
|
4715
|
+
var from = null, to = null;
|
|
4716
|
+
try {
|
|
4717
|
+
from = _parseEpochMs(url && url.searchParams.get("from"), "from");
|
|
4718
|
+
to = _parseEpochMs(url && url.searchParams.get("to"), "to");
|
|
4719
|
+
} catch (e) { if (!(e instanceof TypeError)) throw e; }
|
|
4720
|
+
var report = null, notice = null;
|
|
4721
|
+
if (!jurisdiction) {
|
|
4722
|
+
notice = "Enter a jurisdiction and a window to run a remittance report.";
|
|
4723
|
+
} else if (from == null || to == null) {
|
|
4724
|
+
notice = "Enter both a from and a to date (epoch-ms) for the report window.";
|
|
4725
|
+
} else {
|
|
4726
|
+
try { report = await salesTaxFilings.auditReportForJurisdiction({ jurisdiction: jurisdiction, from: from, to: to }); }
|
|
4727
|
+
catch (e) { if (!(e instanceof TypeError)) throw e; notice = (e.message || "").replace(/^salesTaxFilings[.:]\s*/, ""); }
|
|
4728
|
+
}
|
|
4729
|
+
_sendHtml(res, 200, renderAdminTaxFilingReport({
|
|
4730
|
+
shop_name: deps.shop_name, nav_available: navAvailable,
|
|
4731
|
+
report: report, jurisdiction: jurisdiction, notice: notice,
|
|
4732
|
+
}));
|
|
4733
|
+
},
|
|
4734
|
+
));
|
|
4735
|
+
|
|
4736
|
+
async function _filingDetailModel(id) {
|
|
4737
|
+
return await salesTaxFilings.getFiling(id);
|
|
4738
|
+
}
|
|
4739
|
+
|
|
4740
|
+
router.get("/admin/tax-filings/:id", _pageOrApi(true,
|
|
4741
|
+
R(async function (req, res) {
|
|
4742
|
+
var filing;
|
|
4743
|
+
try { filing = await _filingDetailModel(req.params.id); }
|
|
4744
|
+
catch (e) { if (e instanceof TypeError) return _problem(res, 404, "filing-not-found", e.message); throw e; }
|
|
4745
|
+
if (!filing) return _problem(res, 404, "filing-not-found");
|
|
4746
|
+
_json(res, 200, filing);
|
|
4747
|
+
}),
|
|
4748
|
+
async function (req, res) {
|
|
4749
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
4750
|
+
var filing;
|
|
4751
|
+
try { filing = await _filingDetailModel(req.params.id); }
|
|
4752
|
+
catch (e) { if (!(e instanceof TypeError)) throw e; filing = null; }
|
|
4753
|
+
if (!filing) return _sendHtml(res, 404, renderAdminTaxFilings({
|
|
4754
|
+
shop_name: deps.shop_name, nav_available: navAvailable,
|
|
4755
|
+
filings: [], upcoming: [], kinds: salesTaxFilings.KINDS, statuses: salesTaxFilings.STATUSES,
|
|
4756
|
+
notice: "Filing not found.",
|
|
4757
|
+
}));
|
|
4758
|
+
_sendHtml(res, 200, renderAdminTaxFiling({
|
|
4759
|
+
shop_name: deps.shop_name, nav_available: navAvailable, filing: filing,
|
|
4760
|
+
computed: url && url.searchParams.get("computed"),
|
|
4761
|
+
created: url && url.searchParams.get("created"),
|
|
4762
|
+
submitted: url && url.searchParams.get("submitted"),
|
|
4763
|
+
paid: url && url.searchParams.get("paid"),
|
|
4764
|
+
amended: url && url.searchParams.get("amended"),
|
|
4765
|
+
notice: (url && url.searchParams.get("err"))
|
|
4766
|
+
? String(url.searchParams.get("err_msg") || "That action couldn't be completed for the filing.")
|
|
4767
|
+
: null,
|
|
4768
|
+
}));
|
|
4769
|
+
},
|
|
4770
|
+
));
|
|
4771
|
+
|
|
4772
|
+
// Lifecycle transitions — each is a browser POST that PRGs back to the
|
|
4773
|
+
// detail. A bad transition (wrong status) or bad input is a ?err notice
|
|
4774
|
+
// on the detail page, never a 500. The bearer JSON contract returns the
|
|
4775
|
+
// updated filing (or a problem document).
|
|
4776
|
+
function _filingActionRoute(suffix, audit, okParam, run) {
|
|
4777
|
+
router.post("/admin/tax-filings/:id/" + suffix, _pageOrApi(false,
|
|
4778
|
+
W("tax_filing." + audit, async function (req, res) {
|
|
4779
|
+
var filing;
|
|
4780
|
+
try { filing = await run(req.params.id, req.body || {}); }
|
|
4781
|
+
catch (e) {
|
|
4782
|
+
if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message);
|
|
4783
|
+
if (e.code === "SALES_TAX_FILING_NOT_FOUND") return _problem(res, 404, "filing-not-found", e.message);
|
|
4784
|
+
if (e.code === "SALES_TAX_FILING_BAD_TRANSITION") return _problem(res, 409, "filing-bad-transition", e.message);
|
|
4785
|
+
throw e;
|
|
4786
|
+
}
|
|
4787
|
+
_json(res, 200, filing);
|
|
4788
|
+
return { id: filing.id };
|
|
4789
|
+
}),
|
|
4790
|
+
async function (req, res) {
|
|
4791
|
+
var enc = encodeURIComponent(req.params.id);
|
|
4792
|
+
try {
|
|
4793
|
+
await run(req.params.id, req.body || {});
|
|
4794
|
+
} catch (e) {
|
|
4795
|
+
if (!(e instanceof TypeError) &&
|
|
4796
|
+
e.code !== "SALES_TAX_FILING_NOT_FOUND" &&
|
|
4797
|
+
e.code !== "SALES_TAX_FILING_BAD_TRANSITION") throw e;
|
|
4798
|
+
var msg = (e && e.message || "").replace(/^salesTaxFilings[.:]\s*/, "");
|
|
4799
|
+
return _redirect(res, "/admin/tax-filings/" + enc + "?err=1&err_msg=" + encodeURIComponent(msg));
|
|
4800
|
+
}
|
|
4801
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".tax_filing." + audit, outcome: "success", metadata: { id: req.params.id } });
|
|
4802
|
+
_redirect(res, "/admin/tax-filings/" + enc + "?" + okParam + "=1");
|
|
4803
|
+
},
|
|
4804
|
+
));
|
|
4805
|
+
}
|
|
4806
|
+
|
|
4807
|
+
_filingActionRoute("compute", "compute", "computed", function (id) {
|
|
4808
|
+
return salesTaxFilings.computeFiling({ filing_id: id });
|
|
4809
|
+
});
|
|
4810
|
+
_filingActionRoute("submit", "submit", "submitted", function (id, body) {
|
|
4811
|
+
return salesTaxFilings.recordSubmission({
|
|
4812
|
+
filing_id: id,
|
|
4813
|
+
submission_ref: typeof body.submission_ref === "string" ? body.submission_ref.trim() : body.submission_ref,
|
|
4814
|
+
submitted_by: typeof body.submitted_by === "string" ? body.submitted_by.trim() : body.submitted_by,
|
|
4815
|
+
});
|
|
4816
|
+
});
|
|
4817
|
+
_filingActionRoute("pay", "pay", "paid", function (id, body) {
|
|
4818
|
+
return salesTaxFilings.recordPayment({
|
|
4819
|
+
filing_id: id,
|
|
4820
|
+
payment_minor: _strictMinorInt(body.payment_minor, "salesTaxFilings", "payment_minor"),
|
|
4821
|
+
payment_ref: typeof body.payment_ref === "string" ? body.payment_ref.trim() : body.payment_ref,
|
|
4822
|
+
});
|
|
4823
|
+
});
|
|
4824
|
+
_filingActionRoute("amend", "amend", "amended", function (id, body) {
|
|
4825
|
+
return salesTaxFilings.markAmended({
|
|
4826
|
+
filing_id: id,
|
|
4827
|
+
reason: typeof body.reason === "string" ? body.reason.trim() : body.reason,
|
|
4828
|
+
});
|
|
4829
|
+
});
|
|
4830
|
+
}
|
|
4831
|
+
|
|
4585
4832
|
// Read a jurisdiction's rate history for the browser list. A null /
|
|
4586
4833
|
// unset jurisdiction yields an empty list (the page shows the picker
|
|
4587
4834
|
// form). A malformed jurisdiction throws TypeError in the primitive —
|
|
@@ -5645,6 +5892,7 @@ var ADMIN_NAV_ITEMS = [
|
|
|
5645
5892
|
{ key: "discounts", href: "/admin/discounts", label: "Discounts", requires: "autoDiscount" },
|
|
5646
5893
|
{ key: "quantity-discounts", href: "/admin/quantity-discounts", label: "Quantity breaks", requires: "quantityDiscounts" },
|
|
5647
5894
|
{ key: "tax", href: "/admin/tax-rates", label: "Tax", requires: "taxRates" },
|
|
5895
|
+
{ key: "tax-filings", href: "/admin/tax-filings", label: "Tax filings", requires: "salesTaxFilings" },
|
|
5648
5896
|
{ key: "shipping", href: "/admin/shipping", label: "Shipping", requires: "shippingZones" },
|
|
5649
5897
|
{ key: "pick-lists", href: "/admin/pick-lists", label: "Pick lists", requires: "pickLists" },
|
|
5650
5898
|
{ key: "announcements", href: "/admin/announcements", label: "Announcements", requires: "announcementBar" },
|
|
@@ -7166,6 +7414,280 @@ function renderAdminTaxRates(opts) {
|
|
|
7166
7414
|
return _renderAdminShell(opts.shop_name, "Tax rates", body, "tax", opts.nav_available);
|
|
7167
7415
|
}
|
|
7168
7416
|
|
|
7417
|
+
// Money for the filings screens. A filing aggregates orders that may span
|
|
7418
|
+
// currencies, so the row stores raw minor-unit totals with no single
|
|
7419
|
+
// currency column. Display them through the same pricing formatter the rest
|
|
7420
|
+
// of the console uses, in the operator's display currency (defaults to USD)
|
|
7421
|
+
// — the number is the reconciliation figure regardless of the symbol.
|
|
7422
|
+
var TAX_FILING_DISPLAY_CURRENCY = "USD";
|
|
7423
|
+
function _filingMoney(minor) {
|
|
7424
|
+
return pricing.format(Number(minor) || 0, TAX_FILING_DISPLAY_CURRENCY);
|
|
7425
|
+
}
|
|
7426
|
+
|
|
7427
|
+
// Sales-tax-filings list screen — open filings (filterable by jurisdiction
|
|
7428
|
+
// + status), an upcoming-due strip, and the open-a-period form. Read-heavy:
|
|
7429
|
+
// the lifecycle actions live on each filing's detail page.
|
|
7430
|
+
function renderAdminTaxFilings(opts) {
|
|
7431
|
+
opts = opts || {};
|
|
7432
|
+
var filings = opts.filings || [];
|
|
7433
|
+
var upcoming = opts.upcoming || [];
|
|
7434
|
+
var kinds = opts.kinds || ["monthly", "quarterly", "annual"];
|
|
7435
|
+
var statuses = opts.statuses || ["draft", "computed", "submitted", "paid", "amended"];
|
|
7436
|
+
var jurisdiction = opts.jurisdiction || "";
|
|
7437
|
+
var statusFilter = opts.status_filter || "";
|
|
7438
|
+
var created = opts.created ? "<div class=\"banner banner--ok\">Filing period opened.</div>" : "";
|
|
7439
|
+
var notice = opts.notice ? "<div class=\"banner banner--err\">" + _htmlEscape(opts.notice) + "</div>" : "";
|
|
7440
|
+
|
|
7441
|
+
// Status pill class reuse: map filing status onto the order pill palette so
|
|
7442
|
+
// the visual language matches the rest of the console.
|
|
7443
|
+
function _statusPill(s) {
|
|
7444
|
+
var cls = s === "paid" ? "paid"
|
|
7445
|
+
: s === "submitted" ? "shipped"
|
|
7446
|
+
: s === "computed" ? "fulfilling"
|
|
7447
|
+
: s === "amended" ? "cancelled"
|
|
7448
|
+
: "pending";
|
|
7449
|
+
return "<span class=\"status-pill " + cls + "\">" + _htmlEscape(s) + "</span>";
|
|
7450
|
+
}
|
|
7451
|
+
|
|
7452
|
+
// Filter form — jurisdiction text + status select. GET so the filter
|
|
7453
|
+
// lives in the URL.
|
|
7454
|
+
var statusOpts = [{ value: "", label: "All statuses" }].concat(statuses.map(function (s) { return { value: s, label: s }; }));
|
|
7455
|
+
var filterForm =
|
|
7456
|
+
"<form method=\"get\" action=\"/admin/tax-filings\" class=\"order-filters\">" +
|
|
7457
|
+
"<label class=\"form-field\"><span>Jurisdiction</span>" +
|
|
7458
|
+
"<input type=\"text\" name=\"jurisdiction\" value=\"" + _htmlEscape(jurisdiction) + "\" placeholder=\"US or US-CA\" maxlength=\"6\" class=\"input-code\"></label>" +
|
|
7459
|
+
_selectField("Status", "status", statusOpts, statusFilter, "", "") +
|
|
7460
|
+
"<button class=\"btn\" type=\"submit\">Filter</button>" +
|
|
7461
|
+
"<a class=\"btn btn--ghost\" href=\"/admin/tax-filings\">Clear</a>" +
|
|
7462
|
+
"</form>";
|
|
7463
|
+
|
|
7464
|
+
// Due/overdue strip — open filings due within the next 30 days OR already
|
|
7465
|
+
// past their due date (upcomingDue filters due_date <= now+30d, no lower bound).
|
|
7466
|
+
var dueRows = upcoming.map(function (f) {
|
|
7467
|
+
return "<tr>" +
|
|
7468
|
+
"<td><a class=\"order-id\" href=\"/admin/tax-filings/" + _htmlEscape(f.id) + "\">" + _htmlEscape(f.jurisdiction) + "</a></td>" +
|
|
7469
|
+
"<td>" + _htmlEscape(f.kind) + "</td>" +
|
|
7470
|
+
"<td>" + _statusPill(f.status) + "</td>" +
|
|
7471
|
+
"<td>" + _htmlEscape(_fmtDate(f.due_date)) + "</td>" +
|
|
7472
|
+
"</tr>";
|
|
7473
|
+
}).join("");
|
|
7474
|
+
var dueBlock = upcoming.length
|
|
7475
|
+
? "<section><h2>Due soon or overdue</h2><div class=\"panel\">" +
|
|
7476
|
+
"<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>" +
|
|
7477
|
+
"</div></section>"
|
|
7478
|
+
: "";
|
|
7479
|
+
|
|
7480
|
+
// Filings table.
|
|
7481
|
+
var rows = filings.map(function (f) {
|
|
7482
|
+
return "<tr>" +
|
|
7483
|
+
"<td><a class=\"order-id\" href=\"/admin/tax-filings/" + _htmlEscape(f.id) + "\">" + _htmlEscape(f.jurisdiction) + "</a></td>" +
|
|
7484
|
+
"<td>" + _htmlEscape(f.kind) + "</td>" +
|
|
7485
|
+
"<td>" + _htmlEscape(_fmtDate(f.period_start)) + " → " + _htmlEscape(_fmtDate(f.period_end)) + "</td>" +
|
|
7486
|
+
"<td>" + _htmlEscape(_fmtDate(f.due_date)) + "</td>" +
|
|
7487
|
+
"<td>" + _statusPill(f.status) + "</td>" +
|
|
7488
|
+
"<td class=\"num\">" + _htmlEscape(_filingMoney(f.tax_collected_minor)) + "</td>" +
|
|
7489
|
+
"<td class=\"num\">" + _htmlEscape(_filingMoney(f.tax_owed_minor)) + "</td>" +
|
|
7490
|
+
"</tr>";
|
|
7491
|
+
}).join("");
|
|
7492
|
+
var table = filings.length
|
|
7493
|
+
? "<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>"
|
|
7494
|
+
: "<p class=\"empty\">No filings" + (jurisdiction ? " for " + _htmlEscape(jurisdiction) : "") + " yet. Open a period below.</p>";
|
|
7495
|
+
|
|
7496
|
+
// Open-a-period form.
|
|
7497
|
+
var kindOpts = kinds.map(function (k) { return { value: k, label: k }; });
|
|
7498
|
+
var createForm =
|
|
7499
|
+
"<div class=\"panel mt mw-34\">" +
|
|
7500
|
+
"<h3 class=\"subhead\">Open a filing period</h3>" +
|
|
7501
|
+
"<form method=\"post\" action=\"/admin/tax-filings\">" +
|
|
7502
|
+
_setupField("Jurisdiction", "jurisdiction", "", "text", "ISO 3166-1 country, optionally -subdivision (e.g. US, US-CA, DE-BY).", " maxlength=\"6\" class=\"input-code\" required") +
|
|
7503
|
+
_selectField("Kind", "kind", kindOpts, "quarterly", "Filing cadence the authority expects.", " required") +
|
|
7504
|
+
_setupField("Period start (epoch-ms)", "period_start", "", "number", "First instant of the filing window.", " min=\"0\" required") +
|
|
7505
|
+
_setupField("Period end (epoch-ms)", "period_end", "", "number", "Exclusive end of the window (must be after start).", " min=\"0\" required") +
|
|
7506
|
+
_setupField("Due date (epoch-ms)", "due_date", "", "number", "When the filing is due (must be on or after the period end).", " min=\"0\" required") +
|
|
7507
|
+
"<div class=\"actions-row\"><button class=\"btn\" type=\"submit\">Open period</button></div>" +
|
|
7508
|
+
"</form>" +
|
|
7509
|
+
"</div>";
|
|
7510
|
+
|
|
7511
|
+
// Report shortcut.
|
|
7512
|
+
var reportForm =
|
|
7513
|
+
"<div class=\"panel mt mw-34\">" +
|
|
7514
|
+
"<h3 class=\"subhead\">Remittance report</h3>" +
|
|
7515
|
+
"<form method=\"get\" action=\"/admin/tax-filings/report\">" +
|
|
7516
|
+
_setupField("Jurisdiction", "jurisdiction", jurisdiction, "text", "Roll up every filing whose period intersects the window.", " maxlength=\"6\" class=\"input-code\" required") +
|
|
7517
|
+
_setupField("From (epoch-ms)", "from", "", "number", "Window start.", " min=\"0\" required") +
|
|
7518
|
+
_setupField("To (epoch-ms)", "to", "", "number", "Window end.", " min=\"0\" required") +
|
|
7519
|
+
"<div class=\"actions-row\"><button class=\"btn btn--ghost\" type=\"submit\">Run report</button></div>" +
|
|
7520
|
+
"</form>" +
|
|
7521
|
+
"</div>";
|
|
7522
|
+
|
|
7523
|
+
var body =
|
|
7524
|
+
"<section><h2>Sales tax filings</h2>" + created + notice + filterForm + table + "</section>" +
|
|
7525
|
+
dueBlock +
|
|
7526
|
+
"<section><div class=\"two-col\">" + createForm + reportForm + "</div></section>";
|
|
7527
|
+
return _renderAdminShell(opts.shop_name, "Tax filings", body, "tax-filings", opts.nav_available);
|
|
7528
|
+
}
|
|
7529
|
+
|
|
7530
|
+
// Sales-tax-filing detail — the snapshot totals, the per-rate breakdown, the
|
|
7531
|
+
// audit trail, and the lifecycle action forms gated on the filing's status.
|
|
7532
|
+
function renderAdminTaxFiling(opts) {
|
|
7533
|
+
opts = opts || {};
|
|
7534
|
+
var f = opts.filing;
|
|
7535
|
+
var banners =
|
|
7536
|
+
(opts.created ? "<div class=\"banner banner--ok\">Filing period opened.</div>" : "") +
|
|
7537
|
+
(opts.computed ? "<div class=\"banner banner--ok\">Snapshot computed.</div>" : "") +
|
|
7538
|
+
(opts.submitted ? "<div class=\"banner banner--ok\">Submission recorded.</div>" : "") +
|
|
7539
|
+
(opts.paid ? "<div class=\"banner banner--ok\">Payment recorded.</div>" : "") +
|
|
7540
|
+
(opts.amended ? "<div class=\"banner banner--ok\">Filing amended.</div>" : "") +
|
|
7541
|
+
(opts.notice ? "<div class=\"banner banner--err\">" + _htmlEscape(opts.notice) + "</div>" : "");
|
|
7542
|
+
if (!f) {
|
|
7543
|
+
return _renderAdminShell(opts.shop_name, "Filing", "<section><h2>Filing</h2>" + banners + "<p class=\"empty\">Filing not found.</p></section>", "tax-filings", opts.nav_available);
|
|
7544
|
+
}
|
|
7545
|
+
var enc = encodeURIComponent(f.id);
|
|
7546
|
+
|
|
7547
|
+
var meta =
|
|
7548
|
+
"<section><h2>" + _htmlEscape(f.jurisdiction) + " · " + _htmlEscape(f.kind) + "</h2>" + banners +
|
|
7549
|
+
"<p class=\"meta\">Window: " + _htmlEscape(_fmtDate(f.period_start)) + " → " + _htmlEscape(_fmtDate(f.period_end)) +
|
|
7550
|
+
" · Due " + _htmlEscape(_fmtDate(f.due_date)) +
|
|
7551
|
+
" · Status <span class=\"status-pill " + _htmlEscape(f.status) + "\">" + _htmlEscape(f.status) + "</span></p>" +
|
|
7552
|
+
"</section>";
|
|
7553
|
+
|
|
7554
|
+
var stats =
|
|
7555
|
+
"<section><h2>Snapshot</h2><div class=\"stat-grid\">" +
|
|
7556
|
+
_statCard("Gross revenue", _filingMoney(f.gross_revenue_minor)) +
|
|
7557
|
+
_statCard("Taxable revenue", _filingMoney(f.taxable_revenue_minor)) +
|
|
7558
|
+
_statCard("Exempt revenue", _filingMoney(f.exempt_revenue_minor)) +
|
|
7559
|
+
_statCard("Tax collected", _filingMoney(f.tax_collected_minor)) +
|
|
7560
|
+
_statCard("Tax owed", _filingMoney(f.tax_owed_minor), true) +
|
|
7561
|
+
"</div>" +
|
|
7562
|
+
(f.computed_at == null ? "<p class=\"meta\">Not computed yet — run the snapshot to aggregate orders in the window.</p>"
|
|
7563
|
+
: "<p class=\"meta\">Computed " + _htmlEscape(_fmtDate(f.computed_at)) + ".</p>") +
|
|
7564
|
+
"</section>";
|
|
7565
|
+
|
|
7566
|
+
// Per-rate breakdown table — one row per rate bucket the snapshot produced.
|
|
7567
|
+
var breakdown = f.by_rate_breakdown || {};
|
|
7568
|
+
var bkeys = Object.keys(breakdown);
|
|
7569
|
+
var breakdownRows = bkeys.map(function (k) {
|
|
7570
|
+
var bkt = breakdown[k] || {};
|
|
7571
|
+
var label = k === "__none__" ? "no rate matched"
|
|
7572
|
+
: k === "__exempt__" ? "exempt"
|
|
7573
|
+
: k === "__unknown__" ? "unknown rate"
|
|
7574
|
+
: _fmtBps(k);
|
|
7575
|
+
return "<tr>" +
|
|
7576
|
+
"<td>" + _htmlEscape(label) + (k === "__none__" || k === "__exempt__" || k === "__unknown__" ? "" : " <span class=\"meta\">(" + _htmlEscape(String(k)) + " bps)</span>") + "</td>" +
|
|
7577
|
+
"<td class=\"num\">" + _htmlEscape(String(bkt.order_count == null ? 0 : bkt.order_count)) + "</td>" +
|
|
7578
|
+
"<td class=\"num\">" + _htmlEscape(_filingMoney(bkt.taxable_minor)) + "</td>" +
|
|
7579
|
+
"<td class=\"num\">" + _htmlEscape(_filingMoney(bkt.tax_minor)) + "</td>" +
|
|
7580
|
+
"</tr>";
|
|
7581
|
+
}).join("");
|
|
7582
|
+
var breakdownBlock = bkeys.length
|
|
7583
|
+
? "<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>"
|
|
7584
|
+
: "";
|
|
7585
|
+
|
|
7586
|
+
// Audit trail — submission + payment + amendment columns once recorded.
|
|
7587
|
+
var trail = "";
|
|
7588
|
+
if (f.submission_ref || f.payment_ref || f.amended_reason) {
|
|
7589
|
+
var trailRows = "";
|
|
7590
|
+
if (f.submission_ref) {
|
|
7591
|
+
trailRows += "<tr><td>Submitted</td><td>" + _htmlEscape(f.submission_ref) +
|
|
7592
|
+
(f.submitted_by ? " <span class=\"meta\">by " + _htmlEscape(f.submitted_by) + "</span>" : "") +
|
|
7593
|
+
(f.submitted_at != null ? " <span class=\"meta\">" + _htmlEscape(_fmtDate(f.submitted_at)) + "</span>" : "") + "</td></tr>";
|
|
7594
|
+
}
|
|
7595
|
+
if (f.payment_ref || f.payment_minor != null) {
|
|
7596
|
+
trailRows += "<tr><td>Paid</td><td>" + _htmlEscape(_filingMoney(f.payment_minor)) +
|
|
7597
|
+
(f.payment_ref ? " <span class=\"meta\">ref " + _htmlEscape(f.payment_ref) + "</span>" : "") +
|
|
7598
|
+
(f.paid_at != null ? " <span class=\"meta\">" + _htmlEscape(_fmtDate(f.paid_at)) + "</span>" : "") + "</td></tr>";
|
|
7599
|
+
}
|
|
7600
|
+
if (f.amended_reason) {
|
|
7601
|
+
trailRows += "<tr><td>Amended</td><td>" + _htmlEscape(f.amended_reason) +
|
|
7602
|
+
(f.amended_at != null ? " <span class=\"meta\">" + _htmlEscape(_fmtDate(f.amended_at)) + "</span>" : "") + "</td></tr>";
|
|
7603
|
+
}
|
|
7604
|
+
trail = "<section><h2>Audit trail</h2><div class=\"panel\"><table><tbody>" + trailRows + "</tbody></table></div></section>";
|
|
7605
|
+
}
|
|
7606
|
+
|
|
7607
|
+
// Lifecycle actions — gated on the FSM. draft → compute; computed →
|
|
7608
|
+
// submit or amend; submitted → pay or amend; paid → amend.
|
|
7609
|
+
var actions = "";
|
|
7610
|
+
if (f.status === "draft" || f.status === "computed") {
|
|
7611
|
+
actions +=
|
|
7612
|
+
"<form method=\"post\" action=\"/admin/tax-filings/" + enc + "/compute\" class=\"form-inline\">" +
|
|
7613
|
+
"<button class=\"btn\" type=\"submit\">" + (f.status === "computed" ? "Recompute snapshot" : "Compute snapshot") + "</button>" +
|
|
7614
|
+
"</form>";
|
|
7615
|
+
}
|
|
7616
|
+
if (f.status === "computed") {
|
|
7617
|
+
actions +=
|
|
7618
|
+
"<form method=\"post\" action=\"/admin/tax-filings/" + enc + "/submit\" class=\"stack\">" +
|
|
7619
|
+
_setupField("Submission reference", "submission_ref", "", "text", "The authority's confirmation number (e.g. DR-123-456).", " maxlength=\"200\" required") +
|
|
7620
|
+
_setupField("Submitted by", "submitted_by", "", "text", "Who filed it.", " maxlength=\"200\" required") +
|
|
7621
|
+
"<div class=\"actions-row\"><button class=\"btn\" type=\"submit\">Record submission</button></div>" +
|
|
7622
|
+
"</form>";
|
|
7623
|
+
}
|
|
7624
|
+
if (f.status === "submitted") {
|
|
7625
|
+
actions +=
|
|
7626
|
+
"<form method=\"post\" action=\"/admin/tax-filings/" + enc + "/pay\" class=\"stack\">" +
|
|
7627
|
+
_setupField("Payment (minor units)", "payment_minor", "", "number", "Remitted amount in minor units — may be a partial / installment payment.", " min=\"0\" required") +
|
|
7628
|
+
_setupField("Payment reference", "payment_ref", "", "text", "The authority's payment confirmation.", " maxlength=\"200\" required") +
|
|
7629
|
+
"<div class=\"actions-row\"><button class=\"btn\" type=\"submit\">Record payment</button></div>" +
|
|
7630
|
+
"</form>";
|
|
7631
|
+
}
|
|
7632
|
+
if (f.status === "computed" || f.status === "submitted" || f.status === "paid") {
|
|
7633
|
+
actions +=
|
|
7634
|
+
"<form method=\"post\" action=\"/admin/tax-filings/" + enc + "/amend\" class=\"stack\">" +
|
|
7635
|
+
_setupField("Amendment reason", "reason", "", "text", "Why this filing is being corrected. The snapshot stays for the audit trail.", " maxlength=\"1000\" required") +
|
|
7636
|
+
"<div class=\"actions-row\"><button class=\"btn btn--danger\" type=\"submit\">Mark amended</button></div>" +
|
|
7637
|
+
"</form>";
|
|
7638
|
+
}
|
|
7639
|
+
var actionsBlock = actions
|
|
7640
|
+
? "<section><h2>Actions</h2><div class=\"panel\">" + actions + "</div></section>"
|
|
7641
|
+
: "<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>";
|
|
7642
|
+
|
|
7643
|
+
var back = "<p class=\"mt\"><a class=\"btn btn--ghost\" href=\"/admin/tax-filings\">Back to filings</a></p>";
|
|
7644
|
+
return _renderAdminShell(opts.shop_name, "Filing", meta + stats + breakdownBlock + trail + actionsBlock + back, "tax-filings", opts.nav_available);
|
|
7645
|
+
}
|
|
7646
|
+
|
|
7647
|
+
// Per-jurisdiction remittance report — the totals across every filing whose
|
|
7648
|
+
// period intersects the window. Read-only.
|
|
7649
|
+
function renderAdminTaxFilingReport(opts) {
|
|
7650
|
+
opts = opts || {};
|
|
7651
|
+
var report = opts.report;
|
|
7652
|
+
var notice = opts.notice ? "<div class=\"banner banner--warn\">" + _htmlEscape(opts.notice) + "</div>" : "";
|
|
7653
|
+
if (!report) {
|
|
7654
|
+
return _renderAdminShell(opts.shop_name, "Remittance report",
|
|
7655
|
+
"<section><h2>Remittance report</h2>" + notice +
|
|
7656
|
+
"<p class=\"mt\"><a class=\"btn btn--ghost\" href=\"/admin/tax-filings\">Back to filings</a></p></section>",
|
|
7657
|
+
"tax-filings", opts.nav_available);
|
|
7658
|
+
}
|
|
7659
|
+
var stats =
|
|
7660
|
+
"<div class=\"stat-grid\">" +
|
|
7661
|
+
_statCard("Filings", String(report.filing_count)) +
|
|
7662
|
+
_statCard("Gross revenue", _filingMoney(report.total_gross_revenue_minor)) +
|
|
7663
|
+
_statCard("Taxable revenue", _filingMoney(report.total_taxable_revenue_minor)) +
|
|
7664
|
+
_statCard("Exempt revenue", _filingMoney(report.total_exempt_revenue_minor)) +
|
|
7665
|
+
_statCard("Tax collected", _filingMoney(report.total_tax_collected_minor)) +
|
|
7666
|
+
_statCard("Tax owed", _filingMoney(report.total_tax_owed_minor), true) +
|
|
7667
|
+
_statCard("Tax paid", _filingMoney(report.total_tax_paid_minor)) +
|
|
7668
|
+
"</div>";
|
|
7669
|
+
var rows = (report.filings || []).map(function (f) {
|
|
7670
|
+
return "<tr>" +
|
|
7671
|
+
"<td><a class=\"order-id\" href=\"/admin/tax-filings/" + _htmlEscape(f.id) + "\">" + _htmlEscape(f.kind) + "</a></td>" +
|
|
7672
|
+
"<td>" + _htmlEscape(_fmtDate(f.period_start)) + " → " + _htmlEscape(_fmtDate(f.period_end)) + "</td>" +
|
|
7673
|
+
"<td><span class=\"status-pill " + _htmlEscape(f.status) + "\">" + _htmlEscape(f.status) + "</span></td>" +
|
|
7674
|
+
"<td class=\"num\">" + _htmlEscape(_filingMoney(f.tax_collected_minor)) + "</td>" +
|
|
7675
|
+
"<td class=\"num\">" + _htmlEscape(_filingMoney(f.tax_owed_minor)) + "</td>" +
|
|
7676
|
+
"</tr>";
|
|
7677
|
+
}).join("");
|
|
7678
|
+
var table = (report.filings || []).length
|
|
7679
|
+
? "<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>"
|
|
7680
|
+
: "<p class=\"empty\">No filings intersect this window.</p>";
|
|
7681
|
+
|
|
7682
|
+
var body =
|
|
7683
|
+
"<section><h2>Remittance report · " + _htmlEscape(report.jurisdiction) + "</h2>" + notice +
|
|
7684
|
+
"<p class=\"meta\">Window: " + _htmlEscape(_fmtDate(report.from)) + " → " + _htmlEscape(_fmtDate(report.to)) + "</p>" +
|
|
7685
|
+
stats + table +
|
|
7686
|
+
"<p class=\"mt\"><a class=\"btn btn--ghost\" href=\"/admin/tax-filings\">Back to filings</a></p>" +
|
|
7687
|
+
"</section>";
|
|
7688
|
+
return _renderAdminShell(opts.shop_name, "Remittance report", body, "tax-filings", opts.nav_available);
|
|
7689
|
+
}
|
|
7690
|
+
|
|
7169
7691
|
function renderAdminShipping(opts) {
|
|
7170
7692
|
opts = opts || {};
|
|
7171
7693
|
var rows = opts.zones || [];
|
package/lib/asset-manifest.json
CHANGED
package/package.json
CHANGED