File size: 4,116 Bytes
da6ee5e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
"""Document CRUD and validation API."""

from __future__ import annotations

from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import FileResponse
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.serializers import document_out
from app.db import get_session
from app.models import Document
from app.schemas import DocStatus, DocumentOut, DocumentUpdate
from app.services.cleanup import cleanup_orphan_documents
from app.services.paths import resolve_stored_path
from app.services.validation import (
    apply_duplicate_flags,
    apply_post_extract_checks,
    average_from_detail,
    mark_fields_confirmed,
)

router = APIRouter(prefix="/api/documents", tags=["documents"])


@router.get("", response_model=list[DocumentOut])
async def list_documents(
    status: str | None = None,
    doc_type: str | None = None,
    limit: int = Query(100, ge=1, le=500),
    session: AsyncSession = Depends(get_session),
) -> list[DocumentOut]:
    """List processed documents (files that still exist)."""
    stmt = select(Document).order_by(Document.created_at.desc())
    if status:
        stmt = stmt.where(Document.status == status)
    if doc_type:
        stmt = stmt.where(Document.doc_type == doc_type)
    stmt = stmt.limit(limit * 5)
    result = await session.execute(stmt)
    docs = []
    for d in result.scalars().all():
        if len(docs) >= limit:
            break
        try:
            if resolve_stored_path(d.file_path).is_file():
                docs.append(document_out(d))
        except FileNotFoundError:
            continue
    return docs


@router.post("/cleanup")
async def cleanup_documents(
    session: AsyncSession = Depends(get_session),
) -> dict[str, int]:
    """Remove orphan DB rows whose files were deleted."""
    return await cleanup_orphan_documents(session)


@router.get("/{doc_id}", response_model=DocumentOut)
async def get_document(
    doc_id: str,
    session: AsyncSession = Depends(get_session),
) -> DocumentOut:
    """Fetch one document."""
    doc = await session.get(Document, doc_id)
    if not doc:
        raise HTTPException(404, "Document not found")
    return document_out(doc)


@router.get("/{doc_id}/file")
async def get_document_file(
    doc_id: str,
    session: AsyncSession = Depends(get_session),
) -> FileResponse:
    """Serve original uploaded file for validation UI."""
    doc = await session.get(Document, doc_id)
    if not doc:
        raise HTTPException(404, "Document not found")
    try:
        path = resolve_stored_path(doc.file_path)
    except FileNotFoundError as exc:
        raise HTTPException(
            404,
            "Original file is no longer available",
        ) from exc
    if not path.is_file():
        raise HTTPException(
            404,
            "Original file is no longer available",
        )
    return FileResponse(
        path,
        filename=doc.filename,
        media_type=doc.mime_type or "application/octet-stream",
        content_disposition_type="inline",
    )


@router.patch("/{doc_id}", response_model=DocumentOut)
async def update_document(
    doc_id: str,
    body: DocumentUpdate,
    session: AsyncSession = Depends(get_session),
) -> DocumentOut:
    """Apply manual corrections and re-run checks."""
    doc = await session.get(Document, doc_id)
    if not doc:
        raise HTTPException(404, "Document not found")

    if body.doc_type:
        doc.doc_type = body.doc_type
    doc.extracted = body.extracted

    issues, is_dup, dup_of = await apply_post_extract_checks(
        session,
        document_id=doc.id,
        doc_type=doc.doc_type,
        data=body.extracted,
    )
    apply_duplicate_flags(
        doc,
        is_duplicate=is_dup,
        duplicate_of=dup_of,
    )

    conf = mark_fields_confirmed(body.extracted, doc.confidence)
    doc.confidence = conf
    doc.avg_confidence = average_from_detail(conf) or 1.0
    doc.issues = issues
    doc.status = DocStatus.VALIDATED.value
    await session.commit()
    await session.refresh(doc)
    return document_out(doc)