# Final plan — roa2web-telegram-bonuri > Recovered from planning session JSONL `b740c3df-d2b9-447b-bf8e-0100e0dd9fbf` after the > approved status was accidentally reset and a new planning session was started. This file > contains the longest structured assistant turn from that session (the CEO review). --- Running plan-eng-review on the design doc. CEO review just completed — now focusing on what eng review uniquely covers: code patterns, test coverage diagram, performance specifics, and parallelization. --- ## /plan-eng-review — Telegram Bonuri Fiscale Design doc: `~/.gstack/projects/romfast-echo-core/moltbot-master-design-20260504-221523.md`. Reviewed by /office-hours (8/10), /plan-ceo-review just completed (5 findings folded back into plan). --- ### Step 0 — Scope Challenge **Existing code already solves:** | Sub-problem | Existing | Plan reuses? | |---|---|---| | OCR async queue submit + poll | `backend/scripts/whatsapp_import/ocr_client.py` | YES (direct import) ✅ | | Oracle write via PACK_CONTAFIN | `backend/scripts/whatsapp_import/process_v2.py:60-135` | Adapts pattern (not import) ✅ | | Telegram auth check | `backend/modules/telegram/auth/linking.py::get_user_auth_data` | YES ✅ | | File download from Telegram | `python-telegram-bot::Bot.get_file().download_to_drive()` | YES ✅ | | Date parse / nul-safe field handling | `process_v2.py:64-67` (date), `process_v2.py:84-87` (nrract) | Plan must replicate, NOT importable as-is | | CUI → cont mapping | `process_v2.py::get_cont_cheltuiala` (hardcoded Romfast) | Adapted with warning fallback | **Rebuilding:** `_save_to_oracle` is being adapted from `process_v2.py`, not imported. Reason: `process_v2.py` is a CLI script that reads from a `result` object with `.attribute` access. Telegram handler will receive a `dict`. Different shape, can't import. **The Oracle write logic should be extracted to a shared helper** — `backend/modules/data_entry/services/oracle_receipt_writer.py` — so the next channel (WhatsApp, email) doesn't trigger a third copy. See Architecture finding #1. **Minimum set of changes** — design doc proposes 5 file touches. Eng-review verdict: 5 is the minimum. **Complexity check** — 5 files. Under threshold. No new classes (flat functions per existing pattern). Clean. **Search check (Layer 1/2/3):** - python-telegram-bot async file download → built-in `bot.get_file().download_to_drive()`. Plan uses it. ✅ [Layer 1] - `concurrent_updates=True` for parallel users → built-in flag in `Application.builder()`. Plan should set it (CEO finding FINDING-ARCH-1). [Layer 1] - Async wrapper for sync DB driver → `asyncio.to_thread()` (Python 3.9+). Plan uses it. ✅ [Layer 1] - Per-user state → `context.user_data` (PTB built-in). Plan uses it. ✅ [Layer 1] No reinvention. No L2/L3 patterns introduced. **TODOS cross-reference:** No `TODOS.md` exists in roa2web. Recommend adding one as part of this PR with the deferred items from the CEO review (server_id DB persistence, "view in ERP" link, unify-via-receipt-service). **Distribution check:** No new artifact. Code lives inside the existing roa2web bot process. N/A. **Step 0 verdict:** Scope is right. Proceed to sections. --- ### Section 1 — Architecture Review ``` ROA2WEB BOT PROCESS (single worker) ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ bot_main.py::create_telegram_application() │ │ │ │ │ ├─ Application.builder().token(T).build() │ │ │ └─ ⚠ MISSING: .concurrent_updates(True) │ │ │ │ │ ├─ CommandHandler(...) × N │ │ ├─ MessageHandler(TEXT & ~COMMAND, handle_text_message) │ │ │ │ │ ├─ ⓢ MessageHandler(Document.PDF | Document.IMAGE, doc_handler) │ NEW │ ├─ ⓢ MessageHandler(PHOTO, photo_handler) │ NEW │ ├─ ⓢ CallbackQueryHandler(receipt_cb, pattern=r"^receipt:") │ NEW │ │ │ │ └─ CallbackQueryHandler(button_callback) ← catch-all │ │ (receipt:* MUST be registered before this — handler order) │ │ │ │ receipt_handlers.py (NEW) │ │ handle_document_message ─┐ │ │ handle_photo_message ├─→ _submit_ocr_and_preview │ │ │ │ │ │ │ ├─ submit_ocr_job (whatsapp_imp.)│ │ │ ├─ poll wait_for_result │ │ │ └─ edit_message + keyboard │ │ │ │ │ handle_receipt_callback ──┘ │ │ │ ├─ confirm: asyncio.to_thread(_save_to_oracle) │ │ └─ cancel: unlink temp + edit_message │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ ▼ ▼ SQLite OCR queue Oracle DB data/ocr_queue/ ACT_TEMP via PACK_CONTAFIN ⚠ hardcoded path cred: secrets/_write.oracle_pass ``` **FINDING-ARCH-1** `[P2] (confidence: 8/10)` `_save_to_oracle` duplication risk. The plan adapts `process_v2.py`'s PACK_CONTAFIN logic into `receipt_handlers.py::_save_to_oracle`. This makes 2 copies of the 3-step Oracle write. WhatsApp adapter is the third channel waiting to land — that becomes 3 copies. DRY violation. Recommendation: extract to `backend/modules/data_entry/services/oracle_receipt_writer.py::write_receipt(receipt_dict, oracle_cfg) -> tuple[int, str]`. Both `process_v2.py` and `receipt_handlers.py` import it. Effort: ~20 min (CC) for the extraction + adapt both callers. This connects to engineering preference: "DRY is important — flag repetition aggressively." **FINDING-ARCH-2** `[P3] (confidence: 9/10)` `bot/__init__.py` import boundary. The plan imports from `backend.scripts.whatsapp_import.ocr_client`. Importing from `scripts/` into `modules/` is backwards — scripts should depend on modules, not the reverse. Recommendation: move `ocr_client.py` to `backend/modules/data_entry/services/ocr/queue_client.py`. The whatsapp scripts then import FROM modules. Then receipt_handlers imports from a proper module path. Bonus: solves the hardcoded `/workspace/roa2web/...` path natively, since the path becomes relative to `Path(__file__).parents[3]` from inside the modules tree. Effort: ~15 min (CC) for the move + 3 import updates. **FINDING-ARCH-3** `[P1] (confidence: 9/10)` Already in CEO review as FINDING-ARCH-1: `concurrent_updates=False` default. One-line fix: `Application.builder().token(T).concurrent_updates(True).build()`. Without it, the 120s OCR poll blocks every other user. Critical for any team with ≥2 active users. **Production failure scenarios:** | Scenario | Plan accounts for? | |---|---| | OCR worker process down | ✗ — plan polls until 120s timeout, generic error. Should health-check first. | | Oracle DB unreachable | ✓ — `try/except` around `_save_to_oracle` + `finally` cleanup | | Telegram file download fails (40MB+) | ✗ — no try/except around `download_to_drive()` | | User auth revoked between submit and confirm | ✗ — no re-check at confirm time | | Bot restart between submit and confirm | ✓ — pending lost (in-memory), pending file orphaned, startup cleanup scan handles it | | `secrets/` file missing | Partial — returns empty password, ORA-01017, generic error | **Coupling concerns:** `receipt_handlers` → `whatsapp_import` (BACKWARDS). FINDING-ARCH-2 fixes it. **Scaling:** OCR poll dominates. With concurrent_updates=True (FINDING-ARCH-3), 10 simultaneous users × 120s each is bounded by the OCR worker throughput, not handler concurrency. Worker is `data_entry/services/ocr/job_worker.py` — single worker, queue-based — so OCR is the real serialization point. Acceptable for a 4-accountant team. **SPOF:** OCR worker. Documented above. **Rollback:** `git revert + restart`. If cherry-pick #3 (server_id DB column) is implemented, additive nullable column → safe rollback. **STOP** — 3 architecture findings. Going with recommendations: ACCEPT all three. --- ### Section 2 — Code Quality Review **FINDING-CQ-1** `[P2] (confidence: 8/10)` `_format_receipt_preview` will be a string concat for ~10 fields with None handling. The plan describes it as 1 function returning 1 multi-line string. With the cherry-pick #1 (low-confidence warning) added, this function does 2 things: format + warn. Recommendation: split into `_format_receipt_preview(ocr_result)` and `_confidence_warning(confidence)` returning `Optional[str]`. Compose them in the caller. Keeps each function under 10 lines, testable in isolation. Effort: trivial. Maps to preference: "explicit over clever." **FINDING-CQ-2** `[P2] (confidence: 9/10)` Magic numbers in plan: `120` (OCR timeout seconds), `0.3` (poll interval), `600` (TTL seconds), `0.60` (low-confidence threshold). Spread across the file as hardcoded literals. Recommendation: top of `receipt_handlers.py`: ```python OCR_TIMEOUT_S = 120 OCR_POLL_INTERVAL_S = 0.3 PENDING_TTL_S = 600 LOW_CONFIDENCE_THRESHOLD = 0.60 TEMP_FILE_PREFIX = "receipt_" ``` Effort: 5 min. Maps to preference: "bias toward explicit." **FINDING-CQ-3** `[P2] (confidence: 8/10)` Error handling in plan uses `except Exception as e` (line 172 of design doc). This is the catch-all anti-pattern flagged in the skill's prime directives. Recommendation: catch the specific exceptions: ```python except oracledb.DatabaseError as e: msg = _format_oracle_error(e) # CEO finding FINDING-ERR-3 await query.edit_message_text(f"❌ Eroare bază de date: {msg}") logger.exception("receipt: oracle_write_failed user=%s", oracle_username) except (asyncio.TimeoutError, OSError) as e: await query.edit_message_text(f"❌ Eroare sistem: {e}") logger.exception("receipt: system_error") ``` Don't catch `Exception` — let unexpected ones crash the handler so PTB's error_handler logs them with full traceback. This is how `bot_main.py:148` already does it (`add_error_handler(error_handler)`). **FINDING-CQ-4** `[P3] (confidence: 7/10)` `pending_receipt` dict is structural — created in 1 place, consumed in 1 place. With 6 keys including a TTL field, this is on the edge of needing a `dataclass`. Going with explicit dict for now is fine (matches existing handlers.py style, avoids new abstraction). Flag: if a third consumer appears, refactor. Maps to preference: "engineered enough — not over-engineered." **STOP** — 4 code quality findings. Going with recommendations: ACCEPT all four. --- ### Section 3 — Test Review (MANDATORY) **Framework detection:** ```bash [ -f roa2web/pytest.ini ] || [ -f roa2web/pyproject.toml ] ls roa2web/tests/ ``` roa2web uses pytest + pytest-asyncio. Test fixtures already exist at `tests/fixtures/ocr-samples/`. **Coverage diagram:** ``` CODE PATHS USER FLOWS [+] receipt_handlers.py [+] PDF receipt flow ├── handle_document_message() ├── [GAP] [→E2E] Send PDF → preview → confirm → Oracle │ ├── [GAP] auth check → not linked ├── [GAP] [→E2E] Send PDF → preview → cancel │ ├── [GAP] auth check → linked, MIME PDF └── [GAP] Send wrong file type (e.g. .docx) │ ├── [GAP] auth check → linked, MIME PNG │ ├── [GAP] auth check → linked, MIME unknown [+] Photo receipt flow │ └── [GAP] [cherry-pick #2] pending_receipt exists ├── [GAP] [→E2E] Send photo → preview → confirm │ └── [GAP] Send photo while pending exists ├── handle_photo_message() │ ├── [GAP] auth check → not linked [+] Error states │ ├── [GAP] auth check → linked ├── [GAP] OCR failed → "OCR eșuat" with retry suggestion │ └── [GAP] [cherry-pick #2] pending exists ├── [GAP] OCR timeout → "OCR timeout" with retry │ ├── [GAP] Oracle ORA-01017 → translated message ├── _submit_ocr_and_preview() ├── [GAP] Oracle ORA-00001 (duplicate) → translated │ ├── [GAP] OSError on download (FINDING-ERR-1) ├── [GAP] Sesiune expirată după 10 min → cancel msg │ ├── [GAP] submit_ocr_job aiosqlite.Error └── [GAP] Double-press Confirm → "Sesiune expirată" │ ├── [GAP] poll → timeout │ ├── [GAP] poll → success [+] Boundary states │ ├── [GAP] poll → success but low confidence ├── [GAP] OCR confidence 0.0 → warning shown │ └── [GAP] poll → result has None fields ├── [GAP] OCR all fields None → N/A in preview │ ├── [GAP] amount = 0 → still saves (or rejects?) ├── _format_receipt_preview() ├── [GAP] receipt_date None → defaults to today │ ├── [GAP] all fields populated └── [GAP] CUI not in CUI_TO_CONT → fallback "6028" │ ├── [GAP] confidence < 0.60 → ⚠ warning │ └── [GAP] all fields None → N/A everywhere │ ├── _confidence_warning() [+] Concurrency │ ├── [GAP] confidence 0.50 → returns warning string └── [GAP] [→E2E] 2 users send receipts simultaneously │ └── [GAP] confidence 0.85 → returns None (validates concurrent_updates=True) │ ├── _build_oracle_write_config() │ ├── [GAP] server_id present, write_user in config │ ├── [GAP] server_id present, no write_user → derive _AUTO │ ├── [GAP] server_id None → fallback default server │ ├── [GAP] [FINDING-ERR-2] server_id unknown → fallback to default │ └── [GAP] secrets file missing → empty password │ ├── _save_to_oracle() [→E2E with real Oracle dev DB:] │ ├── [GAP] happy path └── tests/e2e/test_receipt_oracle_write.py │ ├── [GAP] CUI not in mapping → log + fallback │ ├── [GAP] receipt_date None → datetime.now() │ ├── [GAP] receipt_date malformed → try/except │ ├── [GAP] amount/tva None → 0 │ └── [GAP] PACK_CONTAFIN raises → DatabaseError up │ ├── _format_oracle_error() [FINDING-ERR-3] │ ├── [GAP] ORA-01017 → "Credențiale invalide" │ ├── [GAP] ORA-00001 → "Bon duplicat" │ ├── [GAP] ORA-12541 → "Bază de date inaccesibilă" │ └── [GAP] unknown ORA → original message │ └── handle_receipt_callback() ├── [GAP] confirm → save success ├── [GAP] confirm → save fail (DatabaseError) ├── [GAP] confirm → pending None (expired) ├── [GAP] confirm → TTL > 600s → expired ├── [GAP] cancel → file unlinked + msg ├── [GAP] double-press confirm → 2nd press gets None └── [GAP] file already deleted at confirm → missing_ok=True [+] bot_main.py └── concurrent_updates=True wired [GAP] verify via integration test [+] config.py::OracleServerConfig └── write_user: Optional[str] field added [GAP] unit test: load config with/without write_user COVERAGE: 0/47 paths tested (0%) — entire feature is new QUALITY: ★★★:0 ★★:0 ★:0 GAPS: 47 total (5 E2E, 0 eval) ``` **Critical gap:** Every path is a gap because `receipt_handlers.py` is a new file. The plan must include all of these tests as part of the implementation step, not deferred. **Test plan to add to design doc Assignment:** 1. **Unit tests** — `tests/modules/telegram/test_receipt_handlers.py`: - `test_format_receipt_preview_*` (3 cases: full, all None, low confidence) - `test_confidence_warning_*` (2 cases) - `test_build_oracle_write_config_*` (5 cases incl. unknown server_id fallback) - `test_format_oracle_error_*` (4 ORA codes) - `test_save_to_oracle_*` (mock oracledb, 6 cases) - `test_handle_receipt_callback_*` (7 cases incl. expired, double-press, cancel) - `test_handle_document_message_*` (5 cases) - `test_handle_photo_message_*` (3 cases) 2. **E2E tests** — `tests/e2e/test_receipt_telegram_flow.py` (using PTB test bot or mocks): - PDF send → preview → confirm - Photo send → preview → cancel - 2 simultaneous users (validates concurrent_updates=True) 3. **Oracle integration test** — `tests/integration/test_oracle_receipt_writer.py` (after extracting per FINDING-ARCH-1): - Real dev Oracle DB write + rollback (or use a transaction wrap) **Test plan artifact:** ```markdown # Test Plan: Telegram Bonuri Fiscale Generated by /plan-eng-review on 2026-05-05 Branch: feature/telegram-bonuri-fiscale (roa2web) Repo: romfast/roa2web ## Affected Pages/Routes - Telegram bot: send PDF/JPG/PNG document → preview message - Telegram bot: send photo → preview message - Telegram inline button: ✅ Confirmă → Oracle write - Telegram inline button: ❌ Anulează → cleanup ## Key Interactions to Verify - Send PDF receipt → OCR processing message → preview with extracted data → Confirmă button works → Oracle write succeeds → success message with COD - Send photo receipt → same flow with JPEG - Send unsupported file (.docx) → reject message - Press Anulează → cleanup message, file deleted from /tmp ## Edge Cases - Send 2nd file while pending exists → "Ai un bon în așteptare" message - Wait > 10 minutes after preview, then press Confirmă → "Sesiune expirată" - Double-press Confirmă rapidly → 1 save, 2nd press shows "Sesiune expirată" - OCR confidence < 60% → ⚠ warning at top of preview - OCR returns all-None fields → preview shows N/A everywhere, no crash - Oracle ORA-01017 (bad password) → user sees "Credențiale invalide" ## Critical Paths - Auth user → submit PDF → confirm → verify ACT_TEMP row in Oracle dev DB - 2 users submit photo simultaneously → both get preview within 30s (validates concurrent_updates) ``` I'll write this to `~/.gstack/projects/romfast-echo-core/moltbot-master-eng-review-test-plan-20260505.md` after the review completes. **STOP** — Test review complete. 47 gaps, all mandatory. Going with recommendation: write all 47 tests as part of implementation (not deferred). --- ### Section 4 — Performance Review **FINDING-PERF-1** `[P3] (confidence: 7/10)` Oracle connection per save. `_save_to_oracle` calls `oracledb.connect()` on every confirm. roa2web has an existing Oracle connection pool — `backend/modules/data_entry/db/oracle_pool.py` per the recent commits. Recommendation: use the existing pool. `from backend.modules.data_entry.db.oracle_pool import get_pool`. If the pool is per-server-id, pass the server_id. Saves ~100-300ms per receipt (TCP handshake + auth round-trip). For a single user at human pace this is barely noticeable, but it's the existing pattern — don't reinvent. [Layer 1] — use the built-in pool, don't roll your own. **FINDING-PERF-2** `[P4] (confidence: 6/10)` `asyncio.sleep(0.3)` × 400 = 400 wakeups for a single OCR job. Each wakeup re-queries SQLite. With 4 simultaneous users × 400 wakeups = 1600 SQLite queries per minute. Not catastrophic, but `wait_for_result` could use a longer poll interval (1.0s) with no perceptible UX delta. 120 wakeups instead of 400. Recommendation: change `OCR_POLL_INTERVAL_S = 1.0`. User won't notice — OCR results don't arrive faster than ~5s anyway. **FINDING-PERF-3** `[P4] (confidence: 5/10)` Memory: `pending_receipt` dict in `context.user_data` holds the full OCR result + file path. For 100 active users this is ~50KB. Negligible. No issue. **FINDING-PERF-4** `[P3] (confidence: 7/10)` Caching: `_build_oracle_write_config` reads the secrets file from disk on every confirm. Cache it in module-level dict at first read. Recommendation: ```python _PASSWORD_CACHE: dict[str, str] = {} def _read_oracle_password(server_id: str) -> str: if server_id not in _PASSWORD_CACHE: pass_file = PROJECT_ROOT / "secrets" / f"{server_id}_write.oracle_pass" _PASSWORD_CACHE[server_id] = pass_file.read_text().strip() if pass_file.exists() else "" return _PASSWORD_CACHE[server_id] ``` Saves a disk read per confirm. Trivial. Maps to preference: "explicit > clever, but cache cheap reads." **STOP** — 4 perf findings. ACCEPT FINDING-PERF-1 (use pool) and FINDING-PERF-2 (1s poll). FINDING-PERF-3 = no action. FINDING-PERF-4 = ACCEPT (trivial). --- ### Outside Voice — skip Codex availability check + 2-minute outside voice run is valuable for big architecture decisions. This plan is contained: 1 new file, 4 file touches, well-trodden patterns. Skipping. --- ### Failure Modes Summary | Codepath | Failure | Test? | Error handling? | Silent? | |---|---|---|---|---| | `_download_file` OSError (/tmp full) | unlikely but real | ✗ | ✗ (FINDING-ERR-1) | YES — **CRITICAL GAP** | | `_build_oracle_write_config` unknown server_id | possible after server delete | ✗ | ✗ (FINDING-ERR-2) | YES — **CRITICAL GAP** | | `_save_to_oracle` ORA-01017 | wrong password file | ✗ | ✓ (raw msg) | NO (visible to user) | | OCR worker process down | bot deployed without worker | ✗ | ✓ (timeout) | NO (timeout msg) | | `pending_receipt` race (rapid double-confirm) | reasonable scenario | ✗ | ✓ (pop returns None) | NO ✅ | | `secrets/` file missing | first deploy mistake | ✗ | Partial (empty pwd → ORA-01017) | NO (visible) | | `receipt_date` malformed in OCR result | model output drift | ✗ | ✗ (FINDING-DATA-1) | YES — **CRITICAL GAP** | 3 critical gaps. All 3 are addressed by CEO findings already accepted into the plan. --- ### Worktree Parallelization Sequential implementation, no parallelization opportunity. The plan is one feature in one file with cascading dependencies (tests depend on impl, bot_main wiring depends on receipt_handlers.py existing). No independent workstreams. Single worktree, single agent. --- ### NOT in Scope | Item | Rationale | |---|---| | WhatsApp parity for the same flow | Separate channel, separate PR. Architecture #1 (extract `oracle_receipt_writer.py`) makes this easier later. | | `oracle_receipt_writer.py` extraction | RECOMMEND DOING IT — see FINDING-ARCH-1. If user defers: explicitly list as TODO. | | `ocr_client.py` move from scripts/ to modules/ | RECOMMEND DOING IT — FINDING-ARCH-2. If user defers: TODO. | | "View in ERP" deep link after save | CEO cherry-pick #4 — needs URL pattern confirmation, defer post-validation | | Unify write path through `receipts.py` API | CEO cherry-pick #5 — too much scope (JWT auth for service account, SQLAlchemy session) | | Multi-company `CUI_TO_CONT` mapping in DB | Deferred from existing whatsapp_import. Separate refactor. | | Web push notification when receipt saved | Out of scope — would couple to web SSE infra | --- ### What Already Exists | Already in repo | Plan reuses correctly? | |---|---| | `ocr_client.py::submit_ocr_job` / `wait_for_result` | YES (with path-fix preconditions) | | `process_v2.py` PACK_CONTAFIN logic | Adapts pattern (recommend extracting per ARCH-1) | | `get_user_auth_data()` | YES | | `OracleServerConfig` model | Extended with `write_user` field | | `secrets/.oracle_pass` convention | Followed for `_write.oracle_pass` | | `tests/fixtures/ocr-samples/` (pre-loaded PDFs) | Available for E2E tests | | `oracle_pool.py` connection pool | NOT in plan — recommend adding (FINDING-PERF-1) | | Existing handler order pattern in `bot_main.py` | YES — receipt callbacks before catch-all | --- ### TODOS.md Proposed Items (Since no TODOS.md exists in roa2web — these are deferral notes for the design doc Open Questions section.) 1. **Persist `oracle_server_id` in `telegram_users` table** — Already accepted as cherry-pick #3 in CEO review, included in plan. 2. **Extract `oracle_receipt_writer.py` shared helper** — FINDING-ARCH-1. Going with recommendation: include in this PR. 3. **Move `ocr_client.py` from scripts/ to modules/** — FINDING-ARCH-2. Going with recommendation: include in this PR. 4. **"View in ERP" deep link after save** — CEO cherry-pick #4 deferred. 5. **Unify Telegram write through `receipts.py` API endpoint** — CEO cherry-pick #5 deferred. 6. **Multi-company CUI→cont mapping in DB or config** — already noted in design doc's Open Question #4. --- ### Updated Assignment (revised checklist for implementation) ``` 1. git checkout -b feature/telegram-bonuri-fiscale (in roa2web) 2. Move ocr_client.py: scripts/whatsapp_import/ → modules/data_entry/services/ocr/queue_client.py Update imports in process_v2.py + test_ocr_simple.py Replace hardcoded QUEUE_DIR with Path(__file__).parents[3] / "data/ocr_queue" 3. Extract oracle_receipt_writer.py: Create modules/data_entry/services/oracle_receipt_writer.py with write_receipt(receipt_dict, oracle_cfg) -> tuple[int, str] Adapt process_v2.py to call it 4. Add backend/config.py: write_user: Optional[str] = None to OracleServerConfig 5. Migration: alembic revision --autogenerate -m "add oracle_server_id to telegram_users" Modify link_user_to_oracle to accept + store server_id Modify get_user_auth_data to return server_id 6. Implement receipt_handlers.py: - Module constants (FINDING-CQ-2) - _format_receipt_preview + _confidence_warning (FINDING-CQ-1) - _build_oracle_write_config with None-server fallback (FINDING-ERR-2) - _format_oracle_error (FINDING-ERR-3) - Caching for password file (FINDING-PERF-4) - _save_to_oracle delegates to oracle_receipt_writer - handle_document/photo/callback with proper error handling (FINDING-CQ-3) - Date parse with try/except (FINDING-DATA-1) - OSError handling around download (FINDING-ERR-1) - Duplicate pending guard (cherry-pick #2) - Low-confidence warning in preview (cherry-pick #1) - Structured logging at each step (FINDING-OBS-1) 7. Wire bot_main.py: - Add .concurrent_updates(True) to Application.builder() (FINDING-ARCH-3) - Register 3 new handlers BEFORE catch-all CallbackQueryHandler - Add startup cleanup scan: glob /tmp/receipt_*.* and unlink at start 8. Create secrets/_write.oracle_pass on server (deploy step, not in git) 9. Verify .gitignore covers secrets/*.oracle_pass (FINDING-SEC-1) 10. Write all 47 unit tests per Section 3 coverage diagram 11. Write 3 E2E tests (PDF flow, photo flow, concurrent users) 12. Write Oracle integration test against dev DB 13. End-to-end manual test with real bon → confirm in Oracle ACT_TEMP 14. Verify on production-like setup that OCR worker is running before deploy ``` 15 steps. Up from 9. Every addition is grounded in a finding. --- ### Completion Summary - **Step 0: Scope Challenge** — Scope accepted as-is (5 file touches, no over-build) - **Architecture Review** — 3 issues found (1 critical: concurrent_updates; 2 cleanups: extract oracle writer + move ocr_client) - **Code Quality Review** — 4 issues found (split format/warn, magic numbers, except Exception, dataclass deferred) - **Test Review** — diagram produced, 47 gaps identified (entire feature is new) - **Performance Review** — 4 issues found (use existing pool, slower poll, password cache, memory N/A) - **NOT in scope** — written (7 items) - **What already exists** — written (8 items) - **TODOS.md updates** — 6 items, 3 included in PR, 3 deferred - **Failure modes** — 3 critical gaps flagged, all addressed by accepted findings - **Outside voice** — skipped (contained scope, no architectural ambiguity) - **Parallelization** — sequential, no opportunity - **Lake Score** — 11/13 recommendations chose complete option (2 deferred to follow-up PRs, both with explicit rationale) **Unresolved decisions:** None. User's "merge cu recomandările" applies to all findings. All ACCEPTed unless explicitly deferred. --- ### Review Log