Skip to content

Release 0.7.59-rc.3: integrated fixes and hardened release gates - #341

Merged
fabiodalez-dev merged 73 commits into
mainfrom
release/0.7.59-rc.1
Aug 13, 2026
Merged

Release 0.7.59-rc.3: integrated fixes and hardened release gates#341
fabiodalez-dev merged 73 commits into
mainfrom
release/0.7.59-rc.1

Conversation

@fabiodalez-dev

@fabiodalez-dev fabiodalez-dev commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Integration branch for the 0.7.59-rc.1 release candidate. Bundles four open PRs on a dedicated branch (main untouched until the RC validates) and fixes the migration versioning + a plugin-hook self-heal found while verifying the live install.

What's integrated

Migration versioning fix

Only #340 ships a migration. It was named migrate_0.7.58.sql, but 0.7.58 is already released, so the updater would version-skip it for installs already on 0.7.58. Renamed to migrate_0.7.59-rc.1.sql (RC-named so it runs on the RC upgrade AND on the eventual stable 0.7.59); version.json bumped to 0.7.59-rc.1. Audited the other three PRs: none needs a migration (pre-existing columns/ENUM, settings with safe defaults + self-heal).

Conflict resolution

PrestitiController::update() date validation overlapped between #335 (separate error codes, F028) and #337 (strict-ISO validator). Resolved as a hybrid that satisfies both test suites: isStrictIsoDate validator + separate invalid_date_format / invalid_dates codes. Locale files reconciled to 6784-key parity.

Extra fix

PluginManager now self-heals orphan hook rows (a hook a plugin registered in an old version and dropped in a new one lingered in plugin_hooks and logged "Method not found" on every request, because upgrades don't re-run onActivate()). Deletes the dead row on first sight; logs once.

Validation

PHPStan level 5 clean, 100+ unit tests, migration guard, i18n parity, and the full reinstall-test (Test A fresh install + Test B real admin-UI upgrade 0.7.58 → 0.7.59-rc.1) green. RC published as a prerelease and verified on the live install.

Summary by CodeRabbit

  • Nuove funzionalità

    • Gestione delle serie complete con indicatore e modifica dello stato.
    • Configurazione del fuso orario e della durata predefinita dei prestiti.
    • Rinnovi con scadenza aggiornata, rinnovi residui e notifiche email.
    • Approvazione automatica delle richieste e storico con prestiti annullati o scaduti.
  • Miglioramenti

    • Validazione delle date, disponibilità e conflitti più accurati.
    • Ordinamento naturale dei codici inventario nelle etichette.
    • Accessibilità migliorata nello scanner e interfaccia più chiara.
  • Correzioni

    • Esclusione dei libri eliminati da conteggi e disponibilità.
    • Le richieste rifiutate restano visibili come annullate.

fabiodalez-dev and others added 25 commits August 10, 2026 13:50
…lete

Full-domain coherence review of the loan/reservation system. Eleven fixes,
each verified first-hand before changing anything:

Soft-delete & data exposure
- getBookAvailabilityData() now guards `deleted_at IS NULL` and returns null
  for missing/soft-deleted books; every caller (calendar route, localized
  availability route, disponibilita endpoint, mobile API) 404s cleanly. The
  localized public route used to serve real per-day occupancy for
  soft-deleted books to anonymous clients.

One clock everywhere (app timezone via DateHelper)
- calculateAvailability() default start was `new DateTime()` (process TZ):
  the mobile calendar began on "yesterday" between midnight and 2am app time.
- DashboardStats mixed three clocks (SQL server date, process date(),
  DateHelper) for the same `data_prestito <= today` predicate.
- DataIntegrity expired prenotazioni on server NOW() while
  ReservationManager used the app clock for the same rows.
- User dashboard and pending-loans views computed overdue with time()
  (process TZ), flagging loans overdue on the afternoon of the due day —
  the cron only does so from the following day.
- crea_prestito prefills now use the app today + the configured
  loan_duration_days (was process today + '+1 month').

Availability payload coherence
- next_due on /api/books/{id}/availability filtered only `attivo = 1`: it
  could return the past due date of an overdue loan or the far-future date
  of a scheduled one. Now holding-states only and never in the past.
- occupied_ranges on /api/libri/{id}/disponibilita omitted the
  pendente-with-copy arm and the active reservation queue, contradicting
  first_available computed per-day in the same response.
- Web calendar routes now exclude the requesting user's own reservations
  (query param the mobile app and the server write gate already honoured),
  so the picker no longer paints red days the server would accept.

Guards & lifecycle symmetry
- PrestitiController::store() strict-validates both dates as ISO like
  update() does: the old strtotime guard was defeated by int<=false
  coercion on unparseable input, and '12/03/2026' parsed American-style.
- LoanRepository::update() fallback honoured a hardcoded +14 days — half
  the seeded loan_duration_days default; now reads the setting.
- rejectLoan now promotes the waitlist after freeing capacity (it was the
  only release path that left a freed copy idle until the next maintenance
  run) and flushes deferred notifications after commit.

New regression guard: tests/loan-coherence-audit.unit.php (10 checks).
Verified: PHPStan level 5 clean; 99 standalone unit tests pass; E2E
loan-reservation-complete 26/26, loan-state-model + loan-overlap 43/43,
full-test 137 passed / 8 install-skipped; soft-delete guard exercised live
(200 → soft-delete 404 → restore 200).
…ng, reject audit, renewal email

Follow-ups agreed after the coherence audit, plus the second report on #301.
Every fix was verified behaviourally and the whole branch diff went through an
adversarial multi-agent review whose three confirmed findings are fixed here too.

Auto-approval on the real entry point (#301)
- The book-detail modal posts to ReservationsController::createReservation,
  which never consulted `auto_approve_requests` — the option only worked on
  the other (form) entry point, so real users' requests always landed in the
  approval queue. The modal path now promotes through the same race-safe
  canonical approval pipeline; with the option on, an available-copy request
  lands directly in "waiting for pickup" (pickup confirmation deliberately
  stays). The modal SweetAlert now branches on auto_approved instead of
  telling the user to wait for an approval that already happened.
- New behavioural test drives the real endpoint with the option off and on
  (10 assertions: off → pending preserved; on → approved, copy assigned,
  pickup deadline set).

app.timezone becomes a real setting
- ConfigStore default + per-locale installer seeds (it→Rome, de→Berlin,
  fr→Paris, da→Copenhagen, en_US→UTC) + a validated select in the loans
  settings tab (DateTimeZone::listIdentifiers whitelist; invalid input is
  ignored). The adversarial review caught that loadDatabaseSettings()'s
  app-category allowlist never mapped the row back — without that mapping the
  whole feature was inert. Verified end-to-end over HTTP: save Copenhagen →
  the select reads back Copenhagen; existing installs without the row keep
  the previous Europe/Rome behaviour.

Reject with audit instead of DELETE
- rejectLoan was the only terminal transition that destroyed its row. It now
  marks the request annullato with processed_by + reason note, preserving
  history and statistics; the affected_rows race guard and the duplicate-check
  semantics (user can re-request) are unchanged.

Renewal confirmation email
- New loan_renewed template (base + the 4 locale overrides) and
  sendLoanRenewedNotification, fired post-commit by renew(): the borrower now
  learns the new due date even when the librarian renews at the desk.
  Verified live via Mailpit (renewal moved the due date and delivered the
  email with the new date and remaining renewals).

Availability badge copy
- "Non Disponibile" → "Non disponibile oggi" on both the hero badge and the
  sidebar state (the review caught the sidebar leftover): the aggregate badge
  is a today snapshot and no longer reads as contradicting the calendar's
  free future days.

i18n: 8 new keys in all 5 locales (parity 6774 everywhere); the mail-template
count check now derives from the base catalogue instead of hardcoding 22.

Verified: PHPStan level 5 clean; 100 standalone unit tests pass (incl. the
new reservation-path #301 test and 21 coherence guards); E2E loan suites
69/69 and full-test 137 passed / 8 install-skipped; timezone save/read-back
and renewal email exercised live over HTTP.
#333 — a loan cancelled by the user (stato='annullato') showed as
"Unknown" in every admin view and looked stuck: no badge/label switch
handled the enum value. Render it everywhere (loans list SSR + DataTables,
details page, user details, book page history), add an Annullato filter
button and CSV-export state, and stop showing "not yet returned" for
loans closed without a return (annullato/scaduto).

#334 — the loans-overview and integrity-report page headers were sticky
at top-0 z-30, the same z-index as the layout header but later in the
DOM, so they painted over the app header and its notifications dropdown.
Drop the sticky positioning.

#336 — editing a loan's dates re-checked capacity over the WHOLE new
window, so a commitment already coexisting with the current period (e.g.
a queued reservation) bounced ANY date edit — even shortening — with
no_copies_available. update() now checks only the newly-claimed date
segments (renew()'s extension-window convention) and bulk extension
checks the extension window only. Also surface clear error banners:
/admin/loans explains capacity conflicts and renew() failures, and the
admin book page now recognizes the 'extension_conflicts' /
'renewal_failed' keys renew() actually emits (it only knew the never
emitted 'renewal_conflict'). New strings translated in all bundled
locales.

Closes #333, closes #334, closes #336.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KsK1ikTUGbUr5W3xrSJ6w
… in user history

Follow-up to #333. Instead of five per-view copies of the stato →
badge switch (the reason 'annullato' was missed in the first place),
app/Views/partials/loan-status-badge.php is now the single source of
truth for color/icon/label of every prestiti.stato value. All admin
surfaces consume it: the loans list (both the SSR rows and the
DataTables column, via a PHP-generated JS map), the loan details page,
the user details page and the book page loan history. Labels come from
the existing translate_loan_status() helper, already used by the PDF
and CSV export, so text and badges cannot diverge. The partial lives
under app/Views (not app/helpers.php) because Tailwind's content globs
only scan app/Views/** — moving the class names out of the views would
drop them from the compiled CSS.

User-facing change: cancelled (annullato) and pickup-expired (scaduto)
loans now appear in the user's loan history (profile + account
dashboard + history counter) instead of vanishing. They sort by their
closing time (COALESCE with updated_at) so a NULL return date doesn't
sink them to the bottom, show a dedicated icon/label, and hide the
"leave a review" button since the book never went out (the server
would reject the review anyway).

Regression guards extended accordingly (36 checks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KsK1ikTUGbUr5W3xrSJ6w
Completes the #333 centralization sweep. New loan_status_label_map()
in app/helpers.php (enum → label via translate_loan_status) feeds the
JS lookups that previously kept hand-maintained copies: the stats
'loans by status' chart and the admin dashboard calendar popup. The
ICS feed (IcsGenerator::translateStatus), the book page occupancy
calendar, the user history views and the loans-overview state chips
now delegate to translate_loan_status() as well; the user dashboard
active-loan badges keep their frontend styling but take their label
text from the helper.

Side effect worth noting: the calendar/ICS alias "Scaduto" for
in_ritardo is gone — since 'scaduto' (pickup expired) is a real enum
state, that alias had become genuinely ambiguous; everything now says
"In Ritardo".

Two deliberate exceptions are documented in place: the per-copy row
hint on the book page ("In prestito" describes the physical copy, not
the loan state) and the ICS event titles (event naming with emoji, not
status labels). The return form's outcome select also stays local: it
mixes loan outcomes with copy destinations ("Restituito — copia in
manutenzione"), which are not stato labels.

Regression guards extended to 44 checks, including 'no local stato →
label literal map left' probes on the swept views.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KsK1ikTUGbUr5W3xrSJ6w
…abel (1.4.3)

The mobile history had the same gap just fixed on the web: cancelled
(annullato) and pickup-expired (scaduto) loans vanished from
/me/loans history. The endpoint now returns them, ordered by closing
time (COALESCE with updated_at) so rows without a return date don't
sink. 'status' is documented as the raw prestiti.stato value, so the
extra states are an additive, spec-compatible change.

Every loan payload also gains 'status_label': the server-localized
label from the canonical translate_loan_status() helper (#333), so
clients no longer need their own stato→label map and future enum
values degrade gracefully. Documented in the OpenAPI schema; plugin
version bumped to 1.4.3.

The public API needs no change: it only ever exposes a book's active
loan (attivo=1), which cancelled loans never are.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KsK1ikTUGbUr5W3xrSJ6w
- update(): re-read data_prestito/data_scadenza (and utente_id) in the
  FOR UPDATE query and rebuild the effective values + range validation
  from the LOCKED row. The claimed-windows capacity math previously used
  the pre-transaction read, so a concurrent update between the initial
  read and the lock could get its dates silently overwritten and the
  capacity check computed against a stale old window. The pre-transaction
  validation stays as a fast-fail.
- History ordering: drop DATE() from the COALESCE fallback in the three
  history queries (profile, user dashboard, mobile API) so two loans
  closed the same day sort by their real closing moment.
- /admin/loans banner: handle 'renewal_failed' explicitly.
- user dashboard review button: htmlspecialchars(..., ENT_QUOTES) instead
  of HtmlHelper::e() in the data-book-title attribute (path rule).
- Regression test: noisy source reader (missing/empty file → exit 1),
  ordered-position guards + non-empty assertions on the extracted
  update()/applyBulkLoanExtension() sections, so the negative checks can
  no longer pass vacuously.
- mobile-api: additive 'requested_at' (DATE of created_at) on every
  /me/loans payload, documented in OpenAPI — gives clients an honest
  date for cancelled/expired loans, which never went out (pairs with the
  Android PR #30 review); status_label description softened to match the
  fallback-for-unknown-states semantics.

Guards now 51 checks, all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KsK1ikTUGbUr5W3xrSJ6w
- update(): strict date validation replaces strtotime(). New
  isStrictIsoDate() requires exact Y-m-d AND a real calendar date
  (round-trip via createFromFormat), so ambiguous inputs ('2026-2-5')
  and impossible dates ('2026-02-30', which strtotime silently
  normalizes) are rejected instead of flowing — as non-canonical
  strings — into the lexicographic window math, the due-date-changed
  comparison and LoanRepository::update(). Applied to both the
  pre-transaction fast-fail and the under-lock re-validation.
- update(): claimed windows are now the EXACT set difference
  new∖old — the boundary days (old start / old due) are already held
  by this loan, so a commitment sitting only on a boundary day no
  longer produces a false conflict.
- applyBulkLoanExtension(): one interval for one decision — capacity
  and the same-copy overlap check both gate on the added days only
  (day after the current due date → new due date); the copy check
  previously scanned the whole loan window while capacity scanned the
  extension window.
- mobile-api OpenAPI: requested_at declares nullability the 3.1 way
  (type ['string','null'], no 'nullable' flag), matching mapLoan()'s
  explicit null.

Regression guards extended to 54 checks (exact-diff boundaries, no
strtotime in update(), unified bulk interval, 3.1 nullability), all
green; isStrictIsoDate verified against ambiguous/impossible/leap-year
inputs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KsK1ikTUGbUr5W3xrSJ6w
…emantics

CI's loan-reservation-consistency guard pins the exact history predicate
shared by the three history consumers (user dashboard, profile, mobile
API). PR #337 deliberately extended that predicate with the closed
no-return states (annullato, scaduto) so cancelled/expired loans stop
vanishing from history (#333) — update the pinned literal accordingly.
The guard still enforces that all three consumers share one predicate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KsK1ikTUGbUr5W3xrSJ6w
… structural test guards

- isStrictIsoDate(): shape-check via /^\d{4}-\d{2}-\d{2}$/D BEFORE touching
  DateTime — createFromFormat() throws ValueError on input containing NUL
  bytes (data_prestito=2026-01-01%00), which outside the try block surfaced
  as an HTTP 500 instead of invalid_dates. The anchor also rejects trailing
  newlines and non-ASCII digits. Verified against NUL/newline/Arabic-digit
  inputs: all return false, nothing throws.
- Regression guards made structural: the requested_at check now isolates
  the schema line and asserts the ABSENCE of the legacy 'nullable' flag
  alongside the 3.1 type array (the mixed form no longer passes), and the
  exact-diff check asserts the boundary helpers actually feed the
  claimedWindows bounds instead of merely appearing in the source.

54 guards green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KsK1ikTUGbUr5W3xrSJ6w
…al review-guard

- locale/fr_FR.json: 'réservé par un autre prêt' implied the loan reserves
  the copy; 'affecté à un autre prêt' matches the source meaning (the copy
  is already assigned to another loan).
- Regression guard: assert the exact $canReview exclusion expression in
  both user history views (each with its own loop variable, $p / $loan)
  instead of the variable's mere presence.

55 guards green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KsK1ikTUGbUr5W3xrSJ6w
Self-review of the loan-coherence PR surfaced three real regressions the
PR itself introduced (plus one pre-existing bug it touched), all verified
by an independent post-fix reviewer.

[FG-1] F007 + F009 - auto-approve flow (cross-cutting, both mirrored helpers)
- F007: autoApproveLoanRequest() read the auto_approve setting BEFORE its
  own try{, so a post-commit DB hiccup escaped to the outer transaction
  catch and returned HTTP 500 for a request that was already committed -
  and the duplicate-request guard then blocked the user's retry. Moved the
  SettingsRepository construction + read inside the try in BOTH
  ReservationsController and UserActionsController; a throw now degrades to
  the pending_approval path as the contract intends.
- F009: with a future start_date the loan is 'prenotato' (scheduled), but
  the response said "approvato - in attesa di ritiro" and the modal
  promised pickup instructions. The helper collapsed approveLoan's outcome
  to a bool. Now approveLoan exposes loan_state/is_future_loan, the helper
  returns nullable-string, and ReservationsController + book-detail.php
  branch the message/status/footnote on scheduled vs pickup. auto_approved
  stays a real boolean. New scheduled-loan i18n key in all 5 locales.

[FG-2] F033 - dashboard soft-delete (pre-existing, project soft-delete rule)
- The five prestiti sub-counts in DashboardStats::counts() had no libri
  join, so a loan on a soft-deleted book inflated the badge above what its
  destination list (which joins deleted_at IS NULL) can show - a clickable
  "1" linking to an empty page. Added the soft-delete join to every
  sub-count, preserving pickup_pronti's parenthesised OR group and the
  DateHelper clock.

Tests: extended loan-auto-approval-301-reservation-path.unit.php with a
future-date section and a post-commit fault-injection case; added a
soft-delete guard to loan-coherence-audit.unit.php.

Post-fix review: 2/2 groups verified complete; 0 partial; 0 reverted.
…date errors)

The four manual/policy findings from the adamsreview run, fixed concern-aware
(each addressing the verify-pass concern the naive hint would have missed) and
verified by an independent reviewer.

F043 - admin loan form hardcoded the Italian route literal /api/libro; the
endpoint is registered per-locale via RouteTranslator, so it 404s if it_IT is
deactivated or renamed. Now resolves route_path('api_book') into a
JSON_HEX_TAG-escaped JS constant (base path already included - window.BASE_PATH
dropped from that fetch), mirroring the book-detail.php precedent.

F012 - the two availability routes wired the SESSION user as the exclusion
subject, but the admin loan form fetches them with the operator's session while
booking for a different borrower, so it painted days green the write gate
rejects. The subject is now explicit: an admin/staff caller may pass
?for_user=<borrower> (privilege-gated, numeric-checked); everyone else keeps the
session-id default, so the public self-service picker is unchanged. The admin
form threads the selected borrower and re-fetches on borrower change.

F040 - a user already holding an active reservation got an all-green picker
(their own reservation is excluded) then a hard rejection from the date-less
duplicate guard on submit - a dead end. calculateAvailability now returns
has_active_reservation (same predicate as the guard), surfaced in the payload,
and book-detail.php short-circuits with an explanation instead of the doomed
picker. Deliberately did NOT also exclude the user's own holding loans: that
would falsely free an overdue (in_ritardo) copy still physically out.

F028 - a malformed date and an inverted date pair shared one error message.
store() now splits invalid_date_format (naming YYYY-MM-DD) from invalid_dates,
and update()'s twin check was ported from the ambiguous strtotime parse (the
'12/03/2026' American-read bug) to the same strict isISODateFormat validation.

New user-facing strings added to all 5 locales. Verified: PHPStan level 5 clean;
100 unit tests; E2E loan-reservation/state-model/overlap 69/69; independent
post-fix review verified 4/4.
… email settings

Address two review findings on the auto-approve reservation-path test:

- The helper posted application/x-www-form-urlencoded via withParsedBody(), so
  it exercised createReservation's getParsedBody() branch — not the json_decode
  branch the book-detail modal actually hits (getParsedBody does not parse JSON).
  Send a JSON body behind Content-Type: application/json so the test drives the
  real production path.
- Setup overwrote the email driver_mode/type rows but the cleanup restored only
  auto_approve_requests, leaking 'mail' onto every later test in the same runner.
  Capture both originals before the UPDATE and restore them in cleanup.
…blet widths

The related-books grid clips every book past the first row (grid-auto-rows:0 +
overflow:hidden) with no hint that more exist. Below 768px a horizontal
snap-scroll carousel already shows the next card peeking as the cue; between 768
and 1023.98px the grid silently hid the extras, so on a narrow desktop window a
user could not tell there were more related books (mobile was fine).

Raise the carousel breakpoint from 767px to 1023.98px so the narrow/responsive
range gets the same reachable snap-scroll strip, and for the 768-1023.98px
sub-range show two whole books with a third clearly half-visible (flex-basis 38%)
as an unmistakable scroll-for-more cue instead of one dominant cover. Desktop
(>=1024px) grid is unchanged; phone one-book carousel is unchanged. The a11y
inert/aria sync already keyed off computed overflow-x, so it handles the wider
scroll range with no JS change.
…1 integration

Conflict resolution:
- app/Controllers/PrestitiController.php update() date validation: both #335
  (F028) and #337 hardened the pre-transaction date check away from strtotime.
  Kept #337's isStrictIsoDate validator (its test asserts >=4 occurrences and no
  strtotime, and the under-lock re-check already uses it) AND #335's separate
  error codes (invalid_date_format vs invalid_dates) for actionable admin
  feedback. store() keeps DateHelper::isISODateFormat per #335's test; the two
  strict-ISO validators are behaviourally equivalent (both reject 2026-02-30 and
  NUL bytes).
- locale/*.json: union of both sides' added keys (11 from #335 + 3 from #337),
  parity restored at 6784 keys across all 5 locales.

Both suites pass: issues-333-334-336 (55/0), loan-coherence-audit, and the #301
reservation-path test (23/23).
Rename migrate_0.7.58.sql -> migrate_0.7.59-rc.1.sql (and its behavioural test).
0.7.58 is already released, so a migration named after it is version-skipped for
installs already on 0.7.58 (updater runs migrationVersion > fromVersion). Naming
it after the RC that ships it makes it run on the RC upgrade and on the eventual
stable 0.7.59. Bump version.json to 0.7.59-rc.1.

Audited: none of the other bundled changes (#335 settings, #337 badge helpers,
#339 copy ordering) require a migration — every new DB read targets pre-existing
columns/ENUM values or passes a safe default, and app.timezone self-heals on save.
…uest

A hook row registered by an OLD plugin version and dropped in a NEW one lingers
in plugin_hooks because upgrades replace plugin files but never re-run a plugin's
onActivate() (which resyncs hooks via deleteHooks + reinsert). The hook loader
then logged "[PluginManager] Method not found: <method> for hook <hook>" on EVERY
request — e.g. a live install showed Z39ServerPlugin's stale admin.menu.items ->
addAdminMenuItem 73x/day.

When the callback method genuinely does not exist on the (already-instantiated)
plugin class and there is no __call magic, delete that dead hook row. The install
self-heals on the first request after the upgrade; a legitimate future
re-activation reinserts the correct hook set anyway. pruneOrphanHook() reports
whether a row was actually deleted so the removal is logged exactly once (a cached
hook list can re-present the orphan for up to the QueryCache TTL; the DELETE then
matches zero rows silently). Applies to both hook-load paths (prefetched batch and
per-plugin fallback).

Test (tests/plugin-orphan-hook-prune.unit.php, 8 checks): deletes the orphan +
reports true; idempotent second call reports false; and — critically — a
legitimate sibling hook, a non-matching method, and a different plugin_id are all
left untouched (precise match, no over-delete).
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

La versione 0.7.59-rc.2 aggiorna prestiti, prenotazioni, disponibilità, fuso orario, serie, stati, notifiche, API, plugin e CI. Aggiunge controlli per date, soft-delete, capacità, localizzazioni e fixture E2E.

Changes

Gestione prestiti e catalogo

Layer / File(s) Summary
Flusso prestiti e disponibilità
app/Controllers/PrestitiController.php, app/Controllers/ReservationsController.php, app/Controllers/LoanApprovalController.php, app/Routes/web.php
Le date usano il timezone applicativo e la durata configurata. Le modifiche verificano solo le nuove finestre temporali. Le richieste espongono lo stato persistito. Il rifiuto conserva il prestito come annullato.
Serie, copie e configurazione
app/Models/SeriesRepository.php, app/Models/CopyRepository.php, app/Support/ConfigStore.php, app/Controllers/SettingsController.php, installer/database/*
Il catalogo supporta is_completa con fallback per schemi legacy. Le copie usano l’ordinamento naturale. Le impostazioni espongono e validano app.timezone.
Stati, viste e integrazioni
app/Views/partials/loan-status-badge.php, app/Views/prestiti/*, app/Views/user_dashboard/*, storage/plugins/mobile-api/*, app/Support/NotificationService.php, app/Support/PluginManager.php
Le viste usano label e badge canonici. Le cronologie includono prestiti annullati e scaduti. L’API mobile espone status_label e requested_at. Il rinnovo invia una notifica dopo il commit. Gli hook orfani vengono disattivati senza eliminazione.
Test e pipeline CI
tests/*, .github/workflows/*, scripts/*, version.json
La CI aggiunge shard di regressione, audit dei risultati, policy Playwright, test strict, compatibilità PHP, controlli di localizzazione e fixture deterministiche.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Prestatario
  participant ReservationsController
  participant LoanApprovalController
  participant NotificationService
  Prestatario->>ReservationsController: invia richiesta con date
  ReservationsController->>LoanApprovalController: tenta approvazione automatica
  LoanApprovalController-->>ReservationsController: restituisce loan_state persistito
  ReservationsController->>NotificationService: invia notifica se la richiesta resta pendente
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.48% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Il titolo identifica chiaramente la release candidate, l’integrazione delle correzioni e il rafforzamento dei controlli di rilascio.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/0.7.59-rc.1

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (1)
app/Controllers/PrestitiController.php (1)

231-256: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Spostare la validazione ISO prima del calcolo della scadenza.

Quando data_prestito non è ISO e data_scadenza è vuota, strtotime() restituisce false e date() genera un TypeError non gestito prima del redirect invalid_date_format.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Controllers/PrestitiController.php` around lines 231 - 256, Move the ISO
validation in the loan creation flow before the default `data_scadenza`
calculation, after applying only the `data_prestito` default. Ensure invalid
`data_prestito` values redirect to `invalid_date_format` before `strtotime()` is
called, while preserving the existing default loan-duration calculation for
valid ISO dates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Views/prestiti/crea_prestito.php`:
- Around line 576-581: Proteggi il flusso fetch della disponibilità da risposte
obsolete quando cambia rapidamente il prestatario. Nel codice che costruisce
availabilityUrl e aggiorna availabilityByDate e bookAvailability, assegna un
identificatore crescente alla richiesta (oppure usa AbortController) e applica
la risposta solo se corrisponde ancora alla richiesta più recente.

In `@app/Views/prestiti/modifica_prestito.php`:
- Line 80: Update the fallback in the data_prestito value expression to use the
application timezone via DateHelper::today() instead of date('Y-m-d'). Preserve
the existing prestito value, HTML escaping, and default behavior when
data_prestito is present.

In `@locale/de_DE.json`:
- Line 6774: Update the translation for “Predefinito: Europe/Rome” so the
displayed label is localized while the IANA timezone identifier remains exactly
“Europe/Rome”, not “Europe/Berlin”.

In `@locale/en_US.json`:
- Line 6774: Update the locale entry for “Predefinito: Europe/Rome” so its
English translation states “Default: Europe/Rome” and matches the actual
ConfigStore timezone default.

In `@storage/plugins/mobile-api/src/Controllers/ActionsController.php`:
- Around line 107-113: Update the history SQL query in ActionsController to
include pr.data_scadenza in its SELECT list, so mapLoan() can populate due_at
for historical loans, including scaduto entries. Leave the existing filters,
ordering, and limit unchanged.

In `@tests/loan-coherence-audit.unit.php`:
- Around line 37-46: Update the file-loading setup in the loan coherence audit
test to verify each path exists and each read succeeds before casting or using
its contents. Centralize this behavior in a reusable helper, then use it for the
variables loaded from ReservationsController, web routes, model/controller
files, DataIntegrity, and dashboard views so missing files fail clearly with
their path instead of becoming empty strings.
- Around line 61-65: Replace the fixed-length source slices in the audit checks
with bodies delimited by complete method signatures and explicit end-marker
fallbacks, including the checks around $nextDuePos, approval, and rejection
logic. For rejectLoan, locate the exact rejectLoan signature rather than a
prefix match, compute whether the closing boundary was found, and add
$rejectBodyDelimited to the negative assertion so it cannot inspect unrelated
methods or the rest of the file.

In `@tests/plugin-orphan-hook-prune.unit.php`:
- Around line 53-58: Rendi il fixture del plugin nel test indipendente dai seed:
sostituisci la query MIN(id) e l’uscita tramite exit(1) con la creazione di un
plugin dedicato, usa il relativo ID per esercitare pruneOrphanHook e rimuovi il
plugin fixture al termine del test.

In `@tests/settings-orphans-hardening.unit.php`:
- Around line 24-26: Update the locale validation loop around
SettingsMailTemplates::all so it compares each locale’s sorted template keys
against the sorted keys from it_IT, rather than only comparing counts. Preserve
the existing per-locale check structure and ensure the reference key set is
derived from the it_IT templates.

---

Outside diff comments:
In `@app/Controllers/PrestitiController.php`:
- Around line 231-256: Move the ISO validation in the loan creation flow before
the default `data_scadenza` calculation, after applying only the `data_prestito`
default. Ensure invalid `data_prestito` values redirect to `invalid_date_format`
before `strtotime()` is called, while preserving the existing default
loan-duration calculation for valid ISO dates.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9874b0cc-dbdd-497d-8451-08aa6e6a561d

📥 Commits

Reviewing files that changed from the base of the PR and between 69301d0 and d5cee9b.

📒 Files selected for processing (73)
  • app/Controllers/CollaneController.php
  • app/Controllers/LibriController.php
  • app/Controllers/LoanApprovalController.php
  • app/Controllers/PrestitiController.php
  • app/Controllers/ReservationsController.php
  • app/Controllers/SettingsController.php
  • app/Controllers/UserActionsController.php
  • app/Controllers/UserDashboardController.php
  • app/Models/CopyRepository.php
  • app/Models/DashboardStats.php
  • app/Models/LoanRepository.php
  • app/Models/SeriesRepository.php
  • app/Models/SettingsRepository.php
  • app/Routes/web.php
  • app/Support/ConfigStore.php
  • app/Support/DataIntegrity.php
  • app/Support/IcsGenerator.php
  • app/Support/NotificationService.php
  • app/Support/PluginManager.php
  • app/Support/SettingsMailTemplates.php
  • app/Support/mail_templates/da_DK.php
  • app/Support/mail_templates/de_DE.php
  • app/Support/mail_templates/en_US.php
  • app/Support/mail_templates/fr_FR.php
  • app/Views/admin/integrity_report.php
  • app/Views/admin/pending_loans.php
  • app/Views/admin/stats.php
  • app/Views/collane/dettaglio.php
  • app/Views/collane/index.php
  • app/Views/dashboard/index.php
  • app/Views/frontend/book-detail.php
  • app/Views/libri/scheda_libro.php
  • app/Views/partials/loan-status-badge.php
  • app/Views/prestiti/crea_prestito.php
  • app/Views/prestiti/dettagli_prestito.php
  • app/Views/prestiti/index.php
  • app/Views/prestiti/modifica_prestito.php
  • app/Views/profile/reservations.php
  • app/Views/settings/loans-tab.php
  • app/Views/user_dashboard/index.php
  • app/Views/user_dashboard/prenotazioni.php
  • app/Views/utenti/dettagli_utente.php
  • app/helpers.php
  • frontend/js/copy-scanner.js
  • installer/database/data_da_DK.sql
  • installer/database/data_de_DE.sql
  • installer/database/data_en_US.sql
  • installer/database/data_fr_FR.sql
  • installer/database/data_it_IT.sql
  • installer/database/migrations/migrate_0.7.59-rc.1.sql
  • installer/database/schema.sql
  • locale/da_DK.json
  • locale/de_DE.json
  • locale/en_US.json
  • locale/fr_FR.json
  • locale/it_IT.json
  • public/assets/copy-scanner.bundle.js
  • storage/plugins/mobile-api/plugin.json
  • storage/plugins/mobile-api/src/Controllers/ActionsController.php
  • storage/plugins/mobile-api/src/Controllers/CatalogController.php
  • storage/plugins/mobile-api/src/Controllers/OpenApiController.php
  • tests/discussion-238-followup.unit.php
  • tests/full-test.spec.js
  • tests/issues-333-334-336.unit.php
  • tests/label-pdf-content.spec.js
  • tests/loan-auto-approval-301-reservation-path.unit.php
  • tests/loan-bulk-extension-capacity.unit.php
  • tests/loan-coherence-audit.unit.php
  • tests/loan-reservation-consistency.unit.php
  • tests/migration-0.7.59-rc.1.unit.php
  • tests/plugin-orphan-hook-prune.unit.php
  • tests/settings-orphans-hardening.unit.php
  • version.json

Comment thread app/Views/prestiti/crea_prestito.php Outdated
Comment thread app/Views/prestiti/modifica_prestito.php Outdated
Comment thread locale/de_DE.json Outdated
Comment thread locale/en_US.json Outdated
Comment thread storage/plugins/mobile-api/src/Controllers/ActionsController.php Outdated
Comment thread tests/loan-coherence-audit.unit.php Outdated
Comment thread tests/loan-coherence-audit.unit.php Outdated
Comment thread tests/plugin-orphan-hook-selfheal.unit.php Outdated
Comment thread tests/settings-orphans-hardening.unit.php Outdated

@fabiodalez-dev fabiodalez-dev left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Verdict

needs changes

Primary Goal

Assemble a 0.7.59-rc.1 integration branch that bundles PRs #335/#337/#339/#340, corrects the migration versioning, and adds a PluginManager orphan-hook self-heal.

Overview

TL;DR

Solid, well-documented integration branch with genuinely good reasoning behind the conflict resolution and the migration rename — but three things should be settled before this becomes the RC tag: the rejectLoan() note-length guard, the RC-versioned migration's behaviour on the 0.7.58 → 0.7.59 (stable, no RC) upgrade path, and the fact that the PluginManager self-heal silently DELETEs rows.


What this PR does

This is an explicit integration branch for 0.7.59-rc.1, keeping main clean until the RC validates. It merges four independent PRs and adds two out-of-band fixes found while verifying the live install:

Area Source Change
Series completeness #340 collane.is_completa column, capability check threaded through CollaneController::index()/show(), admin toggle
Loan/reservation coherence #335 DateHelper::today() prefill, timezone setting, soft-delete on rejection, #301 auto-approve on the reservation modal, availability fixes
Status helpers + issues #333/#334/#336 #337 Canonical status badge/label helpers, strict ISO date validator
Copy ordering + scanner focus #339 CopyRepository::sortByInventoryNumber() applied to the label PDF, loan-form scanner focus
Migration versioning net-new migrate_0.7.58.sqlmigrate_0.7.59-rc.1.sql, version.json bump
Plugin hooks net-new PluginManager prunes orphan plugin_hooks rows

What's good

  • The conflict resolution is the right one. Merging #335's separate invalid_date_format / invalid_dates error codes with #337's isStrictIsoDate validator gives you both precise error reporting and a real guard — instead of the strtotime($a) <= strtotime($b) pattern where a parse failure returns false, gets coerced to 0, and silently passes the comparison. Validating first, then comparing lexicographically on Y-m-d, is correct and cheap.
  • Exposing loan_state / is_future_loan in the approveLoan() payload removes date re-derivation from autoApproveLoanRequest(). One authority for state, consumed rather than recomputed — exactly right.
  • rejectLoan() going from hard DELETE to an annullato transition is a clear win for auditability and for statistics that would otherwise silently lose rows.
  • The migration reasoning is sound. Shipping a migration named after an already-released version is a real version-skip bug, and catching it during RC verification is the whole point of an RC branch.

Concerns

1. rejectLoan() — the length guard doesn't bound what it claims to.
The 500-char truncation is applied to the user-supplied reason, but the value written is CONCAT(existing_note, "\n[Admin] …: ", reason). The cap therefore bounds the appended fragment, not the resulting column value. An already-long note still overflows in strict mode — the exact failure the guard is described as preventing. Either check the column type (TEXT makes this a non-issue and the comment should say so) or bound the result.

2. RC-named migration on the stable upgrade path.
The description says the RC name makes it run "on the RC upgrade AND on the eventual stable 0.7.59". That holds only if the updater's version comparison treats 0.7.59-rc.1 the way semver does (0.7.59-rc.1 < 0.7.59) and applies every migration strictly greater than the installed version. Two paths deserve an explicit test, not just the 0.7.58 → 0.7.59-rc.1 one that's already green:

  • 0.7.58 → 0.7.59 (stable, RC never installed) — must apply the migration.
  • 0.7.59-rc.1 → 0.7.59 (stable, RC already installed) — must not re-apply, or must be idempotent (ADD COLUMN IF NOT EXISTS / guarded).

Also confirm schema.sql carries is_completa so fresh installs don't depend on the migration at all.

3. The plugin-hook self-heal deletes rows.
Deleting on first sight is the most aggressive of the available repairs. A hook row that looks orphaned because a plugin file failed to load — a transient autoload failure, a partially-deployed upgrade, a permissions blip — is indistinguishable at that moment from a hook the plugin genuinely dropped, and the DELETE is unrecoverable without re-running onActivate(). Marking the row disabled, or requiring the plugin to have loaded successfully before pruning its hooks, gets the same "stop logging on every request" outcome without destroying state. Worth also confirming the "logs once" mechanism is safe under concurrent requests.

4. Transaction semantics in rejectLoan().
The new ReservationManager waitlist promotion runs under setExternalTransaction(true), after the rejection has already been persisted. Worth being explicit about what happens if promotion throws mid-transaction — does the caller roll back the rejection too, and is that the behaviour you want?

5. Committed build artifact.
public/assets/copy-scanner.bundle.js changes alongside frontend/js/copy-scanner.js. That's presumably the established pattern here, but for an RC it's worth confirming the bundle was rebuilt from the branch's final source rather than an intermediate state — a stale bundle is invisible to PHPStan and to the unit suite.

6. Behaviour tightening worth a release note.
Strict ISO validation is a tightening: any caller that previously posted a loosely-parseable date and got away with it now redirects with invalid_date_format. The admin UI is covered by the tests, but confirm the mobile-api plugin controllers and any import path always send Y-m-d.

Validation

The validation story is unusually complete for an integration branch — PHPStan level 5, 100+ unit tests, migration guard, i18n parity at 6784 keys, and a real admin-UI upgrade test. The gaps above are the ones that specifically fall outside what those checks cover.

Scope Assessment

Breadth is by design here — this is a declared integration branch and every area maps to a named upstream PR, so normal drift rules don't apply to the merged content. Two items are genuinely net-new to this branch rather than merged: the migration rename (defensible and arguably release-blocking — it's exactly the class of bug an RC exists to catch) and the PluginManager orphan-hook self-heal. The latter is behavioural, deletes database rows, and has nothing to do with any of the four bundled PRs; it would be easier to review, revert, and reason about on its own. Not blocking, but flagged.

Risk Assessment

Highest risk is the migration versioning itself, ironically — the fix is correct in intent but the stable 0.7.59 upgrade path (RC never installed) and the rc.1 → stable path are the ones that will hit real users and are not covered by the green 0.7.58 → 0.7.59-rc.1 test. If the updater's comparison doesn't treat the RC suffix as semver-lower, is_completa is silently missing on stable upgrades. Second, rejectLoan(): it converts a terminal delete into a state update plus waitlist promotion inside an externally-managed transaction, and its length guard bounds the appended fragment rather than the final note value. Third, the PluginManager DELETE is unrecoverable and fires on a condition (hook method not found) that can be produced by transient load failures, not only by genuine plugin drift. Lower risk: strict ISO date validation is a tightening that will now reject inputs previously accepted — verify the mobile-api plugin and any import path.

Reuse Notes

  • DateHelper::today() and the new isStrictIsoDate validator are the canonical date entry points now — any remaining strtotime()-based date comparison in the loan/reservation path should route through them rather than re-implementing the check.
  • The canonical status badge/label helpers from #337 (app/Views/partials/loan-status-badge.php and the helper functions) should be the single source for loan status rendering; worth a sweep to confirm no view still hardcodes badge markup or status strings.
  • CopyRepository::sortByInventoryNumber() now exists as the natural-ordering primitive — any other copy listing (admin lists, exports, the mobile-api catalog controller) that still sorts by raw string should reuse it rather than duplicating the comparator.
  • ReservationManager::setExternalTransaction() is the established pattern for nesting reservation work inside a caller-owned transaction — keep new callers on it instead of opening their own.

Action Items

  • Fix or re-document the rejectLoan() length guard: bound the resulting note value, or confirm the column is TEXT and adjust the comment so it doesn't claim protection it isn't providing.
  • Add migration tests for the two uncovered upgrade paths: 0.7.58 → 0.7.59 (stable, RC never installed) and 0.7.59-rc.1 → 0.7.59 (already applied). Make the migration idempotent if it isn't already.
  • Confirm installer/database/schema.sql includes collane.is_completa so fresh installs don't depend on the migration.
  • Soften the PluginManager self-heal: mark the orphan row disabled rather than DELETEing it, or gate the prune on the plugin having loaded successfully — and verify the once-only logging is safe under concurrent requests.
  • Document the failure semantics of the waitlist promotion inside rejectLoan()'s external transaction: does a throw roll back the rejection, and is that intended?
  • Verify $supportsCompleteFlag is actually consumed by the collane/index view — otherwise it's a dead local.
  • Confirm public/assets/copy-scanner.bundle.js was rebuilt from the branch's final frontend/js/copy-scanner.js, not an intermediate state.
  • Add a release note for the strict ISO date tightening, and check the mobile-api plugin controllers always send Y-m-d.

Separate PR Suggestions

  • PluginManager orphan-hook self-heal — this is a net-new behavioral change (it DELETEs rows from plugin_hooks on first sight of a dead hook) discovered during RC verification, not part of any of the four integrated PRs. As its own PR it would get a focused review of the deletion predicate and the once-only logging, and would be independently revertable if it turns out to delete a hook that was merely temporarily unregistered.
  • Unifying PrestitiController::isStrictIsoDate() with DateHelper::isISODateFormat() — a small, mechanical consolidation that is easier to review on its own than inside a four-PR integration branch, and which would let the RC ship with the hybrid as-is.
  • Extract the repeated loan-status HOLDING predicate (stato IN ('in_corso','in_ritardo','da_ritirare','prenotato'), plus the pendente variant) into a single shared constant or helper, and update web.php, NotificationService, and ReservationManager to consume it. This is a cross-cutting cleanup that would be hard to review safely inside an already-large RC integration branch.
  • Introduce (or route through) a proper auth accessor for role checks inside route closures in app/Routes/web.php, replacing direct $_SESSION['user']['tipo_utente'] reads. This touches many handlers and deserves its own reviewable diff.
  • Extract the PluginManager orphan-hook self-heal (pruneOrphanHook() + the two call-site changes) into its own PR. It is not part of #335/#337/#339/#340, it introduces a destructive DB write on the request read path, and bundling it into a release-candidate integration branch means it ships without a review cycle of its own. It also deserves its own test: a plugin whose class fails to load should NOT lose its hook rows.
  • Il passaggio da HtmlHelper::e() a htmlspecialchars() inline in app/Views/prestiti/modifica_prestito.php (e la rimozione del relativo use): se è una scelta deliberata di stile, va fatta in modo uniforme su tutte le viste in una PR dedicata, non su un singolo file dentro una release candidate.
  • Il cambio del default della scadenza da 14 a 30 giorni: è una modifica di comportamento funzionale, merita una PR propria con il razionale e, idealmente, l'aggancio all'impostazione di durata prestito già presente in settings/loans-tab.php.
  • Self-hosting the zxing-wasm .wasm assets instead of relying on the upstream locateFile default that fetches from fastly.jsdelivr.net. This is a build/deploy concern unrelated to the RC integration and would be easier to validate on its own branch.
  • Extract the $check reporter and the fail-loud $src() source loader into a shared tests/_bootstrap.php and migrate the existing tests/*.unit.php guard files onto it, so no future suite re-introduces the silent-pass-on-empty-file behaviour.
  • Estrarre un tests/bootstrap-db.php condiviso con test_db_connect(): mysqli (parsing .env, fallback E2E_DB_*DB_*, gestione socket vs TCP, set_charset) e un helper withSetting() per il pattern salva-imposta-ripristina delle system_settings. Il blocco di connessione è oggi duplicato quasi identico in più script di tests/, e ogni copia può divergere sul fallback DB_PASS/DB_PASSWORD. Rifattorizzazione a rischio zero sul comportamento, ma tocca molti file: meglio fuori da una PR di release.
  • Estrarre il bootstrap DB condiviso (parsing .env + connessione mysqli con fallback E2E_DB_*) in un helper unico per i test, e migrarci i due nuovi test più eventuali altri già esistenti che duplicano lo stesso blocco. Piccolo, meccanico, e fuori dal percorso critico della RC.
  • Promuovere a test comportamentali i check di loan-coherence-audit.unit.php che lo consentono (#1 soft-delete → 404 sull'endpoint availability, #5 rispetto di loan_duration_days nel fallback di LoanRepository::update(), #8 validazione ISO strict in PrestitiController::store()), lasciando come source-invariant solo quelli che lo sono per natura (#4 clock unico). L'infrastruttura DB per farlo esiste già, come dimostra il test del prune in questa stessa PR.
  • PluginManager orphan-hook self-heal — unrelated to any of the four bundled PRs, changes runtime behaviour, and deletes database rows. As its own PR it can be reviewed on its merits (delete vs. disable, transient-failure handling, concurrency) and reverted independently if the RC needs to ship without it.
  • If the storage/plugins/mobile-api/* controller changes are not strictly required by #335/#337/#339/#340, split them out — plugin-side API changes have a separate compatibility surface (mobile clients) from the core app and benefit from their own review and release note.

Comment thread app/Controllers/LoanApprovalController.php Outdated
Comment thread app/Controllers/LoanApprovalController.php
Comment thread app/Controllers/CollaneController.php
Comment thread app/Controllers/PrestitiController.php
Comment thread app/Controllers/PrestitiController.php
Addresses the RC review: the orphan-hook self-heal now DISABLES the dead row
(is_active = 0) instead of removing it. The "method missing" signal is also what
a partially-deployed upgrade or a transient class-load glitch would produce, and
an irreversible removal would be unrecoverable. Disabling keeps the row for audit
and is restored by a legitimate re-activation, while still stopping the
per-request "Method not found" log: both hook-load SELECTs now filter
is_active = 1 (which also makes the existing setHooksActive() deactivate path
actually take effect in the loader). Concurrency-safe: the WHERE is_active = 1
clause is re-evaluated under the row lock, so at most one request logs the flip.

Test renamed to plugin-orphan-hook-selfheal.unit.php (9 checks): the orphan is
disabled not removed, the loader would no longer load it, the flip is idempotent,
and a legitimate sibling / non-matching method / different plugin_id are never
touched.

Also hardens the series migration test (19 checks) with the updater's
migration-selection contract for every upgrade path: migrate_0.7.59-rc.1 fires on
0.7.58 -> 0.7.59-rc.1 AND on 0.7.58 -> 0.7.59 stable (RC never installed), and
does NOT re-fire on 0.7.59-rc.1 -> 0.7.59 (already applied; idempotent backstop).

Reviewed and confirmed already-correct (no change needed): schema ships
is_completa for fresh installs; the rejection-note fragment is bounded and its
column is TEXT; the mobile-api reservation endpoint already validates Y-m-d via
DateHelper::isISODateFormat; and copy-scanner.bundle.js rebuilds byte-identical
from its source with the wasm assets self-hosted (no CDN fetch).
…nullato transition

#335 changed loan rejection from a DELETE to a state transition (stato='annullato',
attivo=0) — the row is kept for audit and to let the user re-request — but three
E2E specs still asserted the old "rejection deletes the row" behavior. They slipped
through because the RC upgrade regression runs full-test.spec.js only, not these.

- loan-reservation.spec.js 5.2: assert the row survives as 'annullato' (attivo=0)
  instead of count===0.
- swal-loans-reservations.spec.js: the index-widget and details-page reject tests
  poll for stato='annullato' instead of the row disappearing; titles updated.
- loan-overlap.spec.js F.34: reject via UPDATE stato='annullato' (not a raw DELETE)
  and assert the copy is freed — this now verifies the capacity sweep treats a
  cancelled loan as not holding its copy, which is the real post-#335 behavior.

All green: loan-reservation 21, swal-loans-reservations 14, loan-overlap 39,
loan-state-model 4, plus full-test 137 and the plugin-activation preflight.
@fabiodalez-dev fabiodalez-dev changed the title Release 0.7.59-rc.1: integrate #335/#337/#339/#340 + migration versioning + plugin-hook self-heal Release 0.7.59-rc.3: integrated fixes and hardened release gates Aug 13, 2026
@fabiodalez-dev
fabiodalez-dev merged commit 405f5e7 into main Aug 13, 2026
44 of 45 checks passed
@fabiodalez-dev
fabiodalez-dev deleted the release/0.7.59-rc.1 branch August 13, 2026 10:58
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