feat(app): scan-mode sync, confirmation-gated documents, single-pass product classification

Fixes reported from APK field testing: DO/Product scan mode was inconsistent
between the camera drawer and documents screen (now one shared provider,
with an orange/green color cue); unconfirmed scans leaked into history with
placeholder data before the user tapped confirm (backend now gates
GET /documents on a new `confirmed` column, flipped only by PUT); and
Product Scan ran the GPU classifier twice, once at upload and again on
review (now a single pass at upload, persisted and read directly by the
editor). Also removes the unused "Hubungkan ke PO" field and fabricated
PO/SO/DO placeholder values from the Product Scan flow, closes out the
per-document-polling and save-recovery tasks (6.1/6.3), and splits several
touched files to stay under the repo's 256-line guideline.

Full detail in docs/iteration-log.md and backend/docs/iteration-log.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Rafhan Mazaya FathurrahmanandClaude Sonnet 5 committed 2026-07-10 15:19:32 +07:00
1 parent 2febe0c886
commit ada6488592
67 files changed
+5705 -1142

No files matched your search

+134
View File
@@ -216,6 +216,140 @@ used as the parse response's values instead of OCR.
*Suggested order: 7.3 → 7.1 → 7.2 (bootstrap first — account seeding FK-depends
on it; login profile last, it's additive).*
## 9. Flutter Client Contract — v1 Surface Completion
`api/v1/documents/`, `api/v1/master/skus/`, `api/parse/route.ts`, `db/init.ts`
Added 2026-07-10 via a user-directed, explicitly backend-scoped `e` run auditing
the full Flutter↔backend request/response contract. **Context doc:
[../../docs/api-contract-map.md](../../docs/api-contract-map.md)** (repo-root
`docs/`) — endpoint inventory, envelopes, lifecycle, and gap IDs (G1-G10) cited
below. These are the *server* halves; the Flutter halves are root
`plans/next-enhancements.md` §6-7 and consume these, so this section ships first.
Keep the v1 envelope (`{status, data}` / `api-error.ts`) on everything new.
- **9.1** [DONE 2026-07-10] `GET /api/v1/documents/:id` with `parseStatus`/`docType`, `scan_mode` persistence, dedup-stub fix. (See docs/feature-list.md)
- **9.2** [DONE 2026-07-10] Relaxed `GET /api/v1/master/skus` to any authenticated
account (writes stay admin-only); no response-shape change. Picked up via
explicit `n{9.2}` request; user chose "relax existing endpoint" over "add a
new one" when asked. (See docs/feature-list.md)
- **9.3** [DONE 2026-07-10] Authenticated `POST /api/v1/scan-product`, shared classify+match util. (See docs/feature-list.md)
*Section 9 is now fully `[DONE]`. With 9.1-9.3 all shipped, every backend
blocker behind Flutter root `plans/next-enhancements.md` §7.1 (moving the
product editor onto the v1 surface) is cleared. §7.2 (eliminating the
duplicate classification pass) is unblocked in principle but still needs its
own grill-me decision on the client side about which single pass to keep.*
## 10. Backend — Document Confirmation Gate & Data Hygiene
`pfm-web-app/src/db/init.ts`, `app/api/v1/documents/`, `app/api/parse/route.ts`, `utils/document-mapper.ts`
Added 2026-07-10 from user testing feedback on the release APK
([`twinkly-riding-mitten.md`](C:/Users/rafha/.claude/plans/twinkly-riding-mitten.md)).
Backend counterpart to Flutter root `plans/next-enhancements.md` §8.
Root-cause documentation in [../../docs/api-contract-map.md](../../docs/api-contract-map.md)
**G11** (no draft/confirmed distinction) and **G12** (fabricated PO/SO/DO).
**Ship §10 before Flutter §8.2** — Flutter's model change consumes the new
`confirmed` field this section adds. Keep the v1 envelope (`{status, data}` /
`api-error.ts`) on everything new.
- **10.1** [DONE 2026-07-10] **Confirmation-gated document list visibility (Task B backend
half).** Three coordinated changes, one migration:
1. `db/init.ts` — `ALTER TABLE documents ADD COLUMN IF NOT EXISTS confirmed
BOOLEAN NOT NULL DEFAULT true;` (`DEFAULT true` grandfathers every
pre-existing row — today's history stays visible after migration).
2. `v1/documents/upload/route.ts` — add `confirmed = false` to the INSERT
column list for every new upload (dedup-hit branch unchanged — reflects
whatever `confirmed` state the original row already has).
3. `utils/document-mapper.ts` — add `confirmed: boolean` to `DocumentRow`
interface; return `confirmed: doc.confirmed` from `mapDocumentRow()`;
update the three SELECT statements that build a `DocumentRow` (list route,
`[id]` GET, upload dedup-hit SELECT) to include the `confirmed` column.
4. `v1/documents/route.ts` (list) — add `AND confirmed = true` to WHERE
clause, unconditionally for every account including admin (per clarified
answer — existing `kode_toko` scoping for non-admins is untouched).
5. `v1/documents/[id]/route.ts` — PUT handler: add `confirmed = true` to the
UPDATE SET. GET handler: no filter change (poller must keep seeing
pending/unconfirmed docs); just receives `confirmed` via mapper update.
- **Note on `parse/route.ts`'s own INSERT...ON CONFLICT statements (both DO
and Product branches)**: deliberately left untouched for `confirmed` —
in the real mobile flow, `upload/route.ts`'s INSERT always runs first
(explicit `confirmed = false`), so `parse/route.ts`'s upsert always hits
the `ON CONFLICT DO UPDATE` branch; since that branch's `SET` clause
doesn't mention `confirmed`, Postgres leaves the existing value untouched
— exactly the desired behavior (never regress an already-confirmed
document, never reset the pending flag mid-parse). Omission was verified
to be correct, not an oversight.
- Live verification (all 5 steps passed against the running Docker stack):
(a) uploaded a real DO photo as store `WH_JCIBBR1`, did not PUT — absent
from that store's `GET /documents` (count stayed at 3, new id 3400 not
present) while `GET /documents/3400` still returned `parseStatus: "done"`,
`confirmed: false`; (b) PUT (confirm) — doc count became 4, id 3400 present
with the real submitted `namaPenerima`, DB row's `confirmed` flipped to
`true`; (c) covered by (a)'s single-doc check; (d) `admin`'s `GET
/documents` also excluded the unconfirmed doc (13, unchanged) before the
PUT; (e) all 13 pre-existing rows carried `confirmed = true` after the
migration ran (`ALTER TABLE` executed on container restart, verified via
`\d documents` + a `count(*)` query — 13 confirmed, 0 unconfirmed
pre-restart).
- **10.2** [DONE 2026-07-10] **Remove fabricated Product Scan PO/SO/DO placeholders (Task
C, bundled with 10.1 — same file `parse/route.ts`).** Replaced hardcoded
`noPO: "PO-PRODUCT-001"`, `noSO: "1002003004"`, `noDO: "DO-PRODUCT-999"` —
both the flat keys and the mirrored `header.no_po`/`no_so`/`no_do`
sub-object — with empty strings `""`. Scope stayed narrow to exactly these
three fields; `nama_driver: "PRODUCT SCAN"` and `nama_penerima: "STORE STAFF"`
were left untouched (deliberate fixed convention, not a fabricated document
number that could mislead someone reading raw data — different failure mode
from G7's fake SKU/date data). Not user-facing: verified `pdf_service.dart`
and `product_editor_submit_logic.dart` neither reads nor displays these values.
- Verification: uploaded a fresh Product Scan as `WH_JCIBBR1` (doc id 3402),
read the raw unconfirmed `GET /documents/3402` response — `header.no_po`/
`no_so`/`no_do` all returned `""`, not the old fabricated strings.
## 11. Backend — Single-Pass Product Classification
`app/api/parse/route.ts`, `utils/product-scan.ts`, `utils/document-mapper.ts`
Added 2026-07-10 from user feedback ("kenapa harus dilakukan dua kali... GPU
tidak 2x kerja") after noticing Product Scan's editor took visibly longer to
open than DO Scan's. Root-caused as gap **G3** in
[../../docs/api-contract-map.md](../../docs/api-contract-map.md) (deferred
there pending exactly this client-side decision). Backend counterpart to
Flutter root `plans/next-enhancements.md` §7.2.
- **11.1** [DONE 2026-07-10] **Run the classify+match pipeline once per
photo, and persist the full result.** `api/parse/route.ts`'s Product branch
had its own separate, poorer inline `fetch` to the classifier that only
kept `top1_name`/`extracted_sku` — the Flutter editor then had to re-run
the *entire* pipeline a second time via `POST /api/v1/scan-product` (task
9.3) just to get the top-5 candidate list and OCR-extracted expiry date.
Replaced the inline fetch with a call to the same shared
`classifyAndMatchProduct()` (`utils/product-scan.ts`) that route already
uses — one GPU call, richer result, `b64` reused from the DO path's own
computation (not recomputed). The richer result is persisted under a new
`metadata.productScan` JSONB key (`possibleMatches` + `extractedExpiryDate`
— no schema migration needed, same pattern as `header`/`shipment`
coexisting in that column) and surfaced by `document-mapper.ts` as a
top-level `productScan` field on every GET response (list, by-id, upload
dedup). `ocr_items` still stores only the single best-match row, unchanged.
- **Regression caught and fixed during implementation**: delegating to
`classifyAndMatchProduct()` silently dropped the 90s
`AbortSignal.timeout` the old inline fetch had (a wedged GPU container
would otherwise hang past the intended fail-fast bound). Added the same
`PIPELINE_TIMEOUT_MS = 90_000` bound directly inside
`classifyAndMatchProduct()` itself, so both callers (`parse/route.ts` and
the live `POST /api/v1/scan-product` route, which never had this bound
either) are protected, not just the one this task touched.
- Verification: uploaded a genuinely fresh image/store combination
(`do-015.jpg` as `WH_JAFATAH`, never uploaded before, so this is
provably a real classify pass and not a dedup hit) — took 9s (one GPU
pass), response was `"Document uploaded successfully"` (not the dedup
branch). Immediate `GET /documents/:id` returned `productScan` with 5
real `possibleMatches` (real SKUs/names/scores from `sku_master`) and
the OCR-extracted expiry date — before any editor interaction. See
root `docs/iteration-log.md` for the Flutter-side verification that the
editor renders this without a second network call.
---
*Sections 1-4 migrated 2026-07-08 from root `plans/next-enhancements.md` sections