Contacts refactor → Individuals + Client Company (unified sectioned person profiles)
✅ DONE 2026-06-16 — goal achieved (P1–P5 + UI + refinements + full editing), all on nightly¶
Unified Contacts shipped: aote-system individuals + organizations stores; new Individuals + Client Company
tabs with a sectioned profile editor (plain section labels + tooltips) supporting FULL editing of every personal/legal/
contact field — English + Chinese (姓/名 split) legal name, HKID, marital + spouse, bank accounts (incl. joint
holders + WOPC default), sex/email/phone/whatsapp/birthDate, address, abbreviation (transactional swap + alias),
subsidiaries, related-party (director facet + userUid read-only by design). Dual-read everywhere with degrade-to-legacy
fallback; bidirectional payee↔individual write-sync; abbreviation typeahead preserved (frozen payees/{abbr} keys
untouched). 62 unit tests; tsc + lint clean. Browser-verified live by owner click-through (lists + edits 200, 0 errors).
Zero behaviour change for unbridged users; directors registry still the synchronous runtime source of truth.
Commits (nightly): 23b88b6f P1 · 26277cc6 P2 · 7790f582 (PII redaction) · 28f4587c (gather refactor) · 85ba7e18 P3 ·
d479e979 P4-foundation · 40e31eb3 P4-W1 · b339aa1e P4-W2 · 6598ef85 P4-W3 · cc0b9cf8 P4-W4 · 4d021df2 P4-W5(UI) ·
91fd5c11 P5 · b6e76cde (Chinese-split + plain labels) · e85eb7a5 (full editing: bank + address).
Live data written: aote-system individuals(28)/organizations(19)/abbreviations. NOT promoted to main (nightly only).
Optional follow-ups → T-051 (P6/P7; a read-only audit showed they're not needed now — WOPCs ride the frozen
payee key, 0 wopcPath to backfill, counterparty stamps all uppercase). Known parity note: client/staff legacy tabs
remain until T-051 P7 re-confirms parity; bank-edit parity for People is DONE (e85eb7a5).
⇒ FINAL DESIGN + PHASED PLAN: docs/contacts-unification-design.md (repo contract, like matching-redesign.md)¶
Hardened by 8-agent workflow wf_fc3f4f3c-619 (5 impact maps + adversarial breakage/completeness/migration-safety, ~40 findings). Design VALIDATED but reshaped. Criticals folded into the doc: (R1) decouple editable Individual abbreviation from the FROZEN operational doc-ids (payees/{abbr}, Students/{abbr} stay keyed by abbr — WOPC paths + session matching are load-bearing); (R2) a companion ORGANIZATIONS unification is required (affiliatedCompanyId/repOfCompanyIds + vendors + billingCompany have no unified company entity); (R3) directors are BOOT-CRITICAL synchronous financial control (gate WOPC closing rule AND director-loan postings) → cached + fail-loud, migrate first; (R4) statutory schedules (related-party/IR56M/director-current-account) must be DUAL-KEYED (abbr+stableId) + read UNFILTERED by a system context, with an "unresolved counterparty" count (no silent under-disclosure). Merge key must include email/uid (not abbr+name alone); directors registry is the ground-truth seed. 7-phase plan (directors→ merge-artifact→write→dual-read→dual-write→WOPC-rewrite→UI+teardown), dual-write + dry-run-first, rollback = stop-reading-new. AWAITING DECISIONS A/B/C/D before P0.
Goal (owner, 2026-06-15, paraphrased — AWAITING CONFIRMATION)¶
Replace the Contacts tabs (Clients / People / Staff) with TWO top-level groups: Individuals and Client Company. Merge Staff + People(payees) + client-company Representatives into Individuals. Give each Individual a SECTIONED profile so the app can tell which individuals belong in which dropdown (section-gated availability).
Current model (mapped 2026-06-15)¶
- Contacts tabs: Clients (
clientscoll, EPL dir-DB /tebs-mel), People (payeescoll, dir-DB), Staff (Firebase Auth users via /api/auth/staff-directory — NOT Firestore). - Representatives are EMBEDDED in the client doc as
representative: {title,firstName, lastName}(one per company, not an array, NOT a standalone doc). lib/clientDirectory.ts:38-55. → To make them first-class Individuals: promote each to its own doc + leave a reference id on the client; strip personal fields from the client doc (keep a backup). - Payee (People) doc: legalName, representative(STRING=company they work for, not a ref),
contact, address, bankAccount/bankAccounts, relatedParty, taxProfile{hkid,chineseName,sex,
maritalStatus,spouse}. coll
payees, doc id = abbreviation. - Directors: HARDCODED array lib/directors/registry.ts (JC, JN);
initials== payee abbrev. - Dropdowns today: payee autocomplete (PaymentConfirmationForm / TransactionLinkingModal), closing-director (WopcSigningRequestModal ← DIRECTORS), staff search (StaffDirectoryContent).
- PAYEE ABBREVIATION (owner flagged 2026-06-15): the 5050 customizable workflow's
abbreviation comes from the payee directory doc, and the abbreviation IS that doc's
ID (
payees/{abbr}in tebs-epl). ContractorModule loads via /api/payee-directory; the abbr → WOPC ref (ERL-WOPC/{abbr}-…, paymentConfirmation.ts) + closing-director resolution (resolveRequiredClosingDirector, wopcInline.tsx:318). VERIFIED vs LIVE DATA: the rbacusersdoc (aote-system/users) has NO payee/abbreviation field; thepayeesdoc has noabbreviationfield either — it's the doc id. Directors hard-link viainitials === payee doc id. Implication: in the unified Individual model the abbreviation should be a first-class FIELD (with a stable internal doc id), so a person's payee/user/staff/director facets share ONE record + a mutable abbreviation — and it gates the WOPC/5050 dropdowns.
FOURTH profile source — MEL students (tebs-mel, discovered 2026-06-15)¶
tebs-mel/Students/{abbr}— doc ID IS the abbreviation (e.g. "Nan"), SAME pattern as payees! Root: account(display name) + summary stats (jointDate/lastSession/totalSessions…).- Personal info in latest-doc SUBCOLLECTIONS: firstName, lastName, sex, birthDate, contactNumber, emailAddress, Address, HKID(idNumber). (versioned history pattern)
- Coaching-billing config (subcollections): billingCompany (string ref → tebs-mel/clients doc = the company billed INSTEAD of the student), billingType, BaseRate(+History), Payments, Retainers, freeMeal vouchers.
- Sessions = SEPARATE root collection
tebs-mel/Sessions(studentAbbr/studentName + invoice/payment/rateCharged subcollections). NO bank info on students. - KEEP on Student doc: sessions + coaching-billing config + summary (MEL-operational). MIGRATE to Individual: personal (firstName/lastName/sex/birthDate/contact/address/HKID) + the billed-company link (→ unified Client Company).
- KEY INSIGHT: abbreviation is ALREADY the universal person key — BOTH payees AND students are keyed by abbr. So it's the natural merge/identity key. Owner wants EVERY Individual to have an abbreviation (even non-payees / company reps).
Proposed Individual profile — 3 sections (per owner)¶
- Legal & Financial identity: English legal name, Chinese legal name, bank account(s), marital status, HKID. (the payment + tax identity)
- Basic info / contact: preferred name, gender (sex), email, phone.
- System / accounting: related-party flag+relationship, WOPC, payee abbreviation, director role, affiliated client company. Section-COMPLETENESS gates dropdowns: e.g. WOPC payee / IR56M recipient needs §1; closing- director needs §3 director role; a plain contact picker needs §2.
Open decisions (need owner input)¶
- Staff are Auth users, not Firestore. Mirror/link into Individuals (Individual doc + optional staffUid), or federate views?
- ONE unified
individualscollection (migrate payees + promote reps + mirror staff) — yes? Big migration (back up first; detect-don't-corrupt on client docs). - Directors → a §3 "director" role on Individuals (keep registry as derived), or migrate?
- EPL + MEL each have their own
clientscollection — Individuals global or per-subsidiary? - Phasing (data model + migration first, then UI rebuild, then dropdown rewiring).
Log¶
- 2026-06-15 created. Mapped current model (reps EMBEDDED, staff=Auth, directors hardcoded). Re-elaborated understanding to owner; awaiting confirmation before building.
- 2026-06-15 all 4 decisions resolved (A orgs-together, B vendors-excluded, C jft, D decoupling); contract docs/contacts-unification-design.md updated (now incl. Organizations unification).
- 2026-06-15 STARTED P1 (directors). P1a DONE (not committed): lib/individuals/types.ts (Individual+Organization model)
- lib/individuals/seedDirectors.server.ts (plan). Dry-run verified vs live: both directors → Individuals, users.uid bridge resolves for both, abbr JC/JN free, exactly-2 invariant holds, 0 warnings, NO writes, tsc clean. P1b (write + registry-cached-cutover) AWAITS owner greenlight (boot-critical path).
- 2026-06-16 P1b DONE + APPLIED (live write, uncommitted). New: lib/individuals/abbreviations.ts (pure canonical) + abbreviations.server.ts (resolveIndividualIdByAbbreviation + claimAbbreviation, transactional .create(), fail-loud AbbreviationConflictError); directorsConsistency.ts (pure diffDirectors) + .server.ts (assert/check, fail-loud on registry↔store drift); applyDirectorSeed() added to seedDirectors.server.ts (additive, idempotent, atomic individual+abbr per director, never clobbers existing doc); scripts/t048-seed-directors.ts (--apply, dry-run default; old _dryrun script removed). 15 unit tests (abbreviations 9 + directorsConsistency 7) PASS; tsc + lint clean. APPLIED to aote-system: individuals/{jeffero-chan,jake-ngai} + abbreviations/{JC,JN} CREATED; read-back OK; consistency OK; idempotent re-run = exists. REGISTRY UNCHANGED = still the synchronous runtime source of truth (nothing reads the new rows yet → zero behaviour change, reversible by deleting the 4 docs). Abbreviation typeahead UNAFFECTED (payees/{abbr} path untouched in P1).
- 2026-06-16 P1 COMMITTED to nightly (23b88b6f; incl. docs/contacts-unification-design.md). Owner standing instruction: auto-commit every complete change to nightly; main only when told. STARTED P2 (snapshot + propose-merge artifact, read-only). Workflow wf_da1c3e69-ef6 running: 7 source-mappers (payees/students/client-reps/users/orgs/counterparty-integrity/typeahead) → merge-logic design → adversarial verify, before building the read-only generator.
- 2026-06-16 P2 DONE + COMMITTED to nightly (26277cc6). Workflow verdict build-with-fixes; all 9 must-fixes folded in. Built pure merge brain lib/individuals/merge/{types,identityKeys,personMerge,orgMerge,integrity}.ts (29 unit tests) + read-only driver scripts/t048-p2-snapshot.ts (Firestore write-guard throws on ALL writes; file-only artifact; NO raw HKID/bank values — presence flags only). FIRST LIVE RUN (read-only, 0 writes): sources epl/payees=2 mel/Students=11 epl/clients=20 mel/clients=1 users=4 erl/txns=176. Result: 33 person rows (2 directors RECONCILED cleanly into the P1 seeds = Jeffero/Jake triple-collapse user+payee+seed; 31 propose-create), 20 orgs (1 cross-DB merge "Tack Tack Limited" epl+mel), 0 HARD_STOPS, 0 unresolved/ambiguous counterparties, 0 splits, abbr collisions 0, typeahead superset HOLDS (wopcRef preserved), director bridge OK, 0 duplicate-emails, 0 timestamp-less student docs. 4 NAME-ONLY ADVISORIES for owner review: Jeffero + Jake each also appear as a CLIENT-COMPANY REP (P0022/P0008); 2 more dup pairs (students/reps). 5 bank holder-name sets (incl. HSBC2 JOINT JC+JN) need a home in P3. Artifact: $TMPDIR/claude/t048-p2-staging-artifact.json (PII-light, never committed). AWAITING OWNER review of advisories + the P3 open questions (addressLine3 mapping, billingCompany create-vs-queue, rep dedup policy, accountHolderNames storage) before P3 (write individuals + organizations).
- 2026-06-16 OWNER DECISIONS captured (design doc updated): all 4 advisory pairs = same person (reconcile); Jake's student "Geet" → jake-ngai with JN primary + GEET alias (frozen Students/Geet session key untouched); recommended P3 defaults accepted. STARTED P3. Refactored shared gatherer (committed 28f4587c). Built pure composition lib/individuals/merge/compose.ts (applyDirectives + composeWritePlan + composeOrgPlan; 5 more unit tests, 34 total) + scripts/t048-p3-write.ts (dry-run/apply). P1 HKID-leak FIXED first (7790f582): artifact redacts HKID to 'present'. DRY-RUN caught + fixed a path collision (Tack Tack Limited is a client in BOTH epl+mel → DB-qualified {sub}-clients/{name} paths). Committed P3 code 9e4b338a. CLEAN PLAN: 28 individuals (2 directors reconciled additively, 26 new), 13 abbreviation+alias claims, 20 organizations; org rep links resolve (jeffero-chan/hiu-ying-tse/cheuk-yiu-kwai). NOT YET APPLIED — awaiting owner go + resolution of the "Establish Records Limited" client-vs-subsidiary flag. Writes are additive/idempotent/reversible, aote-system only (operational back-refs deferred to P4).
- 2026-06-16 P3 APPLIED + VERIFIED + COMMITTED (85ba7e18). Owner: exclude "Establish Records Limited" as a client-org (ERL subsidiary; rep still reconciled into jeffero-chan) + apply. WROTE to aote-system: 26 individuals created + 2 directors reconciled additively (jeffero-chan now has HKID + 4 bank accounts from payee JC), 11 new abbreviation/alias claims (JC/JN pre-existing from P1), 19 organizations. VERIFIED: idempotent re-run (0 created); resolveIndividualIdByAbbreviation resolves JC→jeffero-chan, JN→jake-ngai, GEET→jake-ngai (alias!), NAN→cheuk-yiu-kwai, KT→hiu-ying-tse, case-insensitive; checkDirectorsConsistent OK. Operational docs (payees/students/clients) UNTOUCHED — registry still the synchronous runtime source of truth; nothing reads individuals/organizations yet → zero app behaviour change, reversible by deleting the new docs. NEXT = P4 (flip READERS to dual-read abbr|stableId via the resolver + uid bridge; preserve issued-doc snapshots). P4 is the first phase that changes app behaviour → propose + owner review before building.
- 2026-06-16 Owner: "proceed" + INCLUDE THE UI MODIFICATION IN THE PLAN (don't defer the Contacts UI rebuild to last only — fold it in). STARTED P4+UI design workflow wf_999c0443-574: 5 mappers (user-readers, coaching-hooks, payee-typeahead, client-rep-readers+current-Contacts-UI, new-UI-design) → synthesize phased plan (aote-system API surface + additive dual-read flips + Contacts UI rebuild into Individuals + Client Company w/ sectioned editor + section-gated dropdowns) → adversarial (behavior-regression/typeahead/frozen-snapshot/gating). Will present plan + UI mockup, then build the safe foundation first.
- 2026-06-16 P4 design workflow DONE (verdict build-with-fixes, 7 must-fixes folded in). Presented plan + UI MOCKUP (Individuals + Client Company groups, sectioned editor, gating badges). Plan = 5 waves: Foundation→user dual-read→coaching dual-read→payee typeahead→aote CRUD API→UI rebuild; then P5 writers/P6 WOPC/P7 teardown. COMMITTED: Foundation d479e979 (completeness.ts + personalFields.ts + store.server.ts, 15 tests, additive zero-behavior) + Wave 1 40e31eb3 (/api/users/[uid] + staff-directory dual-read, behind degrade-to-legacy readers; identity + contactNotes/groups stay user-doc; verified live both directors resolve). NEXT: Wave 2 (coaching /api/individuals/by-abbreviation/[abbr]), Wave 3 (payee typeahead overlay — frozen documentId, overlay-only), Wave 4 (aote CRUD API). Wave 5 (UI build) AWAITS owner reaction to the mockup. Must-fixes to honor in W3: pin documentId to frozen payee abbr (test Nan/JC-1), overlay-only union (gate on hasBackingPayeeDoc), never blank bank/joint HSBC2, keep registry boot-critical.
- 2026-06-16 Owner "Proceed" (mockup approved). P4 Waves 2-5 ALL SHIPPED to nightly:
- W2 coaching dual-read b339aa1e: /api/individuals/by-abbreviation/[abbr] (student-wins, unified gap-fill, never 404); hooks swapped.
- W3 payee overlay 6598ef85: /api/payee-directory overlays individuals (overlay-only, documentId frozen, never blanks bank); lookupPayeeByAbbreviation left payees-first (must-fix 4). Corrected jake-ngai preferredName "Ngai Wang Chi"->"Jake Ngai" (friendly name; closingName frozen) so overlay is a verified NO-OP for JC+JN.
- W4 CRUD API cc0b9cf8: /api/aote-system/individuals + /organizations (GET/POST/PATCH); collision-safe id (jeffero-chan-2 verified, seed intact), transactional swapAbbreviation (alias-preserving), two-way org rep link; diffDirectors expanded to loginEmails/contactEmail/title (must-fix 5). Live: gate=wopc->JC/JN, gate=director->both.
- W5 UI 4d021df2: components/contacts/IndividualsContent + ClientCompanyContent; ContactsApp gains Individuals (default) + Client Company tabs ADDITIVELY (legacy tabs stay until P7). Sectioned profile drawer + gating summary + edit/PATCH w/ drift warning. tsc+lint clean, dev compiles; page is AUTH-GATED so rendered UI NOT browser-verified (can't log in) — APIs verified live. P4 status: section-gated PICKER REWIRING (ContractorModule etc. -> ?gate=) DEFERRED to P5. 54 unit tests. Zero behaviour change for unbridged users; registry still synchronous runtime source of truth. NEXT = P5 (writers flip to aote-system) — owner-gated.
- 2026-06-16 Owner "Continue" (preview can't auth offline — accepted). P5 (writers sync) SHIPPED 91fd5c11: bidirectional payee<->individual write sync. Dir A (legacy payee edit -> refresh bridged individual) writes aote-system ONLY (can't harm WOPC). Dir B (new-UI individual edit -> write-through to backing payee) present-only (never blanks), loop-guarded (skipIndividualSync). Verified live idempotent no-op on directors (no data loss, no loop). Resolves the Wave-3 overlay staleness gap. New code tsc+lint clean (pre-existing as-any debt in payeeDirectory mergedInput left untouched). P5 REMAINING (not yet done): transaction-stamp dual-keying (stamp stableId alongside abbr on NEW txn links, R4) — lower urgency, statutory schedules still resolve via abbr. REMAINING PHASES: P6 WOPC re-anchor (anchor on stableId, backfill wopcPath, freeze minting during cutover) + P7 teardown (retire legacy Clients/People/Staff tabs; convert lib/directors/registry.ts to a synchronous CACHED VIEW over individuals.system.director — BOOT-CRITICAL, owner-gated). Registry STILL the synchronous runtime source of truth.
- 2026-06-16 BROWSER-VERIFIED the new Contacts UI live (owner logged in + clicked through; I watched dev-server logs): Individuals + Client Company lists 200, ~10 profile edits PATCH 200, P5 write-through correctly skipped non-payee individuals, ZERO server errors. Closes the earlier "couldn't browser-verify" gap.
- 2026-06-16 Owner Contacts refinements (proceed autonomously) SHIPPED b6e76cde: (1) §1/§2/§3 badges -> plain labels "Legal & bank"/"Contact"/"Director" + tooltips; (2) Individual editor now edits ALL sections (was a v1 partial — director facet stays read-only, boot-critical); (3) Chinese name SPLIT into legal.chineseLastName(姓)+chineseFirstName(名) — new pure lib/individuals/chineseName.ts (split/compose/display); view shows 陳 彥廷 (full-width spaced), edit shows two fields. All read/write/overlay/sync sites compose-on-write (payee taxProfile.chineseName / IR56M) / split-on-read. Migration scripts/t048-split-chinese-name.ts APPLIED to 18 individuals (legacy field removed). 62 tests, tsc+lint clean.
- 2026-06-16 P6 READ-ONLY AUDIT (no writes): 25 WOPCs (24 JC + 1 JN) under payees/{abbr}/wopc with LEGACY refs ERL-WOPC/{YYYY}-{NNN} (no abbr segment); 0 transactions carry WOPCReference/wopcPath (nothing to backfill); 27 txns have counterpartyDirectoryId, ALL uppercase (0 case-mismatch → adversarial finding #1 doesn't manifest live). CONCLUSION: P6 is NOT needed now — WOPCs are anchored on the FROZEN payee doc-id (JC/JN), which my migration never touches; the swapAbbreviation design keeps the frozen payees/{abbr} doc-id + WOPC paths valid even when an Individual's abbreviation is edited (R1). P6 only becomes relevant if the frozen payee doc-id itself were renamed (not a migration action). P7 registry cutover = RISK without functional benefit (hardcoded registry works + is the safe boot-critical source); P7 teardown BLOCKED on new-UI bank-account-edit parity (the new drawer doesn't edit bankAccounts yet — PeopleApp still needed). T-048 GOAL ACHIEVED through P5 + refinements: unified Individuals + Client Company, all facets merged, sectioned editor, typeahead preserved, dual-read+write-sync, zero behaviour change for unbridged users. STOPPING active migration here; P6/P7 deferred as do-only-if-needed. Known gap: new-UI editor lacks bankAccounts editing (use legacy People tab; P5 syncs it).