think
16px
820px

K.37 — Multi-signature + pejabat auto-select · phone standardization · assignment history

Deployed: ahu-ai-ocr:60e1e9b5 @ x056.ahu-demo.ocr.val.id · 2026-07-21
Suites: backend 2824 · frontend 1034 (0 fail)

1. Multi-signature documents — the pejabat's ink is now chosen, not guessed

The verify service's /verify silently used its single best-scoring crop — on an ijazah carrying the student's AND the principal's signatures (or a notarized doc where the notaris co-signs), the WRONG ink could win the comparison. Now:

  • The backend runs /detect itself and keeps every candidate (SignerMatch.signatureDocCandidates, the chosen one marked) — nothing is thrown away.
  • The pejabat's signature is selected by proximity to their printed name: extraction already locates the doc_signer_name box, and people sign next to their printed name. Same-page candidates first, then nearest center distance with vertical distance weighted double. No name box → highest detector score (old behavior).
  • The chosen box is pinned onto /verify via doc_box, so the comparison provably runs on the selected ink. The manual-draw override still wins over everything.
  • Review shows "N tanda tangan lain terdeteksi — tanda tangan pejabat dipilih otomatis dari posisi nama tercetak" with the redraw remedy if the pick is wrong.

Live e2e (fresh apostille run, cbffca7a…): the pipeline logged 2 candidate(s), picked page 1 and persisted both candidates with the selection mark; verify ran OK on the pinned box.

Explicitly still blocked (recon verdicts): verifying the NOTARIS signature needs a notaris-specimen source — none exists today (the apostille registry holds certifying officers only; SABH notaris data has no wired specimen store). Detecting the cap/stempel as its own object needs a new detection class in the Python service (today the scorer only strips stamps to isolate ink). Both are clean follow-ups now that multi-candidate plumbing exists.

2. Phone standardization

lib/phone-normalize.ts: every OCR disguise ("0812 3456 7890", "+62-815-2299-801 3", "(0274) 512345") → one canonical form with country code, mobile/landline kind, and the landline area code detected (longest-match table: 021/022/…/0274/…). Display grouped (+62 812-3456-7890, +62 21 555-0123), e164 as the joinable key. Wired into the contact-info extraction cleanup; unparseable input passes through untouched.

3. Assignment history (verifikator)

Submission.assignedVerifikator pointer + append-only AssignmentEvent (fromAssignee → toAssignee, movedBy, reason, timestamp):

  • POST /api/verifikator/submissions/:id/assign — assign / transfer / return-to-pool (no-op moves record nothing);
  • GET /api/verifikator/submissions/:id/assignment-history — newest-first trail: "dipindah oleh siapa, dari mana".

This is the groundwork the SLA/FIFO escalation and cuti features stand on (both P1 in the K.35 triage).

Commits

60e1e9b5 all three features + migration 20260721060000 + tests