IR56M fill → review → confirm → sign (Filings tab, reuse WOPC signing)
STATUS 2026-06-19 — resumed; JC confirmed a reportable sub-contractor (intent ON)¶
Everything built so far is COMMITTED + PROMOTED TO MAIN (no longer "not committed"):
T-045 research, T-045a candidate scan (wired into the WPP), T-047a particulars storage + HKID
input, T-047c PDF fill engine. Owner gave the go-ahead to build the rest in 3 phases:
- Phase 1 — DONE (origin/nightly c802b81f). POST /api/accounting/ir56m/fill (auth-gated;
IR56MFormData → fillIR56MPdf → application/pdf) + next.config outputFileTracingIncludes
(traces mupdf's wasm + the IRD template). mupdf-in-Next DE-RISKED: next build compiled the
route as a serverless function — mupdf's ESM + wasm + top-level-await bundle cleanly. Residual:
one authenticated runtime hit on the nightly deploy to confirm wasm loads + template is traced.
- Phase 2 — DONE (nightly; owner-verify the tab on the deploy).
- 2a DONE (nightly 83d18cd7→2594d460): lib/taxHK/ir56m/filing.ts (pure: IR56MPayerSnapshot /
IR56MRecipientFiling / assembleIR56MFormData / totalRemuneration / validatePeriodWithinYA) +
filings.server.ts (tebs-erl/ir56mFilings/{endingYear} CRUD, mirrors taxFilings, history[] audit) +
9 unit tests.
- 2b DONE (nightly 05f376a0): seed.server.ts buildIR56MFilingSeed (candidate scan + payee
taxProfile + payer → pre-filled recipients; identity + period seeded, capacity/amounts left to owner;
detect-don't-enforce) + pages/api/accounting/ir56m/filing.ts (GET seed+existing, POST save; not period-gated).
- 2c DONE (nightly 243e94d0) — the Filings-tab UI. components/accounting/tax/IR56MFilingTab.tsx
+ IR56MRecipientModal.tsx; registered in AccountingApp (Layered "Filings" group, order 41). Pick YA
(global period selector; All-time prompts to pick) → candidate table (every payee w/ a YA outflow,
threshold-flagged, "File?" toggle) → per-recipient capture modal (capacity / period [YA-constrained
DatePickers] / category split / withheld / remarks + particulars pre-filled, editable) → preview
via /api/accounting/ir56m/fill (opens the filled PDF) → Save via POST /filing (resumes drafts).
tsc 0; suite 340; eslint clean.
- Phase 3. Signing — reuse WOPC request/sign/chop/upload for an IR56M doc-type + the new
"sign from Records" surface; signed PDF → Drive "14d. IR56M".
(The "OPEN RISK" section below = RESOLVED by phase 1.)
DECISION: fill the OFFICIAL IRD PDF via mupdf-wasm (owner, 2026-06-15)¶
The official IR56M PDF is AES-256 (V5/R6) with an EMPTY user password (opens freely;
owner-password restricts editing; form-filling permitted). pdf-lib can't decrypt it;
mupdf (wasm) opens it transparently, the 61 AcroForm field names are fully readable
+ self-documenting, and saveToBuffer('decrypt') after bake() yields a decrypted,
flattened PDF that pdf-lib can then sign. Added dep: mupdf.
T-047c PDF ENGINE — BUILT + VERIFIED + COMMITTED (2d15d4d7, on main) + ROUTE ADDED (phase 1)¶
lib/taxHK/ir56m/ir56m-template.pdf(official IRD form, committed as the fill template).lib/taxHK/ir56m/fields.ts(pure):IR56MFormData+ exact field-name map +buildIR56MFieldValues(text + capacity/title checkboxes; money = whole HK$).lib/taxHK/ir56m/fillIR56MPdf.server.ts: mupdf open → set fields by name → bake → saveToBuffer('decrypt') → Uint8Array.lib/taxHK/ir56m/mupdf.d.ts: ambient shim (project moduleResolution can't see mupdf's own types). Engine has NOimport 'server-only'(would throw in pages/api).- VERIFIED against the owner's real sample (Ngai Wang Chi → Chan Jeffero/陳彥廷, HKID Y360417(7), subcontractor, 03/09/2024–30/03/2025, $134,800): all field VALUES set (pre-bake read-back), decrypted output opens in pdf-lib, page renders correctly. tsc 0 err, lint clean. Sent the owner the filled sample PDF for visual confirmation.
- Field names (mupdf-confirmed): Reporting Year · Sheet Number · Section Number of Employer's File · Employer's File Number · Name/Address of Payer · Name/BRN of Partnership… · Name/BRN of Sole-proprietorship · Title-Mr/Ms/Miss (checkbox) · English/ Chinese Name of Recipient · HKID Number-Prefix/Digits/Check Digit · Indicator-Sex · Indicator-Marital Status · Name of Recipient's Spouse · Spouse's HKID… · Recipient's Postal Address/Telephone Number · Capacity-Subcontractor/Agent/Writer/Consultant/Coach/ Tutor/Others (checkbox) + Capacity Engaged · Start/End Date · Amount-Subcontracting Fees/ Commission/Writer's…/Artiste's…/Copyright…/Consultancy…/Service Fees · Nature/Amount of Other Income · Amount-Total Incomes · Indicator/Amount-Sum Withheld · Remarks · Name of signer · Designation · Date of Signing · Indicator-Form Completed.
~~OPEN RISK~~ RESOLVED (phase 1, c802b81f): Next.js integration of mupdf¶
mupdf is ESM with top-level await + wasm. ~~NOT yet validated in a Next build/route.~~
RESOLVED: the phase-1 route /api/accounting/ir56m/fill builds — next build compiled it as
a serverless function (mupdf ESM + wasm + TLA bundle cleanly). outputFileTracingIncludes traces
the wasm + template into the function. Residual = a runtime hit on the Vercel/nightly deploy.
Goal (owner, 2026-06-15)¶
A Layered-view Filings tab (the group already exists — Profits Tax tax lives
there) to fill an IR56M, review + confirm, then sign it — mirroring WOPC: pick a
"closing director" who signs by (a) logging in → Records → sign in-app, (b) emailed
signature request, (c) company chop/seal, or (d) manual upload of a signed form.
Owner attached blank IR56M.pdf + IR56M_Filled.pdf; IRD form (4/2026):
https://www.ird.gov.hk/chi/pdf/ir56m.pdf
Form fields learned (IR56M 4/2026)¶
Header: 截至 ___ 年 3 月 31 日止的 1 年度內 = year of assessment (year ending 31 Mar). Checkboxes: 附加表格 (additional) / 修訂表格 (revised); sheet X of Y. 1. Payer 付款人: (a) 僱主檔案號碼 employer file no. [e.g. "6B1 77021233"] or BR/HKID; (b) name; (c) address. 2. Recipient if partnership/unincorporated body: name + BR no. 3.(a) if sole proprietorship/uncertain: name + BR no. (b) if INDIVIDUAL: (i) title + English full name (SURNAME, GIVEN) + Chinese name; (ii) HKID (req); (iii) sex M/F (req); (iv) marital status 1/2; (v) spouse name + HKID/passport if married. 4. Recipient address + phone. 5. 服務身分 capacity: 判商(sub-contractor)/代理人/作家/顧問/教練/導師/其他 (delete N/A). 6. 服務期間 period of service: from–to (DD MM YYYY). ← see rule below. 7. Remuneration by category (HK$, no cents): 承判金 sub-contract / 佣金 commission / 作家投稿費 / 演藝費 / 版權專利費 / 顧問費 / 自由職業者服務費 (instructor/coach/photographer) / 其他; + 總額 total. 8. Tax withheld? 0=no / 1=yes (+ amount). (mandatory) 9. Remarks. Signature block: 公司蓋印處 (company chop area) + 簽署 signature + 姓名 name + 職位 title + 日期 date. Footer: provide a completed copy to the recipient.
服務期間 (field 6) rule — answer to owner¶
Must fall WITHIN the year of assessment = 1 April [Y-1] → 31 March [Y] (the form is "the 1-year period ending 31 March [Y]"). Filled example: 03/09/2024 → 30/03/2025, inside YA 2024/25. The "30 March" end is that engagement's actual end date — NOT a rule; there is no "must end 30 Mar" constraint. Build will VALIDATE from/to to [1 Apr, 31 Mar] of the selected YA. (Owner's "Mar 31 to Mar 30" ≈ the YA window; precisely 1 Apr–31 Mar.)
Data mapping (have vs capture)¶
HAVE: payer name/ZH/address/phone + brNumber (fetchSubsidiaryInfoServer('erl')); recipient name/address/phone (payee directory); YA (ir56m.ts yearOfAssessmentForIso); remuneration total (generateIR56MCandidatesServer). CAPTURE (not stored): recipient HKID, sex, marital status (+ spouse), capacity (field 5), per-category split (field 7), period of service (default first/last payment, owner edits), tax-withheld flag.
OWNER CONTEXT (2026-06-15) — STOP dismissing the Jeffero IR56M¶
Jeffero Chan is a director AND a bona-fide sub-contractor to ERL. Payments to him are 5050 Sub-Contractor Fees, backed by WOPCs (the contract: which project/service, paid for what). So IR56M IS legitimately in scope on the merits — do NOT keep warning "he's a director so probably N/A." Threshold depends on the capacity classification the owner picks (判商 sub-contractor $200k vs consultant/other-service $25k).
CORRECTION — in-app signing from Records is NOT supported today¶
Owner clarified: a WOPC closing/assigned director CANNOT log on → Records → WOPC tab → sign there; they only see a Request Signature button. Actual signing happens via the emailed magic-link page (/wopc/sign/[id]). So "director signs from the Records page" is GENUINELY NEW work — build it for IR56M (and it would benefit WOPC too). My earlier "already supported" was wrong.
T-047a DONE (2026-06-15) — Contacts stores IR56M recipient particulars¶
Owner asked to make the web app STORE the lacking fields (input → store → read back). Built on the payee record (Contacts → People → a person → Edit): HKID, Chinese full name, sex, marital status, spouse name, spouse HKID/passport. - lib/payeeDirectory.ts: PayeeTaxProfile + taxProfile on record; read in BOTH parsers; flat + WriteInput fields. - lib/payeeDirectory.server.ts: buildPayeeDocument writes taxProfile; update-merge + change-detect; server parser reads it. - components/people/PeopleApp.tsx: shared Ir56mParticularsFields (add + edit forms), PayeeRow + form-values + row-map + hydrate + both submit bodies + detail read-back. - API route is a passthrough (no per-field allowlist) so fields flow through. - VERIFIED: server write→read round-trip against real Firestore (throwaway payee, deleted after); tsc 0 errors; 0 NEW lint errors (line-intersection check). Sensitive PII (HKID) never logged. COMMITTED to nightly 56c6b257 (NOT main).
Signing — REUSE WOPC mechanics (paths exist; wire an IR56M doc-type)¶
All four WOPC signing paths are reusable (the magic-link sign page, email request, chop,
manual upload); the NEW part is letting the director sign from the Records page:
- Director registry lib/directors/registry.ts (DIRECTORS: jeffero-chan/JC, jake-ngai/JN)
= the "closing director" picker. Cross-issuance rule: if payee is a director, the OTHER
signs (WOPC-specific anti-self-sign on GL5050; for IR56M the signer is just ERL's
authorized director — revisit).
- State machine draft→assigned→sent→signed (lib/wopc/signingRequests/*).
- (a) in-app draw /wopc/sign/[id]/draw; (b) email request via Resend
lib/email/sendWopcSigningRequestEmail.ts + /api/wopc-signing/[id]/send; (c) chop
/wopc/sign/[id]/chop (secured seal assets); (d) manual upload /wopc/sign/[id]/upload.
- Records WOPC tab actions (RecordsApp.tsx ~2263): Request signature / Send / Sign / Withdraw /
Upload signed; director inbox app/wopc/inbox.
- Signed PDF → Drive "14c. WOPC" (lib/wopc/signing/drive.server.ts). IR56M → new "14d. …" folder.
Layered "Filings" group exists: AccountingApp.tsx layerAssignments (tax = order 40);
add ir56m-filing order 41.
Phase 3 — signing build plan (starting 2026-06-19; investigated the WOPC system)¶
Signature-block TEXT (姓名/職位/日期) is DONE (#5, 3bf3d718). Remaining = ink signature + chop on the
filled PDF + the request/sign workflow. REUSE the WOPC signing system, don't duplicate it. Findings:
- composeSignedWopcPdf (lib/wopc/signing/composition.server.ts) embeds the signature + 11 ERL chop
assets via pdf-lib + has the void-stamp logic — but its PLACEMENT COORDINATES are WOPC-A4-specific
(closing-signature line + chop box). IR56M needs its OWN coordinates (its 簽署 line + 公司蓋印處 box).
So reuse the embed technique + chop assets + void logic; supply IR56M placement.
- drive.server.ts files to hardcoded "14c. WOPC" via the accounting Drive SA + lazy-create — reuse
the pattern, new target "14d. IR56M".
- placement.ts (parse signature/chop placement) — generic, reuse as-is.
- signingRequests/ state machine + types are WOPC-COUPLED (wopcRef + the WOPC doc's locks/void).
Sub-phases:
- 3a — DONE (lib/taxHK/ir56m/sign.server.ts) — composeSignedIR56MPdf (signature scaled into the
IR56M signature box above the 簽署 line — render-verified — + optional chop via the reused generic
composeChopOntoPdf + getChopImageBytes) + uploadSignedIR56MToDrive (→ "14d. IR56M", mirrors the
WOPC 14c Drive pattern). tsc 0. Chop placement is director-picked (calibrated when 3c lands).
- 3b — IR56M signing-REQUEST model. DECISION (owner): generalize the WOPC signingRequest to a
doc-type-agnostic model (docType:'wopc'|'ir56m', docRef) — cleaner, but touches the LIVE audit-
critical WOPC system (risk) — vs a PARALLEL ir56mSigningRequest reusing the lower-level utilities —
safer, some duplication. RECOMMEND parallel-but-shared-utilities.
- 3c — the 4 signing paths (in-app draw / emailed magic-link / chop / manual upload) + the NEW
"sign from Records" surface (directors only get Request-Signature today — new even for WOPC).
- Cross-issuance: JC's IR56M signed by the OTHER director (JN). Relates to T-050 (WOPC cross-issuance
bug) — apply the same rule, do NOT duplicate T-050's fix.
NO OVERLAP: signing is wholly within T-047 ("…→ sign"); T-050 is a separate WOPC bug; the WOPC signing
system is REUSED, not duplicated.
Proposed sub-tasks¶
- T-047a — IR56M filing data model (Firestore ir56mFilings/{id}: YA, payer snapshot, per-payee particulars incl. captured HKID/sex/marital, capacity, period, category split, withheld, status) + pure builder/validators (period-within-YA, totals).
- T-047b — Filings tab UI: pick YA → seed payees from candidate scan → fill/capture missing fields → review + confirm.
- T-047c — IR56M PDF render (pdf-lib over the IRD form, or AcroForm fill) + preview.
- T-047d — Signing: reuse WOPC request/sign mechanics for an IR56M doc-type (closing-director pick, in-app sign on Records, email request, chop, manual upload) → file to Drive 14d.
- T-047e — Records "IR56M" surface so the assigned director can find + sign it.
Reality check (carry from T-045)¶
Only >$25k candidate is JC (director) at $85,300 of 5050 subcontractor fees — payments to a director generally are NOT IR56M. ERL likely has NOTHING to file this YA; feature would be ready for when a real unincorporated payee crosses the threshold. Confirm before/while building.
Log¶
- 2026-06-15 created (owner). Studied IR56M.pdf + IR56M_Filled.pdf + IRD form; mapped WOPC signing (reusable) + Layered Filings group (exists). Answered 服務期間 rule. Awaiting go-ahead.
- 2026-06-19 phases 1+2 built (c802b81f / 2594d460 / 05f376a0 / 243e94d0). Owner reviewed the rendered form vs the official specimen (ir56m_completion_e.pdf) + notes (ir6036c_e.pdf).
- 2026-06-19 RENDERING CORRECTIONS (nightly 30fb0cce) — VERIFIED the title/capacity checkboxes STRIKE-when-checked by rendering the template (a "delete the inapplicable" form): 2d title strikes complement (Mr left, Ms/Miss struck); 2f capacity strikes complement (all 6 roles struck → 其他 + "Sub-contractor"); 2e English name surname+SPACE+given, no comma; 2g period DDMMYYYY no separators; 2a Sheet No. alphabetical from 900001 (note 4); 2h whole YA total → Type 1 承判金 (5050 payees); 2i tax-withheld = 0. + 5 engine tests (ir56mFields.test.ts).
- 2026-06-19 amount right-align (nightly 0515eaa4): field-7/8 amount boxes are comb (MaxLen 9); mupdf ignores /Q, so left-pad with spaces to right-align. No "$" rendered (bare digits).
- 2026-06-19 2b/2c via T-070 (nightly e905deeb): Subsidiary tab stores the employer file no. (renders 僱主檔案號碼) + the registered address incl. region "Hong Kong" (so the address ends ", Hong Kong"). Owner to populate ERL's employer file no. + region in Contacts → Subsidiary.
- 2026-06-19 RENDERING CORRECTIONS round 2 (nightly 3bf3d718, owner review #2): #1 Sheet No. — the form PRE-PRINTS the leading "9"; the field holds the 5 digits after it (MaxLen 5) → fill the suffix 900001→"00001" (was sending 6 → truncated). #2 English name — RESTORE the comma "CHAN, Jeffero" (reverses round-1's no-comma 2e; the form's 3(b)(i) text + bottom example both use a comma). #3 Period of service → default to the full YA the period selector resolves to (1 Apr–31 Mar), not first/last payment dates. #4 Payer block (name/address/employer-file-no) now READ-ONLY display from the stored subsidiary info (Contacts → Subsidiary); no longer inline-editable here; the seed reads employerFileSection/Number. #5 Signature block — new "Signing director" selector fills 姓名 + 職位 (Director); 日期 = today.
- REMAINING on T-047: phase 3 = signing (reuse WOPC + "sign from Records" → Drive 14d).