Skip to content

fix(loans): promote the waitlist head when reservations fully subscribe the copies (#157)#199

Open
fabiodalez-dev wants to merge 1 commit into
mainfrom
fix/reservation-promotion-157
Open

fix(loans): promote the waitlist head when reservations fully subscribe the copies (#157)#199
fabiodalez-dev wants to merge 1 commit into
mainfrom
fix/reservation-promotion-157

Conversation

@fabiodalez-dev

@fabiodalez-dev fabiodalez-dev commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Problem

Reservation maintenance (cron/full-maintenance.phpMaintenanceService::processScheduledReservations) promoted no reservation to a pending loan when a waitlist fully subscribed a title: 1 copy + 2 active reservations → 0 promoted instead of 1.

Cause

The promotion gate (ReservationManager::processBookAvailabilityisDateRangeAvailableCapacityService::hasFreeCapacity) counted every overlapping active reservation as occupying a capacity unit, excluding only the one being promoted. With 1 copy and 2 reservations, promoting the head (queue_position=1) still counted the second (queue_position=2) as occupying the only copy → occ == total → head never promoted. Both runs converted 0; test F.21b (#157) saw 0 pending instead of 1.

Fix

The promotion path now passes the promoted reservation's queue_position, so the capacity gate ignores active reservations with a strictly greater queue_position (lower-priority waitlist entries, promoted in later runs as copies free up).

  • Reservations ahead in the queue (lower position) still count.
  • A legacy NULL queue_position still counts (conservative — never overbook).
  • The new parameter is optional (default null): the admin-creation gate and overbooked-auditor, which share CapacityService, are unchanged.

Verification

  • PHPStan level 5 clean.
  • Cron repro: run 1 → 1 pending, run 2 → still 1 (no double-conversion).
  • E2E: loan-reservation-complete 26/26 (F.21b now passes), loan-overlap 35/35 (overbook prevention intact), loan-state-model 4/4.

Surfaced while running the full E2E suite; pre-existing on main, independent of the Swagger PR #197.

…be the copies (#157)

The reservation-maintenance cron failed to convert any reservation into a pending
loan when a fully-subscribed waitlist queued for a title — e.g. 1 copy + 2 active
reservations promoted 0 instead of 1.

Root cause: the promotion gate (ReservationManager::processBookAvailability →
isDateRangeAvailable → CapacityService::hasFreeCapacity) counted EVERY active
reservation overlapping the period as occupying a capacity unit, excluding only the
one being promoted. With 1 copy and 2 queued reservations, promoting the head (queue
position 1) still counted the trailing entry (position 2) as occupying the only copy,
so occ == total and the head was never promoted. Both runs converted 0; F.21b (#157)
saw 0 pending instead of 1.

Fix: the promotion path now passes the promoted reservation's queue_position so the
capacity gate ignores active reservations with a known queue_position strictly greater
than it — lower-priority waitlist entries promoted in later runs as copies free up.
Reservations AHEAD in the queue (lower position) still count, and a legacy NULL
queue_position still counts (conservative — never overbook). The new parameter defaults
to null, so the admin-creation gate and overbooked auditor sharing CapacityService are
unchanged.

Verified: PHPStan level 5 clean; cron repro now promotes exactly one head (run 1 → 1
pending, run 2 → still 1); E2E loan-reservation-complete 26/26 (F.21b passes),
loan-overlap 35/35 (overbook prevention intact), loan-state-model 4/4.
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

La promozione di una prenotazione ora calcola la posizione di coda della testa e la passa al controllo di capacità. Il calcolo della disponibilità esclude le prenotazioni con queue_position successiva e continua a contare quelle senza posizione.

Changes

Promozione prenotazione e gate di capacità

Layer / File(s) Summary
Passaggio della testa coda
app/Controllers/ReservationManager.php
processBookAvailability() calcola headQueuePos, estende isDateRangeAvailable() e inoltra excludeReservationsAfterQueuePos alla verifica di capacità.
Propagazione dei parametri di capacità
app/Services/CapacityService.php
occupiedCount() e hasFreeCapacity() aggiungono excludeUserId e excludeReservationsAfterQueuePos e propagano i nuovi parametri nel calcolo dell’occupazione.
Filtro delle prenotazioni attive
app/Services/CapacityService.php
activeReservationIntervals() accetta excludeReservationsAfterQueuePos e applica il filtro SQL che esclude le prenotazioni con queue_position maggiore, mantenendo quelle NULL.

Effort di revisione

🎯 3 (Moderate) | ⏱️ ~20 minuti

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 60.00%.
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 descrive correttamente il fix sulla promozione della testa della waitlist quando le copie sono saturate.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/reservation-promotion-157

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: 1

🤖 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/Controllers/ReservationManager.php`:
- Around line 196-199: In ReservationManager::promoteNextReservation, the queue
reordering path still uses nextReservation['queue_position'] directly, which
turns NULL into 0 and corrupts positions. Reuse the resolved queue position
already stored in $headQueuePos, and when it is null, treat the
promotion/reorder as starting from 1 instead of passing a zero value into
updateQueuePositions. Make the fix in the promotion flow around
isDateRangeAvailable and updateQueuePositions so the same validated queue
position is used consistently.
🪄 Autofix (Beta)

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

Run ID: bb211fa2-61d6-42d6-8158-d11bc58d2920

📥 Commits

Reviewing files that changed from the base of the PR and between eab030e and 7536cca.

📒 Files selected for processing (2)
  • app/Controllers/ReservationManager.php
  • app/Services/CapacityService.php

Comment on lines +196 to +199
$headQueuePos = isset($nextReservation['queue_position'])
? (int) $nextReservation['queue_position']
: null;
if ($this->isDateRangeAvailable($bookId, $startDate, $endDate, (int) $nextReservation['id'], $headQueuePos)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Gestisci anche il riordino quando queue_position è NULL.

Qui $headQueuePos può diventare null e la promozione può proseguire; più avanti però updateQueuePositions($bookId, (int) $nextReservation['queue_position']) trasforma NULL in 0, decrementando tutte le posizioni > 0 e creando code con posizione 0. Usa lo stesso valore risolto e riordina da 1 quando manca una posizione valida.

🐛 Fix proposto
-                $headQueuePos = isset($nextReservation['queue_position'])
+                $headQueuePos = isset($nextReservation['queue_position'])
                     ? (int) $nextReservation['queue_position']
                     : null;
                 if ($this->isDateRangeAvailable($bookId, $startDate, $endDate, (int) $nextReservation['id'], $headQueuePos)) {
@@
-                    $this->updateQueuePositions($bookId, (int) $nextReservation['queue_position']);
+                    if ($headQueuePos !== null) {
+                        $this->updateQueuePositions($bookId, $headQueuePos);
+                    } else {
+                        $this->reorderQueuePositions($bookId);
+                    }
🤖 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/ReservationManager.php` around lines 196 - 199, In
ReservationManager::promoteNextReservation, the queue reordering path still uses
nextReservation['queue_position'] directly, which turns NULL into 0 and corrupts
positions. Reuse the resolved queue position already stored in $headQueuePos,
and when it is null, treat the promotion/reorder as starting from 1 instead of
passing a zero value into updateQueuePositions. Make the fix in the promotion
flow around isDateRangeAvailable and updateQueuePositions so the same validated
queue position is used consistently.

@fabiodalez-dev fabiodalez-dev changed the title fix(loans): promuovi la testa della coda quando le prenotazioni saturano le copie (#157) fix(loans): promote the waitlist head when reservations fully subscribe the copies (#157) Jun 27, 2026
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.

1 participant