Documento tecnico per developer review · Aggiornato Aprile 2026
42
API Route
23
Pagine Frontend
14
Componenti UI
14
Utility Lib
| Modello | Campi chiave | Relazioni principali |
|---|---|---|
| Warehouse | name, workshopId, location | InventoryItem[], FiscalYear[] |
| InventoryItem | partNumber, description, expectedQty, unitPrice, abcClass, xyzClass, rotationClass, location, barcode, lastCountedAt, countFrequency | Warehouse, DiscrepancyCorrection[] |
| FiscalYear | name, startDate, endDate, workingDaysPerWeek, holidays (JSON), dailyTarget, status, tranchePeriod | Warehouse, FiscalTranche[], DailyCountPlan[] |
| FiscalTranche | type (QUARTERLY/SEMI_ANNUAL/CUSTOM), startDate, endDate, status, targetCoverage | FiscalYear, InventorySession[] |
| DailyCountPlan | planDate, targetCount, algorithm, status, generatedItems (JSON), algorithmConfig (JSON) | FiscalYear, Warehouse, InventorySession? |
| InventorySession | name, type, status, countingMode (BLIND/GUIDED), fiscalYearId | Warehouse, InventoryCheck[], InventoryReport? |
| InventoryCheck | status, assignedToId | InventorySession, InventoryCheckItem[] |
| InventoryCheckItem | expectedQty, actualQty, discrepancy, checkedAt, barcode, photo | InventoryCheck, InventoryItem |
| DiscrepancyCorrection | expectedQty, actualQty, discrepancy, valueDifference, status, correctedById, confirmedById | InventoryItem, InventorySession |
| InventoryAlert | type, severity, message, warehouseId, isDismissed | — |
| StockSnapshot | snapshotDate, totalPositions, totalQty, totalValue, itemCount | Warehouse |
| InventoryReport | totalItems, checkedItems, discrepancies, accuracy, reportData (JSON), pdfUrl | InventorySession |
POST /api/inventory/importUpload file CSV/Excel. Il wizard (ImportWizard + ColumnMapper) mappa le colonne sorgente ai campi Prisma.
upsert InventoryItemOgni riga viene upserted su partNumber+warehouseId. Campi aggiornati: description, expectedQty, unitPrice, location, barcode, category.
abc-classification.tsReclassificazione ABC in batch: ordina per valore (unitPrice×expectedQty), assegna A=top 20% valore (80% cumulativo), B=20-95%, C=restanti.
rotation-calculator.tsCalcolo rotazione: HIGH/MEDIUM/LOW in base a percentile di countFrequency e lastCountedAt recency.
stock-snapshot.ts → StockSnapshotSnapshot automatico del magazzino: totalPositions, totalQty, totalValue per il giorno corrente.
POST /api/inventory/fiscal-yearsCrea FiscalYear con startDate, endDate, workingDaysPerWeek, holidays, dailyTarget, tranchePeriod.
working-days.tsCalcola totalWorkingDays tra startDate e endDate escludendo weekend e festività configurate.
auto-genera FiscalTranche[]In base a tranchePeriod (QUARTERLY/SEMI_ANNUAL/CUSTOM) crea automaticamente le tranche con date proporzionali.
FiscalYear.status = ACTIVEUn solo anno ACTIVE per warehouse. Il cambio di stato gestisce l'attivazione sequenziale.
POST /api/inventory/daily-plans/generateRichiede warehouseId, date, algorithm, targetCount. Delega all'algorithm-registry.
algorithm-registry.ts → runAlgorithm()6 algoritmi disponibiliFactory pattern: seleziona l'algoritmo e chiama il selettore corrispondente.
SMART_COMPOSITE (La Regia)Score composito: ABC class 25% + scaduto/mai contato 25% + storia discrepanze 20% + rotazione 20% + clustering zona 10%.
ABC_PRIORITYPrioritizza articoli A→B→C in base al valore di magazzino.
ZONE_BASEDSeleziona articoli per zona fisica, ottimizzando il percorso dell'operatore (location-clustering.ts).
RANDOM_SAMPLINGCampionamento stratificato casuale con Fisher-Yates shuffle, configurable %.
HIGH_ROTATIONSolo articoli ad alta rotazione (classe HIGH).
EXCEPTION_BASEDSolo articoli con stock negativo, discrepanze ripetute, o ad alto valore mai contato.
DailyCountPlan.generatedItems (JSON)Salva array di item ID ordinati per score. Algorithm config salvata per audit.
POST /api/inventory/daily-plans/[id]/approveApprova il piano: crea InventorySession + InventoryCheck assegnati. Opzione autoStart.
POST /api/inventory/sessions/[id]/startCambia status IN_PROGRESS. Crea InventoryCheck per ogni operatore assegnato. Distribuisce gli item.
GET /api/inventory/checks/[id]L'operatore carica il suo check. In modalità BLIND: nessun expectedQty visibile. In GUIDED: mostra la quantità attesa.
PATCH /api/inventory/checks/[id]/itemsBatch update degli item: actualQty, note, photo, barcode. Calcola discrepancy = actualQty - expectedQty.
PUT /api/inventory/checks/[id]Aggiorna status check: PENDING→IN_PROGRESS→COMPLETED. Registra startedAt/completedAt.
POST /api/inventory/sessions/[id]/completeGenera InventoryReport (accuracy, totalItems, discrepancies). Crea DiscrepancyCorrection per ogni item con discrepancy ≠ 0. Aggiorna lastCountedAt e countFrequency su InventoryItem.
DiscrepancyCorrection.status = PENDINGAuto-creato al completamento sessione per ogni item con discrepanza. Contiene expectedQty, actualQty, discrepancy, valueDifference.
PUT /api/inventory/corrections/[id] → CORRECTEDIl magazziniere conferma di aver fisicamente verificato e corretto l'articolo nel DMS. Registra correctedById e correctedAt.
PUT /api/inventory/corrections/[id] → CONFIRMEDIl manager conferma la correzione. Registra confirmedById e confirmedAt.
PUT /api/inventory/corrections/[id] → IGNOREDLa discrepanza viene giustificata e ignorata (es. articolo in transito, differenza accettabile).
PATCH /api/inventory/corrections/batchOperazione bulk su più correzioni contemporaneamente.
GET /api/inventory/corrections/summaryAggregati per status: pending/corrected/confirmed/ignored con conteggi e valore totale.
generateAlerts() — alert-engine.tsChiamato automaticamente ad ogni richiesta dashboard. Genera alert solo se non esistono già (deduplicazione 24h).
PERSISTENT_NEGATIVE_STOCKSeverity CRITICAL: articoli con actualQty < 0 da più sessioni consecutive.
REPEATED_DISCREPANCYSeverity WARNING: stesso articolo con discrepanza in ≥3 sessioni nell'anno fiscale corrente.
UNCOUNTED_HIGH_VALUESeverity WARNING: articoli classe A mai contati da >30 giorni.
DAILY_TARGET_MISSEDSeverity INFO: giorni lavorativi passati senza piano approvato.
TRANCHE_DEADLINE_APPROACHINGSeverity WARNING: tranche con scadenza entro 7 giorni e coverage < targetCoverage.
POST /api/inventory/alerts/[id]/dismissDismissione alert. isDismissed=true, non riappare finché non si rigenera.
POST /api/inventory/tranches/[id]/generateGenera il campione per la tranche usando tranche-selector.ts.
tranche-selector.tsMandatory items: articoli con discrepanze dall'ultima tranche + high-rotation + aggiunti manualmente dal manager. Stratified sample: completa fino a targetCoverage con ABC proporzionale.
PATCH /api/inventory/tranches/[id]/mandatory-itemsIl manager può aggiungere/rimuovere articoli dall'elenco obbligatorio prima dell'avvio.
POST /api/inventory/tranches/[id]/startCrea sessione TRANCHE_RECHECK e assegna i check agli operatori.
POST /api/inventory/tranches/[id]/completeCompleta la tranche, aggiorna statistics e prepara obbligatori per la prossima.
POST /api/inventory/year-end/generateUsa year-end-selector.ts: articoli stale (>6 mesi non contati) + correzioni non risolte + top 10% per valore + campione residuo.
POST /api/inventory/year-end/startCrea sessione YEAR_END con tutti gli articoli selezionati. Priorità assoluta nel dashboard.
Session completataCome da flusso standard (4). Il report finale include confronto anno precedente da StockSnapshot.
| Endpoint | Metodi | Permesso |
|---|---|---|
| /api/inventory/alerts | GET | INVENTORY_REPORT |
| /api/inventory/alerts/[id]/dismiss | POST | INVENTORY_REPORT |
| /api/inventory/algorithms | GET | INVENTORY_MANAGE |
| /api/inventory/checks | GET | INVENTORY_CHECK |
| /api/inventory/checks/[id] | GET, PUT | INVENTORY_CHECK |
| /api/inventory/checks/[id]/items | PATCH | INVENTORY_CHECK |
| /api/inventory/corrections | GET | INVENTORY_MANAGE |
| /api/inventory/corrections/[id] | GET, PUT | INVENTORY_MANAGE |
| /api/inventory/corrections/batch | PATCH | INVENTORY_MANAGE |
| /api/inventory/corrections/summary | GET | INVENTORY_MANAGE |
| /api/inventory/daily-plans | GET | INVENTORY_MANAGE |
| /api/inventory/daily-plans/generate | POST | INVENTORY_MANAGE |
| /api/inventory/daily-plans/generate-bulk | POST | INVENTORY_MANAGE |
| /api/inventory/daily-plans/today | GET | INVENTORY_MANAGE |
| /api/inventory/daily-plans/[id] | GET, PUT, DELETE | INVENTORY_MANAGE |
| /api/inventory/daily-plans/[id]/approve | POST | INVENTORY_MANAGE |
| /api/inventory/daily-plans/[id]/skip | POST | INVENTORY_MANAGE |
| /api/inventory/dashboard | GET | INVENTORY_REPORT |
| /api/inventory/fiscal-years | GET, POST | INVENTORY_MANAGE |
| /api/inventory/fiscal-years/[id] | GET, PUT, DELETE | INVENTORY_MANAGE |
| /api/inventory/import | POST | INVENTORY_MANAGE |
| /api/inventory/my-progress | GET | INVENTORY_CHECK |
| /api/inventory/my-tasks | GET | INVENTORY_CHECK |
| /api/inventory/reports | GET | INVENTORY_REPORT |
| /api/inventory/reports/[id] | GET | INVENTORY_REPORT |
| /api/inventory/reports/[id]/excel | GET | INVENTORY_REPORT |
| /api/inventory/schedule | GET, POST, PUT, DELETE | INVENTORY_MANAGE |
| /api/inventory/sessions | GET, POST | INVENTORY_MANAGE |
| /api/inventory/sessions/[id] | GET, PUT, DELETE | INVENTORY_MANAGE |
| /api/inventory/sessions/[id]/start | POST | INVENTORY_MANAGE |
| /api/inventory/sessions/[id]/complete | POST | INVENTORY_MANAGE |
| /api/inventory/stock-snapshots | GET, POST | INVENTORY_MANAGE |
| /api/inventory/stock-snapshots/latest | GET | INVENTORY_MANAGE |
| /api/inventory/tranches | GET | INVENTORY_MANAGE |
| /api/inventory/tranches/[id] | GET, PUT, DELETE | INVENTORY_MANAGE |
| /api/inventory/tranches/[id]/start | POST | INVENTORY_MANAGE |
| /api/inventory/tranches/[id]/complete | POST | INVENTORY_MANAGE |
| /api/inventory/tranches/[id]/generate | POST | INVENTORY_MANAGE |
| /api/inventory/tranches/[id]/mandatory-items | GET, PATCH | INVENTORY_MANAGE |
| /api/inventory/year-end/generate | POST | INVENTORY_MANAGE |
| /api/inventory/year-end/start | POST | INVENTORY_MANAGE |
inventory/page.tsx
8 field mismatch tra frontend e API dashboard: positionPercent, valuePercent, workingDaysRemaining, stockValueTrend, problematicItems → oggetti annidati (positionProgress.percentage, workingDays.remaining, stockTrend, topProblematicItems). Progress ring sempre a 0%, grafici vuoti.
Fix: Allineati tutti i field access al tipo DashboardStats restituito da dashboard-stats.ts.
inventory/corrections/page.tsx
Colonna 'Quantità Contata' leggeva item.countedQty inesistente nel modello DiscrepancyCorrection.
Fix: item.countedQty → item.actualQty.
api/inventory/my-progress/route.ts
valuePercent hardcodato a 0 — stub non implementato. Barra progresso operatori mobile sempre a 0%.
Fix: Implementato calcolo reale: sum(unitPrice×expectedQty) per totale e contati nel fiscal year corrente.
api/inventory/corrections/route.ts + summary/route.ts
Scope overwrite: buildRoleScope() costruisce where.inventoryItem per RBAC, poi warehouseId sovrascriveva la stessa chiave azzerando il filtro di ruolo. Un BRAND_ADMIN poteva vedere correzioni di altri brand.
Fix: Merge invece di overwrite: { ...scopeWhere.inventoryItem, warehouseId } con casting Prisma typed.
api/inventory/corrections/batch/route.ts
PATCH batch eseguiva updateMany({ where: { id: { in: ids } } }) senza verificare che le correzioni appartengano al warehouse dell'utente. IDOR: un operatore poteva modificare correzioni di altri warehouse.
Fix: Aggiunto count pre-update con scopeWhere. Se accessibleCount < ids.length → 403.
api/inventory/alerts/[id]/dismiss/route.ts
findUnique({ where: { id } }) senza join al warehouse. Qualsiasi utente autenticato poteva dismissare alert di altri warehouse.
Fix: Sostituito con findFirst includendo warehouse.workshopId/brandId + inline role check.
api/inventory/reports/[id]/route.ts + excel/route.ts
findUnique non verificava ownership. Un utente poteva leggere o esportare in Excel report di warehouse non accessibili.
Fix: findFirst con buildRoleScope baked nel where — se il record non è accessibile ritorna 404.
api/inventory/sessions/[id]/route.ts (GET, PUT, DELETE)
Tutte le operazioni su sessione per ID usavano findUnique senza verifica warehouse. Possibile accesso cross-tenant.
Fix: Aggiunto checkSessionAccess() helper con include warehouse, controllo workshopId/brandId prima di ogni operazione.
api/inventory/fiscal-years/[id]/route.ts + tranches/[id]/*.ts
Stessa vulnerabilità IDOR su fiscal years, tranches (GET, PUT, start, complete, generate, mandatory-items).
Fix: Aggiunto checkWarehouseAccess() helper in ogni file; tutti i findUnique convertiti in findFirst con scope annidato nel where.
api/inventory/dashboard/route.ts
generateAlerts() era chiamato con await bloccando la risposta dashboard di 200-500ms anche quando non necessario.
Fix: Fire-and-forget: generateAlerts(warehouseId).catch(err => ...) — la dashboard risponde immediatamente.
lib/inventory/alert-engine.ts
N+1 query: per ogni alert generato si eseguiva findFirst + create separati (fino a 30+ query sequenziali per warehouse attivi).
Fix: Batch fetch di tutti gli alert esistenti (last 24h) in una query, costruzione Set di firme (type|itemId), createMany per tutti gli alert nuovi.
lib/inventory/dashboard-stats.ts
Sequenziale: count totale poi count contati poi findMany di tutti gli item per sommare unitPrice×expectedQty in JS (O(n) in memoria).
Fix: Promise.all con 2 count + $queryRaw PostgreSQL: SUM(COALESCE(unit_price,0)*expected_qty) con CASE WHEN per contati — una sola query SQL invece di N.
lib/inventory/rotation-calculator.ts
O(n×m) inner loop: per ogni item di tutti i warehouse veniva chiamato checkCounts.find() — scansione lineare dell'array ad ogni iterazione.
Fix: Map pre-costruita: new Map(checkCounts.map(c => [c.inventoryItemId, c])) → lookup O(1) per ogni item.
prisma/schema.prisma — InventoryCheckItem
La query più frequente (discrepancy trend, alert engine, rotation calculator) filtra su inventoryItemId + checkedAt ma non esisteva un indice composito.
Fix: Aggiunto @@index([inventoryItemId, checkedAt]) sul modello InventoryCheckItem.
inventory/corrections/page.tsx
handleStatusChange non mostrava alcun feedback visivo: successo e errore erano silenziosi. L'utente non sapeva se l'operazione era andata a buon fine.
Fix: Aggiunto addToast({ type: 'success'/'error' }) dopo ogni chiamata PUT /corrections/[id] con chiavi i18n in it/en/de/fr.
src/components/inventory/ProgressRing.tsx — componente SVG definito ma mai importato da nessuna pagina. La dashboard usa già un'implementazione inline equivalente.src/components/inventory/AnnualProgressBar.tsx — barra progresso annuale definita ma mai importata. Nessuna pagina la referenzia.report-generator.ts genera i dati ma la generazione PDF con @react-pdf/renderer non è ancora integrata. I report sono esportabili solo in Excel.