diff --git a/app/modules/bank_statement_analyzer/service.py b/app/modules/bank_statement_analyzer/service.py index 6336da0..b7e59d9 100644 --- a/app/modules/bank_statement_analyzer/service.py +++ b/app/modules/bank_statement_analyzer/service.py @@ -272,16 +272,9 @@ def _process_job(job_id: str) -> None: "financial_year": job.financial_year or "", "classification_enabled": job.classification_enabled, } - for path in paths: - try: - path.unlink(missing_ok=True) - except OSError: - pass - input_dir = Path(job.job_directory) / "Input" - try: - input_dir.rmdir() - except OSError: - pass + # Keep the original uploaded statements until the job expiry time. + # This applies equally to completed and failed jobs and allows the + # uploader to reproduce parser issues during the 24-hour retention window. job.output_file = str(output) job.summary_json = json.dumps(summary) job.status = "completed" @@ -399,6 +392,36 @@ def list_user_jobs(user_id: int, limit: int = 25) -> list[BankStatementAnalysisJ db.close() +def original_statement_files(job: BankStatementAnalysisJob) -> list[Path]: + """Return retained input PDFs that still belong to the job directory.""" + job_root = Path(job.job_directory).resolve() + input_root = (job_root / "Input").resolve() + try: + configured = [Path(value).resolve() for value in json.loads(job.input_files_json or "[]")] + except (TypeError, ValueError, json.JSONDecodeError): + configured = [] + + retained: list[Path] = [] + for path in configured: + try: + path.relative_to(input_root) + except ValueError: + continue + if path.is_file() and path.suffix.lower() == ".pdf": + retained.append(path) + return retained + + +def get_owned_original_statement(user_id: int, job_id: str, file_index: int) -> tuple[BankStatementAnalysisJob, Path] | None: + job = get_owned_job(user_id, job_id) + if not job or job.status not in {"completed", "failed"}: + return None + files = original_statement_files(job) + if file_index < 1 or file_index > len(files): + return None + return job, files[file_index - 1] + + def job_view(job: BankStatementAnalysisJob) -> dict: return { "id": job.id, @@ -415,6 +438,11 @@ def job_view(job: BankStatementAnalysisJob) -> dict: "estimated_wait": estimated_wait(job), "summary": _summary(job), "download_ready": job.status == "completed" and bool(job.output_file) and Path(job.output_file).is_file(), + "original_files": [ + {"index": index, "name": path.name} + for index, path in enumerate(original_statement_files(job), start=1) + ], + "originals_retained": job.status in {"completed", "failed"} and bool(original_statement_files(job)), } diff --git a/app/modules/bank_statement_analyzer/templates/bank_statement_analyzer/index.html b/app/modules/bank_statement_analyzer/templates/bank_statement_analyzer/index.html index ecb5cc6..02a6f93 100644 --- a/app/modules/bank_statement_analyzer/templates/bank_statement_analyzer/index.html +++ b/app/modules/bank_statement_analyzer/templates/bank_statement_analyzer/index.html @@ -13,21 +13,14 @@
-
- - -
-
Automatic layout detection
-

The validated template engine runs first. Existing bank-specific parsers are used automatically when the layout template cannot be reconciled.

-
-
+

Keep Auto Detect or select a bank for direct parser validation.

-
Queue limits
Maximum three processing jobs across all users. Each user may have up to three queued or processing jobs. Completed workbooks remain available for 24 hours.
+
Queue limits
Maximum three processing jobs across all users. Each user may have up to three queued or processing jobs. Completed workbooks and original uploaded statements remain available for 24 hours. Failed-job statements are also retained for 24 hours for debugging.
@@ -37,8 +30,8 @@
Files
{{ active_job.file_count }}
Queue position
{{ active_job.queue_position or '—' }}
Estimated wait
{{ active_job.estimated_wait or '—' }}
Progress
{{ active_job.progress_percent }}%
{% if active_job.status == 'completed' %} -

Analysis completed

Statements
{{ active_job.summary.statement_count or 0 }}
Transactions extracted
{{ active_job.summary.rows_extracted or 0 }}
Exact duplicates
{{ active_job.summary.exact_duplicate_rows or 0 }}
Review items
{{ active_job.summary.review_items or 0 }}
Download ExcelAnalyze Another Bank

The workbook remains available until {{ active_job.expires_at or '24 hours after completion' }}.

- {% elif active_job.status == 'failed' %}
Analysis failed.
{{ active_job.error_message }}
Analyze another statement
+

Analysis completed

Statements
{{ active_job.summary.statement_count or 0 }}
Transactions extracted
{{ active_job.summary.rows_extracted or 0 }}
Exact duplicates
{{ active_job.summary.exact_duplicate_rows or 0 }}
Review items
{{ active_job.summary.review_items or 0 }}
Download Excel{% for file in active_job.original_files %}Download Statement {{ file.index }}{% endfor %}Analyze Another Bank

