Skip to content

Conversation

@fabiodalez-dev
Copy link
Owner

@fabiodalez-dev fabiodalez-dev commented Dec 10, 2025

Summary

This PR introduces significant improvements to the reservation system, updater reliability, and fixes several critical bugs.

Reservation System Improvements

  • New ReservationReassignmentService: Handles automatic reassignment of reservations when:
    • A copy becomes available (returned)
    • A copy is marked as lost/damaged
    • A new copy is added to the library
  • Cron job for expired reservations: New scripts/check-expired-reservations.php script to automatically handle expired reservations
  • New loan states: Added annullato (cancelled) and scaduto (expired) to the prestiti status enum

Updater Reliability Improvements

  • Streaming database backup: Prevents memory exhaustion on large databases by writing directly to file instead of building in memory
  • Shutdown handler: Automatically cleans up maintenance mode on fatal errors
  • Stale maintenance recovery: checkStaleMaintenanceMode() auto-removes maintenance files older than 30 minutes
  • Memory management: Automatically increases memory limit to 256MB for large database operations
  • Concurrent update prevention: File locking prevents multiple simultaneous update attempts

Bug Fixes

  • JavaScript regex fix: Fixed invalid regex in catalog.php caused by PHP HEREDOC interpreting escape sequences (/\\/g became /\/g)
  • Availability calendar fix: getBookTotalCopies() now correctly returns 0 when all copies are lost/damaged (instead of falling back to copie_totali)
  • SQL column fix: Fixed references to non-existent columns in ReservationReassignmentService (nome_completonome, data_inizio/data_finedata_prestito/data_scadenza)

Migrations

  • migrate_0.4.2.sql: Index optimization for prestiti table
  • migrate_0.4.3.sql: Add 'annullato' and 'scaduto' to prestiti status enum

Test Plan

  • Test reservation creation and cancellation
  • Test copy status changes (available → lost → available)
  • Verify availability calendar shows correct data for books with lost copies
  • Test catalog page filters work without JavaScript errors
  • Verify updater backup creates valid SQL file
  • Test maintenance mode cleanup after simulated failure

Summary by CodeRabbit

Release Notes v0.4.2

  • New Features

    • Automatic reservation reassignment when books become available or unavailable
    • Loan cancellation functionality for users
    • Expired reservation check and automatic processing
    • Enhanced error handling and user notifications
  • Bug Fixes

    • Duplicate reservation prevention
    • Data integrity checks for canceled and expired loans
    • Improved book availability recalculation
  • Improvements

    • Loan list now displays most recent entries first
    • Better error messages for session expiration and active reservations

✏️ Tip: You can customize this high-level summary in your review settings.

- Fix table sorting in all admin pages (autori, editori, prestiti, utenti)
  - Parse DataTables order[0][column] and order[0][dir] parameters
  - Dynamic ORDER BY instead of hardcoded values
- Add styled session-expired.php error page for CSRF failures
- CsrfMiddleware detects AJAX vs form requests
  - AJAX: returns JSON error
  - Form: shows styled HTML error page
- Fix table sorting in all admin pages (autori, editori, prestiti, utenti)
  - Parse DataTables order[0][column] and order[0][dir] parameters
  - Dynamic ORDER BY instead of hardcoded values
- Add styled session-expired.php error page for CSRF failures
- CsrfMiddleware detects AJAX vs form requests
  - AJAX: returns JSON error
  - Form: shows styled HTML error page
## Reservation System
- Add ReservationReassignmentService for automatic copy reassignment
- Add scripts/check-expired-reservations.php cron job
- Fix availability calendar to correctly handle lost/damaged copies
- Add 'annullato' and 'scaduto' states to prestiti enum (migrate_0.4.3.sql)

## Updater Improvements
- Implement streaming database backup to prevent memory exhaustion
- Add shutdown handler to cleanup maintenance mode on fatal errors
- Add checkStaleMaintenanceMode() to auto-recover stuck updates
- Add memory limit increase for large database updates
- Add concurrent update prevention with file locking

## Bug Fixes
- Fix JavaScript regex escaping in catalog.php (HEREDOC double-escape)
- Fix getBookTotalCopies() fallback logic for lost copies
- Fix SQL column references in ReservationReassignmentService

## Other Changes
- Add English translations for new Updater log messages
- Update MaintenanceService with admin login hook
- Add copy status change logging in CopyController
@coderabbitai
Copy link

coderabbitai bot commented Dec 10, 2025

Walkthrough

Questa pull request implementa il riassegnamento automatico delle prenotazioni quando le copie diventano disponibili o non disponibili, aggiunge il supporto per soft delete dei libri, estende gli stati delle prenotazioni/prestiti, introduce un nuovo controllo di manutenzione per le prenotazioni scadute, e sostituisce il log di errori generico con SecureLogger in tutta l'applicazione.

Changes

Cohort / File(s) Riepilogo dei cambiamenti
Controllers - Reservation Reassignment
app/Controllers/CopyController.php, app/Controllers/LibriController.php, app/Controllers/PrestitiController.php
Aggiunto supporto per ReservationReassignmentService per riassegnare automaticamente le prenotazioni quando le copie cambiano stato (diventa unavailable o disponibile). Sostituzione di error_log con SecureLogger per il logging strutturato degli errori.
Controllers - Availability & Validation
app/Controllers/ReservationsController.php, app/Controllers/UserActionsController.php
Riepilogato getBookTotalCopies per controllare l'esistenza delle copie in tabella copie prima di contare le copie prestabili. Aggiunto nuovo metodo cancelLoan con gestione transazionale, validazione input, blocco righe, e riassegnamento delle copie riservate.
Services
app/Services/ReservationReassignmentService.php, app/Support/MaintenanceService.php
Introdotto nuovo ReservationReassignmentService per automatizzare il riassegnamento delle prenotazioni con gestione transazionale, locking pessimistico, e notifiche differite. Aggiunto checkExpiredReservations() a MaintenanceService per elaborare le prenotazioni scadute. Sostituzione di error_log con SecureLogger.
Repository & Data Access
app/Models/BookRepository.php, app/Support/DataIntegrity.php
Implementato soft delete filtrando con deleted_at IS NULL in tutte le query di recupero e nel metodo delete(). Aggiunto controllo della data integrità per prestiti con stato annullato/scaduto ma ancora attivi. Rinominato indice utenti da idx_ruolo a idx_tipo_utente.
Database Schema & Migrations
installer/database/schema.sql, installer/database/migrations/migrate_0.4.2.sql, installer/database/migrations/migrate_0.4.3.sql, installer/database/indexes_optimization.sql, installer/database/indexes_optimization_mysql.sql
Aggiunta colonna deleted_at a libri, sede_id a copie, nuovi enum values (prenotato, in_restauro, in_trasferimento) per copie.stato, e (annullato, scaduto) per prestiti.stato. Sostituito indice idx_ruolo con idx_tipo_utente.
Views & UI
app/Views/errors/session-expired.php, app/Views/prestiti/crea_prestito.php, app/Views/prestiti/index.php
Aggiunta nuova pagina di errore per sessione scaduta/CSRF. Aggiunto messaggio di errore duplicate_reservation nel modulo di creazione prestito. Modificato ordering DataTables per applicare il default server (p.id DESC).
Utilities & Infrastructure
app/Support/PluginManager.php, app/Support/Updater.php, storage/plugins/api-book-scraper/wrapper.php
Introdotte variabili prefissate in PluginManager per evitare conflitti di scope nei plugin. Aggiunto logging esteso in Updater, nuovo metodo checkStaleMaintenanceMode(). Rinominata variabile plugin locale in api-book-scraper.
Scripts & Automation
scripts/check-expired-reservations.php
Introdotto nuovo script cron per elaborare le prenotazioni scadute: contrassegna come scadute, aggiorna stato copie, e riassegna tramite ReservationReassignmentService.
Translations
locale/en_US.json
Aggiunti numerosissimi nuovi chiavi di traduzione per flussi di aggiornamento/manutenzione, messaggi di errore, messaggi di notifica, e stato dei servizi di manutenzione.
Version
version.json
Aggiornato numero versione da 0.4.1 a 0.4.2.

Sequence Diagram

sequenceDiagram
    actor User
    participant Ctrl as Controller<br/>(Copy/Libri/Prestiti)
    participant RRS as ReservationReassignment<br/>Service
    participant DB as Database
    participant CopyRepo as CopyRepository
    participant NotifServ as NotificationService
    participant Logger as SecureLogger

    Note over Ctrl,Logger: Scenario 1: Copy diventa non-disponibile (es. danneggiato)
    User->>Ctrl: Aggiorna stato copia
    Ctrl->>DB: BEGIN TRANSACTION
    Ctrl->>DB: UPDATE copie SET stato=?
    Ctrl->>RRS: reassignOnCopyLost(copiaId)
    RRS->>DB: SELECT ... FOR UPDATE prenotazione attiva
    alt Prenotazione attiva trovata
        RRS->>CopyRepo: findAvailableCopy(libroId)
        alt Copia disponibile esiste
            RRS->>DB: UPDATE prestiti SET copia_id=?
            RRS->>DB: UPDATE copie SET stato='prenotato'
            RRS->>Logger: Log reassignment success
            RRS->>NotifServ: deferNotification() (se transazione esterna)
        else No available copy
            RRS->>DB: UPDATE prestiti SET copia_id=NULL
            RRS->>Logger: Log no copy available
            RRS->>NotifServ: notifyUserCopyUnavailable()
        end
    end
    Ctrl->>DB: COMMIT
    RRS->>NotifServ: flushDeferredNotifications()
    RRS-->>User: Notifica via email (copia assegnata/non disponibile)

    Note over Ctrl,Logger: Scenario 2: Nuova copia creata (store libri)
    User->>Ctrl: Crea nuovo libro con copia
    Ctrl->>DB: BEGIN TRANSACTION
    Ctrl->>DB: INSERT INTO libri
    Ctrl->>DB: INSERT INTO copie
    Ctrl->>RRS: reassignOnNewCopy(libroId, newCopiaId)
    RRS->>DB: SELECT ... FOR UPDATE prenotazione pendente più vecchia
    alt Prenotazione trovata
        RRS->>DB: UPDATE prestiti SET copia_id=newCopiaId
        RRS->>DB: UPDATE copie SET stato='prenotato'
        RRS->>NotifServ: deferNotification()
    end
    Ctrl->>DB: COMMIT
    RRS->>NotifServ: flushDeferredNotifications()
    RRS-->>User: Notifica: copia assegnata alla prenotazione

    Note over Ctrl,Logger: Scenario 3: Copia restituita (return prestito)
    User->>Ctrl: Restituisci prestito
    Ctrl->>DB: BEGIN TRANSACTION
    Ctrl->>DB: UPDATE prestiti SET stato='restituito'
    Ctrl->>RRS: reassignOnReturn(copiaId)
    RRS->>DB: SELECT libro da copiaId
    RRS->>RRS: reassignOnNewCopy(libroId, copiaId)
    RRS->>DB: SELECT ... FOR UPDATE prossima prenotazione
    alt Prenotazione in attesa
        RRS->>DB: UPDATE prestiti, copie
        RRS->>NotifServ: deferNotification()
    else Nessuna prenotazione
        RRS->>DB: UPDATE copie SET stato='disponibile'
    end
    Ctrl->>DB: COMMIT
    RRS->>NotifServ: flushDeferredNotifications()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Aree che richiedono attenzione particolare:

  • ReservationReassignmentService (app/Services/ReservationReassignmentService.php): Logica transazionale complessa con SELECT FOR UPDATE, gestione delle notifiche differite, locking pessimistico e multiple scenari di fallimento. Verificare la correttezza della gestione dei deadlock e del rollback.

  • Integrazione tra controller (CopyController, LibriController, PrestitiController): Ogni controller chiama RRS con gestione try-catch e logging. Verificare coerenza del flusso d'errore, che il rollback sia coerente, e che le notifiche siano effettivamente inviate post-commit.

  • Migrazioni database (schema.sql, migrate_0.4.2.sql, migrate_0.4.3.sql): Verifica che i nuovi ENUM values siano supportati dai vincoli di validazione nei controller, e che la soft delete (deleted_at) non crei inconsistenze con le query esistenti non aggiornate.

  • BookRepository soft delete (app/Models/BookRepository.php): Assicurare che tutte le query di lettura includano il filtro deleted_at IS NULL, e che il delete() aggiorna veramente la colonna invece di eliminare righe (verificare la sintassi UPDATE).

  • MaintenanceService.checkExpiredReservations() (app/Support/MaintenanceService.php): Logica per marcare prenotazioni come scadute, liberare copie, e riassegnare. Controllare la corretta sequenza di transazioni, flushing di notifiche, e il calcolo della disponibilità.

  • UserActionsController.cancelLoan() (app/Controllers/UserActionsController.php): Nuovo metodo con gestione transazionale, row locking, e riassegnamento. Verificare il corretto utilizzo di FOR UPDATE e che il commit/rollback sia appropriato in tutti i percorsi.

Possibly related PRs

Poem

🐰 Con code e copie in riassegnamento,
Le prenotazioni trovano il loro momento,
Un servizio nuovo, transazionale e saggio,
Fa giungere i libri al lettore con coraggio!
Soft delete e logging pulito,
Un'opera di manutenzione ben definita!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.14% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Il titolo descrive accuratamente i miglioramenti del sistema di prenotazione e la correzione della disponibilità, che sono i cambiamenti principali del PR.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch prenotazioni

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

♻️ Duplicate comments (1)
app/Controllers/PrestitiController.php (1)

613-625: Uso di ReservationReassignmentService dentro transazione di restituzione (vedi nota sul servizio)

La chiamata a $reassignmentService->reassignOnReturn($copia_id) avviene all’interno di una transazione già aperta in processReturn(). Poiché i metodi del servizio aprono e chiudono a loro volta transazioni, si applicano qui le stesse considerazioni su transazioni nidificate e visibilità dei dati già commentate in App\Services\ReservationReassignmentService (rischio di commit parziali o di non vedere lo stato aggiornato della copia).

Suggerisco di rivedere l’ownership delle transazioni come indicato nel commento sul servizio.

Also applies to: 637-645

🧹 Nitpick comments (17)
app/Models/BookRepository.php (2)

626-667: syncAuthors efficiente, ma fallback di processAuthorId migliorabile

L’early return in syncAuthors quando !$authorIds evita una INSERT loop inutile dopo la DELETE: bene. In processAuthorId, invece, il fallback finale return (int) $authorData; potrebbe trasformare valori inaspettati (es. array/oggetti) in interi con warning di conversione; visto che i soli formati validi sembrano essere “ID numerico” e stringhe new_*, conviene rendere il fallback esplicitamente sicuro.

Suggerimento:

     private function processAuthorId($authorData): int
     {
         // Handle both old format (just ID) and new format (could be temp ID with label)
         if (is_numeric($authorData)) {
-            return (int) $authorData;
+            return (int) $authorData;
         }
 
         // Handle new author format from Choices.js (new_timestamp)
         if (is_string($authorData) && strpos($authorData, 'new_') === 0) {
             // This is a new author that needs to be created
             // For now, just skip it - this would need additional handling
             // based on your form submission logic
             return 0;
         }
-
-        return (int) $authorData;
+
+        // Fallback: formati non numerici/inaspettati non vengono usati
+        return 0;
     }

724-769: updateOptionals più sicuro, con piccolo nit di leggibilità SQL

La costruzione dinamica di $cols con early return quando è vuoto evita UPDATE inutili, e la distinzione di tipo (i per numero_pagine/anno_pubblicazione, s per il resto) è corretta. Micro‑nit: per leggibilità della query, potresti aggiungere uno spazio dopo la virgola in implode:

-        $sql = 'UPDATE libri SET ' . implode(',', $set) . ', updated_at=NOW() WHERE id=?';
+        $sql = 'UPDATE libri SET ' . implode(', ', $set) . ', updated_at=NOW() WHERE id=?';
app/Views/errors/session-expired.php (1)

9-11: Permettere l’override di $pageTitle e $loginUrl dalla chiamante

Qui le variabili vengono sempre sovrascritte, impedendo a chi include la view di passare un titolo personalizzato o un URL di login diverso (/login hardcoded):

- $pageTitle = __('Sessione Scaduta');
- $loginUrl = '/login';
+ $pageTitle = $pageTitle ?? __('Sessione Scaduta');
+ $loginUrl = $loginUrl ?? '/login';

Così mantieni i default attuali ma consenti configurazioni diverse quando necessario.

.gitignore (1)

319-321: Ignorare CHANGELOG.md nel repository

Aggiungere CHANGELOG.md alla sezione “Verbose documentation” rende chiaro che il changelog principale non viene tracciato in git (oltre a essere escluso dai pacchetti). Se è davvero gestito fuori repo o generato automaticamente, la scelta è coerente; altrimenti valuterei se mantenerlo versionato.

installer/database/schema.sql (1)

342-342: Suggerimento: considera un indice su deleted_at per le query di soft delete.

Per ottimizzare le query che filtrano i libri non cancellati (es. WHERE deleted_at IS NULL), potrebbe essere utile aggiungere un indice parziale o un indice standard su questa colonna.

KEY `idx_libri_deleted_at` (`deleted_at`)
scripts/check-expired-reservations.php (1)

40-109: Implementazione corretta dello script per prenotazioni scadute.

Lo script gestisce correttamente le prenotazioni scadute:

Query e selezione (linee 40-49):

  • Seleziona correttamente le prenotazioni con stato='prenotato', attivo=1 e data_scadenza < oggi

Elaborazione transazionale (linee 62-76):

  • Usa transazioni per garantire atomicità
  • Marca la prenotazione come 'scaduto' e attivo=0
  • Aggiunge una nota di sistema con la data

Gestione copia (linee 78-97):

  • Usa FOR UPDATE per il locking corretto (linea 80)
  • Verifica lo stato della copia prima di modificarla (linea 86)
  • Rilascia la copia (disponibile) e attiva il riassegnamento (linea 95)

Nota: Le notifiche utente sono commentate (linee 103-104) con un chiaro promemoria che il metodo deve essere aggiunto. Considera di implementare questa funzionalità per migliorare l'esperienza utente.

Vuoi che generi il metodo notifyUserReservationExpired() per il NotificationService?

locale/en_US.json (3)

1690-1703: Chiavi MaintenanceService ben strutturate

Le chiavi di log per MaintenanceService coprono i vari casi d’errore e di esecuzione e sono denominate in modo chiaro; le traduzioni inglesi sono aderenti all’italiano. Verifica solo che le stesse chiavi esistano anche negli altri file di localizzazione, se supportati.


2474-2474: Label “Scaduta il”

L’etichetta “Scaduta il” → “Expired on” è corretta grammaticalmente e adatta a prenotazioni/risorse al femminile. Assicurati solo di usare la stessa forma ovunque per coerenza con lo stato scaduto/scaduta.


3594-3623: Messaggi per riassegnazione copie e prenotazioni completano bene il dominio

Le nuove stringhe per errori di riassegnazione, notifiche di copia non disponibile e messaggi di messa “in attesa” della prenotazione sono coerenti con il comportamento descritto nel PR e i placeholder %s risultano corretti. L’unica nota è “DataIntegrity warning (store loan)”, lasciata in forma ibrida: se è un puro tag tecnico di log va bene così, altrimenti potresti localizzarla per allinearla al resto.

app/Controllers/UserActionsController.php (6)

185-188: Log errore in cancelLoan

Il passaggio da error_log a SecureLogger::error con messaggio localizzato è positivo. Valuta di includere nel contesto anche loan_id, user_id (e magari copia_id) per rendere i log più utili in fase di debug.


259-262: Uso di SecureLogger in cancelReservation

Anche qui il passaggio a SecureLogger::error è corretto e coerente con il resto del codice. Come sopra, aggiungere reservation_id e user_id nel contesto renderebbe il log più diagnosticabile.


359-362: Log strutturato per changeReservationDate

La gestione degli errori con SecureLogger::error(__('Errore modifica data prenotazione'), [...]) è adeguata. Potresti includere anche reservation_id, user_id, libro_id e la nuova data richiesta per facilitare l’analisi di eventuali problemi in produzione.


446-447: Warning su notifica richiesta prestito non inviata

L’uso di SecureLogger::warning nel catch della notifica (notifyLoanRequest) è appropriato e mantiene l’operazione principale di creazione del prestito indipendente da eventuali problemi di notifica. Anche qui, aggiungere loan_id, libro_id e user_id nel contesto sarebbe utile.


451-454: Gestione errori nel flusso loan()

La sostituzione di error_log con SecureLogger::error(__('Errore richiesta prestito'), ...) nel catch esterno è corretta. Suggerirei di includere nel contesto almeno libro_id e utente_id, così da poter correlare rapidamente gli errori ai record interessati.


562-565: Logging errori in reserve()

Il nuovo log tramite SecureLogger::error(__('Errore prenotazione'), ...) nel catch del metodo reserve è coerente con gli altri flussi di prenotazione/prestito. Anche qui vale la stessa raccomandazione: aggiungere libro_id, utente_id e magari l’intervallo di date richiesto rende il log molto più utile.

app/Support/MaintenanceService.php (1)

61-82: Aggiornare la firma di ritorno di runAll() per includere expired_reservations

runAll() ora popola $results['expired_reservations'], ma la phpdoc del metodo non include questa chiave nella shape dell’array restituito. Per evitare sorprese a chi fa static analysis o IDE completion, converrebbe aggiungere expired_reservations: int alla annotation di ritorno.

Also applies to: 98-105

app/Controllers/PrestitiController.php (1)

824-835: CSV prestiti: includere i nuovi stati annullato e scaduto

Nel CSV:

  • \$validStates non include i nuovi stati enum annullato e scaduto, quindi non è possibile filtrarli via ?stati= anche se esistono in DB.
  • \$statusLabels non ha etichette localizzate per questi stati, quindi nel CSV apparirebbero eventualmente non tradotti.

Per mantenere coerenza con l’enum di prestiti, suggerisco di:

-        $validStates = ['pendente', 'prenotato', 'in_corso', 'in_ritardo', 'restituito', 'perso', 'danneggiato'];
+        $validStates = ['pendente', 'prenotato', 'in_corso', 'in_ritardo', 'restituito', 'perso', 'danneggiato', 'annullato', 'scaduto'];

e di estendere le label:

         $statusLabels = [
             'pendente' => __('Pendente'),
             'prenotato' => __('Prenotato'),
             'in_corso' => __('In Corso'),
             'in_ritardo' => __('In Ritardo'),
             'restituito' => __('Restituito'),
             'perso' => __('Perso'),
             'danneggiato' => __('Danneggiato'),
+            'annullato' => __('Annullato'),
+            'scaduto' => __('Scaduto'),
         ];

Also applies to: 909-917

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26e3414 and 3326929.

