Phase 6: HITL review endpoints + audit trail
- New job_corrections table (append-only audit log) + migration
- Add approved / reviewed_by / reviewed_at columns to jobs
- PATCH /documents/{id} apply field-level corrections
- GET /documents/{id}/history return chronological audit trail
- POST /documents/{id}/approve lock final version (idempotent)
- Dotted field-path applier with root allow-list + list-index support
- Auto-clear `missing_field` review flag when required header keys filled
- Atomic batch apply: malformed path in batch rolls back all changes
- 22 new tests (11 repository-level, 11 API-level); 184 total passing
Co-Authored-By: adrian kuman firmansah <adriancuman@gmail.com>
This commit is contained in:
62
src/ocr_sprint/schemas/review.py
Normal file
62
src/ocr_sprint/schemas/review.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Request / response schemas for the HITL review endpoints (Phase 6).
|
||||
|
||||
The API surface is deliberately small:
|
||||
|
||||
* ``CorrectionRequest`` — body of ``PATCH /documents/{id}``. A list of
|
||||
``FieldCorrection`` entries; each one is applied atomically (all-or-
|
||||
nothing) and recorded in the audit trail.
|
||||
* ``CorrectionEventResponse`` — single row in ``GET /documents/{id}/history``.
|
||||
* ``ApprovalResponse`` — echo back after ``POST /documents/{id}/approve``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class FieldCorrection(BaseModel):
|
||||
"""One field-level correction.
|
||||
|
||||
``path`` is a dotted JSON path into ``ExtractionResult``. Supported
|
||||
roots: ``header``, ``ttd``, ``personel[n]`` (n is a 0-based index),
|
||||
``untuk``. The path is validated by the repository before being
|
||||
applied; unknown roots return 400.
|
||||
"""
|
||||
|
||||
path: str = Field(..., description="Dotted JSON path, e.g. 'header.nomor_sprint'.")
|
||||
value: Any = Field(..., description="New value (any JSON-serialisable payload).")
|
||||
reason: str | None = Field(
|
||||
None, max_length=512, description="Optional free-form reason for the correction."
|
||||
)
|
||||
|
||||
|
||||
class CorrectionRequest(BaseModel):
|
||||
"""PATCH body — one or more field corrections, applied atomically."""
|
||||
|
||||
corrections: list[FieldCorrection] = Field(..., min_length=1)
|
||||
|
||||
|
||||
class CorrectionEventResponse(BaseModel):
|
||||
"""One row of the audit log surfaced by GET /history."""
|
||||
|
||||
id: int
|
||||
job_id: UUID
|
||||
field_path: str
|
||||
old_value: Any | None = None
|
||||
new_value: Any | None = None
|
||||
corrected_by: str | None = None
|
||||
reason: str | None = None
|
||||
corrected_at: datetime
|
||||
|
||||
|
||||
class ApprovalResponse(BaseModel):
|
||||
"""Echo returned after a job is approved."""
|
||||
|
||||
job_id: UUID
|
||||
approved: bool
|
||||
reviewed_by: str | None = None
|
||||
reviewed_at: datetime | None = None
|
||||
Reference in New Issue
Block a user