The workbook and original statement{{ 's' if active_job.file_count != 1 else '' }} remain available until {{ active_job.expires_at or '24 hours after completion' }}.

+ {% elif active_job.status == 'failed' %}
Analysis failed.
{{ active_job.error_message }}
{% if active_job.original_files %}
{% for file in active_job.original_files %}Download Statement {{ file.index }}{% endfor %}

Original statement{{ 's are' if active_job.file_count != 1 else ' is' }} retained until {{ active_job.expires_at or '24 hours after failure' }} for debugging.

{% endif %}Analyze another statement
{% else %}
{% if active_job.status == 'queued' %}Your job is queued. You may safely leave this page and return through My Analysis Jobs.{% else %}Your statements are being processed.{% endif %}
{% endif %} {% endif %} diff --git a/app/modules/bank_statement_analyzer/templates/bank_statement_analyzer/jobs.html b/app/modules/bank_statement_analyzer/templates/bank_statement_analyzer/jobs.html index c2650ce..f26fafa 100644 --- a/app/modules/bank_statement_analyzer/templates/bank_statement_analyzer/jobs.html +++ b/app/modules/bank_statement_analyzer/templates/bank_statement_analyzer/jobs.html @@ -1,4 +1,4 @@ {% extends "ui/templates/base/layout.html" %} {% block content %} -

My Analysis Jobs

Queued, processing, completed and failed bank-statement analyses.

New Analysis
{% for item in jobs %}{% else %}{% endfor %}
SubmittedBankFilesStatusEstimate / ExpiryAction
{{ item.submitted_at }}{{ item.selected_bank|replace('_',' ')|title }}{{ item.file_count }}{{ item.status|title }}{% if item.queue_position %}
Queue position {{ item.queue_position }}
{% endif %}
{% if item.status in ['queued','processing'] %}{{ item.estimated_wait }}{% elif item.status == 'completed' %}Available for 24 hours{% else %}{{ item.error_message[:80] }}{% endif %}
View{% if item.download_ready %}Download{% endif %}{% if item.status != 'processing' %}
{% endif %}
No analysis jobs yet.
+

My Analysis Jobs

Queued, processing, completed and failed bank-statement analyses.

New Analysis
{% for item in jobs %}{% else %}{% endfor %}
SubmittedBankFilesStatusEstimate / ExpiryAction
{{ item.submitted_at }}{{ item.selected_bank|replace('_',' ')|title }}{{ item.file_count }}{{ item.status|title }}{% if item.queue_position %}
Queue position {{ item.queue_position }}
{% endif %}
{% if item.status in ['queued','processing'] %}{{ item.estimated_wait }}{% elif item.status == 'completed' %}Workbook and statements available until {{ item.expires_at or '24 hours' }}{% elif item.status == 'failed' %}Statements retained until {{ item.expires_at or '24 hours' }}{% else %}{{ item.error_message[:80] }}{% endif %}
View{% if item.download_ready %}Download Excel{% endif %}{% for file in item.original_files %}Statement {{ file.index }}{% endfor %}{% if item.status != 'processing' %}
{% endif %}
No analysis jobs yet.
{% endblock %} diff --git a/app/modules/bank_statement_analyzer/ui.py b/app/modules/bank_statement_analyzer/ui.py index 3805de8..97c21a7 100644 --- a/app/modules/bank_statement_analyzer/ui.py +++ b/app/modules/bank_statement_analyzer/ui.py @@ -16,7 +16,18 @@ from app.core.templating import templates from app.modules.core.rbac.deps import get_user_permissions, get_user_roles from .parsers.registry import BANK_OPTIONS -from .service import can_use, create_job_folder, delete_owned_job, enqueue_job, ensure_worker_started, get_owned_job, job_view, list_user_jobs, save_uploads +from .service import ( + can_use, + create_job_folder, + delete_owned_job, + enqueue_job, + ensure_worker_started, + get_owned_job, + get_owned_original_statement, + job_view, + list_user_jobs, + save_uploads, +) router = APIRouter(prefix="/tools/bank-statement-analyzer", tags=["bank-statement-analyzer-ui"]) @@ -186,6 +197,26 @@ def download(job_id: str, request: Request): db.close() +@router.get("/jobs/{job_id}/statements/{file_index}/download") +def download_original_statement(job_id: str, file_index: int, request: Request): + db = CommonSessionLocal() + try: + user, roles, denied = _auth(request, db) + if denied: + return denied + resolved = get_owned_original_statement(user.id, job_id, file_index) + if not resolved: + return not_found_response(request, "Original statement is unavailable or has expired.") + _job, path = resolved + return FileResponse( + path=path, + filename=path.name, + media_type="application/pdf", + ) + finally: + db.close() + + @router.post("/jobs/{job_id}/delete") def delete(job_id: str, request: Request, csrf_token: str = Form(...)): db = CommonSessionLocal()