Commit Graph

276 Commits

Author SHA1 Message Date
Claude Agent
dcc5042586 feat(maintenance): guard DB + log growth (Option B + daily prune + rotation)
Root cause of the 2GB prod import.db: the sync_run_orders audit junction
recorded every order on every run; under the 1-minute scheduler ~98% of
21.7M rows were no-op ALREADY_IMPORTED re-observations. NSSM stdout/stderr
also grew unbounded (rotation never applied to the live service).

Changes:
- sqlite_service: skip ALREADY_IMPORTED rows in sync_run_orders (write-side
  guard, _SKIP_JUNCTION_STATUSES); add prune_sync_history(retention_days)
  with incremental_vacuum.
- maintenance_service (new): cleanup_old_logs + run_daily_maintenance.
- scheduler_service: start_maintenance_job (daily CronTrigger).
- main.py: RotatingFileHandler (sync_comenzi_current.log, 10MB x5) instead
  of a new timestamped file per start; schedule daily maintenance + one-shot
  catch-up at startup.
- scripts/db_maintenance.py (new): one-shot prune + VACUUM + log cleanup,
  plain sqlite3, invoked by deploy.ps1 while the service is stopped.
- deploy.ps1: stop -> run db_maintenance.py -> (re)apply NSSM AppRotate*
  idempotently -> start, so rotation reaches pre-existing services.

Retention defaults: 7 days history, 7 days logs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:59:41 +00:00
Claude Agent
ccc6a933fa feat(scripts): line-level residual check in relink_manual_invoices
Linking the VANZARI header (ID_COMANDA) makes the app dashboard show an
order facturat, but ROA decides facturat at the LINE level
(PACK_FACTURARE.cursor_comanda matches invoiced qty on ID_ARTICOL + exact
PRET). When a manual invoice represented lines differently than the order
(e.g. per-VAT-rate discounts consolidated into one 0%-TVA line), the order
stays nefacturat in ROA despite the header link.

Add order_line_residual(): predicts the residual before --apply (via
extra_idv) and re-verifies after linking. Warns in the plan, the summary
counter, and post-apply when an order will still show nefacturat. The
script never touches COMENZI_ELEMENTE — those need a manual line fix.

Surfaced by orders 5419/5423 (web 492710430/492710513) on 2026-06-26.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:46:59 +00:00
Claude Agent
698d036de9 docs: ghid reconciliere facturi manuale ROA <-> comenzi GoMag
Documenteaza relink_manual_invoices.py/.bat: cand se foloseste, cauza tehnica
(VANZARI.ID_COMANDA NULL), logica de matching conservatoare, clasificarea
LINK/SKIP_*, exceptia facturilor de depozit, si workflow-ul de rulare pe prod.
Link in tabelul Documentatie Tehnica din README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 08:37:19 +00:00
Claude Agent
5dd5acc25e Merge feat/relink-manual-invoices: tool to reconcile manual ROA invoices with GoMag orders 2026-06-26 08:35:07 +00:00
Claude Agent
df684b7183 feat(scripts): add Windows .bat launcher for relink_manual_invoices
Sets the Oracle thick-mode env (TNS_ADMIN + instant client PATH) like start.bat,
resolves venv/script paths relative to itself, forwards all args, and pauses so
output is readable. Run on the VENDING prod server (double-click = dry-run).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 08:34:15 +00:00
Claude Agent
2c3b35294c feat(scripts): reconcile manual ROA invoices with GoMag orders
Tool to relink orphan VANZARI invoices (ID_COMANDA NULL, sters=0) — emitted
manually by the warehouse during an Oracle/sync outage — to their GoMag order,
then populate the SQLite invoice cache like the app does.

Matching is conservative (exact total + partner OR name; handles duplicate
partner records) and only auto-links unambiguous 1:1 matches. Genuine warehouse/
walk-in invoices (no online order behind them) get SKIP_NOMATCH and are left
untouched; anything unclear is reported SKIP_AMBIGUOUS for manual review.

Dry-run by default; --apply / --yes / --days N. Runs on prod (uses app.config
settings + prod import.db). Codifies the 2026-06-26 manual relink (12 invoices).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 08:13:02 +00:00
Claude Agent
19834d193a Merge fix/roa-mass-deletion-guard: ROA mass-deletion guard + Oracle pool auto-recovery
- Guard against falsely mass-marking DELETED_IN_ROA when ROA is recovering
- Oracle pool self-heals per sync cycle; status surfaced; order_detail no longer 500s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:41:21 +00:00
Claude Agent
cd7eb628dd feat(oracle): auto-recover Oracle pool + surface status, stop silent import failures
After a power loss the app started before Oracle was ready; init_oracle() failed
once, the pool stayed None forever (no retry), and every sync silently failed
("Oracle pool not initialized") while still hammering the GoMag API each minute,
and order-detail 500'd.

- database.ensure_oracle_pool(force): thread-safe (re)create of the pool, called
  at the start of every sync cycle → self-heals within one cycle once Oracle is
  back (incl. after an Oracle service restart). init_oracle_client made idempotent
  so re-init can't fall back to thin mode.
- database.oracle_status() exposed; main.py startup is non-fatal via ensure pool.
- run_sync ensures the pool before the GoMag download; on failure it records a
  clear run status instead of crashing and skips the wasted API calls.
- /api/sync/health reports oracle_ready/last_error; dashboard health pill shows
  "Oracle indisponibil" (top priority). Recovery via the existing Start Sync button.
- order_detail degrades gracefully (200 without CODMAT + notice) instead of 500.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:33:01 +00:00
Claude Agent
395e2b997a fix(sync): guard against mass DELETED_IN_ROA when ROA is recovering
After a power loss + reboot, COMENZI was queryable but not yet recovered;
phase 4b-3 read it as empty and sticky-marked 3794 live orders DELETED_IN_ROA
(nulling id_comanda). check_orders_exist also swallowed Oracle errors and
returned a partial set, which callers misread as deletions.

- check_orders_exist now re-raises on Oracle error instead of returning partial
- new invoice_service.deletions_or_guard() raises MassDeletionGuard when the
  would-delete fraction is implausibly high (>30% of >=25 imported orders)
- both deletion sites (auto sync + manual refresh) skip + log on guard trip

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:18:08 +00:00
Claude Agent
4a03fe1016 docs: ghid cautare/selectie client dupa cod fiscal + verificare ANAF
Document de referinta end-to-end (replicabil in alt proiect): normalizare
CUI (strip RO, typo OCR, checksum), client ANAF (v9/tva, contract tri-valent,
cache), gate CUI, mod strict platitor/neplatitor TVA si procedurile Oracle
cauta_partener_dupa_cod_fiscal / cauta_sau_creeaza_partener. Link in README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 06:26:38 +00:00
Claude Agent
71a3b32bd7 Merge fix/mappings-slash-404: query params pentru sku/codmat cu slash 2026-06-08 14:01:19 +00:00
Claude Agent
550d94ff16 fix(mappings): move sku/codmat from path to query — slash in keys 404'd
Codurile ROA contin legitim '/' si '"' (ex. RCR1/4"). Rutele FastAPI
foloseau parametri de cale (/api/mappings/{sku}/{codmat}...); ASGI decodeaza
%2F -> '/' INAINTE de routing, deci o cheie cu slash devine doua segmente si
Starlette nu mai potriveste ruta -> 404 la edit/delete/restore. Cand ambii
parametri au slash, un convertor {path} nu rezolva (split ambiguu).

Solutie: sku/codmat trec din cale in query string. %2F/%22 din query se
decodeaza ca parte a valorii, fara sa atinga routing-ul de cale.

- backend: 4 rute statice + Query(...) (update/edit/delete/restore);
  semnaturile mapping_service raman neschimbate
- frontend: helper mapQS(); toate cele 8 apeluri path -> query; cache-bust ?v=19
- tests: regresie ci-layer (chei-sentinel cu slash, !=404, toate 4 rutele);
  test_integration update/delete convertite la query; e2e delete RCR1/4"
  (slash+ghilimea) prin modalul UI, skip cand Oracle lipseste

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:58:04 +00:00
Claude Agent
b1595f45c7 chore: disable pre-push test hook (run ./test.sh ci manually)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:28:31 +00:00
Claude Agent
2e81e3ca14 chore: make .githooks/pre-push executable
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:25:15 +00:00
Claude Agent
a530ebfaff fix(ui): jsAttrEsc for inline onclick handlers — apostrophe in product_name broke SKU mapping modal
HTML parser decodes &#39; back to ' inside onclick="..." before the JS parser
runs, so esc() left inline handlers vulnerable: product names containing an
apostrophe terminated the JS string literal ("missing ) after argument list").

New jsAttrEsc() escapes for JS-string-inside-HTML-attribute (\\, ', \n first;
then &, ", <, >). Applied to all inline onclick sites that interpolate
user-controlled sku/product_name/codmat: shared.js detail modal (lines
879/939), missing_skus.html (4 sites), mappings.js (3 sites).

Cache-bust: shared.js v49→50, mappings.js v17→18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:07:17 +00:00
Claude Agent
32974e3b85 fix(orders): preserve order_items on mark_order_deleted_in_roa
Detail view for DELETED_IN_ROA orders showed "Niciun articol" because
the soft-delete helper hard-deleted order_items. Now items stay in
SQLite so the detail page displays the original GoMag order alongside
"Comanda stearsa din ROA". On 'Reimporta', add_order_items already
replaces them via DELETE+INSERT inside _safe_upsert_order_items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:06:13 +00:00
Claude Agent
ab20856cd6 fix(sync): sticky exclusion for DELETED_IN_ROA orders
Orders deleted via "Sterge" button were re-imported on the next sync
because classify step only checked Oracle (sters=0), not SQLite status.
Adds a filter step after cancellation handling that drops orders
already marked DELETED_IN_ROA before validation. "Reimporta" remains
the explicit override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:57:32 +00:00
Claude Agent
956667086d fix(import): NFKD normalization for non-Romanian diacritics
Some checks failed
Tests / fast-tests (push) Has been cancelled
Tests / full-tests (push) Has been cancelled
clean_web_text used a hard-coded Romanian-only translation map, so Hungarian
(BALÁZS LORÁNT), German, Czech, Polish names passed through unchanged into
SQLite and Oracle ROA. Replace with unicodedata.normalize('NFKD') + combining
mark strip — covers RO/HU/DE/CZ/PL/FR/ES universally. Romanian cedilla legacy
forms (ş/ţ/Ş/Ţ) remain handled (NFKD decomposes to base + combining cedilla).
Stroke letters not decomposed by NFKD (ß, ł, đ, ø, æ, œ) covered via
_NFKD_OVERRIDES translation map.

sync_service._addr_match.norm migrated off the removed _DIACRITICS constant
to clean_web_text; address matching now also handles non-RO diacritics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:52:50 +00:00
Claude Agent
9b62b2b457 fix(ui): _configureDetailButtons trapped in unterminated JSDoc
Stray /** at shared.js:604 wrapped the function body in a block comment
that only closed at line 747, so _configureDetailButtons was never
defined. renderOrderDetailModal threw ReferenceError, leaving the order
detail modal stuck on "Se incarca...".

Removed the orphan /** opener; bumped shared.js cache-bust to v=49.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:40:02 +00:00
Claude Agent
b13d9a466c fix(ui): show Reimporta button on DELETED_IN_ROA orders
mark_order_deleted_in_roa wipes order_items, so renderOrderDetailModal
hit the items.length===0 early-return BEFORE configuring footer buttons —
leaving DELETED_IN_ROA orders with no way to retry from the UI.

Extract the 3 button configurators (Retry/Resync/Delete) into
_configureDetailButtons() and call it before the early-return. Also
fire onAfterRender on the empty-items path for consistency.

Cache-bust shared.js v47 → v48.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:28:55 +00:00
Claude Agent
18acfd2226 Merge fix/retry-ensure-prices: pre-populate price list before retry 2026-04-29 10:20:11 +00:00
Claude Agent
bcd65d9fd6 fix(retry): pre-populate price list before re-importing failed orders
Production VENDING orders #485841978 and #485841895 (2026-04-28) crashed
on Retry with PL/SQL COM-001 because the retry path skipped the
CRM_POLITICI_PRET_ART pre-population step that bulk sync runs.

The price-list auto-insert (PRET=0) for missing CODMATs was only invoked
in sync_service.run_sync (lines 592-718). retry_service called
import_single_order directly, hitting pack_comenzi.adauga_articol_comanda
NO_DATA_FOUND on every CODMAT without a price entry.

Extracted the validation block into validation_service.pre_validate_order_prices
and call it from both bulk sync and retry. Single source of truth for
SKU validation, dual-policy routing (cont 341/345 → productie),
ARTICOLE_TERTI mapping resolution, and kit component price gating.

Tests: 3 unit + 3 oracle integration covering the regression scenario,
empty input, dual-policy routing, idempotency, and pre-validation
exception propagation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 10:19:38 +00:00
Claude Agent
874ba4ca4e docs: SSH debugging guide pentru prod Windows — ce merge, ce nu merge
Adaugat in sectiunea 'Depanare SSH' din README:
- Comanda corecta ssh cu -i ~/.ssh/id_ed25519
- Deploy pachet Oracle via sqlplus @fisier.pck
- Restart FastAPI cand nssm e indisponibil (kill python + start.ps1)
- Tabel explicit cu ce NU merge (nssm/sc/WMI/pipe/curl -m/here-doc)
  si alternativele corecte
- Metoda scriere SQL ad-hoc: fisier local -> scp -> @fisier.sql

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 10:45:19 +00:00
Claude Agent
e42b1f63b7 fix(address): ORA-06502 pe apart/scara/etaj token lung + strip city+region din adresa GoMag
Doua root cause-uri pentru ORA-12899 la importul comenzii #485841056:

1. Oracle ORA-06502: v_apart/v_scara/v_bloc/v_etaj in cauta_sau_creeaza_adresa
   declarate VARCHAR2(10/20/30) → Oracle mostenea constrangerea pe OUT parametrii
   din parseaza_adresa_semicolon → crash INAINTE de fix-ul overflow de la linia 521.
   Fix: marite la VARCHAR2(100).

2. Python format_address_for_oracle stripuia doar city exact, nu si 'city region'
   sau 'region city'. GoMag trimite adresa cu suffix 'Municipiul Bucuresti Bucuresti'
   (city+region) → token urias pentru apartament → declansa ORA-06502 de mai sus.
   Fix: incearca toate combinatiile city+region, region+city, city, region.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 10:31:05 +00:00
Claude Agent
c8bed18978 fix(plsql): hardening SUBSTR(1,10) neconditional dupa split numar
Safety net dupa blocul de overflow split: garanteaza ca p_numar nu
depaseste 10 caractere chiar daca prefixul inaintea primului spatiu
este el insusi >10 char.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 07:07:50 +00:00
Claude Agent
6620b28ed1 feat(sync): gate CUI invalid/ANAF-notFound → ERROR inainte de import Oracle
Incident 22.04.2026 (#485225171 NONA ROYAL SRL): clientul a inversat
cod_fiscal cu registru in GoMag → sistem a creat partener cu CUI=J1994000194225.

Adauga evaluate_cui_gate() care blocheaza comanda (ERROR) daca:
- CUI format invalid (ex: J.. in loc de cifre)
- CUI nu trece cifra de control
- ANAF returneaza explicit notFound (scpTVA=None + denumire_anaf="")

ANAF down (anaf_data=None) → fallback pass, comportament existent pastrat.
_record_order_error() DRY helper evita duplicarea upsert/add_items.
Contract ANAF down/notFound/found documentat in anaf_service._call_anaf_api.
9 teste unit (inclusiv T5 CRITIC: ANAF down nu blocheaza) + T7 COALESCE.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 09:40:07 +00:00
Claude Agent
7e30523242 feat(retry): allow retry for MALFORMED orders
MALFORMED is now a valid retry source alongside ERROR / SKIPPED /
DELETED_IN_ROA. The next sync will re-run validate_structural and
either reclassify or keep the MALFORMED tag — either way, operators
get the same "Retry" button they have for other failure paths
without needing a separate UI affordance.

278 unit + 33 e2e green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:16:16 +00:00
Claude Agent
bb6f3a3b87 feat(sync): /api/sync/health endpoint + dashboard health pill + MALFORMED UI
Backend:
- GET /api/sync/health returns {last_sync_at, last_sync_status,
  last_halt_reason, recent_phase_failures, escalation_phase, is_healthy}.
  healthy when last run was completed (or none yet), no phase has
  tripped the 3-in-a-row escalation, and recent failures <= 1.
- Dashboard + run-level endpoints include `malformed` count so the
  Defecte pill can render.

Frontend:
- Health pill in .sync-card-controls with three states — healthy
  (success green, check icon), warning (amber, triangle), escalated
  (error red, x-octagon + glow). Tooltip exposes the halt reason and
  the top phases with recent failures.
- Status-dot + badge add MALFORMED treatment via --compare orange,
  distinct from ERROR red. DESIGN.md notes the diagnostic rationale
  (ERROR = runtime, MALFORMED = payload source issue).
- Defecte filter pill on dashboard + logs pages. Mobile segmented
  control includes Defecte count. Counts wired to the malformed key.
- startSync() shows a native confirm modal when state is
  halted_escalation — operator override still possible, not silenced.
- ORDER_STATUS.MALFORMED mirror added to shared.js.
- Cache-bust: style.css v46, shared.js v47, dashboard.js v52,
  logs.js v16.

5 endpoint tests cover empty state, completed, failed, escalated,
single-failure warning. Full CI: 257 unit + 33 e2e green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:14:46 +00:00
Claude Agent
41b142effb feat(sync): per-phase isolation + escalation halt
sync_service gains DATA_ERRORS tuple + two new primitives:
  _record_phase_err(run_id, phase, err)
    Logs, appends to run text log, persists to sync_phase_failures.
  _check_escalation()
    Reads the last 3 runs and returns the first phase that has failed
    all 3 in a row, or (None, counts) otherwise.

run_sync now runs a pre-flight escalation check — if a phase has failed
3 consecutive runs, the incoming sync is halted with
status='halted_escalation' and a descriptive error_message. The
dashboard Start Sync button can still override (UI comes in the next
PR2 phase).

Wrapped phases (DATA_ERRORS caught, sync continues):
  cancelled_batch, already_batch, addresses_batch, skipped_batch,
  price_sync, invoice_check, anaf_backfill.
Partner mismatch retains its existing per-order guards. OperationalError
and OS-level errors still propagate to the top-level handler (halt).

6 unit tests cover record + counts + threshold + mixed-phase +
short-circuit + DATA_ERRORS contract. Full CI green: 251 unit + 33 e2e.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:06:58 +00:00
Claude Agent
1e4e3279f7 feat(sync): sync_phase_failures table for escalation tracking
New table sync_phase_failures(run_id, phase, error_summary, created_at)
with index on (phase, created_at). Minimal schema — no raw payload, no
PII — stores just enough to answer "did phase X fail in the last N
runs?" for the escalation check and the /api/sync/health pill.

Helpers in sqlite_service:
  record_phase_failure(run_id, phase, error_summary)
    INSERT OR REPLACE semantics (one row per run+phase), then prunes
    to the most recent 100 sync_runs. error_summary clipped at 500
    chars defensively.
  get_recent_phase_failures(limit=3) → {phase: count} across the last N
    runs, ordered by started_at desc.

6 unit tests cover creation, counting, pruning, empty state,
idempotency, and limit semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:02:08 +00:00
Claude Agent
47a6bd83a4 feat(sync): per-order SAVEPOINT protection for order_items upsert
_safe_upsert_order_items(db, order_number, items) wraps the
DELETE + INSERT OR REPLACE pair in SAVEPOINT items. On
IntegrityError / ValueError / TypeError it rolls the savepoint
back, tags the parent order MALFORMED, logs to the error history
file, and returns False to the caller. add_order_items now delegates
to this helper so a single bad payload cannot leave order_items in
a split state.

2 integration tests: happy path + simulated INSERT crash via
aiosqlite monkeypatch. Existing order_items overwrite regression
tests still pass (5/5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:00:57 +00:00
Claude Agent
f448f74b2d feat(sync): hybrid batch+savepoint isolation with reconnect fix
save_orders_batch now runs in three tiers:
  1. validate_structural pre-flight splits each payload into valid or
     MALFORMED. MALFORMED rows persist with status + error_message + no
     items, and an append-only entry lands in sync_errors_history.log.
  2. Optimistic executemany over the valid list inside a SAVEPOINT batch.
  3. On IntegrityError / ValueError / TypeError, rollback the savepoint
     and fall back to per-order SAVEPOINT inserts so a single bad row
     cannot poison the rest of the batch.

Mid-loop SAVEPOINT rollback failure now triggers _safe_reconnect:
commit whatever survived, close the broken connection, open a fresh
one and keep processing. Preserves MALFORMED rows recorded earlier —
addresses the outside-voice gap where a crashed connection would lose
uncommitted malformed evidence.

Adds OrderStatus.MALFORMED and helper functions:
  _insert_orders_only  — orders + sync_run_orders, no items
  _insert_valid_batch  — happy-path bulk executemany
  _insert_single_order — per-order execute within savepoint
  _mark_malformed      — non-mutating copy with wiped items
  _safe_reconnect      — commit-close-reconnect guard

8 integration tests covering regression 485224762, structural
pre-flight, per-order isolation on runtime fail, caller-dict
immutability, and reconnect durability. 239 unit + 33 e2e green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 08:59:43 +00:00
Claude Agent
d7610a6f33 feat(sync): persistent append-only error history log
_log_order_error_history(order_number, msg) writes to
logs/sync_errors_history.log via a dedicated RotatingFileHandler
(100MB × 12 backups). Logger is lazy-initialised and non-propagating
so it doesn't pollute the root logger.

Purpose: orders.error_message is overwritten when a retry succeeds,
so the history log preserves permanent audit of every malformed-order
event regardless of later outcome. Helper never raises — callers are
already in a degraded path.

3 unit tests: append semantics, multi-order, exception isolation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 08:54:11 +00:00
Claude Agent
38498bec6d feat(validation): add structural pre-flight validator
validate_structural(order) runs before save_orders_batch insert.
Catches malformed payloads (MISSING_FIELD, INVALID_DATE, EMPTY_ITEMS,
INVALID_QUANTITY, INVALID_PRICE) that would otherwise crash the batch
insert or downstream pipeline. 17 unit tests cover each rule.

Does NOT validate SKU existence — redundant with _dedup_items_by_sku
pass-through and validate_skus Oracle lookup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 08:52:32 +00:00
Claude Agent
f6d283b743 refactor(status): introduce OrderStatus enum, replace string literals
Centralized order status values in api/app/constants.py via a
str-valued Enum so comparisons keep working. Replaced literals in:
- services: sync_service, sqlite_service, retry_service
- routers: sync, dashboard
- templates: dashboard.html, logs.html
- static JS: shared (ORDER_STATUS mirror), dashboard, logs
- tests: requirements, order_items_overwrite, business_rules

MALFORMED intentionally NOT added — introduced in follow-up PR2
(per-order failure isolation).

Full test suite: 231 unit + 33 e2e pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 08:45:32 +00:00
Claude Agent
51790accf9 fix(sync): dedup order_items by sku before insert to avoid UNIQUE crash
Production sync was failing every minute with:
  UNIQUE constraint failed: order_items.order_number, order_items.sku

GoMag occasionally returns the same SKU on multiple lines within one order
(configurable products, promo splits). The order_items PK is
(order_number, sku), so the raw batch insert violates UNIQUE and aborts
the entire sync — blocking partner-mismatch updates, address refresh,
and items repopulation for already-imported orders.

Added _dedup_items_by_sku() helper. Applied in save_orders_batch
(cancelled/already/skipped paths) and add_order_items (retry/sync import
paths). Keeps first price/vat/name, sums quantities on collision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 07:04:49 +00:00
Claude Agent
404bc094cd fix(backfill): init sqlite before reading settings
Script failed with "SQLite not initialized" because module-level connection
state wasn't set up when invoked standalone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 07:02:40 +00:00
Claude Agent
819af221d8 fix(import): resolve duplicate article + order_items repopulation on retry
Two production bugs from VENDING (order 485224762, 2026-04-22):

1. Oracle: ORA-20000 when a GoMag order contains a kit SKU whose expansion
   includes CODMAT X plus a second item with SKU=X. Two article-insert
   call-sites in PACK_IMPORT_COMENZI bypassed merge_or_insert_articol —
   line 622 (NOM_ARTICOLE fallback) and line 538 (kit discount line).
   Both now use merge_or_insert_articol for consistent dedup semantics.
   Regression test added in test_complete_import.py covering the exact
   kit-plus-direct scenario.

2. SQLite: retry_service._download_and_reimport refreshed orders row but
   never repopulated order_items. Combined with mark_order_deleted_in_roa
   (which wipes items), any retry/resync left the UI showing "Niciun
   articol" despite successful Oracle import. Retry now rebuilds items
   from the fresh GoMag download on both success and error paths,
   mirroring sync_service.

Includes scripts/backfill_order_items.py — one-shot recovery for orders
already in this bad state. Reads settings, re-fetches from GoMag,
rewrites order_items without touching Oracle or order status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:58:20 +00:00
Claude Agent
b8a9480784 fix(address): numar overflow split, SAT → localitate
In parseaza_adresa_semicolon, text după NR ("5 la non stop", "21 sat
Grozavesti corbii mari") era împins în p_numar și trunchiat brutal la
10 chars ("5 LA NON S", "21 SAT GRO").

Fix: când p_numar > 10 chars, prima componentă rămâne numar; restul se
clasifică:
- "SAT X ..." → p_localitate := "X ..." (satul = localitate, TIER
  L1/L2/L3 existent rezolvă id_loc)
- "COM/ORAS/MUN X" → aruncat (deja în p_localitate din GoMag city)
- altceva (landmark ex "LA NON STOP") → concatenat în p_strada

Semnătura parseaza_adresa_semicolon neschimbată. Zero callers afectați.

Teste: landmark → strada, SAT → localitate, numar normal neschimbat.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 06:13:08 +00:00
Claude Agent
d15f8b085d fix(missing-skus): reconcile stale false positives against Oracle
SKUs mapped externally (via SSH script or direct SQL) never triggered
resolve_missing_sku(), leaving them stuck as unresolved=0 indefinitely.
New reconcile_unresolved_missing_skus() revalidates ALL unresolved SKUs
against Oracle at sync, rescan, and CSV import time. Fail-soft on Oracle
down. Clears the 7 prod false positives on next sync or manual rescan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 11:42:43 +00:00
Claude Agent
3bcb26b0bd fix(import): 3 production bugs — items cache, CUI lookup, ANAF name
Some checks failed
Tests / fast-tests (push) Has been cancelled
Tests / full-tests (push) Has been cancelled
1. SQLite order_items overwrite on re-import (VELA CAFE #484669620):
   add_order_items, save_orders_batch, mark_order_deleted_in_roa now use
   DELETE + INSERT so GoMag quantity changes propagate to dashboard.

2. PL/SQL strict CUI lookup tolerates whitespace (FG COFFE #485065210):
   cauta_partener_dupa_cod_fiscal regex ^RO\d → ^RO\s*\d; IN-set uses
   canonical v_ro_cui. Platitor/neplatitor business rule preserved.
   Python defensive: re.sub whitespace collapse in determine_partner_data.

3. New PJ partners use ANAF official denumire (denumire_override) instead
   of GoMag company_name. Existing partners (found by CUI) untouched.

Tests: 18 new (5 SQLite unit, 8 Python unit, 5 Oracle PL/SQL). All green
locally: 228 unit + 26 oracle + 33 e2e.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:32:59 +00:00
Claude Agent
5397bec35d adaugare adauga_comanda_pe_factura() 2026-04-15 14:42:17 +00:00
Claude Agent
5cdd919226 fix(partners): exclude soft-deleted parteneri in denumire lookup
cauta_partener_dupa_denumire nu filtra sters=0, deci importul PF
putea lega comanda la un partener sters=1 (bug GoMag #484668145 —
RADULESCU ANA MARIA, id_part=21946). Adaugat NVL(sters,0)=0 si
ORDER BY inactiv ASC pentru a prefera parteneri activi, in linie cu
cauta_partener_dupa_cod_fiscal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:08:44 +00:00
Claude Agent
db60d955bf fix(dashboard): show billing vs shipping marker for PJ and PF ramburs
PJ: tooltip shows company on Facturat (display) vs shipping person.
PF ramburs: tooltip shows billing person vs shipping person when they
differ. Adds aria-label + title on indicator for keyboard/screen reader.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 12:27:14 +00:00
Claude Agent
520f0836bf fix(autocomplete): add keyboard navigation and fix scroll/blur in all CODMAT dropdowns
Extract shared setupAutocomplete() into shared.js so all three autocomplete
instances (mappings modal, inline add, quick-map modal) get keyboard nav
(ArrowDown/Up/Enter/Escape), scroll-safe blur handling, and capture-phase
keydown to prevent browser interception. Remove old onmousedown inline
handlers, use data-codmat/data-label attributes instead.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-09 16:07:49 +00:00
Claude Agent
84e5d55592 fix(dashboard): fix kebab menu delete/resync and status dot refresh
Kebab dropdown delete/resync used inlineConfirmAction which breaks inside
Bootstrap dropdowns (dropdown closes on click, hiding confirm state).
Replaced with confirm() dialog + direct async action with row feedback.

Detail modal resync/delete/retry now trigger onStatusChange callback to
refresh the orders table, so status dots update without page reload.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-09 15:24:26 +00:00
Claude Agent
e223128565 fix(dashboard): replace hover row-actions with kebab menu, fix modal button reset and delete color
- Bug 1: hover actions covered total column; replaced with kebab dropdown in dedicated 44px column
- Bug 2: resync/delete buttons kept stale state across modal opens; reset in modal init block
- Bug 3: delete success button was green (btn-success); changed to red (btn-danger)
- Dropdown styled per DESIGN.md: warm shadow, 8px radius, dark mode tokens

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-09 15:10:40 +00:00
Claude Agent
25f73db64d feat(orders): add resync and delete order buttons
Resync soft-deletes from Oracle then re-imports from GoMag with fresh
article data. Delete soft-deletes and marks DELETED_IN_ROA. Both have
invoice safety gates (refuse if invoiced or Oracle unavailable).

UI: split modal footer (Delete left, Resync+Close right), inline
confirm pattern (no native confirm()), dashboard row hover action
icons, disabled+tooltip for invoiced orders. 8 unit tests for safety
gates and happy paths.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-09 13:10:01 +00:00
Claude Agent
90a4906d87 ADRESE 2026-04-08 22:55:09 +00:00
Claude Agent
5eba87976b fix(address): use SOUNDEX city matching and strip SECTORUL from city
Fixes false negatives where city spellings differ slightly (e.g.
"Sfântu Ilie" vs "SFINTU ILIE") or ROA stores "BUCURESTI SECTORUL 1"
while GoMag sends "Municipiul București". Both backend (_addr_match)
and frontend (addrMatch) now use identical SOUNDEX logic mirroring
Oracle's implementation.

Also fixes field order: etaj before apart in r_street concatenation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 22:31:36 +00:00