Commit Graph

86 Commits

Author SHA1 Message Date
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
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
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
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
Claude Agent
f48c2d62c6 fix(address): extract scara/etaj/apartament from comma-less addresses
Oracle parser failed to extract sc/ap/et when GoMag addresses had no
commas. Added REGEXP_REPLACE to insert commas before address keywords
in v_strada before the comma-split, ensuring the token parser always
fires. Also added 5 Oracle integration tests calling
parseaza_adresa_semicolon directly, and improved diacritics handling
in addr_match (Python + JS).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 22:00:17 +00:00
Claude Agent
1d59f1a484 refactor(price): remove price comparison UI and catalog sync
GoMag vs ROA price comparison generated too many false positives
(kits, volume discounts, special prices). Removes comparison columns,
dots, badges, catalog sync endpoints, and ~950 lines of dead code.
Keeps WRITE path (sync_prices_from_order) for kit pricing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 20:30:34 +00:00
Claude Agent
5584dd3c4f fix(partner-mismatch): fix 3 infinite-loop bugs in mismatch detection cycle
Three root causes caused partner_mismatch=1 to loop indefinitely:

1. No-CUI company orders (is_pj=1, no cod_fiscal): old code flagged as
   mismatch every cycle. Fixed by requiring new_cf to be non-null for
   PF→PJ detection. Stale flags from old code cleared via new
   clear_stale_partner_mismatches_no_cui() for out-of-window orders.

2. same_partner resync path did not update cod_fiscal_gomag in SQLite.
   On next cycle GoMag returned a CUI but stored_cf was still NULL →
   re-detected as mismatch. Fixed by also calling update_partner_resync_data
   (not just update_partner_mismatch_batch) in the same_partner branch.

3. GoMag sends CUI with space: 'RO 17922480'. The _strip_ro() regex
   ^RO left the space → ' 17922480' != '17922480' → false mismatch.
   Fixed: changed regex to ^RO\s* and added .strip().

Also adds diagnostic logger.info lines for mismatch detection/resync counts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 17:29:44 +00:00
Claude Agent
89c3d1d07f feat(partner): detect and resync partner mismatches on already-imported orders
Detects PF↔PJ transitions and CUI changes after import; auto-resyncs
uninvoiced orders (max 5/cycle) and shows visual alert for invoiced ones.
- SQLite: partner_mismatch column + batch helpers
- sync_service: detection loop + _resync_partner_for_order
- dashboard: red dot + attention card indicator
- modal: alert with contextual message and resync button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 16:19:26 +00:00
Claude Agent
bf194eb088 fix(price): remove baseprice detection, use directional price match
baseprice > price was wrongly treated as "quantity discount" — it's just
GoMag's promotional price. Now: price_gomag <= pret_roa is always OK,
only flag when GoMag charges MORE than ROA. Reset cached price_match
at startup for re-evaluation. Fix dashboard dot color for mismatches.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-07 20:41:54 +00:00
Claude Agent
057e62fc04 feat(price): detect quantity discounts via baseprice, show Disc. badge
GoMag sends baseprice (catalog price) alongside price (discounted).
When baseprice > price, the item is volume-discounted — skip ROA
price comparison and show amber "Disc." badge instead of false
mismatch. Strikethrough baseprice in price column for transparency.

Pipeline: parse baseprice → store in SQLite → skip in validation →
pass flag to frontend → render badge (desktop + mobile pill badge
with aria-label, opacity 0.6 for dark mode).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 17:06:37 +00:00
Claude Agent
0f817b2130 fix(address): normalize SECTORUL + etaj in addr_match, fix Oracle duplicate addrs
- _addr_match / addrMatch: add SECTORUL\s*\d* branch to strip sector
  number; add (?:\b|(?=\d)) to catch glued keywords (sc1, ap94);
  include etaj field in rStreet concat
- database.py: replace duplicate addr_match impl with import from sync_service
- import_service.py: short-circuit billing addr Oracle call when
  billing == shipping (avoids duplicate address creation)
- PL/SQL: normalize MUNICIPIUL BUCURESTI → BUCURESTI SECTORUL X before
  TIER 1; resolve id_localitate before search; TIER 1 now matches on
  id_loc instead of text locality
- Add scripts/cleanup_duplicate_addresses.sql for manual prod cleanup
- Add 5 new tests: sectorul, keyword+digit gluing, etaj, short-circuit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 13:48:49 +00:00
Claude Agent
5b4b317636 fix(anaf): handle notFound integers, skip 4xx retry, propagate errors to run log
ANAF notFound items are plain integers, not dicts — caused 'int has no
attribute get'. 4xx errors (like 404) no longer retry uselessly. ANAF
errors now appear in the UI sync log via log_fn callback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:47:25 +00:00
Claude Agent
ecde7fe440 feat(address): ROA address cache refresh — 8-field format + manual refresh endpoint
Phase 5 address format upgrade (pre-existing working tree changes):
- import_service: extend vadrese_parteneri query to 8 fields (strada/numar/bloc/scara/apart/etaj/localitate/judet); strip trailing city name from address string passed to Oracle
- sync_service: extend _addr_match to compare bloc/scara/apart in addition to strada/numar
- 05_pack_import_parteneri.pck: updated PL/SQL package

New: address cache refresh mechanism:
- sqlite_service: add get_order_address_ids(), update_order_address_cache() (targeted 3-column update, no ANAF fields touched), get_orders_with_address_ids()
- sync.py: POST /api/orders/{order_number}/refresh-address endpoint (404/422/503/200); batch Oracle address refresh in refresh_invoices (single IN roundtrip, per-order mismatch recomputed)
- UI: refresh button (⟳) in ADRESE modal header (base.html); refreshOrderAddress() with loading state + toast (dashboard.js v43); window._detailOrderNumber global (shared.js v32)
- tests: TestRefreshOrderAddress — 4 tests (404, 422, 503, 200 with 8-field assert)

Oracle prod fix applied directly: ADRESE_PARTENERI id_adresa=4116 STRADA VASILE→VASILE GOLDIS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 12:35:18 +00:00
Claude Agent
86e8d54d5e fix(sync): backfill address_mismatch for orders missing blue dot
Orders synced before address_mismatch was deployed had stale 0 values,
causing missing blue dots in the dashboard. Adds startup backfill from
stored address JSON + recomputes on each sync for ALREADY_IMPORTED orders.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-06 16:05:32 +00:00
Claude Agent
4bff1aada1 fix(anaf): deduplicate TVA/CUI badges, remove address from DIFFS filter
CUI adjustment (RO prefix change) already implies TVA mismatch, so
suppress redundant TVA badge when CUI badge is shown. Remove
address_mismatch from DIFFS pill count since it's not an ANAF difference.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-06 14:56:04 +00:00
Claude Agent
2b212b933e fix(anaf): show TVA badge only on GoMag vs ANAF mismatch
Previously the TVA badge/dot appeared whenever ANAF reported neplatitor.
Now it only appears when GoMag CUI prefix (RO=platitor) disagrees with
ANAF status — aligned across JS badges, red dots, SQL filter and count.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-06 14:49:30 +00:00
Claude Agent
31095c07f7 fix(address+ui): remove TIER 2 reuse, typed diff badges, false positive reduction
- Remove TIER 2 address lookup (county+city without street) from PL/SQL — creates
  new address when street differs instead of reusing wrong one
- Replace generic "N diferente" badge with typed micro-badges (CUI, Denumire, TVA,
  Adr. livr., Adr. fact., Preturi) with red/amber semantic colors
- Extend addrMatch() regex to strip full Romanian address words (STRADA, NUMAR, BLOC,
  COMUNA, SAT, MUNICIPIUL, etc.) — fixes "Strada X" vs "X" false positives
- Extend normalize_company_name() for II, PFA, INTREPRINDERE INDIVIDUALA legal forms
- Persist address_mismatch to SQLite so "Dif." filter includes address-only diffs
- Add red/amber indicator dots to desktop table and mobile list rows
- 12 unit tests for normalization and server-side address matching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:28:57 +00:00
Claude Agent
fc1013bff6 fix(partners): prevent duplicate PF partners on firstname/lastname swap
Customers often swap firstname/lastname in GoMag forms, causing duplicate
partner creation in Oracle. Fix with two layers:

- Python: sort PF name words alphabetically before Oracle lookup
- PL/SQL: add Step 2b permutation search (2-3 word names, PF only)
- Normalize name order to lastname+firstname across all Python files
- Add diagnostic SQL for finding existing reversed-name duplicates
- Add Oracle integration test for reverse-name matching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:06:55 +00:00
Claude Agent
0992744490 refactor(anaf): remove dead code in sanitize_cui, fix empty test
Remove unreachable OCR-skip fallback (raw_bare can't be all-digits
if strip_ro_prefix changed it via OCR fix). Add real test for the
checksum result==10→0 branch using CUI 14186770.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:46:10 +00:00
Claude Agent
b64a99d4e6 feat(anaf): add CUI checksum validation + sanitize at import time
Romanian CUI check digit algorithm (key 753217532) validates CUIs
before ANAF lookup. New sanitize_cui() fixes OCR typos (O→0, I→1)
and verifies checksum, logging warnings for invalid CUIs.

Applied at both ANAF batch verification and per-order import steps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:38:58 +00:00
Claude Agent
2ec1fc0f19 fix(anaf): sanitize OCR-like typos in CUI (O→0, I→1, L→1)
GoMag order #4815967771 had CUI "49033O51" (letter O instead of zero).
validate_cui rejected it so ANAF badge showed "?". strip_ro_prefix now
translates common letter/digit confusions before validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:33:14 +00:00
Claude Agent
5d631c12fa feat(sync): add batched ANAF backfill for orders missing TVA status
Orders imported before the ANAF dedup feature had no anaf_platitor_tva.
Step 4c now auto-backfills on each sync: batch cache lookup, single
ANAF API call for uncached CUIs, bulk DB update in one transaction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:27:32 +00:00
Claude Agent
ffd4cc0800 fix(price): skip price check for kit/complex articles
Kit articles (multi-component or cantitate_roa≠1) have expected price
differences between GoMag (commercial) and ROA (component sum).
Skip comparison entirely, mark with kit=True flag for UI badge.

Fix kit detection to use float()!=1 (catches cantitate_roa<1 like 0.5).
Update 3 existing tests + add multi-component kit test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:01:41 +00:00
Claude Agent
219c821df4 feat(plsql): CUI strict search + country filter for ANAF
PL/SQL cauta_partener_dupa_cod_fiscal gains p_strict_search param:
- strict (=1): search only exact CUI form (ANAF-determined)
- dual (NULL): search all forms (existing anti-dedup behavior)
Skip denomination fallback when strict to force new partner creation.

Python: country filter excludes foreign companies from ANAF batch,
anaf_strict flag threaded sync→import→PL/SQL, normalize RO-space
in cod_fiscal_adjusted comparison to eliminate false positives.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:01:34 +00:00
Claude Agent
e8b42088e3 fix(anaf-dedup): fix ANAF parsing, facturare addr, compact modal layout
- Fix ANAF API: extract CUI from date_generale (not top-level), fix
  notFound casing (capital F)
- Fix missing facturare address when same ID as livrare (copy instead
  of skip)
- Replace ANAF cache pre-population stub with real logic (3-month CUIs)
- Restructure order detail modal: inline 2-col GOMAG|ROA layout with
  compact address lines replacing collapsed sections
- Fix addrMatch() to use field-level comparison with Romanian
  abbreviation stripping (STR, NR, BL, SC, AP, ET, ETAJ, APART)
- Add dashboard "Diferente" filter pill for ANAF-adjusted orders
- Update e2e test for new modal structure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:07:37 +00:00
Claude Agent
2f593c30f6 feat(anaf-dedup): ANAF partner dedup + address fix + UI enrichment
Prevent partner duplicates via ANAF CUI verification and dual PL/SQL
search. Fix address matching with street-level comparison and diacritics
normalization. Show partner/address comparison in order detail modal.

- New anaf_service.py: batch ANAF API client with chunking, retry, cache
- PL/SQL: dual CUI search (bare/RO+bare/RO space+bare), 3-tier address
  search (street+city+id_loc → city+id_loc → create), strip_diacritics
  at storage for addresses and partner names
- SQLite: anaf_cache table, 12 new order columns for partner/address data
- import_service: cod_fiscal_override param, return partner/address from Oracle
- sync_service: ANAF batch integration, denomination mismatch detection,
  cache pre-population trigger
- Router: enriched order_detail with partner_info + addresses JSON
- UI: collapsible Detalii Partener + Adrese Comparativ sections in modal,
  auto-expand on mismatch, ANAF badges, mobile address cards
- Dashboard: address quality attention indicator
- New scan_duplicate_partners.py script for one-time duplicate audit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:36:52 +00:00
Claude Agent
aa9e580e1b fix(scheduler): prevent auto-enable with wrong interval on page load
Root cause: GET /api/sync/schedule returned interval_minutes=null when
scheduler was stopped, causing dropdown to stay on first HTML option
(1 min). Setting .value programmatically could trigger onchange, sending
a second PUT with interval=1 right after the user's intended interval.

- GET schedule falls back to DB/default (10 min) when scheduler is off
- Add _schedulerLoading flag to block onchange during loadSchedulerStatus
- Default interval 10 min everywhere (was 5 in backend)
- Cache bust dashboard.js v=33

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:57:22 +00:00
Claude Agent
1a912b5fa4 fix(price): normalize cantitate_roa < 1 in price comparison (false mismatch)
Kit detection only checked cantitate_roa > 1, missing fractional values
like 0.5 (GoMag 50buc/set → ROA 100buc/set). This caused false price
difference alerts (e.g. GoMag 7.00 vs ROA 14.00 for order #481595156).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:23:09 +00:00
Claude Agent
e4237fc0f7 fix(cancel): correct table name comenzi_detalii → comenzi_elemente (ORA-00942)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 09:50:22 +00:00
Claude Agent
e478c35ecd fix(address): smart parser splits numar into bloc/scara/apart/etaj (ORA-12899)
Addresses with commas after street (e.g. "Str.Dacia NR.15 BLOC Z2,SCARA A,AP.7")
caused NUMAR column overflow (max 10 chars). Parser now tokenizes by comma and
routes BL/SC/AP/ET/NR prefixes to proper columns. Also extracts NR/BLOC embedded
in street text. Import service now blocks orders when address creation fails
(returns ERROR instead of silently importing without address).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 09:19:47 +00:00
Claude Agent
6acb73b9ce fix(dashboard): cache ROA price status and align nefacturate counts
- Add price_match column to SQLite, cached on order detail view
- Background backfill on startup checks all unchecked imported orders
- Extract _enrich_items_with_codmat() helper to deduplicate SKU enrichment
- Attention card now shows same nefacturate count as filter pill

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 14:47:03 +00:00
Claude Agent
b52313faf6 feat(flow): map SKU + auto-retry consolidated banner
After saving a SKU mapping, check for SKIPPED orders containing that
SKU and show a floating banner with count + "Importa" button. Batch
retries up to 20 orders and shows result feedback.

Backend:
- get_skipped_orders_with_sku() in sqlite_service.py
- GET /api/orders/by-sku/{sku}/pending endpoint
- POST /api/orders/batch-retry endpoint (max 20, sequential)

Frontend:
- Auto-retry banner after quickMap save with batch import button
- Success/error feedback, auto-dismiss after 15s

Cache-bust: shared.js?v=19

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:38:10 +00:00
Claude Agent
7a789b4fe7 feat(flow): retry failed orders
Add ability to re-import individual ERROR/SKIPPED orders directly from
the order detail modal. Downloads narrow date range from GoMag API,
finds the specific order, and re-runs import_single_order().

Backend:
- New retry_service.py with retry_single_order() — downloads order_date
  ±1 day from GoMag, finds order by number, imports via import_service
- Guard: blocks retry during active sync (_sync_lock check)
- POST /api/orders/{order_number}/retry endpoint

Frontend:
- "Reimporta" button in modal footer (visible only for ERROR/SKIPPED)
- Spinner during retry, success/error feedback with auto-refresh

Cache-bust: shared.js?v=18

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:34:51 +00:00
Claude Agent
a10a00aa4d feat(safety): needs attention card on dashboard
Add a "Needs Attention" card above the orders table that surfaces:
- Import errors count (click → ERROR filter)
- Unmapped SKUs count (click → Missing SKUs page)
- Uninvoiced orders >3 days (click → UNINVOICED filter)
Shows green "Totul in ordine" when all metrics are zero.

Backend: add uninvoiced_old count to get_orders() and unresolved_skus
from get_dashboard_stats() to dashboard/orders API response.

Cache-bust: style.css?v=21, dashboard.js?v=29

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:28:18 +00:00
Claude Agent
3bd0556f73 feat(safety): price comparison on order detail
Add ROA price comparison to order detail modal — operators can now see
if GoMag prices match Oracle before invoicing. Eliminates the #1 risk
of invoicing with wrong prices.

Backend:
- New get_prices_for_order() in validation_service.py — batch Oracle
  query with dual-policy routing (sales/production by cont 341/345),
  PRETURI_CU_TVA handling, kit total calculation
- Extend GET /api/sync/order/{orderNumber} with per-item pret_roa and
  order-level price_check summary
- GET /api/dashboard/orders returns price_match=null (lightweight)

Frontend:
- Modal: price check badge (green/red/grey), "Pret GoMag" + "Pret ROA"
  columns, match dot per row, mismatch rows highlighted
- Dashboard: price dot column (₽) in orders table
- Mobile: inline mismatch indicator

Cache-bust: shared.js?v=16, dashboard.js?v=28

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:25:02 +00:00
Claude Agent
f2bf6805b4 cleanup resolved missing skus 2026-03-25 22:29:33 +00:00
Claude Agent
91ddb4fbdd fix(mappings): allow SKU=CODMAT mappings for quantity conversion
Remove validation that blocked creating mappings when SKU matches an
existing CODMAT. Users need this for unit quantity conversion (e.g.,
website sells 50 units per SKU but ROA tracks 100, requiring
cantitate_roa=0.5).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:01:30 +00:00
Claude Agent
47b5723f92 fix(sync): prevent kit/bax price sync from overwriting individual CRM prices
Three code paths could overwrite CRM list prices with wrong values when
web unit (50 buc) differs from ROA unit (100 buc):

- price_sync_service: kit path now skips components that have their own
  ARTICOLE_TERTI mapping (individual path handles them with correct ÷0.5)
- validation_service: sync_prices_from_order now skips bax SKUs
  (cantitate_roa > 1) in addition to multi-component kits
- pack_import_comenzi: skip negative kit discount (markup), ROUND prices
  to nzecimale_pretv decimals

Also adds:
- SQL script for 6 ARTICOLE_TERTI mappings (cantitate_roa=0.5) for cup
  articles where web=50buc, ROA=100buc/set
- Oracle schema reference documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 19:05:49 +00:00