Skip to content

feat(tools/nat): nat-verify — spot-verify single invoices via offline invNo-filtered jobs - #10

Merged
linyiru merged 2 commits into
repo-toolsfrom
nat-single-invoice-verify
Aug 9, 2026
Merged

linyiru merged 2 commits into
repo-toolsfrom
nat-single-invoice-verify

Conversation

@linyiru

@linyiru linyiru commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

What

A single-invoice verification path against the MoF platform, plus the platform findings that shaped it.

Findings (documented in NAT-PLATFORM.md)

  • The online (即時) btb411w query only serves the current month. One-day unfiltered probes on every older month (previous month included) return 0 rows; only the running month returns data.
  • The online invoiceNumberStart/End filter did not match live even for an invoice that the same unfiltered query returns — exact param encoding unconfirmed, so the tool does not rely on it.
  • The 非即時 job's invNoStart/invNoEnd works and is now verified live: a job with both set to one 發票號碼 + a date window returns exactly that invoice's M/D rows, for months of any age, regardless of which value-added centre transmitted it.
  • Online result-row tokens decode exactly like job tokens (JWT payload → base64 data → JSON) into full invoice fields (invoiceNumber, amounts, extStatus, buyer/seller, senderId/Name = the VAC, migType, …) — field list documented.

Changes

  • createReportJob: optional invNoStart/invNoEnd passthrough.
  • NatClient.decodeDataToken: generic btb token decoder; decodeJobToken delegates to it.
  • nat-verify.ts: CLI — bun run nat-verify.ts <YYYY-MM[..YYYY-MM]> <發票號碼...> [--in] [--json]. One offline CSV job per invoice per ≤2-month chunk, then prints the decoded M-row summary (狀態/日期/金額/parties/傳送方/品項數). A seqNo watermark fences off same-month jobs from the same run so a poll can't pick up the previous invoice's job. Exit 1 when anything is not found.
  • README: usage section + files table row.

Why

Gateway (加值中心) exports drift: we hit a real invoice that the MoF shows as 作廢已確認 (voided on the platform weeks after issuance, then re-issued at a corrected amount) while the gateway's export still says 是否作廢=否. Any local DB built from gateway exports alone silently overstates sales. The MoF record is the statutory truth — this tool makes checking against it a one-liner.

Test plan

  • bun run test: 24 pass (17 existing + 7 new: chunkRange single/even/odd/malformed, monthEnd incl. leap Feb, decodeDataToken/decodeJobToken round-trip).
  • Live (own 營業人 account): verified a 2024-06 voided invoice (status/amount/最後異動時間 all match the platform), its 2024-07 replacement, and a 2025-12 invoice transmitted by a different VAC; a not-found number exits 1.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a single-invoice “spot verification” workflow to the NAT tooling so operators can validate specific invoices against the MoF platform record (via offline invNo-filtered jobs), addressing drift/staleness in gateway exports and local DB snapshots.

Changes:

  • Extend offline report job creation to optionally pass through invNoStart / invNoEnd for invoice-number filtering.
  • Introduce nat-verify.ts CLI to run one offline CSV job per invoice (chunked to the API’s ≤2-month limit) and print decoded invoice summaries or JSON.
  • Document platform behavior findings (online query current-month limitation, token decoding) and add tests for range chunking and token decoding.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tools/nat/README.md Adds user-facing usage docs for nat-verify and updates the tools table.
tools/nat/nat-verify.ts Implements the new CLI and its date-range chunking / job polling logic.
tools/nat/NAT-PLATFORM.md Documents verified platform behaviors (history window, token payload fields, invNo job filtering).
tools/nat/nat-client.ts Adds invNoStart/End support to createReportJob and introduces a generic decodeDataToken.
tools/nat/tests/nat-verify.test.ts Adds unit tests for chunkRange, monthEnd, and token decoding delegation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tools/nat/nat-verify.ts
Comment thread tools/nat/nat-verify.ts
Comment on lines +37 to +47
export function chunkRange(arg: string): Array<{ from: string; to: string }> {
const m = arg.match(/^(\d{4}-\d{2})(?:\.\.(\d{4}-\d{2}))?$/);
if (!m) throw new Error(`bad range "${arg}" — expected YYYY-MM or YYYY-MM..YYYY-MM`);
const months = monthsBetween(m[1], m[2] ?? m[1]);
const chunks: Array<{ from: string; to: string }> = [];
for (let i = 0; i < months.length; i += 2) {
const last = months[Math.min(i + 1, months.length - 1)];
chunks.push({ from: `${months[i]}-01`, to: monthEnd(last) });
}
return chunks;
}

@linyiru linyiru Aug 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in b20f15c — chunkRange() now rejects months outside 01-12 and reversed ranges up front with explicit errors (before login). Agreed on the severity: an empty chunk list silently turned a typo into '✗ not found on the MoF platform', which is exactly the wrong answer for a verification tool to fake. Tests added for both paths (26 pass).

linyiru added 2 commits August 9, 2026 23:40
… invNo-filtered jobs

The online (即時) btb411w query turns out to serve only the CURRENT month
(one-day probes on any older month return 0 rows even unfiltered), and its
documented invoiceNumberStart/End filter did not match live even for an
in-window invoice. The reliable single-invoice path is the 非即時 job with
invNoStart == invNoEnd == 發票號碼 — verified live: returns exactly that
invoice's M/D rows for months of any age, including invoices transmitted
by a different value-added centre.

- createReportJob: optional invNoStart/invNoEnd passthrough
- NatClient.decodeDataToken: generic btb row/job token decoder
  (decodeJobToken now delegates); online invoice-row tokens decode to
  full fields (invoiceNumber, amounts, extStatus, buyer/seller,
  sender = the VAC, migType, ...) — documented in NAT-PLATFORM.md
- nat-verify.ts: CLI — range + invoice numbers -> per-invoice offline
  CSV job -> parsed M row summary (狀態/金額/parties/傳送方) or --json;
  seqNo watermark fences off same-month jobs from the same run
- NAT-PLATFORM.md: online current-month-only window, row-token field
  list, verified invNoStart/End notes; README: usage section
- tests: chunkRange/monthEnd/decodeDataToken (24 pass)

Motivation: a gateway export kept a stale un-voided state for an invoice
the MoF had voided (and replaced) weeks later — gateway snapshots drift;
the MoF record is the statutory truth worth checking against.
… range bounds

- runInvoiceJob: also match queryEndDate month so a concurrent job that
  shares the start month (e.g. a monthly export running in parallel)
  can't be picked up as this chunk's result
- chunkRange: reject months outside 01-12 and reversed ranges up front —
  both used to yield zero chunks, making every invoice read as 'not
  found on the platform', which is a misleading answer for a
  verification tool
- tests for both (26 pass); live smoke re-run OK
@linyiru
linyiru force-pushed the nat-single-invoice-verify branch from 70f5679 to b20f15c Compare August 9, 2026 23:41
@linyiru
linyiru requested a lite review from Copilot August 9, 2026 23:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (2)

tools/nat/nat-verify.ts:72

  • runInvoiceJob() polls listJobs() with its default apply-date window ("today" computed inside listJobs() on each call). If the run crosses UTC midnight (e.g. job created near 23:59Z), subsequent polls can switch to the new day and no longer see the job, causing a false failure.
  const stamp = Date.now();
  await client.createReportJob({ ...opts, fileType: "CSV", invNoStart: opts.invNo, invNoEnd: opts.invNo });
  let job: ReportJob | undefined;
  for (let i = 0; i < 40 && !job; i++) {
    await new Promise((r) => setTimeout(r, 3000));
    const jobs = await client.listJobs();
    job = jobs

tools/nat/nat-client.ts:434

  • decodeDataToken() decodes the JWT payload segment using base64, but JWT segments are base64url-encoded (and may be unpadded). This can break decoding for tokens containing -/_ or missing padding, and the current code also throws a fairly opaque error if the token is malformed. Consider normalizing base64url + validating the expected shape before JSON parsing.
  /** Decode any btb list-row token (JWT → base64 `data` → JSON) — jobs and invoices alike. */
  static decodeDataToken<T = Record<string, unknown>>(token: string): T {
    const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString("utf8"));
    return JSON.parse(Buffer.from(payload.data, "base64").toString("utf8"));
  }

@linyiru
linyiru merged commit 902ec8d into repo-tools Aug 9, 2026
5 checks passed
@linyiru
linyiru deleted the nat-single-invoice-verify branch August 9, 2026 23:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants