Files
roa2web-service-auto/data-entry-app/docs/IMPLEMENTATION_PLAN_AUTH_UNITAR.md
2025-12-13 01:56:03 +02:00

15 KiB

Plan: Sincronizare Nomenclatoare Oracle + Auth SSO + OCR Furnizori

Obiective

  1. Sincronizare nomenclatoare din Oracle în SQLite (furnizori, casa/banca)
  2. Auth pentru data-entry-app cu SSO (frontend-uri separate pe path)
  3. OCR: căutare furnizor după CUI + creare locală dacă nu există
  4. Deploy Windows IIS cu path routing

Arhitectura Aleasă

roa2web.romfast.ro (IIS + ARR)
│
├── /reports/              → reports-app/frontend/
├── /data/                 → data-entry-app/frontend/
│
├── /api/reports/*         → reports-backend:8001
├── /api/data/*            → data-entry-backend:8003
└── /api/auth/*            → reports-backend (auth provider)

URL-uri compacte:

  • roa2web.romfast.ro/reports/ - Rapoarte
  • roa2web.romfast.ro/data/ - Introducere date (bonuri fiscale)
  • roa2web.romfast.ro/api/reports/ - API rapoarte
  • roa2web.romfast.ro/api/data/ - API introducere date

SSO: Același domeniu = localStorage partajat = token JWT valid pentru ambele


Faza 1: Auth pentru Data-Entry-App

1.1 Backend - Integrare shared/auth/

Fișiere de modificat:

  • data-entry-app/backend/app/main.py
  • data-entry-app/backend/app/routers/receipts.py
  • data-entry-app/backend/app/core/config.py

Acțiuni:

# main.py - Adăugare middleware
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent / "shared"))

from auth.middleware import AuthenticationMiddleware
from auth.dependencies import get_current_user

app.add_middleware(
    AuthenticationMiddleware,
    excluded_paths=["/docs", "/redoc", "/openapi.json", "/health", "/"]
)
# receipts.py - Înlocuire placeholder
from auth.dependencies import get_current_user
from auth.models import CurrentUser

@router.get("/")
async def list_receipts(
    current_user: CurrentUser = Depends(get_current_user)
):
    # folosește current_user.username

1.2 Frontend - Auth Store + Login Page

Fișiere de creat/copiat din reports-app:

  • data-entry-app/frontend/src/stores/auth.js (copiat)
  • data-entry-app/frontend/src/views/LoginView.vue (copiat)
  • data-entry-app/frontend/src/router/index.js (adăugat guard)
  • data-entry-app/frontend/src/services/api.js (axios interceptor)

Decizie SSO:

  • Frontend data-entry folosește /api/auth/login de pe reports-backend
  • Sau: redirect la /login (reports-app) care setează token în localStorage
  • Token valid pentru ambele (același JWT_SECRET_KEY)

Faza 2: Sincronizare Nomenclatoare Oracle → SQLite

2.1 Noi Modele SQLModel

Fișier: data-entry-app/backend/app/db/models/nomenclature.py

class SyncedSupplier(SQLModel, table=True):
    """Furnizori sincronizați din Oracle"""
    __tablename__ = "synced_suppliers"

    id: int = Field(primary_key=True)  # ID din Oracle (ID_PART)
    company_id: int = Field(index=True)
    name: str = Field(max_length=200)  # DEN_PART
    fiscal_code: Optional[str] = Field(max_length=20, index=True)  # COD_FISCAL
    address: Optional[str] = Field(max_length=500)
    synced_at: datetime = Field(default_factory=datetime.utcnow)

class LocalSupplier(SQLModel, table=True):
    """Furnizori creați local din OCR (neexistenți în Oracle)"""
    __tablename__ = "local_suppliers"

    id: Optional[int] = Field(default=None, primary_key=True)
    company_id: int = Field(index=True)
    name: str = Field(max_length=200)
    fiscal_code: str = Field(max_length=20, unique=True, index=True)
    address: Optional[str] = Field(max_length=500)
    created_by: str = Field(max_length=100)
    created_at: datetime = Field(default_factory=datetime.utcnow)
    oracle_synced: bool = Field(default=False)  # True când e creat în Oracle

class SyncedCashRegister(SQLModel, table=True):
    """Case/Bănci sincronizate din Oracle"""
    __tablename__ = "synced_cash_registers"

    id: int = Field(primary_key=True)  # ID din Oracle
    company_id: int = Field(index=True)
    name: str = Field(max_length=100)
    account_code: str = Field(max_length=20)  # 5311, 5121 etc.
    register_type: str = Field(max_length=20)  # CASA sau BANCA
    synced_at: datetime = Field(default_factory=datetime.utcnow)

2.2 Alembic Migration

Fișier: data-entry-app/backend/migrations/versions/xxx_add_nomenclature_tables.py

2.3 Sync Service

Fișier: data-entry-app/backend/app/services/sync_service.py

class NomenclatureSyncService:
    """Sincronizare nomenclatoare din Oracle în SQLite"""

    @staticmethod
    async def sync_suppliers(company_id: int, schema: str) -> int:
        """Sincronizează furnizori pentru o companie"""
        async with oracle_pool.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute(f"""
                SELECT ID_PART, DEN_PART, COD_FISCAL, ADRESA
                FROM {schema}.NOM_PARTENERI
                WHERE TIP_PART IN ('F', 'A')  -- Furnizori sau Ambele
            """)
            # Upsert în SQLite

    @staticmethod
    async def sync_cash_registers(company_id: int, schema: str) -> int:
        """Sincronizează case și bănci"""
        # Similar pentru NOM_CASE și NOM_BANCI

    @staticmethod
    async def get_schema_for_company(company_id: int) -> str:
        """Obține schema Oracle pentru o companie"""
        # Folosește cache din shared sau query V_NOM_FIRME

2.4 Strategia de Sync Hibrid

  1. La startup app: Sync automat (background task)
  2. Periodic: Task programat la 4h
  3. On-demand: Căutare live în Oracle când CUI nu există local

Fișier: data-entry-app/backend/app/main.py

@app.on_event("startup")
async def startup_sync():
    # Background sync pentru company-urile active
    asyncio.create_task(sync_nomenclatures_background())

Faza 3: OCR + Căutare Furnizor după CUI

3.1 Flow Căutare Furnizor

OCR extrage CUI
      ↓
Căutare în SyncedSupplier (SQLite)
      ↓ (nu găsit)
Căutare în LocalSupplier (SQLite)
      ↓ (nu găsit)
Căutare LIVE în Oracle (NOM_PARTENERI)
      ↓ (nu găsit)
Creare LocalSupplier cu date OCR
      ↓
Utilizator poate edita înainte de submit

3.2 Endpoint Căutare Furnizor

Fișier: data-entry-app/backend/app/routers/nomenclature.py

@router.get("/suppliers/search")
async def search_supplier(
    company_id: int,
    fiscal_code: Optional[str] = None,
    name: Optional[str] = None,
    current_user: CurrentUser = Depends(get_current_user)
) -> SupplierSearchResult:
    """
    Caută furnizor:
    1. În SQLite (synced + local)
    2. Live în Oracle dacă nu găsit
    3. Returnează sugestie creare dacă nu există
    """

@router.post("/suppliers/local")
async def create_local_supplier(
    supplier: LocalSupplierCreate,
    current_user: CurrentUser = Depends(get_current_user)
) -> LocalSupplier:
    """Crează furnizor local din date OCR"""

3.3 Modificare OCR Flow în Frontend

Fișier: data-entry-app/frontend/src/views/ReceiptCreateView.vue

// După OCR, caută automat furnizor
async function handleOCRResult(ocrData) {
    if (ocrData.cui) {
        const result = await api.get('/api/data-entry/suppliers/search', {
            params: { company_id: selectedCompany.id, fiscal_code: ocrData.cui }
        });

        if (result.found) {
            form.partner_id = result.supplier.id;
            form.partner_name = result.supplier.name;
        } else {
            // Afișează opțiune creare locală
            showCreateSupplierDialog(ocrData);
        }
    }
}

Faza 4: Deploy Windows IIS

4.1 Serviciu Windows pentru data-entry-backend

Fișier: deployment/windows/scripts/Install-DataEntry.ps1

Similar cu Install-ROA2WEB.ps1 dar:

  • ServiceName: ROA2WEB-DataEntry
  • Port: 8003
  • BackendPath: C:\inetpub\wwwroot\roa2web\data-entry-app\backend
  • FrontendPath: C:\inetpub\wwwroot\roa2web\data-entry-app\frontend

Actualizare Install-ROA2WEB.ps1 pentru structura unitară:

  • BackendPath: C:\inetpub\wwwroot\roa2web\reports-app\backend
  • FrontendPath: C:\inetpub\wwwroot\roa2web\reports-app\frontend

4.2 Actualizare web.config

Fișier: deployment/windows/config/web.config

Reguli URL compacte (/reports/, /data/, /api/reports/, /api/data/):

<!-- API Auth (comun) -->
<rule name="Auth API" stopProcessing="true">
    <match url="^api/auth/(.*)" />
    <action type="Rewrite" url="http://localhost:8001/api/auth/{R:1}" />
</rule>

<!-- API Data Entry -->
<rule name="Data Entry API" stopProcessing="true">
    <match url="^api/data/(.*)" />
    <action type="Rewrite" url="http://localhost:8003/api/{R:1}" />
</rule>

<!-- API Reports -->
<rule name="Reports API" stopProcessing="true">
    <match url="^api/reports/(.*)" />
    <action type="Rewrite" url="http://localhost:8001/api/{R:1}" />
</rule>

<!-- Frontend Data Entry SPA (/data/) -->
<rule name="Data Entry SPA" stopProcessing="true">
    <match url="^data($|/.*)" />
    <conditions>
        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
    </conditions>
    <action type="Rewrite" url="/data/index.html" />
</rule>

<!-- Frontend Reports SPA (/reports/) -->
<rule name="Reports SPA" stopProcessing="true">
    <match url="^reports($|/.*)" />
    <conditions>
        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
    </conditions>
    <action type="Rewrite" url="/reports/index.html" />
</rule>

<!-- Root redirect la /reports/ -->
<rule name="Root Redirect" stopProcessing="true">
    <match url="^$" />
    <action type="Redirect" url="/reports/" redirectType="Found" />
</rule>

IIS Virtual Directories (pentru URL-uri compacte):

# /reports/ → reports-app/frontend/
New-WebVirtualDirectory -Site "Default Web Site" -Name "reports" `
    -PhysicalPath "C:\inetpub\wwwroot\roa2web\reports-app\frontend"

# /data/ → data-entry-app/frontend/
New-WebVirtualDirectory -Site "Default Web Site" -Name "data" `
    -PhysicalPath "C:\inetpub\wwwroot\roa2web\data-entry-app\frontend"

4.3 Structura Foldere (UNITARĂ - identică dev/prod)

În development (git repo):

roa2web/
├── reports-app/
│   ├── backend/           # FastAPI port 8001
│   ├── frontend/          # Vue.js port 3000
│   └── telegram-bot/      # Bot Telegram
├── data-entry-app/
│   ├── backend/           # FastAPI port 8003
│   └── frontend/          # Vue.js port 3010
└── shared/                # Cod partajat (auth, database)

În producție (Windows IIS) - IDENTIC:

C:\inetpub\wwwroot\roa2web\
├── reports-app/
│   ├── backend/           # Serviciu Windows port 8001
│   └── frontend/          # Servit de IIS pe /
├── data-entry-app/
│   ├── backend/           # Serviciu Windows port 8003
│   └── frontend/          # Servit de IIS pe /data-entry/
├── telegram-bot/          # Serviciu Windows port 8002
└── shared/                # Cod partajat

Avantaje structură unitară:

  • Deploy simplu: xcopy /E /Y source\reports-app dest\reports-app
  • Path-uri identice în cod (no surprises)
  • Un singur script de deploy pentru ambele medii

Faza 5: Configurare Dev (identic cu prod)

5.1 Vite Config pentru URL-uri Compacte

Fișier: data-entry-app/frontend/vite.config.js

export default defineConfig({
    base: '/data/',  // URL compact în producție
    server: {
        proxy: {
            '/api/auth': 'http://localhost:8001',
            '/api/data': 'http://localhost:8003'
        }
    }
})

Fișier: reports-app/frontend/vite.config.js (ACTUALIZAT)

export default defineConfig({
    base: '/reports/',  // URL compact în producție (era '/')
    server: {
        proxy: {
            '/api/auth': 'http://localhost:8001',
            '/api/reports': 'http://localhost:8001'
        }
    }
})

IMPORTANT: Actualizare API calls în frontend:

  • Reports: /api/reports/companies, /api/reports/invoices, etc.
  • Data Entry: /api/data/receipts, /api/data/suppliers, etc.
  • Auth (comun): /api/auth/login, /api/auth/refresh

5.2 Script Start Unificat

Fișier: start-all.sh (nou)

#!/bin/bash
# Pornește toate serviciile pentru dev

# SSH tunnel
./ssh_tunnel.sh start

# Reports backend
cd reports-app/backend && uvicorn app.main:app --port 8001 &

# Data entry backend
cd data-entry-app/backend && uvicorn app.main:app --port 8003 &

# Reports frontend
cd reports-app/frontend && npm run dev -- --port 3000 &

# Data entry frontend
cd data-entry-app/frontend && npm run dev -- --port 3010 &

wait

Ordine Implementare

# Task Efort Dependențe
1 Modele SQLModel nomenclatoare 30 min -
2 Alembic migration 15 min #1
3 Sync service (Oracle → SQLite) 2h #2
4 Auth middleware în data-entry-backend 1h -
5 Auth store + login în data-entry-frontend 1h #4
6 Endpoint căutare furnizor 1h #3
7 Frontend OCR + furnizor flow 1.5h #6
8 web.config IIS actualizat 30 min -
9 Script deploy data-entry Windows 1h #8
10 Testare end-to-end 1h all

Total estimat: ~10h


Fișiere Critice de Modificat/Creat

Backend data-entry-app:

  • app/main.py - middleware auth + startup sync
  • app/db/models/nomenclature.py - noi modele (CREARE)
  • app/services/sync_service.py - sync Oracle (CREARE)
  • app/services/nomenclature_service.py - refactorizare
  • app/routers/nomenclature.py - endpoint-uri noi (CREARE)
  • app/routers/receipts.py - auth dependencies
  • migrations/versions/xxx_nomenclature.py - migrare (CREARE)

Frontend data-entry-app:

  • src/stores/auth.js - copiat din reports-app
  • src/views/LoginView.vue - copiat + adaptat
  • src/router/index.js - auth guard
  • src/services/api.js - axios config
  • src/views/ReceiptCreateView.vue - OCR + supplier flow

Deploy (structură unitară):

  • deployment/windows/config/web.config - reguli noi + actualizate
  • deployment/windows/scripts/Install-ROA2WEB.ps1 - ACTUALIZAT pentru structura unitară
  • deployment/windows/scripts/Install-DataEntry.ps1 - NOU
  • deployment/windows/scripts/Build-ROA2WEB.ps1 - ACTUALIZAT pentru ambele apps
  • deployment/windows/docs/WINDOWS_DEPLOYMENT.md - ACTUALIZAT cu noua structură

Shared:

  • Nu necesită modificări (refolosim exact ce există)

Întrebări Rezolvate

Întrebare Răspuns
Furnizor nou din OCR? Creare automată în SQLite (LocalSupplier)
Sync strategy? Hibrid: startup + periodic 4h + on-demand
Auth sharing? Frontend-uri separate pe path, același token JWT (SSO via localStorage)
Deployment? IIS path routing, servicii Windows separate
Structura directoare? UNITARĂ - grupat pe app ({app}/backend, {app}/frontend) identic dev/prod
SSO cum funcționează? Același domeniu IIS → localStorage partajat → token valid pentru ambele API-uri
URL-uri? COMPACTE: /reports/, /data/, /api/reports/, /api/data/
Root (/)? Redirect automat la /reports/