Skip to content

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 NO import '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).