📒 Files selected for processing (27)
  • .gitignore (1 hunks)
  • .rsync-filter (1 hunks)
  • app/Controllers/AutoriApiController.php (1 hunks)
  • app/Controllers/CopyController.php (2 hunks)
  • app/Controllers/EditoriApiController.php (2 hunks)
  • app/Controllers/LibriController.php (5 hunks)
  • app/Controllers/PrestitiApiController.php (2 hunks)
  • app/Controllers/PrestitiController.php (10 hunks)
  • app/Controllers/ReservationsController.php (1 hunks)
  • app/Controllers/UserActionsController.php (7 hunks)
  • app/Controllers/UtentiApiController.php (1 hunks)
  • app/Middleware/CsrfMiddleware.php (1 hunks)
  • app/Models/BookRepository.php (18 hunks)
  • app/Services/ReservationReassignmentService.php (1 hunks)
  • app/Support/MaintenanceService.php (7 hunks)
  • app/Support/PluginManager.php (2 hunks)
  • app/Support/Updater.php (6 hunks)
  • app/Views/errors/session-expired.php (1 hunks)
  • app/Views/frontend/catalog.php (5 hunks)
  • app/Views/prestiti/crea_prestito.php (1 hunks)
  • installer/database/migrations/migrate_0.4.2.sql (1 hunks)
  • installer/database/migrations/migrate_0.4.3.sql (1 hunks)
  • installer/database/schema.sql (3 hunks)
  • locale/en_US.json (5 hunks)
  • scripts/check-expired-reservations.php (1 hunks)
  • storage/plugins/api-book-scraper/wrapper.php (1 hunks)
  • updater.md (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
app/Views/prestiti/crea_prestito.php (1)
app/helpers.php (1)
  • __ (25-28)
app/Middleware/CsrfMiddleware.php (1)
app/helpers.php (1)
  • __ (25-28)
app/Models/BookRepository.php (4)
app/Support/MaintenanceService.php (1)
  • __construct (31-34)
app/Models/PublisherRepository.php (1)
  • __construct (10-10)
app/Models/AuthorRepository.php (1)
  • __construct (10-10)
app/Models/DashboardStats.php (1)
  • __construct (10-10)
storage/plugins/api-book-scraper/wrapper.php (1)
storage/plugins/api-book-scraper/ApiBookScraperPlugin.php (1)
  • ApiBookScraperPlugin (14-609)
app/Controllers/UserActionsController.php (2)
app/Services/ReservationReassignmentService.php (1)
  • ReservationReassignmentService (17-379)
app/Support/DataIntegrity.php (2)
  • DataIntegrity (9-1222)
  • recalculateBookAvailability (234-348)
app/Controllers/CopyController.php (1)
app/Services/ReservationReassignmentService.php (3)
  • ReservationReassignmentService (17-379)
  • reassignOnCopyLost (113-180)
  • reassignOnReturn (186-203)
app/Support/Updater.php (1)
app/Controllers/UpdateController.php (1)
  • createBackup (122-152)
🪛 LanguageTool
updater.md

[uncategorized] ~11-~11: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ...Quando aggiungi un file a .gitignore, **DEVI aggiungerlo ANCHE a .rsync-filter...

(GR_09_001)


[uncategorized] ~105-~105: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ...ipt per creare pacchetti di release | | Filter Rules | .rsync-filter | **⭐ R...

(GR_09_001)


[uncategorized] ~105-~105: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ... | Filter Rules | .rsync-filter | **⭐ Regole include/exclude per rsync (PRIM...

(GR_09_001)


[uncategorized] ~296-~296: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ... ## File version.json Questo file è la fonte di verità per la versione dell'a...

(GR_09_001)


[uncategorized] ~512-~512: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ...# Scopo Questo file contiene lo schema COMPLETO del database per le fresh ins...

(GR_09_001)


[uncategorized] ~702-~702: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ... .rsync-filter | rsync filter rules | Primario (raccomandato) | | `.distigno...

(GR_09_001)


[uncategorized] ~707-~707: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ...i gitignore con negazioni (!path) che NON FUNZIONANO con `rsync --exclude-fr...

(GR_09_001)


[uncategorized] ~738-~738: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ... pattern` | | Ordine | Non importante | CRITICO - primo match vince | | Plugin...

(GR_09_001)


[uncategorized] ~764-~764: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ... nel Pacchetto Il pacchetto di release INCLUDE tutto il necessario per funzio...

(GR_09_001)


[uncategorized] ~775-~775: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "*".
Context: ... node_modules?
1. Gli utenti possono personalizzare CSS/JS senza dover conf...

(GR_09_001)


[uncategorized] ~776-~776: In questo caso l'uso della 'd' eufonica è ammesso: "ed eseguire".
Context: ...ta modificare i sorgenti in frontend/ e eseguire npm run build 3. Senza node_modules, ...

(ST_03_001)


[uncategorized] ~778-~778: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ... su hosting condivisi 4. Il pacchetto è autosufficiente - funziona subito, per...

(GR_09_001)


[uncategorized] ~836-~836: In questo caso l'uso della 'd' eufonica è ammesso: "ed estrae".
Context: ... | downloadUpdate($version) | Scarica e estrae il pacchetto | | createBackup() | Cre...

(ST_03_001)


[uncategorized] ~986-~986: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ... Versione ### Prima del Release - [ ] version.json aggiornato con nuova vers...

(GR_09_001)


[uncategorized] ~987-~987: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "*".
Context: ...n
aggiornato con nuova versione - [ ] Migrazione SQL creata (se modifiche DB...

(GR_09_001)


[uncategorized] ~988-~988: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "*".
Context: ...ne SQL
creata (se modifiche DB) - [ ] schema.sql sincronizzato con migrazion...

(GR_09_001)


[uncategorized] ~989-~989: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "*".
Context: ...ql
sincronizzato con migrazione - [ ] data_it_IT.sql aggiornato (se nuovi se...

(GR_09_001)


[uncategorized] ~990-~990: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "*".
Context: ....sql
aggiornato (se nuovi seed) - [ ] data_en_US.sql aggiornato (se nuovi se...

(GR_09_001)


[uncategorized] ~991-~991: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "*".
Context: ....sql
aggiornato (se nuovi seed) - [ ] locale/en_US.json aggiornato (se nuove...

(GR_09_001)


[uncategorized] ~992-~992: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "*".
Context: ...
aggiornato (se nuove stringhe) - [ ] CHANGELOG.md aggiornato (opzionale) - ...

(GR_09_001)


[uncategorized] ~1148-~1148: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ... Sotto "Workflow permissions" seleziona "Read and write permissions" 3. Spunta...

(GR_09_001)


[uncategorized] ~1168-~1168: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ...i CI) Prima di creare un tag, verifica SEMPRE: ```bash # 1. package-lock.jso...

(GR_09_001)


[uncategorized] ~1317-~1317: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ...e esclusioni, ma rsync --exclude-from NON supporta questa sintassi. ```giti...

(GR_09_001)


[uncategorized] ~1427-~1427: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ...e permissions" 2. Oppure (consigliato): Fai la build in locale e usa `gh relea...

(GR_09_001)


[uncategorized] ~1479-~1479: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ...alla distribuzione deve essere aggiunto ESPLICITAMENTE a .rsync-filter: ```...

(GR_09_001)


[uncategorized] ~1517-~1517: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "**".
Context: ...
: Il plugin scraping-pro è un plugin PREMIUM venduto separatamente. **NON ...

(GR_09_001)


[uncategorized] ~1609-~1609: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ...ione idempotente (CREATE IF NOT EXISTS, NO AFTER) - [ ] schema.sql sincroniz...

(GR_09_001)


[uncategorized] ~1682-~1682: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ...ile di Migrazione I file di migrazione NON DEVONO MAI essere eliminati, anche...

(GR_09_001)


[uncategorized] ~1686-~1686: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ... nel Pacchetto Il pacchetto di release INCLUDE la cartella vendor/ producti...

(GR_09_001)

🪛 markdownlint-cli2 (0.18.1)
updater.md

80-80: Link fragments should be valid

(MD051, link-fragments)


113-113: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


330-330: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


339-339: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


506-506: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


568-568: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


649-649: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


863-863: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


897-897: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


1697-1697: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🔇 Additional comments (32)
app/Models/BookRepository.php (5)

8-12: Costruttore con property promotion e type hint mysqli

Buona scelta usare public function __construct(private mysqli $db) in linea con gli altri repository (AuthorRepository, PublisherRepository, ecc.): meno boilerplate e dipendenza tipizzata chiaramente.


46-126: Gestione null in getById e caricamento autori

L’early return if (!$row) return null; è coerente con la firma ?array e rende esplicito il caso “libro non trovato”. Il loop sugli autori con l’array autori inizializzato esplicitamente migliora la leggibilità senza cambiare il comportamento.


176-391: Normalizzazione input e cast espliciti in createBasic/updateBasic

I cast espliciti per ID (scaffale/mensola/posizione/posizione_id), codici (isbn*, ean), importi (peso, prezzo) e copie (copie_totali, copie_disponibili), insieme alla conversione di stringhe vuote in null, rendono più robusta l’INSERT/UPDATE contro input “sporchi” da form. Anche il logging con cast (int) per insert_id e code mantiene i payload coerenti.

Also applies to: 393-599


601-624: Gestione enum e cache di metadati colonne

La normalizzazione degli enum tramite trim((string) $value) e confronto case‑insensitive contro le opzioni di getEnumOptions() è sensata e tollera maiuscole/minuscole dall’UI. La cache per colonne ($columnCache, $tableColumnCache) e per enum ($enumCache con chiave $table . '.' . $column) riduce round‑trip a INFORMATION_SCHEMA/SHOW COLUMNS senza complicare troppo il codice. La whitelist di tabelle in hasColumnInTable() è una buona difesa contro SQL injection sul nome tabella, con l’avvertenza che future tabelle dovranno essere aggiunte lì.

Also applies to: 771-785, 787-823, 828-849


669-679: Gestione null in getScaffaleLetter/getMensolaLevel

Il controllo sul risultato di fetch() e il cast esplicito di $level solo quando non è null rendono i metodi più sicuri rispetto a valori mancanti in DB; il contratto che restituisce null in caso di nessun risultato è chiaro e facile da consumare da buildCollocazioneString.

Also applies to: 681-694

.rsync-filter (1)

175-179: Esclusione esplicita di CHANGELOG.md dal pacchetto

L’aggiunta di CHANGELOG.md agli EXCLUDES rende esplicito che il changelog non deve finire nel pacchetto di distribuzione, in linea con la gestione della documentazione altrove. Nessuna criticità.

storage/plugins/api-book-scraper/wrapper.php (1)

11-20: Rinomina a $_pluginInstance e registry globale

L’uso di $_pluginInstance al posto di $plugin evita conflitti con la variabile $plugin del chiamante e mantiene coerente la registrazione in $GLOBALS['plugins']['api-book-scraper']. Comportamento invariato, cambio pulito.

app/Controllers/UtentiApiController.php (1)

87-115: Ordinamento DataTables lato server implementato in modo sicuro

La gestione dell’ordinamento tramite columnMap e normalizzazione di dir ad ASC/DESC garantisce un ORDER BY sicuro e sotto controllo applicativo. Il fallback su u.cognome + u.nome mantiene un ordinamento deterministico. Sembra tutto a posto.

Also applies to: 119-120

app/Controllers/EditoriApiController.php (1)

99-124: Ordinamento dinamico editori coerente con filtri e HAVING

La mappa columnMap e la normalizzazione di dir permettono di costruire ORDER BY sicuri, inclusa la colonna derivata libri_count. L’uso di $subOrderColumn nella subquery quando è presente HAVING mantiene lo stesso comportamento di ordinamento anche sui risultati filtrati per numero di libri. Implementazione solida.

Also applies to: 174-191

app/Controllers/AutoriApiController.php (1)

145-169: Ordinamento dinamico autori ben integrato con colonne opzionali

La gestione di columnMap con la colonna nazionalità risolta via $colNaz e l’alias libri_count rende l’ordinamento flessibile ma sicuro. L’ORDER BY $orderColumn $orderDir usa solo colonne note e direzioni normalizzate, mantenendo intatti i filtri esistenti. Nessuna anomalia evidente.

Also applies to: 171-175

app/Support/PluginManager.php (1)

1139-1177: Isolamento delle variabili del plugin in loadPlugin

L’introduzione delle variabili prefissate ($_pluginId, $_pluginName, $_pluginPath, $_mainFile) prima del require_once elimina il rischio che il file del plugin sovrascriva $plugin e rompa la logica successiva. L’uso coerente di $_pluginId per setPluginId() e registerPluginHooks() e il logging su SecureLogger migliorano robustezza e tracciabilità del caricamento dei plugin.

installer/database/migrations/migrate_0.4.3.sql (1)

1-5: LGTM - Migrazione corretta per estendere l'enum stato.

La migrazione aggiunge correttamente i nuovi valori annullato e scaduto all'enum prestiti.stato, mantenendo i valori esistenti e il default. La modifica è allineata con schema.sql (linea 596).

app/Views/prestiti/crea_prestito.php (1)

75-77: LGTM - Nuovo caso di errore ben integrato.

Il nuovo caso duplicate_reservation segue correttamente il pattern esistente degli altri casi di errore e utilizza la funzione __() per l'internazionalizzazione.

installer/database/schema.sql (2)

596-596: LGTM - Estensione enum prestiti.stato corretta.

I nuovi valori annullato e scaduto sono allineati con la migrazione migrate_0.4.3.sql e supportano la gestione delle prenotazioni scadute descritta negli obiettivi del PR.


113-115: sede_id field design is intentional.

The sede_id field is added without a foreign key constraint because the sedi table does not exist in the schema. This is appropriate as a preparatory field for future functionality. Once the sedi table is created, consider adding a foreign key constraint to sede_id at that time.

app/Controllers/CopyController.php (2)

8-8: LGTM - Import corretto.

L'import di SecureLogger è necessario per il logging degli errori nella nuova logica di reassignment.


155-160: LGTM - Ordine corretto delle operazioni.

Il ricalcolo della disponibilità del libro (recalculateBookAvailability) avviene correttamente dopo la logica di reassignment, garantendo che i contatori siano aggiornati dopo tutte le modifiche.

installer/database/migrations/migrate_0.4.2.sql (1)

1-15: Remove migration files or implement migration execution system.

The migration files (migrate_0.4.2.sql, migrate_0.4.3.sql) exist but are never executed by the installer. The Installer.php only processes schema.sql, data_*.sql, and triggers.sql. These migration files have no execution path in the codebase and will remain unused. Either remove them or implement a migration runner that loads and executes migrations in version order during installation.

Likely an incorrect or invalid review comment.

app/Controllers/PrestitiApiController.php (1)

107-141: Implementazione corretta dell'ordinamento con whitelist.

L'approccio all'ordinamento dinamico è sicuro:

  1. L'indice della colonna è convertito a int (linea 123)
  2. La direzione è validata (ASC o DESC, linea 124)
  3. Solo le colonne nella whitelist $columnMap possono essere usate (linee 126-129)

L'interpolazione diretta nella clausola ORDER BY (linea 141) è necessaria perché ORDER BY non può essere parametrizzato nei prepared statement. La whitelist fornisce protezione adeguata contro SQL injection.

Nota: La colonna calcolata 'utente' (linea 115) viene ordinata usando l'alias dalla SELECT. Questo funziona correttamente in MySQL, ma verifica che l'ordinamento per nome utente produca i risultati attesi.

app/Middleware/CsrfMiddleware.php (1)

63-126: Implementazione corretta della gestione CSRF con supporto AJAX.

L'implementazione distingue correttamente tra richieste AJAX e form tradizionali:

Richieste AJAX (linee 66-83):

  • Restituisce JSON con codici di errore appropriati (SESSION_EXPIRED o CSRF_INVALID)
  • Include un URL di redirect quando la sessione è scaduta
  • Usa le traduzioni per i messaggi di errore

Richieste form (linee 84-94):

  • Mostra una pagina HTML stilizzata per una migliore UX
  • Usa il pattern ob_start/ob_get_clean per il rendering della view

Metodo isAjaxRequest (linee 105-126):

  • Controlla X-Requested-With header (jQuery e framework comuni)
  • Controlla Accept header per preferenza JSON
  • Controlla Content-Type per richieste JSON
  • L'approccio euristico è solido e copre i casi d'uso comuni

Nota: L'uso di require (linea 89) invece di include causerà un fatal error se la view non esiste, ma questo è accettabile dato che indica un'installazione danneggiata.

updater.md (1)

1-1698: Documentazione completa e ben strutturata del sistema di aggiornamento.

Il documento fornisce una guida esauriente per il sistema di aggiornamento Pinakes:

Punti di forza:

  1. Sezione critica iniziale (linee 5-62): Evidenzia chiaramente la regola fondamentale che .gitignore e .rsync-filter sono sistemi separati
  2. Struttura completa: Copre tutti gli aspetti (versioning, migrazioni, build, troubleshooting)
  3. Lezioni apprese (linee 1309-1546): Documentazione preziosa dei problemi incontrati e delle soluzioni
  4. Checklist pratiche: Fornisce checklist verificabili per evitare errori comuni
  5. Flow consigliato (linee 1213-1280): Guida passo-passo per creare release

Nota: I problemi di formattazione markdown segnalati dall'analisi statica (mancanza di identificatori lingua nei code block, uso di emphasis invece di heading) sono minori e non impattano la leggibilità.

Suggerimento: Considera di aggiungere un indice cliccabile all'inizio per facilitare la navigazione in un documento così lungo.

app/Controllers/ReservationsController.php (1)

377-417: Comportamento del fallback per i dati legacy è intenzionale e coerente.

La logica è corretta: il metodo restituisce accuratamente 0 quando tutti i dati sono tracciati nella tabella copie e tutti risultano non disponibili (persi/danneggiati/in manutenzione).

Il fallback per i dati legacy (linea 409) usa GREATEST(IFNULL(copie_totali, 1), 1) che restituisce sempre almeno 1. Questo è un design intenzionale confermato da:

  • Commento esplicito nel codice: "This handles legacy data where copies weren't tracked individually"
  • Implementazione identica in ReservationManager.php con cross-reference: "This ensures consistency with ReservationsController::getBookTotalCopies()"
  • Sistema a due livelli: tracciamento moderno (tabella copie, può tornare 0) vs. dati legacy (colonna copie_totali, minimo 1)

La validazione di sistema impedisce comunque che copie_totali sia 0 (enforced in LibriController), quindi questa assunzione di minimo 1 per dati senza tracciamento individuale è ragionevole.

locale/en_US.json (2)

110-129: Messaggi di avanzamento/errore aggiornamento coerenti

Le nuove stringhe per stato avanzamento, backup e errori di aggiornamento sono coerenti con il resto del file e i placeholder risultano corretti. Nessun problema da segnalare.


2271-2271: Nuovo messaggio per doppio prestito/prenotazione

La stringa “Questo utente ha già un prestito o una prenotazione attiva per questo libro.” riflette correttamente la logica di prevenzione dei duplicati ed è tradotta in modo chiaro.

app/Controllers/UserActionsController.php (2)

8-8: Import di SecureLogger corretto

L’import di App\Support\SecureLogger è coerente con i nuovi utilizzi nel file e non introduce problemi di namespace.


217-221: Commento esplicativo in cancelReservation

Il commento aggiunto sul possibile uso improprio di reservation_id per oggetti prestiti chiarisce bene un edge case lato frontend. Nessun problema funzionale.

app/Controllers/LibriController.php (2)

671-741: Allineamento gestione scraped_author_bio in store/update

L’uso di $scrapedAuthorBio / $scrapedAuthorBioUpdate è ora consistente tra store() e update(), e l’aggiornamento condizionale delle biografie solo se vuote per il primo autore selezionato è coerente con il commento. Nessun problema evidente da segnalare.

Also applies to: 1198-1265


1340-1375: Riassegnazione e gestione lista d’attesa su nuove copie

La sequenza “crea copia → ReservationReassignmentService::reassignOnNewCopy()ReservationManager::processBookAvailability() su ogni nuova copia è coerente con il modello a due livelli (prestiti prenotati + coda prenotazioni) e non introduce problemi di concorrenza evidenti, dato che il servizio gestisce i lock sulla copia. L’eventuale costo di istanziare i servizi dentro il loop è marginale rispetto al lavoro DB.

app/Controllers/PrestitiController.php (2)

114-127: Guard contro prestiti/prenotazioni duplicate per stesso utente/libro

La query di controllo su prestiti con attivo = 1 e stato IN ('in_corso','prenotato','pendente','in_ritardo') blocca correttamente la creazione di un nuovo prestito quando l’utente ha già un prestito/prenotazione attiva per lo stesso libro, come descritto nel commento “Case 8”. Comportamento chiaro e coerente con la regola di business.


760-773: Logging del rinnovo con SecureLogger

L’uso di SecureLogger::warning per la validazione fallita e SecureLogger::error nel catch del rinnovo fornisce contesto strutturato (loan_id + messaggio) senza alterare il flusso di controllo. Nessuna anomalia evidente.

app/Support/Updater.php (2)

490-594: Backup DB in streaming: soluzione robusta per tabelle grandi

La nuova implementazione di backupDatabase() che usa:

  • handle di file aperto in scrittura,
  • SHOW TABLES + SHOW CREATE TABLE per la struttura,
  • real_query() + use_result() per leggere i dati riga per riga,
  • INSERT scritti direttamente via fwrite(),

riduce sensibilmente il rischio di esaurire la memoria su database voluminosi. La chiusura esplicita dell’handle e l’eliminazione del file parziale in caso di eccezione sono gestite correttamente.
Nessun problema funzionale evidente.


1458-1607: Lock di aggiornamento, gestione memoria e recovery modalità manutenzione

Le aggiunte in performUpdate() e metodi correlati sono nel complesso ben strutturate:

  • Il lock file storage/cache/update.lock con flock(LOCK_EX|LOCK_NB) previene aggiornamenti concorrenti e fornisce un PID per il debug.
  • Il register_shutdown_function che, in caso di errore fatale, logga il problema e rimuove sia .maintenance che il lock riduce il rischio di rimanere bloccati in modalità manutenzione.
  • L’aumento automatico del memory_limit ad almeno 256M (tramite parseMemoryLimit()) aiuta i backup/aggiornamenti su installazioni con limiti troppo stretti.
  • checkStaleMaintenanceMode() fornisce un recovery automatico se un precedente update ha lasciato .maintenance vecchio di oltre 30 minuti.

La logica di cleanup finale (cleanup() + rilascio lock + unlink($lockFile)) copre correttamente sia il caso di successo che di errore.

Also applies to: 1612-1631, 1637-1664

Comment on lines 348 to 449
/**
* Check and expire reservations past their due date (Case 4)
*
* Finds prestiti with stato='prenotato' where data_scadenza < today,
* marks them as 'scaduto', frees assigned copies, and triggers
* reassignment to next user in queue.
*
* @return int Number of reservations expired
* @throws \RuntimeException If query preparation fails
*/
public function checkExpiredReservations(): int
{
$today = date('Y-m-d');

// Find expired reservations
$stmt = $this->db->prepare("
SELECT id, libro_id, copia_id, utente_id
FROM prestiti
WHERE stato = 'prenotato'
AND attivo = 1
AND data_scadenza < ?
");

if (!$stmt) {
throw new \RuntimeException('Failed to prepare expired reservations query');
}

$stmt->bind_param('s', $today);
$stmt->execute();
$result = $stmt->get_result();
$expiredReservations = $result ? $result->fetch_all(MYSQLI_ASSOC) : [];
$stmt->close();

$expiredCount = 0;
$reassignmentService = new \App\Services\ReservationReassignmentService($this->db);
$integrity = new DataIntegrity($this->db);

foreach ($expiredReservations as $reservation) {
$this->db->begin_transaction();

try {
$id = (int) $reservation['id'];
$copiaId = $reservation['copia_id'] ? (int) $reservation['copia_id'] : null;
$libroId = (int) $reservation['libro_id'];

// Mark as expired
$updateStmt = $this->db->prepare("
UPDATE prestiti
SET stato = 'scaduto',
attivo = 0,
updated_at = NOW(),
note = CONCAT(COALESCE(note, ''), '\n[System] " . __('Scaduta il') . " " . date('d/m/Y') . "')
WHERE id = ?
");
$updateStmt->bind_param('i', $id);
$updateStmt->execute();
$updateStmt->close();

// If a copy was assigned, make it available (if currently 'prenotato')
if ($copiaId) {
$checkCopy = $this->db->prepare("SELECT stato FROM copie WHERE id = ? FOR UPDATE");
$checkCopy->bind_param('i', $copiaId);
$checkCopy->execute();
$copyResult = $checkCopy->get_result();
$copyState = $copyResult ? $copyResult->fetch_assoc() : null;
$checkCopy->close();

if ($copyState && $copyState['stato'] === 'prenotato') {
// Update copy to available
$updateCopy = $this->db->prepare("UPDATE copie SET stato = 'disponibile' WHERE id = ?");
$updateCopy->bind_param('i', $copiaId);
$updateCopy->execute();
$updateCopy->close();

// Trigger reassignment logic for this copy
$reassignmentService->reassignOnReturn($copiaId);
}
}

// Recalculate book availability
$integrity->recalculateBookAvailability($libroId);

$this->db->commit();
$expiredCount++;

SecureLogger::info(__('MaintenanceService prenotazione scaduta'), [
'prestito_id' => $id,
'libro_id' => $libroId,
'copia_id' => $copiaId
]);

} catch (\Throwable $e) {
$this->db->rollback();
SecureLogger::error(__('MaintenanceService errore scadenza prenotazione'), [
'prestito_id' => $reservation['id'],
'error' => $e->getMessage()
]);
}
}

return $expiredCount;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*ReservationReassignmentService*"

Repository: fabiodalez-dev/Pinakes

Length of output: 115


🏁 Script executed:

cat -n app/Services/ReservationReassignmentService.php | head -100

Repository: fabiodalez-dev/Pinakes

Length of output: 4339


🏁 Script executed:

cat -n app/Services/ReservationReassignmentService.php | tail -n +100

Repository: fabiodalez-dev/Pinakes

Length of output: 12128


🏁 Script executed:

sed -n '348,449p' app/Support/MaintenanceService.php

Repository: fabiodalez-dev/Pinakes

Length of output: 4055


🏁 Script executed:

rg -n "CONCAT.*__\(" app/ --type php -A 2 -B 2

Repository: fabiodalez-dev/Pinakes

Length of output: 512


🏁 Script executed:

rg -n "bind_param.*note" app/ --type php -A 3 -B 3

Repository: fabiodalez-dev/Pinakes

Length of output: 2204


Nested transactions in checkExpiredReservations() and SQL string concatenation for note field require fixes

Two critical issues:

  1. Nested transactions with ReservationReassignmentService

    • reassignOnReturn() calls reassignOnNewCopy(), which opens begin_transaction() inside the outer transaction of checkExpiredReservations()
    • MySQL doesn't support true nested transactions; the inner commit()/rollback() can interfere with the outer transaction, causing state visibility issues and potential partial commits
    • Move $reassignmentService->reassignOnReturn($copiaId) outside the transaction (after commit()) so reassignment is attempted only after the reservation is persisted, or remove transaction management from the service to let the caller control the transaction scope
  2. Localized string concatenated directly in SQL query

    • The note update concatenates __('Scaduta il') and date('d/m/Y') directly into the SQL string without parameterization
    • If the translated string contains quotes or special characters, it breaks SQL syntax or enables injection
    • Pass the note suffix as a bound parameter instead:
    $noteSuffix = "\n[System] " . __('Scaduta il') . ' ' . date('d/m/Y');
    $updateStmt = $this->db->prepare("
        UPDATE prestiti
        SET stato = 'scaduto',
            attivo = 0,
            updated_at = NOW(),
            note = CONCAT(COALESCE(note, ''), ?)
        WHERE id = ?
    ");
    $updateStmt->bind_param('si', $noteSuffix, $id);
🤖 Prompt for AI Agents
In app/Support/MaintenanceService.php around lines 348 to 449, fix two issues:
(1) avoid nested transactions by removing the call to
$reassignmentService->reassignOnReturn($copiaId) from inside the transaction —
commit the outer transaction first and then call reassignOnReturn() (or refactor
ReservationReassignmentService to not start/commit/rollback transactions so
callers control the transaction scope); (2) stop concatenating localized strings
into the SQL: build the note suffix in PHP (e.g. "\n[System] " . __('Scaduta
il') . ' ' . date('d/m/Y')) and pass it as a bound parameter to the UPDATE
statement using CONCAT(COALESCE(note, ''), ?) with appropriate bind_param('si',
$noteSuffix, $id).

if (genreDisplay.level > 0) {
const backValue = genreDisplay.level === 1 ? '' : (genreDisplay.parent?.nome || '');
const backEscaped = backValue.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const backEscaped = backValue.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, read the actual file to see the context around the flagged lines
head -c 500000 app/Views/frontend/catalog.php | wc -l

Repository: fabiodalez-dev/Pinakes

Length of output: 70


🏁 Script executed:

# Get line count of the file
wc -l app/Views/frontend/catalog.php

Repository: fabiodalez-dev/Pinakes

Length of output: 101


🏁 Script executed:

# Read the specific lines mentioned in the review
sed -n '1760,1780p' app/Views/frontend/catalog.php

Repository: fabiodalez-dev/Pinakes

Length of output: 1254


🏁 Script executed:

# Also check the other lines flagged: 1777, 1804, 1815, 1838
sed -n '1800,1820p' app/Views/frontend/catalog.php
sed -n '1835,1845p' app/Views/frontend/catalog.php

Repository: fabiodalez-dev/Pinakes

Length of output: 2377


🏁 Script executed:

# Test the regex patterns to verify the reviewer's claim about what they match
node <<'EOF'
// Test the old pattern vs new pattern
const testStrings = [
  'test',           // no backslash
  'test\\value',    // single backslash (in JavaScript string: \\)
  'test\\\\value',  // double backslash (in JavaScript string: \\\\)
  'test\\\\\\value', // triple backslash
];

console.log("=== OLD PATTERN: /\\\\/g (matches single backslash) ===");
testStrings.forEach(str => {
  const result = str.replace(/\\/g, '\\\\');
  console.log(`Input: "${str}" -> Output: "${result}"`);
});

console.log("\n=== NEW PATTERN: /\\\\\\\\/g (matches double backslash) ===");
testStrings.forEach(str => {
  const result = str.replace(/\\\\/g, '\\\\\\\\');
  console.log(`Input: "${str}" -> Output: "${result}"`);
});

console.log("\n=== Actual regex breakdown ===");
console.log("Old /\\\\/g : matches single backslash (\\)");
console.log("New /\\\\\\\\/g : matches two backslashes (\\\\)");
EOF

Repository: fabiodalez-dev/Pinakes

Length of output: 642


🏁 Script executed:

# Check git history for this file
git log --oneline -20 app/Views/frontend/catalog.php

Repository: fabiodalez-dev/Pinakes

Length of output: 134


🏁 Script executed:

# Look at the actual diff to understand what changed
git log -p --max-count=5 app/Views/frontend/catalog.php | grep -A 10 -B 10 "replace.*\\\\\\\\" | head -100

Repository: fabiodalez-dev/Pinakes

Length of output: 48


🏁 Script executed:

# Search for references to this pattern in commits
git log --all --grep="escaping\|backslash\|HEREDOC" --oneline | head -10

Repository: fabiodalez-dev/Pinakes

Length of output: 134


🏁 Script executed:

# Search for database migrations or seeding files
fd -e php -e sql | grep -i "migrat\|seed\|database" | head -20

Repository: fabiodalez-dev/Pinakes

Length of output: 2562


🏁 Script executed:

# Look for generi and editori table references
rg -n "generi|editori" --type=php -i | grep -i "CREATE\|INSERT\|TABLE" | head -20

Repository: fabiodalez-dev/Pinakes

Length of output: 2492


🏁 Script executed:

# Check for any data fixtures or seeders
find . -type f \( -name "*seed*" -o -name "*fixture*" -o -name "*migration*" \) | head -20

Repository: fabiodalez-dev/Pinakes

Length of output: 105


🏁 Script executed:

# Search for where generi.nome and editori.nome are populated or referenced
rg -n "generi.*nome|editori.*nome" --type=sql --type=php | head -30

Repository: fabiodalez-dev/Pinakes

Length of output: 3130


🏁 Script executed:

# Look for HEREDOC syntax around the flagged lines
sed -n '1700,1850p' app/Views/frontend/catalog.php | head -200

Repository: fabiodalez-dev/Pinakes

Length of output: 7251


🏁 Script executed:

# Search for any HEREDOC declarations in the file
rg -n "<<<" app/Views/frontend/catalog.php | head -20

Repository: fabiodalez-dev/Pinakes

Length of output: 93


🏁 Script executed:

# Check if there's documentation or comments about the escaping issue
rg -n -B 5 "replace.*\\\\\\\\" app/Views/frontend/catalog.php | head -80

Repository: fabiodalez-dev/Pinakes

Length of output: 2046


🏁 Script executed:

# Look at how the escaped value is used - specifically the onclick handler
sed -n '1763,1770p' app/Views/frontend/catalog.php
sed -n '1777,1784p' app/Views/frontend/catalog.php
sed -n '1804,1811p' app/Views/frontend/catalog.php

Repository: fabiodalez-dev/Pinakes

Length of output: 1612


🏁 Script executed:

# Create a test script to understand PHP HEREDOC escaping
php <<'EOF'
<?php
// Test how backslashes work in HEREDOC
$test_heredoc = <<<JS
const pattern1 = /\\/g;
const pattern2 = /\\\\/g;
const replacement1 = '\\\\';
const replacement2 = '\\\\\\\\';
JS;

echo "=== PHP HEREDOC Raw Content ===\n";
echo $test_heredoc;
echo "\n\n=== Interpreted by JavaScript ===\n";
echo json_encode($test_heredoc);
EOF

Repository: fabiodalez-dev/Pinakes

Length of output: 356


🏁 Script executed:

# Now test what the actual regex patterns do
php <<'EOF'
<?php
// Simulate what the PHP code actually does
$test_values = [
    'normal_genre',
    "genre'with'quote",
    'genre\\with\\backslash',
    'genre\\\\with\\\\double'
];

$heredoc = <<<JS
// This is what's in the actual code:
function testEscaping(value) {
    return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
}
JS;

echo "PHP HEREDOC source code:\n";
echo $heredoc;
echo "\n\n=== What JavaScript will receive ===\n";
echo json_encode($heredoc, JSON_PRETTY_PRINT);
EOF

Repository: fabiodalez-dev/Pinakes

Length of output: 415


🏁 Script executed:

# Test the actual escaping with Node.js to see what JavaScript receives from PHP
node <<'EOF'
// This represents what PHP sends to the browser (after HEREDOC interpretation)
const phpHeredocOutput = `function testEscaping(value) {
    return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
}`;

console.log("=== What PHP HEREDOC sends to browser ===");
console.log(phpHeredocOutput);

console.log("\n=== Testing the regex pattern ===");
const testCases = [
    'normal_genre',
    "genre'with'quote",
    'genre\\with\\backslash',
    'genre\\\\with\\\\double'
];

testCases.forEach(testVal => {
    const result = testVal.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
    console.log(`Input: ${JSON.stringify(testVal)} -> Output: ${JSON.stringify(result)}`);
});
EOF

Repository: fabiodalez-dev/Pinakes

Length of output: 495


🏁 Script executed:

# Test with character-by-character inspection
node <<'EOF'
function inspect(str) {
    let chars = '';
    for (let i = 0; i < str.length; i++) {
        chars += `[${str.charCodeAt(i)}:${str[i]}]`;
    }
    return chars;
}

// Test what the actual regex pattern does
const testCases = [
    { desc: 'single backslash', input: 'a\\b' },
    { desc: 'double backslash', input: 'a\\\\b' },
    { desc: 'triple backslash', input: 'a\\\\\\b' },
    { desc: 'quad backslash', input: 'a\\\\\\\\b' }
];

console.log("=== Testing /\\\\/g pattern (from PHP HEREDOC) ===");
testCases.forEach(tc => {
    const result = tc.input.replace(/\\\\/g, '\\\\\\\\');
    console.log(`${tc.desc}:`);
    console.log(`  Input chars: ${inspect(tc.input)}`);
    console.log(`  Input visual: "${tc.input}"`);
    console.log(`  Output chars: ${inspect(result)}`);
    console.log(`  Output visual: "${result}"`);
    console.log();
});

console.log("\n=== What /\\\\/g actually matches ===");
const testStr = 'a\\b\\\\c\\\\\\d';
console.log(`Test string: "${testStr}"`);
console.log(`Character breakdown: ${inspect(testStr)}`);
console.log(`Matches for /\\\\\\\\/g:`);
const matches = testStr.match(/\\\\/g);
console.log(`Found ${matches ? matches.length : 0} matches`);
EOF

Repository: fabiodalez-dev/Pinakes

Length of output: 967


🏁 Script executed:

# Extract the exact pattern as it appears in the source code
sed -n '1763p' app/Views/frontend/catalog.php | cat -A

Repository: fabiodalez-dev/Pinakes

Length of output: 165


🏁 Script executed:

# Use hexdump to see the exact bytes
sed -n '1763p' app/Views/frontend/catalog.php | od -c

Repository: fabiodalez-dev/Pinakes

Length of output: 108


🏁 Script executed:

# Search for any recent changes to this pattern using git
git log -p --all -- app/Views/frontend/catalog.php | grep -B 5 -A 5 "replace.*backslash\|replace.*\\\\\\\\\|updateFilter.*genere" | head -150

Repository: fabiodalez-dev/Pinakes

Length of output: 48


🏁 Script executed:

# Check if there are any comments or issues mentioning this escaping change
rg -n "escap\|HEREDOC\|regex.*issue" app/Views/frontend/catalog.php -A 2 -B 2 | head -50

Repository: fabiodalez-dev/Pinakes

Length of output: 48


🏁 Script executed:

# Look for the PR or commit message that mentions "JavaScript regex issue"
git log --all --oneline | grep -i "escap\|regex\|javascript\|HEREDOC" | head -20

Repository: fabiodalez-dev/Pinakes

Length of output: 48


🏁 Script executed:

# Check if there's a way to see what the pattern was before
git show HEAD:app/Views/frontend/catalog.php | sed -n '1763p' 2>/dev/null || echo "Pattern not found in HEAD"

Repository: fabiodalez-dev/Pinakes

Length of output: 164


🏁 Script executed:

# Test if the current escaping prevents JavaScript errors in onclick handlers
node <<'EOF'
// Simulate the escaping and onclick handler execution
function escapeForOnclick(value) {
    return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
}

// Test cases that could appear in genre/publisher names
const testCases = [
    'Science Fiction',
    "O'Reilly",
    'C++ Programming',
    'Linux\\Unix (hypothetical)',
    'Back\\\\slash (hypothetical)'
];

console.log("=== Testing escaping for onclick safety ===\n");

testCases.forEach(testVal => {
    const escaped = escapeForOnclick(testVal);
    const onclickCode = `onclick="updateFilter('genere', '${escaped}'); return false;"`;
    
    console.log(`Original: "${testVal}"`);
    console.log(`Escaped: "${escaped}"`);
    console.log(`Onclick: ${onclickCode}`);
    
    // Try to parse the onclick as JavaScript to check for syntax errors
    try {
        // Simulate what the browser would see: the onclick attribute value
        const jsCode = `updateFilter('genere', '${escaped}'); return false;`;
        // Just check if it's valid JavaScript syntax
        new Function(jsCode);
        console.log(`✓ Valid JavaScript syntax`);
    } catch (e) {
        console.log(`✗ SYNTAX ERROR: ${e.message}`);
    }
    console.log();
});
EOF

Repository: fabiodalez-dev/Pinakes

Length of output: 970


🏁 Script executed:

# Search for any README or documentation about escaping
find . -name "README*" -o -name "*.md" | grep -v node_modules | xargs grep -l "escap\|HEREDOC" 2>/dev/null | head -5

Repository: fabiodalez-dev/Pinakes

Length of output: 141


🏁 Script executed:

# Check the catalog.php file for any context comments
sed -n '1450,1470p' app/Views/frontend/catalog.php

Repository: fabiodalez-dev/Pinakes

Length of output: 811


🏁 Script executed:

# Look for updateFilter function definition to understand parameter handling
rg -n "function updateFilter\|const updateFilter" app/Views/frontend/catalog.php -A 10 | head -40

Repository: fabiodalez-dev/Pinakes

Length of output: 48


🏁 Script executed:

# Search for any CSV files or data imports that might contain backslashes
find . -name "*.csv" -o -name "*import*" -o -name "*seed*" | grep -v vendor | grep -v node_modules

Repository: fabiodalez-dev/Pinakes

Length of output: 140


🏁 Script executed:

# Look for sample data or documentation about genre names
find . -type f \( -name "*.sql" -o -name "*.json" \) | xargs grep -l "generi\|genre" 2>/dev/null | grep -v vendor | head -5

Repository: fabiodalez-dev/Pinakes

Length of output: 267


🏁 Script executed:

# Check if there's any validation or sanitization of genre/publisher names
rg -n "nome.*sanitize\|nome.*filter\|nome.*trim\|nome.*strip" app/Controllers/ --type=php | head -20

Repository: fabiodalez-dev/Pinakes

Length of output: 48


🏁 Script executed:

# Look for database schema definition
find . -name "*.sql" -type f | grep -v vendor | xargs grep -n "CREATE TABLE.*generi\|CREATE TABLE.*editori" 2>/dev/null

Repository: fabiodalez-dev/Pinakes

Length of output: 186


🏁 Script executed:

# Check migrations folder
find . -type d -name "*migration*" | grep -v vendor

Repository: fabiodalez-dev/Pinakes

Length of output: 97


🏁 Script executed:

# Check the schema for generi and editori tables
sed -n '198,220p' installer/database/schema.sql
sed -n '147,170p' installer/database/schema.sql

Repository: fabiodalez-dev/Pinakes

Length of output: 3101


🏁 Script executed:

# Check the sample data to see if any backslashes exist
head -50 installer/database/data_it_IT.sql | grep -i "generi\|editori"

Repository: fabiodalez-dev/Pinakes

Length of output: 5448


🏁 Script executed:

# Search for backslashes in the SQL data files
grep -n "\\\\" installer/database/data_*.sql | head -20

Repository: fabiodalez-dev/Pinakes

Length of output: 19608


Escaping pattern appears correct for actual use case, but verify data requirements

The pattern /\\\\/g (in source code) translates to /\\/g in JavaScript and correctly matches single backslashes in runtime data. Testing confirms it properly escapes the apostrophe case (O'ReillyO\'Reilly), which is essential to prevent JavaScript syntax errors in the onclick handler.

Sample genre and publisher data contains no backslashes—all special characters are apostrophes in Italian contractions (e.g., "d'avventura", "dell'orrore"). The escaping chain works correctly for this realistic data.

However, clarify the PR context: no git history shows a prior pattern change from /\\/g to /\\\\/g. If this was recently changed, verify the old pattern also worked correctly or document what specific issue this fix resolves. If backslashes may appear in imported data (CSV uploads), add explicit validation to remove or reject them rather than relying on escaping.

`sede_id` int DEFAULT NULL,
`numero_inventario` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`stato` enum('disponibile','prestato','manutenzione','perso','danneggiato') COLLATE utf8mb4_unicode_ci DEFAULT 'disponibile',
`stato` enum('disponibile','prestato','prenotato','manutenzione','in_restauro','perso','danneggiato','in_trasferimento') COLLATE utf8mb4_unicode_ci DEFAULT 'disponibile',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistenza: validazione stati in CopyController non aggiornata.

L'enum copie.stato ora include prenotato, in_restauro, e in_trasferimento, ma la validazione in CopyController.php (linea 60) accetta solo ['disponibile', 'prestato', 'manutenzione', 'danneggiato', 'perso']. Inoltre, la logica di reassignment (linea 140) usa in_restauro che non è nella lista di validazione.

Questo causerà errori se si tenta di impostare questi nuovi stati tramite l'interfaccia admin.

È necessario aggiornare CopyController.php:

-        $statiValidi = ['disponibile', 'prestato', 'manutenzione', 'danneggiato', 'perso'];
+        $statiValidi = ['disponibile', 'prestato', 'prenotato', 'manutenzione', 'in_restauro', 'danneggiato', 'perso', 'in_trasferimento'];

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In installer/database/schema.sql line 115 and CopyController.php around lines 60
and 140, the DB enum for copie.stato now includes 'prenotato', 'in_restauro',
and 'in_trasferimento' but the controller's validation list (line ~60) and
reassignment logic (line ~140) are out of sync; update the validation array to
include 'prenotato', 'in_restauro', and 'in_trasferimento' and ensure the
reassignment logic uses the same canonical state names (either include
'in_restauro' in the allowed list or remove its special-case use) so the
controller accepts and handles the new DB states consistently.

- Fix nested transactions in ReservationReassignmentService
  - Added isInTransaction() to detect active transactions via @@in_transaction
  - Added beginTransactionIfNeeded(), commitIfOwned(), rollbackIfOwned() helpers
  - Updated reassignOnNewCopy() and reassignOnCopyLost() to use transaction-aware methods
  - Fixed race condition in reassignOnCopyLost() - now verifies copy state after lock

- Add missing states to CopyController validation
  - Added prenotato, in_restauro, in_trasferimento to match DB enum

- Add soft delete filtering to BookRepository queries
  - Added WHERE l.deleted_at IS NULL to listWithAuthors(), getById(),
    getByAuthorId(), getByPublisherId()

- Add prenotazioni table check in book deletion guard
  - Now blocks deletion if active waitlist entries exist (stato='attiva')

- Add annullato/scaduto states to CSV export
  - Updated validStates and statusLabels arrays
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/Services/ReservationReassignmentService.php (1)

368-380: Logging dell’email in chiaro: PII non necessaria nei log

Nel ramo di successo/errore:

SecureLogger::info(__('Notifica prenotazione disponibile inviata'), [
    'prestito_id' => $prestitoId,
    'email' => $data['email']
]);
...
SecureLogger::warning(__('Invio notifica prenotazione fallito'), [
    'prestito_id' => $prestitoId,
    'email' => $data['email']
]);

stai registrando l’indirizzo email completo. Per minimizzare PII nei log, valuterei di:

  • rimuovere il campo email dai log, oppure
  • mascherarlo (es. solo dominio, oppure hash irreversibile) se serve per correlazioni.

Esempio:

SecureLogger::info(__('Notifica prenotazione disponibile inviata'), [
    'prestito_id' => $prestitoId,
    // 'email' omessa o mascherata per privacy
]);
🧹 Nitpick comments (1)
app/Services/ReservationReassignmentService.php (1)

95-167: Finestra di race sulla prenotazione selezionata (stato può cambiare prima dell’UPDATE)

reassignOnNewCopy() seleziona la prenotazione “bloccata” fuori dalla transazione, poi dentro la transazione aggiorna solo copia_id:

$reservation = ...
...
UPDATE prestiti
SET copia_id = ?
WHERE id = ?

Nel frattempo, un altro processo può aver marcato quella prenotazione come annullato / scaduto / attivo=0. In questo caso assegneresti comunque una copia a una prenotazione ormai non più valida.

Per rendere il flusso più robusto:

  • aggiungi una condizione all’UPDATE (WHERE id = ? AND stato = 'prenotato' AND attivo = 1),
  • e controlla affected_rows per capire se l’aggiornamento è andato a buon fine (in caso contrario esci/rollback).

Questo evita di “rianimare” prenotazioni che sono state chiuse in parallelo.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3326929 and 741196a.

📒 Files selected for processing (5)
  • app/Controllers/CopyController.php (3 hunks)
  • app/Controllers/LibriController.php (5 hunks)
  • app/Controllers/PrestitiController.php (12 hunks)
  • app/Models/BookRepository.php (22 hunks)
  • app/Services/ReservationReassignmentService.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
app/Controllers/PrestitiController.php (3)
app/Models/LoanRepository.php (1)
  • close (101-175)
app/helpers.php (1)
  • __ (25-28)
app/Services/ReservationReassignmentService.php (2)
  • ReservationReassignmentService (17-459)
  • reassignOnReturn (266-283)
app/Services/ReservationReassignmentService.php (2)
app/Support/NotificationService.php (1)
  • NotificationService (11-1362)
app/Models/CopyRepository.php (2)
  • CopyRepository (8-245)
  • updateStatus (92-100)
app/Models/BookRepository.php (3)
app/Models/AuthorRepository.php (1)
  • __construct (10-10)
app/Models/PublisherRepository.php (1)
  • __construct (10-10)
app/Models/DashboardStats.php (1)
  • __construct (10-10)
app/Controllers/CopyController.php (1)
app/Services/ReservationReassignmentService.php (3)
  • ReservationReassignmentService (17-459)
  • reassignOnCopyLost (173-245)
  • reassignOnReturn (266-283)
🔇 Additional comments (22)
app/Controllers/CopyController.php (2)

59-61: Allineamento stati validi con i casi gestiti a valle

L’estensione di $statiValidi per includere prenotato, in_restauro e in_trasferimento rende coerente la validazione con i casi di business gestiti più sotto (es. branch su in_restauro). Buon allineamento enum ↔ logica applicativa.


135-153: Integrazione con ReservationReassignmentService ben incapsulata

L’invocazione del servizio di riassegnazione dopo l’UPDATE copie:

  • gestisce il caso “copia non più utilizzabile” (perso, danneggiato, manutenzione, in_restauro) tramite reassignOnCopyLost,
  • e il caso “copia torna disponibile” tramite reassignOnReturn,
  • incapsulata in un try/catch che logga con SecureLogger ma non blocca il flusso utente.

Struttura solida e coerente con il nuovo servizio; non vedo problemi di correttezza qui.

app/Models/BookRepository.php (11)

23-46: Filtro soft delete correttamente applicato alle liste

L’aggiunta di WHERE l.deleted_at IS NULL in listWithAuthors() e nelle altre query principali nasconde i libri soft-deleted senza cambiare la firma pubblica dei metodi. Questo allinea il repository alla semantica del nuovo delete() che aggiorna deleted_at.


48-123: getById() coerente con soft delete e con gerarchia generi

L’aggiunta del filtro l.deleted_at IS NULL in getById() e la riorganizzazione dei nomi di genere/radice in base ai livelli (nonno_id ecc.) sono coerenti con il modello dati esistente; il ciclo su $authorsRes è semplice e sicuro. Nessuna issue evidente.


181-396: Normalizzazione input in createBasic robusta (ISBN/EAN, collocazione, copie, tipi numerici)

La nuova logica:

  • converte scaffale/mensola/posizione in interi con sentinel null per valori ≤ 0,
  • normalizza ISBN/EAN eliminando whitespace e convertendo stringhe vuote in null,
  • forza peso/prezzo a float solo quando presenti, e
  • inizializza copie_totali/copie_disponibili con cast espliciti a int.

Questo riduce parecchi edge case su vincoli DB e tipi; il binding dinamico via $addField resta coerente.


398-604: updateBasic allineato alla logica di creazione (normalizzazioni + logging mirato)

La simmetria con createBasic (cast per scaffale/mensola/posizione, normalizzazione ISBN/EAN/peso/prezzo, gestione campi opzionali tramite $addSet) migliora la consistenza tra insert e update. Anche il logging debug condizionato ad APP_ENV=development è prudente lato sicurezza.


631-654: syncAuthors: early-return su lista vuota rende più chiaro il flusso

L’early-return quando $authorIds è vuoto evita l’esecuzione inutile dell’INSERT, mantenendo comunque la cancellazione precedente dei legami autore–libro. Flusso pulito e prevedibile.


656-672: processAuthorId: gestione backward‑compat per formati diversi di autore

La funzione gestisce sia:

  • ID numerici puri, sia
  • formati stringa tipo new_... (Choices.js) che vengono ignorati (ritorno 0), sia
  • altri valori che vengono castati a int.

È una soluzione pragmatica per supportare formati legacy e nuovi senza rompere il codice chiamante.


720-727: Implementazione soft delete su delete()

Sostituire il DELETE fisico con UPDATE libri SET deleted_at = NOW() preserva storia e integrità referenziale con il minimo impatto. Dato che le principali query ora filtrano deleted_at IS NULL, il comportamento esterno è coerente con “cancellazione” logica.


729-774: updateOptionals: aggiornamento dinamico dei campi opzionali ben tipizzato

La costruzione di $cols e poi di $set/$types/$vals:

  • converte correttamente numero_pagine/anno_pubblicazione a interi e il resto a stringa,
  • onora la presenza di colonne tramite hasColumn,
  • evita UPDATE se $cols è vuoto.

Buon compromesso tra flessibilità e sicurezza tipica.


776-789: Cache colonne tabella per ridurre round‑trip al DB

hasColumn() con cache in $columnCache evita SHOW COLUMNS ripetuti. Logica semplice e thread-safe per singola richiesta; nessuna issue.


791-807: Whitelist tabelle in hasColumnInTable: mitigazione SQL injection su INFORMATION_SCHEMA

La whitelist esplicita di tabelle (libri, autori, libri_autori, ecc.) prima di interrogare INFORMATION_SCHEMA.COLUMNS è un buon hardening: impedisce che nomi tabella controllati dall’utente entrino nella query.


833-854: getEnumOptions con cache per chiavi table.column

La cache $enumCache con chiave $table . '.' . $column evita query ripetute su INFORMATION_SCHEMA e semplifica l’uso di normalizeEnumValue. Implementazione corretta e leggibile.

app/Controllers/LibriController.php (3)

9-10: Uso coerente di SecureLogger nel controller libri

L’import di App\Support\SecureLogger allinea questo controller agli altri (prestiti, copie) per logging strutturato, mantenendo error_log solo dove strettamente necessario. Buona convergenza di stile.


1351-1374: Aggancio della riassegnazione e della waitlist durante l’aumento delle copie

Nel ramo update() dove aumenti copie_totali:

  • crei nuove copie via CopyRepository::create,
  • per ogni nuova copia invochi ReservationReassignmentService->reassignOnNewCopy($id, $newCopyId) in try/catch,
  • poi ReservationManager->processBookAvailability($id) per trasformare prenotazioni in prestiti quando c’è capacità.

Questo collega bene il lifecycle delle copie con la coda di prenotazione, e l’uso di SecureLogger nei catch evita che un errore di riassegnazione blocchi l’update del libro.


1661-1695: Guard di cancellazione libro ora considera sia prestiti sia prenotazioni attive

Il nuovo blocco in delete():

  • conta prestiti attivi con stati 'in_corso','prenotato','pendente','in_ritardo',
  • conta prenotazioni da prenotazioni con stato='attiva',
  • blocca la cancellazione se uno dei due conteggi è > 0, con messaggio chiaro.

Questo chiude il buco segnalato in passato (prenotazioni attive non considerate) e previene inconsistenze lato utente.

app/Controllers/PrestitiController.php (5)

114-127: Blocco di doppi prestiti/prenotazioni attive per stesso utente/libro

Il controllo:

SELECT id FROM prestiti
WHERE libro_id = ? AND utente_id = ? AND attivo = 1
AND stato IN ('in_corso', 'prenotato', 'pendente', 'in_ritardo')

prima dell’inserimento impedisce che lo stesso utente abbia più prestiti/prenotazioni “attive” sullo stesso libro nella tabella prestiti. È una buona salvaguardia lato UX; tieni solo presente che non considera la tabella prenotazioni, quindi eventuali code ancora lì verranno gestite dal nuovo sistema e non da questo guard (scelta di design, non un bug).


588-626: Riassegnazione della copia restituita + gestione lista d’attesa ben incapsulate

Nel processReturn():

  • dopo aver reso la copia disponibile, chiami ReservationReassignmentService->reassignOnReturn($copia_id) per assegnarla alla prenotazione bloccata più vecchia (nuovo sistema basato su prestiti),
  • poi ReservationManager->processBookAvailability($libro_id) per gestire prenotazioni future/legacy,
  • il tutto dentro try/catch con logging tramite SecureLogger.

Questo collega correttamente il flusso di restituzione con la coda di prenotazioni, e il log d’errore (Riassegnazione copia fallita) fornisce contesto utile senza interrompere la restituzione.


609-611: Log di validazione prestito non bloccante ma visibile

In processReturn() se validateAndUpdateLoan() fallisce:

SecureLogger::warning(__('Validazione prestito fallita'), ['loan_id' => $id, 'message' => $validationResult['message']]);

è una buona scelta: l’utente non viene bloccato dalla restituzione, ma l’anomalia resta tracciata per diagnosi.


759-773: Rinnovo: logging strutturato su fallimenti di validazione/DB

Nel renew():

  • se validateAndUpdateLoan() fallisce, logghi un warning con loan_id e messaggio,
  • in caso di eccezione, rollback e SecureLogger::error('Rinnovo prestito fallito', ...).

Questo rende il percorso di rinnovo molto più osservabile rispetto a error_log, senza cambiare il comportamento per l’utente finale.


821-919: Export CSV: supporto per nuovi stati e logging errori query

In exportCsv():

  • validStates e $statusLabels includono ora annullato e scaduto, allineati al nuovo enum,
  • se la query senza parametri fallisce, logghi l’errore DB con SecureLogger::error,
  • il resto della generazione CSV (sanitizzazione formula-injection, header i18n) resta invariato.

L’aggiornamento è coerente con le nuove transizioni di stato.

app/Services/ReservationReassignmentService.php (1)

313-381: Method exists and is correctly implemented

The method sendReservationBookAvailable() does exist in NotificationService at lines 990–991. The call is correct and will not cause a runtime error. The suggested alternative method name notifyReservationBookAvailable() does not exist in the codebase.

Likely an incorrect or invalid review comment.

- Fix PII logging: replace email with utente_id in ReservationReassignmentService
- Fix nested transactions: use setExternalTransaction(true) in MaintenanceService
- Fix SQL injection: use bound parameter for note suffix in checkExpiredReservations
- Fix idx_ruolo index: rename to idx_tipo_utente (ruolo column doesn't exist in utenti)
- Add idx_tipo_utente to installer schema.sql for fresh installations
- Add DataIntegrity check #11: verify annullato/scaduto loans have attivo=0
- Add DataIntegrity fix #3b: correct annullato/scaduto loans with attivo=1
- Update version to 0.4.2
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
installer/database/schema.sql (1)

115-115: LGTM! L'enum copie.stato è stato esteso correttamente.

I nuovi stati (prenotato, in_restauro, in_trasferimento) sono coerenti con le funzionalità di riassegnazione prenotazioni introdotte in questo PR.

Nota: esiste già un commento precedente che segnala la necessità di aggiornare CopyController.php per accettare questi nuovi stati nella validazione.

app/Services/ReservationReassignmentService.php (1)

207-216: Branch di fallback incompleto: la seconda findAvailableCopy non riassegna la prenotazione.

Quando la prima copia candidata non è più disponibile (race condition), il codice cerca una nuova copia con findAvailableCopy() ma se la trova, ritorna senza tentare la riassegnazione. La prenotazione rimane senza copia assegnata.

Soluzione: se $nextCopyId è trovato, dovrebbe ritentare l'assegnazione (possibilmente con un loop limitato per evitare cicli infiniti).

             if (!$copyStatus || $copyStatus['stato'] !== 'disponibile') {
                 $this->rollbackIfOwned($ownTransaction);
                 // Prova a cercare un'altra copia
                 $nextCopyId = $this->findAvailableCopy((int) $reservation['libro_id'], $copiaId);
                 if (!$nextCopyId) {
                     // Nessuna copia disponibile
                     $this->handleNoCopyAvailable((int) $reservation['id']);
+                } else {
+                    // Riprova con la nuova copia trovata (ricorsione limitata)
+                    $this->reassignOnCopyLost($copiaId);
                 }
                 return;
             }
🧹 Nitpick comments (2)
app/Support/MaintenanceService.php (1)

68-82: Allinea le PHPDoc con il nuovo campo expired_reservations

runAll() e runIfNeeded() ora popolano expired_reservations nel risultato, ma le PHPDoc degli array-shape non lo dichiarano. Questo può confondere gli strumenti statici e gli IDE.

Suggerisco di aggiornare le annotazioni:

  • In runIfNeeded(): aggiungere expired_reservations?: int al tipo di ritorno.
  • In runAll(): aggiungere expired_reservations: int al tipo di ritorno.

Also applies to: 45-59, 551-557

app/Services/ReservationReassignmentService.php (1)

250-260: handleNoCopyAvailable esegue UPDATE fuori dalla transazione.

Il metodo handleNoCopyAvailable è chiamato dopo rollbackIfOwned() (linea 209) o nel branch else (linea 243), ma esegue un UPDATE senza gestione transazionale propria. Se chiamato dopo un rollback, l'UPDATE viene eseguito senza protezione atomica.

Considera di avvolgere l'UPDATE in una transazione o di documentare che è intenzionalmente non-transazionale.

     private function handleNoCopyAvailable(int $reservationId): void
     {
+        $ownTransaction = $this->beginTransactionIfNeeded();
+        try {
             $stmt = $this->db->prepare("UPDATE prestiti SET copia_id = NULL WHERE id = ?");
             $stmt->bind_param('i', $reservationId);
             $stmt->execute();
             $stmt->close();
+            $this->commitIfOwned($ownTransaction);
+        } catch (Exception $e) {
+            $this->rollbackIfOwned($ownTransaction);
+            SecureLogger::error(__('Errore gestione prenotazione senza copia'), [
+                'reservation_id' => $reservationId,
+                'error' => $e->getMessage()
+            ]);
+        }

         $this->notifyUserCopyUnavailable($reservationId, 'lost_copy');
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 741196a and 4a70c0b.

📒 Files selected for processing (8)
  • app/Services/ReservationReassignmentService.php (1 hunks)
  • app/Support/DataIntegrity.php (3 hunks)
  • app/Support/MaintenanceService.php (7 hunks)
  • installer/database/indexes_optimization.sql (1 hunks)
  • installer/database/indexes_optimization_mysql.sql (1 hunks)
  • installer/database/schema.sql (4 hunks)
  • locale/en_US.json (6 hunks)
  • version.json (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • version.json
🔇 Additional comments (14)
locale/en_US.json (1)

1689-1703: Nuove chiavi di traduzione per manutenzione/prenotazioni coerenti

Le stringhe aggiunte per MaintenanceService, integrità dati e flussi di prenotazione/prestito sono consistenti con l’uso nel codice (__() usa esattamente queste chiavi) e i placeholder (%d, %s) risultano corretti. Nulla da segnalare lato funzionale.

Also applies to: 3448-3624

installer/database/indexes_optimization_mysql.sql (1)

79-83: Indice idx_tipo_utente su utenti coerente con gli accessi attesi

La sostituzione/aggiunta dell’indice su utenti.tipo_utente via add_index_if_not_exists è allineata alle query tipiche (filtri per ruolo utente) e sicura grazie al controllo su INFORMATION_SCHEMA.STATISTICS. Nulla da correggere.

installer/database/indexes_optimization.sql (1)

86-91: Allineamento indice idx_tipo_utente anche nello script generico

L’aggiunta di idx_tipo_utente (tipo_utente) nello script MariaDB mantiene consistente la strategia d’indicizzazione tra ambienti e supporta i filtri per tipo utente. Modifica corretta.

app/Support/MaintenanceService.php (1)

223-251: La struttura transazionale è corretta: ReservationReassignmentService rispetta il contratto setExternalTransaction(true)

La verifica conferma che il servizio implementa correttamente il pattern per evitare transazioni annidate:

  • setExternalTransaction() è presente (riga 35-39 del service) e imposta il flag $externalTransaction
  • Quando il flag è true, isInTransaction() ritorna true (riga 46-48), causando beginTransactionIfNeeded() a non eseguire begin_transaction()
  • Solo commit() e rollback() vengono eseguiti se il servizio ha iniziato la transazione ($ownTransaction è true)
  • In MaintenanceService, il flag è correttamente impostato a riga 384 prima di usare il servizio nel loop
  • reassignOnReturn() è chiamato dentro la stessa transazione (riga 408) e rispetta il vincolo

Il coordinamento transazionale è solido e previene i problemi di transazioni annidate in MySQL.

installer/database/schema.sql (4)

342-342: LGTM! Soft delete implementato correttamente.

La colonna deleted_at nullable è il pattern standard per il soft delete. Assicurati che le query di lettura escludano i record con deleted_at IS NOT NULL dove appropriato.


596-596: LGTM! Nuovi stati prestiti aggiunti.

Gli stati annullato e scaduto sono coerenti con le migrazioni migrate_0.4.3.sql e con i nuovi controlli in DataIntegrity.php.


749-750: LGTM! Indici aggiuntivi per ottimizzazione query.

Gli indici su data_ultimo_accesso e tipo_utente miglioreranno le performance delle query di reporting e filtraggio utenti.


113-113: No action needed. The sedi table does not exist in the schema, so the concern about missing foreign key constraints on sede_id is not applicable to this database schema. If sedi is intended to be a reference table, it should be defined in the schema; otherwise, the sede_id column may be managed through application logic rather than database-level constraints.

Likely an incorrect or invalid review comment.

app/Support/DataIntegrity.php (3)

568-586: LGTM! Verifica coerente per prestiti terminati.

Il controllo per prestiti con stato annullato/scaduto che hanno ancora attivo = 1 è corretto e la severità error è appropriata dato che rappresenta un'incoerenza nei dati.


706-714: LGTM! Correzione automatica coerente.

La correzione che imposta attivo = 0 per i prestiti terminati è l'azione corretta per risolvere l'incoerenza rilevata dal check #11.


1076-1076: LGTM! Nome indice allineato allo schema.

Il cambio da idx_ruolo a idx_tipo_utente è coerente con la definizione dello schema in schema.sql (linea 750).

app/Services/ReservationReassignmentService.php (3)

440-458: LGTM! Gestione URL base con fallback robusto.

La logica gestisce correttamente:

  • Variabile d'ambiente APP_CANONICAL_URL
  • Fallback a $_SERVER con validazione del formato host
  • Default sicuro a localhost

1-29: LGTM! Struttura del servizio ben organizzata.

La classe segue buone pratiche:

  • Strict types abilitati
  • Dependency injection del database connection
  • Servizi correlati inizializzati nel costruttore
  • Flag per gestione transazioni esterne

95-167: Gestione transazioni migliorata rispetto alla versione precedente.

La logica di reassignOnNewCopy ora utilizza correttamente beginTransactionIfNeeded() e commitIfOwned() per supportare sia chiamate standalone che chiamate all'interno di transazioni esterne. Il lock FOR UPDATE e la verifica dello stato della copia proteggono dalle race condition.

- Replace MariaDB-specific @@in_transaction with MySQL-compatible @@autocommit check
- Fix wrong enum value 'autore' -> 'principale' in author query
- Add retry loop with max attempts in reassignOnCopyLost for race conditions
- Wrap handleNoCopyAvailable UPDATE in proper transaction handling
- Add new findAvailableCopyExcluding method for efficient copy exclusion
- Add English translations for new log messages
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
app/Services/ReservationReassignmentService.php (4)

97-169: Rischio di race sulla scelta della prenotazione in reassignOnNewCopy

La logica è chiara e l’uso di SELECT ... FOR UPDATE sulla copia + notifica post‑commit è ottimo. Rimane però una piccola finestra di race:

  • La prenotazione “bloccata” viene letta fuori transazione (righe 101‑116).
  • Due worker che chiamano reassignOnNewCopy() quasi in parallelo (es. per due nuove copie dello stesso libro) potrebbero selezionare la stessa prenotazione e poi aggiornarla entrambi, con l’ultimo commit che vince e una delle nuove copie marcata prenotato ma senza prestito associato.

Per ridurre questo rischio ti proporrei, quando/ se la concorrenza diventa un problema:

  • spostare la SELECT della prenotazione dentro la transazione e usare FOR UPDATE su prestiti, oppure
  • usare una clausola aggiuntiva nell’UPDATE tipo ... WHERE id = ? AND stato = 'prenotato' e controllare affected_rows per capire se qualcuno l’ha già presa nel frattempo, per poi ritentare.

Non è un blocker per un contesto a bassa concorrenza, ma vale la pena tenerlo a mente.


267-290: handleNoCopyAvailable: semantica corretta, ma attenzione all’uso in transazioni esterne

La scelta di azzerare copia_id e lasciare la prenotazione attiva (in coda) è sensata, e la notifica viene giustamente inviata dopo il commit. Solo una nota di utilizzo:

  • Anche qui usi beginTransactionIfNeeded() / commitIfOwned(). Se in futuro questo metodo venisse chiamato dentro una transazione aperta altrove senza setExternalTransaction(true), si torna al problema di possibile nested‑transaction/implicit commit discusso sopra.

Valuterei di documentare chiaramente che i metodi pubblici del servizio (compreso il percorso che passa da handleNoCopyAvailable) vanno usati fuori da transazioni esplicite, a meno di impostare esplicitamente l’“external transaction mode”.


315-341: Metodo privato non usato findAvailableCopy

PHPMD segnala (e il file conferma) che findAvailableCopy() non è referenziato: tutta la logica reale passa per findAvailableCopyExcluding().

Per evitare dead code e futuri fraintendimenti, ti suggerirei di:

  • rimuovere il metodo se non è realmente previsto un utilizzo, oppure
  • promuoverlo a helper comune e riusarlo (ad es. facendolo chiamare da findAvailableCopyExcluding() quando excludeCopiaIds è vuoto).

452-501: notifyUserCopyUnavailable: coerenza con NotificationService, ma il docblock è un po’ fuorviante

La funzione usa NotificationService::createNotification('general', ...) per creare una notifica admin e logga solo prestito_id/utente_id/reason, senza PII sensibili: bene.

L’unica cosa leggermente fuorviante è il docblock:

  • il commento dice “Notifica l’utente che la copia prenotata non è più disponibile”, ma in pratica non viene inviata nessuna email all’utente, solo una notifica admin.

Se l’intenzione è effettivamente solo quella di avvisare gli admin, potresti aggiornare la descrizione (o aggiungere in futuro un invio email qui, analogo a notifyUserCopyAvailable).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a70c0b and c0e0553.

📒 Files selected for processing (2)
  • app/Services/ReservationReassignmentService.php (1 hunks)
  • locale/en_US.json (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/Services/ReservationReassignmentService.php (2)
app/Support/NotificationService.php (3)
  • NotificationService (11-1362)
  • sendReservationBookAvailable (990-992)
  • createNotification (1067-1096)
app/Models/CopyRepository.php (2)
  • CopyRepository (8-245)
  • updateStatus (92-100)
🪛 PHPMD (2.15.0)
app/Services/ReservationReassignmentService.php

315-341: Avoid unused private methods such as 'findAvailableCopy'. (undefined)

(UnusedPrivateMethod)

🔇 Additional comments (8)
locale/en_US.json (4)

110-129: ✓ Aggiornamento e backup - Traduzioni coerenti e complete

Le nuove voci relative al flusso di aggiornamento (linee 110-129) seguono pattern coerenti con il resto del file. Esempi ben tradotti:

  • "Aggiornamento completato con successo" → "Update completed successfully" ✓
  • "Avvio backup database" → "Starting database backup" ✓
  • "Memory limit aumentato" → "Memory limit increased" ✓

Nessun problema di traduzione rilevato.


1689-1703: ✓ MaintenanceService - Messaggi coerenti e ben localizzati

Le voci MaintenanceService (linee 1689-1703) mantengono un pattern coerente con prefisso "MaintenanceService " seguono da descrizione dell'errore/stato. Le traduzioni sono tecnicamente accurate:

  • "MaintenanceService connessione database fallita" → "MaintenanceService database connection failed" ✓
  • "MaintenanceService errore generazione ICS" → "MaintenanceService ICS generation error" ✓
  • "MaintenanceService prenotazione convertita in prestito" → "MaintenanceService reservation converted to loan" ✓

Tutte le voci seguono lo stesso pattern strutturale. Nessun problema rilevato.


2271-2271: ✓ Messaggi utente - Traduzioni chiare e contestualmente appropriate

Linea 2271: "Questo utente ha già un prestito o una prenotazione attiva per questo libro." → "This user already has an active loan or reservation for this book." - Traduzione accurata che mantiene il significato del vincolo di business logic. ✓

Linea 2474: "Scaduta il" → "Expired on" - Introduzione appropriata di nuovo stato per prenotazioni scadute (allineato con "scaduto" su linea 2473). ✓

Also applies to: 2474-2474


3448-3448: ✓ Messaggio di integrità dati - Terminology coerente

Linea 3448: "Prestito ID %d con stato '%s' ha ancora attivo = 1" → "Loan ID %d with status '%s' still has active = 1"

Traduzione corretta che mantiene il riferimento tecnico al campo database "attivo". Il formato dell'ID prestito e stato è coerente con altri messaggi diagnostici nel file.

app/Services/ReservationReassignmentService.php (4)

175-262: Buona gestione della copia persa con tentativi multipli e fallback

La nuova implementazione di reassignOnCopyLost() è decisamente più robusta:

  • loop con maxRetries e lista $excludedCopies per evitare loop infiniti;
  • lock della copia candidata con SELECT ... FOR UPDATE e verifica dello stato prima di aggiornare prestiti e lo stato della copia;
  • in caso di errori/race, rollback condizionato e log strutturato con attempt;
  • alla fine, log di warning e handleNoCopyAvailable() se non si trova una copia valida.

Questa struttura copre bene il race condition evidenziato nelle review precedenti ed evita di lasciare la prenotazione appesa alla copia persa.


296-313: reassignOnReturn: integrazione semplice e leggibile

La funzione che risale al libro_id dalla copia e delega a reassignOnNewCopy() rende il comportamento del “ritorno copia” consistente con l’aggiunta di una nuova copia. Logica lineare, niente da segnalare.


382-447: Notifica “copia disponibile”: query e payload coerenti, fix enum autore corretto

Questa parte è ben strutturata:

  • query con join su utenti e libri + controllo robusto su $data['email'];
  • recupero dell’autore principale con ORDER BY la.ruolo = 'principale' DESC che corregge il precedente uso del valore enum inesistente 'autore';
  • costruzione delle variabili per il template email con fallback sensati e link basati sulla getBaseUrl().

Nel complesso il flusso è chiaro e aderente al resto del sistema di notifiche.


506-524: getBaseUrl: gestione robusta di URL canonica e fallback host

La costruzione della base URL è difensiva:

  • preferisce APP_CANONICAL_URL validata con filter_var;
  • in assenza, usa protocollo da HTTPS + HTTP_HOST ma lo valida con una regex restrittiva, con fallback sicuro su localhost.

Implementazione pulita e consistente con un contesto web/CLI misto.

Comment on lines 3595 to 3626
"Eccezione creazione tabella %s:": "Exception creating table %s:",
"Errore riassegnazione copia": "Copy reassignment error",
"Errore riassegnazione copia persa": "Lost copy reassignment error",
"Impossibile notificare utente: dati mancanti": "Cannot notify user: missing data",
"Notifica prenotazione disponibile inviata": "Reservation available notification sent",
"Invio notifica prenotazione fallito": "Reservation notification failed",
"Impossibile notificare utente copia non disponibile": "Cannot notify user copy unavailable",
"La copia assegnata è stata segnalata come persa o danneggiata": "The assigned copy has been reported as lost or damaged",
"La prenotazione è scaduta": "The reservation has expired",
"La copia non è più disponibile": "The copy is no longer available",
"Prenotazione: copia non disponibile": "Reservation: copy unavailable",
"Prenotazione per \"%s\" (utente: %s) messa in attesa. %s.": "Reservation for \"%s\" (user: %s) put on hold. %s.",
"Notifica copia non disponibile creata": "Copy unavailable notification created",
"Errore gestione cambio stato copia": "Copy status change handling error",
"DataIntegrity warning (store loan)": "DataIntegrity warning (store loan)",
"Notifica prestito fallita": "Loan notification failed",
"Validazione prestito fallita": "Loan validation failed",
"Riassegnazione copia fallita": "Copy reassignment failed",
"Errore elaborazione restituzione": "Return processing error",
"Rinnovo prestito fallito": "Loan renewal failed",
"Errore export CSV": "CSV export error",
"Errore annullamento prestito": "Loan cancellation error",
"Errore annullamento prenotazione": "Reservation cancellation error",
"Errore modifica data prenotazione": "Reservation date change error",
"Notifica richiesta prestito fallita": "Loan request notification failed",
"Errore richiesta prestito": "Loan request error",
"Errore prenotazione": "Reservation error",
"Riassegnazione prenotazione nuova copia fallita": "New copy reservation reassignment failed",
"Elaborazione lista attesa fallita": "Waitlist processing failed",
"La tua prenotazione per \"%s\" è stata messa in attesa. %s. Verrai notificato quando sarà disponibile una nuova copia.": "Your reservation for \"%s\" has been put on hold. %s. You will be notified when a new copy becomes available.",
"Esauriti tentativi riassegnazione copia": "Exhausted copy reassignment attempts",
"Errore gestione copia non disponibile": "Copy unavailable handling error"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's verify the file and line numbers exist
head -c 500 locale/en_US.json | tail -c 200
wc -l locale/en_US.json
sed -n '3595,3626p' locale/en_US.json | head -20

Repository: fabiodalez-dev/Pinakes

Length of output: 1823


🏁 Script executed:

# Search for usage of these translation keys in PHP code
rg "Errore riassegnazione copia|Impossibile notificare utente|Notifica prenotazione disponibile" --type=php -n | head -30

Repository: fabiodalez-dev/Pinakes

Length of output: 738


🏁 Script executed:

# Check for ReservationReassignmentService and related classes
rg "ReservationReassignmentService|MaintenanceService" --type=php -n -A 3 | head -50

Repository: fabiodalez-dev/Pinakes

Length of output: 3093


🏁 Script executed:

# Verify Italian translations exist in locale/it_IT.json
ls -la locale/it_IT.json 2>/dev/null || echo "File not found"
wc -l locale/it_IT.json 2>/dev/null || echo "Cannot check file"

Repository: fabiodalez-dev/Pinakes

Length of output: 153


🏁 Script executed:

# Search for the English keys in PHP to verify usage
rg "Copy reassignment error|Cannot notify user|Loan notification failed" --type=php -n | head -30

Repository: fabiodalez-dev/Pinakes

Length of output: 48


🏁 Script executed:

# Read the Italian locale file completely
cat locale/it_IT.json

Repository: fabiodalez-dev/Pinakes

Length of output: 3428


🏁 Script executed:

# Search for more of the 32 translation keys in PHP code
rg "Invio notifica prenotazione fallito|La prenotazione è scaduta|La copia non è più disponibile|DataIntegrity warning|Notifica prestito fallita|Validazione prestito fallita|Riassegnazione copia fallita|Errore elaborazione restituzione|Rinnovo prestito fallito" --type=php -n

Repository: fabiodalez-dev/Pinakes

Length of output: 1710


🏁 Script executed:

# Check how many of the new keys are actually referenced in PHP files
echo "=== Checking all 32 new keys usage ==="
rg "Eccezione creazione tabella|Errore riassegnazione copia|Errore riassegnazione copia persa|Impossibile notificare utente|Notifica prenotazione disponibile inviata|Invio notifica prenotazione fallito|Impossibile notificare utente copia non disponibile|La copia assegnata è stata segnalata come persa|La prenotazione è scaduta|La copia non è più disponibile|Prenotazione: copia non disponibile|Prenotazione per|Notifica copia non disponibile creata|Errore gestione cambio stato copia|DataIntegrity warning|Notifica prestito fallita|Validazione prestito fallita|Riassegnazione copia fallita|Errore elaborazione restituzione|Rinnovo prestito fallito|Errore export CSV|Errore annullamento prestito|Errore annullamento prenotazione|Errore modifica data prenotazione|Notifica richiesta prestito fallita|Errore richiesta prestito|Errore prenotazione|Riassegnazione prenotazione nuova copia fallita|Elaborazione lista attesa fallita|La tua prenotazione per|Esauriti tentativi riassegnazione copia|Errore gestione copia non disponibile" --type=php | wc -l

Repository: fabiodalez-dev/Pinakes

Length of output: 107


🏁 Script executed:

# List all files that use __() translation function
rg "__('" --type=php -l | head -20

Repository: fabiodalez-dev/Pinakes

Length of output: 123


🏁 Script executed:

# Search for specific usage of each key group to count exactly which ones are used
echo "=== Checking key usage in detail ==="
rg "Errore riassegnazione copia|Errore riassegnazione copia persa|Impossibile notificare utente|Notifica prenotazione disponibile inviata|Invio notifica prenotazione fallito" --type=php -c | head -20

Repository: fabiodalez-dev/Pinakes

Length of output: 152


🏁 Script executed:

# Check if Italian keys are missing by searching en_US.json structure
head -20 locale/en_US.json
tail -20 locale/en_US.json

Repository: fabiodalez-dev/Pinakes

Length of output: 2322


🏁 Script executed:

# Check all locale files in the repository
find locale -type f -name "*.json" | head -20

Repository: fabiodalez-dev/Pinakes

Length of output: 151


🏁 Script executed:

# Try to find orphaned keys - those in en_US.json but not used in PHP
echo "=== Keys that might be orphaned ==="
rg "Errore export CSV|Errore annullamento prestito|Errore annullamento prenotazione|Errore modifica data prenotazione|Notifica richiesta prestito fallita|Errore richiesta prestito|Errore prenotazione|Riassegnazione prenotazione nuova copia fallita|Elaborazione lista attesa fallita|Esauriti tentativi riassegnazione copia|Errore gestione copia non disponibile" --type=php

Repository: fabiodalez-dev/Pinakes

Length of output: 1603


🏁 Script executed:

# Final verification: Count total keys in en_US.json vs it_IT.json
echo "=== Locale file sizes ==="
echo "en_US.json lines: $(wc -l < locale/en_US.json)"
echo "it_IT.json lines: $(wc -l < locale/it_IT.json)"

# Check if it_IT.json has similar structure (should have corresponding keys)
echo -e "\n=== Checking if it_IT.json mirrors en_US.json structure ==="
head -5 locale/it_IT.json | grep -o ":" | wc -l
grep -c "\".*\":" locale/en_US.json || echo "Keys in en_US.json"

Repository: fabiodalez-dev/Pinakes

Length of output: 203


🏁 Script executed:

# Verify one more key that was supposedly "orphaned" to confirm all are used
rg "Errore export CSV|Errore annullamento prestito" --type=php -B 2 -A 2 | head -20

Repository: fabiodalez-dev/Pinakes

Length of output: 959


All 32 new translation keys are actively used; Italian locale file severely incomplete

Verification confirms all 32 translation keys introduced in lines 3595-3626 are actively used throughout the codebase:

  • ReservationReassignmentService.php uses 9 keys
  • PrestitiController.php uses 8+ keys
  • UserActionsController.php uses 7 keys
  • LibriController.php uses 2 keys
  • LoanRepository.php uses 1 key

No orphaned translations exist. However, a critical localization gap has been identified: locale/it_IT.json contains only 42 lines with approximately 4 key-value pairs, while locale/en_US.json contains 3,627 lines with ~3,625 translations. The Italian locale file is missing not just these 32 keys, but the entire set of library management, loan, and reservation translations—leaving Italian users unable to view any error messages in their language.

Action required: Populate locale/it_IT.json with Italian translations for all entries in locale/en_US.json, or determine if a different localization strategy is in place.

- DataTable: use server default ordering (p.id DESC) to show most recent
  loans/reservations first instead of ordering by book title column
- MaintenanceService: add expired_reservations to PHPDoc array-shape for
  runAll() and runIfNeeded() return types
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Views/prestiti/index.php (1)

5-26: Allineare la vista ai nuovi stati annullato e scaduto dell’enum prestiti.status

In questa vista i nuovi stati annullato e scaduto non sono mai gestiti (badge, filtri rapidi, colonna stato della DataTable, export CSV). I prestiti con questi stati finiranno come “Sconosciuto” e non saranno filtrabili/esportabili per stato, disallineando la UI rispetto al modello dati introdotto nel PR.

Suggerisco di aggiungere esplicitamente i due stati in tutti i punti interessati:

  1. Badge di stato (getStatusBadge)
-        case 'danneggiato':
-            return "<span class='$baseClasses bg-red-100 text-red-800'><i class='fas fa-times-circle mr-2'></i>" . __("Danneggiato") . "</span>";
+        case 'danneggiato':
+            return "<span class='$baseClasses bg-red-100 text-red-800'><i class='fas fa-times-circle mr-2'></i>" . __("Danneggiato") . "</span>";
+        case 'annullato':
+            return "<span class='$baseClasses bg-gray-100 text-gray-800'><i class='fas fa-ban mr-2'></i>" . __("Annullato") . "</span>";
+        case 'scaduto':
+            return "<span class='$baseClasses bg-red-100 text-red-800'><i class='fas fa-calendar-times mr-2'></i>" . __("Scaduto") . "</span>";
  1. Pulsanti filtro rapidi sopra la tabella
               <button data-status="in_ritardo" class="status-filter-btn btn-secondary px-3 py-1.5"><?= __("In Ritardo") ?></button>
+              <button data-status="scaduto" class="status-filter-btn btn-secondary px-3 py-1.5"><?= __("Scaduto") ?></button>
+              <button data-status="annullato" class="status-filter-btn btn-secondary px-3 py-1.5"><?= __("Annullato") ?></button>
               <button data-status="restituito" class="status-filter-btn btn-secondary px-3 py-1.5"><?= __("Restituito") ?></button>
  1. Render dello stato nella colonna DataTables
                         case 'danneggiato':
                             return `<span class='${baseClasses} bg-red-100 text-red-800'><i class='fas fa-times-circle mr-2'></i><?= __("Danneggiato") ?></span>`;
+                        case 'annullato':
+                            return `<span class='${baseClasses} bg-gray-100 text-gray-800'><i class='fas fa-ban mr-2'></i><?= __("Annullato") ?></span>`;
+                        case 'scaduto':
+                            return `<span class='${baseClasses} bg-red-100 text-red-800'><i class='fas fa-calendar-times mr-2'></i><?= __("Scaduto") ?></span>`;
                         default:
  1. Stati selezionabili nell’export CSV
                     <label class="flex items-center gap-2 cursor-pointer p-2 hover:bg-gray-50 rounded">
                         <input type="checkbox" name="export-status" value="in_ritardo" class="export-status-cb w-4 h-4 text-yellow-600 rounded" checked>
                         <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800"><i class="fas fa-exclamation-triangle mr-1"></i>${__('In Ritardo')}</span>
                     </label>
+                    <label class="flex items-center gap-2 cursor-pointer p-2 hover:bg-gray-50 rounded">
+                        <input type="checkbox" name="export-status" value="scaduto" class="export-status-cb w-4 h-4 text-red-600 rounded" checked>
+                        <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800"><i class="fas fa-calendar-times mr-1"></i>${__('Scaduto')}</span>
+                    </label>
+                    <label class="flex items-center gap-2 cursor-pointer p-2 hover:bg-gray-50 rounded">
+                        <input type="checkbox" name="export-status" value="annullato" class="export-status-cb w-4 h-4 text-gray-600 rounded" checked>
+                        <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800"><i class="fas fa-ban mr-1"></i>${__('Annullato')}</span>
+                    </label>
                     <label class="flex items-center gap-2 cursor-pointer p-2 hover:bg-gray-50 rounded">
                         <input type="checkbox" name="export-status" value="restituito" class="export-status-cb w-4 h-4 text-green-600 rounded" checked>
                         <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"><i class="fas fa-check-circle mr-1"></i>${__('Restituito')}</span>
                     </label>

Così la UI resta coerente con le nuove regole di dominio introdotte dalle migration.

Also applies to: 188-193, 325-345, 494-520

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c0e0553 and 327d125.

📒 Files selected for processing (2)
  • app/Support/MaintenanceService.php (8 hunks)
  • app/Views/prestiti/index.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/Support/MaintenanceService.php (5)
app/Controllers/UserActionsController.php (1)
  • loan (380-456)
app/Controllers/PrestitiController.php (1)
  • close (496-505)
app/Services/ReservationReassignmentService.php (3)
  • ReservationReassignmentService (17-525)
  • setExternalTransaction (35-39)
  • reassignOnReturn (296-313)
app/Support/DataIntegrity.php (2)
  • DataIntegrity (9-1252)
  • recalculateBookAvailability (234-348)
app/Controllers/CopyController.php (1)
  • updateCopy (51-161)
🔇 Additional comments (6)
app/Support/MaintenanceService.php (5)

43-43: LGTM: Annotazioni di tipo aggiornate correttamente.

Le annotazioni di tipo per expired_reservations sono state aggiunte in modo coerente sia a runIfNeeded() che a runAll(), e il contatore è correttamente inizializzato nell'array dei risultati.

Also applies to: 68-68, 75-75


88-88: LGTM: Migrazione a SecureLogger completata correttamente.

La migrazione da error_log a SecureLogger è stata implementata in modo coerente in tutto il file, con contesto strutturato appropriato (ID, messaggi di errore, stato). Questo migliora l'osservabilità e la sicurezza del logging.

Also applies to: 95-95, 103-103, 110-110, 121-121, 129-129, 133-133, 247-250, 327-330, 338-341, 438-442, 446-449, 533-535, 551-557, 565-567


96-104: LGTM: Integrazione di checkExpiredReservations pulita e coerente.

L'integrazione del nuovo task di manutenzione segue lo stesso pattern degli altri task esistenti, con gestione degli errori appropriata e logging strutturato.


382-384: LGTM: Transazioni nidificate risolte correttamente.

L'uso di setExternalTransaction(true) risolve la preoccupazione precedente sulle transazioni nidificate, delegando il controllo della transazione al chiamante (MaintenanceService) invece che al servizio di riassegnazione.

Based on learnings, this addresses the previous major concern about nested transaction handling.


395-409: LGTM: Concatenazione SQL corretta con parametri vincolati.

Il suffisso della nota è ora costruito in PHP e passato come parametro vincolato tramite bind_param('si', $noteSuffix, $id), risolvendo il rischio di SQL injection precedentemente segnalato.

Based on learnings, this addresses the previous major concern about SQL string concatenation.

app/Views/prestiti/index.php (1)

288-372: order: [] correctly delegates ordering to server with proper default

The implementation is sound. With serverSide: true and order: [], the client sends no initial order parameters. The backend correctly applies ORDER BY p.id DESC as the default (lines 108–109 of PrestitiApiController), and respects any order parameters sent by DataTables on subsequent interactions (lines 122–130). DataTables 2.3.4 fully supports this pattern.

- Add deferred notifications queue to ReservationReassignmentService
- Notifications are queued when externalTransaction=true
- MaintenanceService calls flushDeferredNotifications() after commit
- Prevents race condition where notifications sent before data persisted

Also fixes session-expired.php:
- Allow $pageTitle and $loginUrl override via null coalescing
- Replace CDN Font Awesome with local /assets/vendor.css
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
locale/en_US.json (1)

3595-3627: Nuove stringhe errori/manutenzione per riassegnazione e integrità dati: placeholder OK, copertura completa

Le ~32 nuove chiavi (eccezioni tabelle, errori riassegnazione copia/prenotazione, export CSV, validazione prestiti, lista d’attesa, notifica differita) hanno:

  • conteggio placeholder %s coerente tra IT/EN
  • messaggi ben distinti per i vari rami di errore (log + notifiche)

Unico follow‑up: assicurarsi che le stesse chiavi esistano e siano tradotte anche nelle altre locale (in particolare locale/it_IT.json), per evitare fallback misti in produzione.

🧹 Nitpick comments (4)
app/Services/ReservationReassignmentService.php (2)

80-130: Rilevamento della transazione tramite @@autocommit è fragile; chiarire il contratto d’uso

L’uso di SELECT @@autocommit in isInTransaction() presuppone che tutte le transazioni esplicite vengano aperte con mysqli::begin_transaction() (che disattiva l’autocommit). Se in futuro qualche chiamante usasse START TRANSACTION o SET autocommit = 0 in modo diverso, il servizio potrebbe:

  • non riconoscere una transazione già aperta e chiamare begin_transaction() (rischio di commit impliciti/non attesi), oppure
  • considerare “in transazione” una connessione solo perché l’autocommit è 0 di default.

Suggerirei di rendere il contratto esplicito in docblock (es. “se usato dentro transazioni esterne è obbligatorio setExternalTransaction(true) oppure begin_transaction() di mysqli”) o, meglio, basare isInTransaction() solo su externalTransaction e rimuovere l’euristica su @@autocommit.


508-557: notifyUserCopyUnavailable: solo notifica admin (no email utente) — comportamento intenzionale?

Qui viene:

  • caricata prenotazione + utente + libro
  • scelta del messaggio in base a $reason (lost_copy, expired, default)
  • creata una notifica in‑app con createNotification, senza invio email all’utente

Se l’intenzione è informare solo l’amministratore (e non l’utente finale) sul fatto che la prenotazione è tornata “in attesa”, il comportamento è ok; in caso contrario valuterei l’aggiunta di una mail utente analoga a quella di disponibilità.

app/Support/MaintenanceService.php (2)

427-430: Considera di isolare la riassegnazione in un try-catch separato per garantire che la scadenza venga sempre completata.

Attualmente, se reassignOnReturn() alla riga 428 lancia un'eccezione, il blocco catch alla riga 447 esegue il rollback dell'intera transazione, impedendo che la prenotazione scaduta venga marcata come 'scaduto'. Dal punto di vista della logica di business, la marcatura di una prenotazione scaduta dovrebbe avere successo anche se la riassegnazione fallisce, poiché la scadenza è un requisito obbligatorio mentre la riassegnazione è un'ottimizzazione.

Anche se ReservationReassignmentService probabilmente gestisce gli errori internamente, errori imprevisti del database durante la riassegnazione potrebbero comunque causare il rollback.

Applica questa modifica per garantire che la scadenza venga sempre completata:

                     }
                 }
 
+                // Tentativo di riassegnazione (non bloccare la scadenza se fallisce)
+                if ($copiaId) {
+                    try {
+                        $reassignmentService->reassignOnReturn($copiaId);
+                    } catch (\Throwable $reassignError) {
+                        // Log ma non bloccare la scadenza
+                        SecureLogger::warning(__('Errore durante riassegnazione post-scadenza'), [
+                            'prestito_id' => $id,
+                            'copia_id' => $copiaId,
+                            'error' => $reassignError->getMessage()
+                        ]);
+                    }
+                }
+
                 // Recalculate book availability
                 $integrity->recalculateBookAvailability($libroId);

E rimuovi la chiamata originale alle righe 427-429.

Also applies to: 447-452


348-457: Implementazione solida di checkExpiredReservations() con buona gestione delle transazioni e del locking.

Punti di forza dell'implementazione:

  • Documentazione chiara che spiega lo scopo del metodo (Case 4 del piano di prenotazione)
  • Query preparata con binding corretto dei parametri (righe 363-379)
  • Locking pessimistico con FOR UPDATE sulla copia (riga 413) per prevenire race conditions
  • Controllo difensivo dello stato della copia (riga 420) per evitare di liberare copie già prestate o danneggiate
  • Logging strutturato con IDs per la tracciabilità (righe 441-445, 449-452)
  • Isolamento degli errori tramite transazioni separate per ogni prenotazione, così i fallimenti non bloccano l'intero processo

Nota prestazionale (opzionale): Il loop processa ogni prenotazione in una transazione separata, il che garantisce l'isolamento ma potrebbe essere lento con molte prenotazioni scadute simultaneamente. Per la natura di un task di manutenzione periodico, questo approccio è accettabile. Se in futuro si osservassero timeout o rallentamenti, si potrebbe considerare il batching o la limitazione del numero di prenotazioni processate per esecuzione.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 327d125 and 8ed995a.

📒 Files selected for processing (4)
  • app/Services/ReservationReassignmentService.php (1 hunks)
  • app/Support/MaintenanceService.php (8 hunks)
  • app/Views/errors/session-expired.php (1 hunks)
  • locale/en_US.json (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Views/errors/session-expired.php
🧰 Additional context used
🧬 Code graph analysis (2)
app/Services/ReservationReassignmentService.php (2)
app/Support/NotificationService.php (2)
  • NotificationService (11-1362)
  • sendReservationBookAvailable (990-992)
app/Models/CopyRepository.php (2)
  • CopyRepository (8-245)
  • updateStatus (92-100)
app/Support/MaintenanceService.php (2)
app/Services/ReservationReassignmentService.php (4)
  • ReservationReassignmentService (17-581)
  • setExternalTransaction (42-46)
  • reassignOnReturn (352-369)
  • flushDeferredNotifications (52-70)
app/Support/DataIntegrity.php (2)
  • DataIntegrity (9-1252)
  • recalculateBookAvailability (234-348)
🪛 PHPMD (2.15.0)
app/Services/ReservationReassignmentService.php

371-397: Avoid unused private methods such as 'findAvailableCopy'. (undefined)

(UnusedPrivateMethod)

🔇 Additional comments (13)
locale/en_US.json (4)

110-129: Stringhe updater/backup coerenti con il nuovo flusso di aggiornamento

Le nuove chiavi per avanzamento aggiornamento/backup sembrano consistenti con la logica dell’updater (nessun placeholder mancante o disallineato). Nulla da segnalare dal punto di vista tecnico.


1690-1703: Messaggi MaintenanceService allineati al codice di manutenzione

Le stringhe di log per MaintenanceService coprono in modo granulare i vari rami (prestiti, prenotazioni, ICS, notifiche). I testi sono chiari e coerenti; verificate solo che i nomi corrispondano esattamente ai messaggi usati nel codice per evitare chiavi “quasi duplicate”.


2271-2271: Messaggio blocco doppio prestito/prenotazione semanticamente corretto

"Questo utente ha già un prestito o una prenotazione attiva per questo libro." descrive bene il vincolo applicato lato dominio; nulla da eccepire sulle stringhe.


2474-2474: Label “Scaduta il” in linea con i nuovi stati di prestito/prenotazione

La nuova chiave per data di scadenza passata è coerente con l’introduzione dello stato scaduto; naming chiaro e allineato alle altre etichette di stato.

app/Services/ReservationReassignmentService.php (5)

136-216: Flusso reassignOnNewCopy: logica e concorrenza gestite in modo sensato

  • Selezione FIFO della prenotazione bloccata (stato = 'prenotato', attivo = 1, copia nulla o non disponibile) è coerente.
  • Lock della nuova copia con SELECT ... FOR UPDATE e ricontrollo stato = 'disponibile' riduce bene le race condition.
  • Aggiornamento di prestiti.copia_id + copie.stato = 'prenotato' dentro transazione, con notifica post‑commit o differita, è corretto.

Nulla di critico da segnalare qui.


222-309: reassignOnCopyLost: buon handling con retry ed esclusione copie già provate

La logica:

  • trova la prenotazione attiva sulla copia persa
  • tenta fino a maxRetries di assegnare copie disponibili diverse tramite findAvailableCopyExcluding
  • per ogni tentativo blocca la copia con FOR UPDATE e ricontrolla stato = 'disponibile'
  • in caso di saturazione o errori ripetuti, logga warning e passa a handleNoCopyAvailable

È un buon compromesso tra robustezza e semplicità; l’early‑return finale evita che la prenotazione rimanga agganciata alla copia persa.


314-346: handleNoCopyAvailable: comportamento chiaro per prenotazioni senza copia disponibile

Impostare copia_id = NULL e spostare la prenotazione in “coda senza copia” è semanticamente pulito; la distinzione tra notifica immediata e differita quando externalTransaction è true è coerente col resto del servizio. Log di errore adeguato in caso di eccezioni.


438-503: notifyUserCopyAvailable: recupero dati e variabili template ben strutturati

  • La query unisce prestiti, utenti, libri con le colonne necessarie.
  • Recupero autore principale ordinando per la.ruolo = 'principale' è coerente con l’enum dello schema.
  • Scelta ISBN (isbn13isbn10 → vuoto) e costruzione di book_url / profile_url con getBaseUrl() risultano sensate.
  • Logging usa solo prestito_id e utente_id, evitando PII superflue.

Nessun problema evidente.


562-580: getBaseUrl: gestione robusta tra APP_CANONICAL_URL e fallback HTTP_HOST

  • Usa APP_CANONICAL_URL se presente e valido, ripulito e senza slash finale.
  • In fallback costruisce l’URL da protocollo + HTTP_HOST con validazione regex, ripiegando su localhost se l’host non è sicuro.

La funzione è allineata alle nuove stringhe di diagnostica su APP_CANONICAL_URL; niente di anomalo.

app/Support/MaintenanceService.php (4)

382-384: Ottimo lavoro: i problemi segnalati nelle review precedenti sono stati risolti correttamente.

Le modifiche implementate risolvono efficacemente tutti e tre i problemi critici identificati in precedenza:

  1. Transazioni annidate risolte: L'uso di setExternalTransaction(true) alla riga 384 impedisce al ReservationReassignmentService di avviare transazioni annidate, lasciando il controllo transazionale al chiamante.

  2. Concatenazione SQL risolta: Il suffisso della nota è ora costruito in PHP (riga 396) e passato come parametro vincolato (riga 407), eliminando i rischi di SQL injection da stringhe tradotte con caratteri speciali.

  3. Notifiche premature risolte: La chiamata a flushDeferredNotifications() alla riga 439 avviene DOPO il commit della transazione (riga 435), garantendo che le notifiche vengano inviate solo se le modifiche al database persistono con successo.

L'implementazione dimostra una buona comprensione dei pattern di gestione transazionale e della sicurezza SQL.

Also applies to: 396-409, 428-428, 435-435, 439-439


88-88: Eccellente migrazione al logging centralizzato.

La sostituzione di error_log con SecureLogger è implementata in modo consistente in tutto il file, con logging strutturato che include contesto rilevante (ID delle entità, messaggi di errore, ecc.). Questo migliora notevolmente l'osservabilità e facilita il debugging in produzione.

Also applies to: 95-95, 103-103, 110-110, 121-121, 129-129, 133-133, 247-250, 327-330, 338-341


536-538: Aggiornamenti al hook di login admin ben implementati.

Le modifiche includono:

  • Logging strutturato degli errori di connessione al database (righe 536-538)
  • Inclusione di expired_reservations nelle metriche registrate dopo l'esecuzione delle manutenzioni (riga 558)
  • Gestione coerente degli errori con SecureLogger (righe 568-570)

La gestione delle risorse è corretta, con chiusura appropriata della connessione DB anche in caso di errore.

Also applies to: 554-560, 568-570


43-43: Integrazione pulita del task expired_reservations nel workflow di manutenzione.

Le modifiche sono ben strutturate:

  • Documentazione PHPDoc aggiornata per riflettere il nuovo campo nei risultati (righe 43, 68)
  • Inizializzazione corretta nel array $results (riga 75)
  • Gestione errori coerente con gli altri task di manutenzione (righe 99-104)

L'integrazione segue gli stessi pattern degli altri task esistenti, mantenendo coerenza nel codice.

Also applies to: 68-68, 75-75, 99-104

Comment on lines +371 to +397
private function findAvailableCopy(int $libroId, ?int $excludeCopiaId = null): ?int
{
$sql = "
SELECT id
FROM copie
WHERE libro_id = ?
AND stato = 'disponibile'
";
$params = [$libroId];
$types = "i";

if ($excludeCopiaId) {
$sql .= " AND id != ?";
$params[] = $excludeCopiaId;
$types .= "i";
}

$sql .= " LIMIT 1";

$stmt = $this->db->prepare($sql);
$stmt->bind_param($types, ...$params);
$stmt->execute();
$res = $stmt->get_result()->fetch_assoc();
$stmt->close();

return $res ? (int) $res['id'] : null;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for all usages of findAvailableCopy in the codebase
rg -n "findAvailableCopy" --type=php -C2

Repository: fabiodalez-dev/Pinakes

Length of output: 1416


Rimuovere il metodo privato findAvailableCopy: è codice morto non utilizzato

La ricerca nel codebase conferma che findAvailableCopy() non viene mai invocato. Il metodo è stato probabilmente creato durante un refactoring ma mai integrato nella logica effettiva. Esiste già findAvailableCopyExcluding() (riga 404) che viene utilizzato correttamente (riga 249) e gestisce l'esclusione di più copie.

Eliminare il metodo inutilizzato per evitare confusione nella manutenzione e ridurre la complessità del servizio.

🧰 Tools
🪛 PHPMD (2.15.0)

371-397: Avoid unused private methods such as 'findAvailableCopy'. (undefined)

(UnusedPrivateMethod)

🤖 Prompt for AI Agents
In app/Services/ReservationReassignmentService.php around lines 371 to 397, the
private method findAvailableCopy is dead code (never called) and duplicates
functionality provided by findAvailableCopyExcluding; remove the entire private
method declaration and any related unused variables/imports, then run tests/grep
to confirm there are no remaining references and update any docblocks or
comments that referenced it.

- Increase memory limit to 256MB for large updates
- Add comprehensive logging at every step (GitHub API, download, ZIP, migrations)
- Add connection diagnostics (DNS, SSL, cURL test) on failure
- Helps troubleshoot update issues on different server configurations
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Support/Updater.php (1)

1383-1386: Splitting SQL per punto e virgola è fragile.

La divisione delle istruzioni SQL usando explode(';', $sql) non gestisce correttamente i punti e virgola all'interno di stringhe, procedure, o trigger. Questo potrebbe causare errori di migrazione con SQL complesso.

Per migrazioni semplici va bene, ma se in futuro si aggiungono procedure o trigger, considerare l'uso di un parser SQL più robusto o eseguire i file di migrazione direttamente con mysqli_multi_query.

🧹 Nitpick comments (4)
app/Support/Updater.php (4)

268-272: Limitare il logging della risposta HTTP.

La preview della risposta (primi 500 caratteri) potrebbe contenere dati sensibili in caso di errori API che includono token o informazioni di autenticazione nei messaggi.

         $this->debugLog('DEBUG', 'Risposta HTTP ricevuta', [
             'response_length' => $response !== false ? strlen($response) : 0,
             'response_headers' => $responseHeaders,
-            'response_preview' => $response !== false ? substr($response, 0, 500) : 'FALSE'
+            'response_preview' => $response !== false ? '[REDACTED - ' . strlen($response) . ' bytes]' : 'FALSE'
         ]);

538-557: Caricamento completo in memoria per download di grandi dimensioni.

Il file viene scaricato interamente in memoria con file_get_contents prima di essere scritto su disco. Per pacchetti di aggiornamento molto grandi, questo potrebbe causare esaurimento della memoria nonostante l'aumento del limite a 256MB.

Considerare l'uso di streaming per il download:

// Alternativa con streaming
$sourceStream = @fopen($downloadUrl, 'r', false, $context);
$destStream = @fopen($zipPath, 'w');
if ($sourceStream && $destStream) {
    stream_copy_to_stream($sourceStream, $destStream);
    fclose($sourceStream);
    fclose($destStream);
}

608-613: Lettura inefficiente per diagnostic hex dump.

file_get_contents legge l'intero file ZIP solo per ottenere i primi 20 byte. Per file grandi questo spreca memoria.

-                    'file_first_bytes' => bin2hex(substr(file_get_contents($zipPath), 0, 20))
+                    'file_first_bytes' => bin2hex(file_get_contents($zipPath, false, null, 0, 20))

Oppure usare fread:

$handle = fopen($zipPath, 'rb');
$firstBytes = $handle ? bin2hex(fread($handle, 20)) : 'N/A';
if ($handle) fclose($handle);

948-968: Backup database senza lock potrebbe essere inconsistente.

L'implementazione streaming è corretta, ma il backup non utilizza LOCK TABLES o START TRANSACTION WITH CONSISTENT SNAPSHOT. Se i dati vengono modificati durante il backup, il risultato potrebbe essere inconsistente.

Considerare l'aggiunta di un lock per garantire consistenza:

+            // Lock all tables for consistent backup
+            $lockQuery = "LOCK TABLES " . implode(" READ, ", array_map(fn($t) => "`{$t}`", $tables)) . " READ";
+            $this->db->query($lockQuery);
+
             foreach ($tables as $table) {
                 // ... backup logic
             }
+
+            $this->db->query("UNLOCK TABLES");
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ed995a and d7f1ece.

📒 Files selected for processing (1)
  • app/Support/Updater.php (32 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/Support/Updater.php (1)
app/Controllers/UpdateController.php (1)
  • createBackup (122-152)
🔇 Additional comments (7)
app/Support/Updater.php (7)

59-89: LGTM!

L'inizializzazione è corretta. L'uso di uniqid('', true) per il path temporaneo fornisce sufficiente entropia, e la creazione della directory di backup con controllo di errore è appropriata.


339-410: LGTM!

La diagnostica della connessione è ben implementata. Verifica DNS, SSL, e fallback cURL con corretta chiusura delle risorse.


1903-1917: LGTM! Gestione robusta degli errori fatali.

Lo shutdown handler per la pulizia del maintenance mode in caso di errori fatali è una buona aggiunta per migliorare l'affidabilità. Il file locking previene correttamente aggiornamenti concorrenti.


2058-2089: LGTM!

L'implementazione di checkStaleMaintenanceMode() è solida. Il timeout di 30 minuti è ragionevole per gestire aggiornamenti interrotti, e la verifica del timestamp nel file JSON previene rimozioni accidentali.


1274-1288: LGTM! Buona protezione contro path traversal.

Le verifiche di sicurezza sono appropriate: controllo per .. e null bytes, verifica con realpath che i file restino nella directory di destinazione, e skip dei symlink per prevenire attacchi.


836-862: LGTM!

La validazione del path per l'eliminazione del backup è corretta. Il controllo regex per separatori di path e .., combinato con la verifica realpath, previene directory traversal.


2-9: Documentazione chiara e utile.

Il docblock con le istruzioni per filtrare i log (grep -i "updater") è un'aggiunta utile per il troubleshooting.

Comment on lines +94 to +110
private function debugLog(string $level, string $message, array $context = []): void
{
$fullMessage = "[Updater DEBUG] [{$level}] {$message}";

// Always log to error_log for immediate visibility
error_log($fullMessage . ' ' . json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

// Also log to SecureLogger if available
if (class_exists(SecureLogger::class)) {
$method = strtolower($level);
if (method_exists(SecureLogger::class, $method)) {
SecureLogger::$method($fullMessage, $context);
} else {
SecureLogger::info($fullMessage, $context);
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Attenzione ai dati sensibili nei log.

Il metodo debugLog registra il contesto completo in error_log. In varie parti del codice vengono loggati contenuti SQL, percorsi di file, e preview di risposte HTTP. Verificare che non vengano mai inclusi dati sensibili (credenziali, token, PII) nel contesto passato a questo metodo.

 private function debugLog(string $level, string $message, array $context = []): void
 {
     $fullMessage = "[Updater DEBUG] [{$level}] {$message}";

+    // Rimuovi eventuali dati sensibili dal contesto prima del logging
+    unset($context['password'], $context['token'], $context['secret']);
+
     // Always log to error_log for immediate visibility
     error_log($fullMessage . ' ' . json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

Comment on lines +2037 to +2056
private function parseMemoryLimit(string $limit): int
{
$limit = trim($limit);
$last = strtolower($limit[strlen($limit) - 1]);
$value = (int) $limit;

switch ($last) {
case 'g':
$value *= 1024 * 1024 * 1024;
break;
case 'm':
$value *= 1024 * 1024;
break;
case 'k':
$value *= 1024;
break;
}

return $value;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Edge case: stringa vuota causerebbe errore.

Se $limit è una stringa vuota dopo il trim(), la riga $last = strtolower($limit[strlen($limit) - 1]) accederà a un indice negativo.

 private function parseMemoryLimit(string $limit): int
 {
     $limit = trim($limit);
+    if ($limit === '' || $limit === '-1') {
+        return PHP_INT_MAX;
+    }
     $last = strtolower($limit[strlen($limit) - 1]);
     $value = (int) $limit;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private function parseMemoryLimit(string $limit): int
{
$limit = trim($limit);
$last = strtolower($limit[strlen($limit) - 1]);
$value = (int) $limit;
switch ($last) {
case 'g':
$value *= 1024 * 1024 * 1024;
break;
case 'm':
$value *= 1024 * 1024;
break;
case 'k':
$value *= 1024;
break;
}
return $value;
}
private function parseMemoryLimit(string $limit): int
{
$limit = trim($limit);
if ($limit === '' || $limit === '-1') {
return PHP_INT_MAX;
}
$last = strtolower($limit[strlen($limit) - 1]);
$value = (int) $limit;
switch ($last) {
case 'g':
$value *= 1024 * 1024 * 1024;
break;
case 'm':
$value *= 1024 * 1024;
break;
case 'k':
$value *= 1024;
break;
}
return $value;
}
🤖 Prompt for AI Agents
In app/Support/Updater.php around lines 2037-2056, the parseMemoryLimit()
function will error when $limit is an empty string because
$limit[strlen($limit)-1] is invalid; add a guard after trim() to handle empty
input (e.g. if $limit === '' return 0 or an appropriate default/unlimited
indicator) and only compute $last and cast $value when strlen($limit) > 0;
ensure the function still correctly handles numeric-only values and units after
this check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants