Skip to content

Settings: granular 0.9.0 controls, safe upgrades and selectable holidays - #512

Closed
fabiodalez-dev wants to merge 61 commits into
test/0.9.0-coveragefrom
feat/090-feature-toggles
Closed

fabiodalez-dev wants to merge 61 commits into
test/0.9.0-coveragefrom
feat/090-feature-toggles

Conversation

@fabiodalez-dev

@fabiodalez-dev fabiodalez-dev commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

This makes the 0.9.0 modules optional while preserving each library’s existing behaviour during upgrades. Printing cards no longer requires photos, consultation restrictions no longer require patron-type rules, and opening the ledger does not silently enable overdue fines or debt blocking.

Settings and behaviour

  • 18 saved controls: ten in Settings → Modules and eight in Settings → Loans, using the existing switches and form styling. Separate controls cover photos, consultation-only copies, automatic card validity, automatic late fines, debt blocking, loss/damage mirroring and attendance retention while collection is suspended.
  • Independent financial choices: manual ledger, automatic overdue fines, debt blocking and mirroring assessed loss/damage penalties. Automatic fines require the ledger, their own switch and a positive resolved rate; debt blocking additionally requires its own switch and threshold. Type/material overrides cannot bypass the switches. Web and mobile balance indicators honour the effective debt-block setting.
  • Configurable calculations: grace as a threshold (the existing default) or waived days; card renewal from today (the existing default) or a future existing expiry. Confirmation and persistence use the same date calculation, including lifetime cards.
  • Configuration before activation: administrators can prepare card layouts, patron types, material rules and closures while modules are off. Operational routes stay gated; configuration access retains authentication, CSRF and role checks.
  • Attendance: collection and retention while suspended are separate choices. The retention period remains editable while collection is off; zero always prevents automatic deletion.
  • Holidays for every country: the holiday card lists every country and territory (250), named in the interface language. 22 countries keep their hand-written rule tables (any year); every other one uses the official holidays of the python-holidays open data (MIT, 2010–2060, regenerated by scripts/generate-holiday-dataset.py), with the own-language and English names, the category (government, bank, school, a single community — offered unticked) and lunar dates marked as estimated. A weekend holiday with a day in lieu closes both days; moved holidays keep only the new date. Manual and ICS closures are preserved; selected rules are evaluated for the requested year; preset writes and maintenance serialize selection changes. The preview year is a number field (1900–2199).
  • The library's own holidays: a new card adds holidays on a fixed day, the nth weekday of a month, days from Western or Orthodox Easter, a date in the Islamic (Umm al-Qura), Hebrew, Chinese (published lunar table 1900–2100), Persian, Indian, Ethiopian or Coptic calendar, or listed dates; one or more days, a weekend rule, first/last year, every branch or one. Stored in the new festivita_personalizzate table, evaluated for any year, editable, suspendable and deletable.

Upgrade guarantees

The migration only inserts missing preferences. Explicit OFF values, zeroes, inherited rules, unknown settings and existing data survive repeated upgrades. New granular preferences inherit their former parent only when absent; old forms freeze those preferences before changing the parent.

A library whose ledger was off keeps automatic fines, debt blocking and loss mirroring off, even with positive rates or assessed penalties already stored. Subsequently enabling only the ledger leaves those automations off. Existing amounts are preserved; disabled ledger/loss controls prevent the migration from posting legacy penalties. Retention while collection is suspended remains opt-in. Existing loan due dates are preserved. No holiday selection and no custom holiday exist after an upgrade, so due dates stay those of a library open every day until the administrator chooses otherwise.

Validation

  • All 325 standalone PHP suites passed locally with no skips, including 37 settings integration scenarios against a disposable database.
  • Repeated full migrations cover all-on, all-off, mixed and catalogue configurations, plus the real 0.8.9 schema. An additional regression checks that positive rates and legacy penalties cannot reactivate previously disabled fines.
  • Browser checks passed for switches and permissions, configuration while disabled, fines by patron/material/calendar, photos, card expiry and renewal confirmation. Desktop and 390 px settings layouts were inspected.
  • PHPStan 2.1.56 with the repository’s full configuration and the schema/migration gate passed.
  • CI results and the detailed control map are recorded in the review report, with the exact tested commits.

All ten CI workflows passed for 0471c66f, the head that includes the review follow-ups, the holidays of every country and the library's own holidays: Code Quality, the four-database matrix, E2E (240 circulation + 258 catalogue tests and the general suite), migrations, both upgrade gates, deep regression (four browser shards and five localized installers), three-browser/accessibility/DAST checks, supply-chain security and CodeQL. Locally all 327 standalone unit tests pass. Exact run links and earlier tested commits are in the review report.

Each country lists its public holidays as rules with the native name:
fixed dates, Easter offsets, nth weekday of a month, weekday on or
before/after a date, and a fixed date on given weekdays with a fallback.
Weekend rules cover the US, UK, Irish, Canadian and Australian substitute
days, Koningsdag and the Argentine movable holidays; from/until years and
one-year overrides (VE Day 2020, the Jubilees) are part of the rule.
Regions: German Länder, UK nations, Canadian provinces, Australian states,
Alsace-Moselle and six Swiss cantons.
The library's choice (country, region, ticked rules) is saved in
system_settings and materialised in calendario_chiusure, marked with
regola_festivita: a fixed date as one yearly row, any other rule as one
row per year up to two years ahead. Applying again adds nothing,
unticking removes only that rule's rows, manual and ICS closures are
never touched, and rows of the former Italian preset are adopted instead
of duplicated. A closure with attiva = 0 stays stored and closes nothing.
Migration section 'holiday presets' adds both columns and the index.
App\Support\Features holds the eleven keys with their upgrade default (also
the code fallback) and their fresh-install default. ConfigStore maps the
features category, so a saved switch is read back. Settings gains a Modules
tab for administrators; the three circulation modules are switched from the
Loans tab. FeatureGateMiddleware refuses the routes of a module that is off
(302 to the dashboard, or 404 feature_disabled for JSON callers).

Fresh defaults are seeded in the five data_*.sql files; the upgrade defaults
go in a new feature toggles section of migrate_0.9.0.sql, which turns a
module on when the library already has data for it.
… and cron

Sidebar entries, book list and book record actions, user list and record
actions, form fields and settings tabs follow their module. The controllers
re-check the switch after the route gate. Services carry the switch for
every channel at once: CirculationPolicy::rulesActive() (patron types,
rules, consultation copies), PatronLedger::active() (charges, debt block,
previews, backfill, fine emails), LibraryCalendar::fromDatabase() (an empty
calendar), RDA overrides and supplier writes. The visitor log purge waits
while the module is off, the statistics sections follow their module, and
the Presenze entry now also disappears in catalogue mode.

Mobile API: /health.features gains fines, closures_calendar and
circulation_rules; /me/movements answers 404 feature_disabled with fines off.
The z39-server copy cataloguing routes follow the MARC module.
The holiday card of /admin/calendar lists the holidays of the chosen
country and region with their native names and the dates of the preview
year; the librarian ticks them (select all / none) and applies. Holiday
rows carry a badge in the closures list and are changed from the
selection only. A new card switches the yearly closures added by hand or
from ICS on and off without deleting them. Maintenance keeps the movable
holidays dated two years ahead and logs holiday_rows_added. Interface
strings in the five locales; the old Italian-only import strings go.
holidays-preset.spec.js picks countries and regions, previews dates,
applies a subset, reapplies, unticks, checks a due date on a ticked and
on an unticked holiday, switches country next to manual and ICS
closures, suspends a yearly closure and adopts former Italian rows. The
closures-calendar-admin preset tests use the new card. The spec joins
the circulation group of the 0.9.0 E2E job.
…section, settings and stocktake

The unit suites of the modules themselves switch their module on with
Features::override(), so they test the module whatever the database says.
… the reader record hides the patron type with the module off
…eature toggle suites

Each suite of a module that is off by default on some installs enables it
for its run and restores the stored value. Zero configuration now means the
fines module is off, so the reader record shows no Movimenti section. The
debt-block CLI probe loads .env like the cron bootstrap, so ConfigStore reads
the module switch of the test database. CHANGELOG entry for the toggles.
… calendar switch

The holiday preset and yearly closures routes carry the closures_calendar gate like the rest of the calendar, and the maintenance run adds no holiday row while the module is off.
- The fines probe of the migration also keeps fines on when a debt threshold or a fine is configured globally, on a patron type or in the rules matrix, so an upgrade from a release candidate that used only the debt block keeps it.
- An ICS closure on a day a ticked holiday already covers is imported as its own closure: unticking the holiday later no longer reopens a day the file says is closed.
- Switching the closures calendar on or off from Settings > Loans re-checks the pending pickup deadlines at once, as saving the calendar does.
@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@fabiodalez-dev

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review skipped: 118 files exceed the limit of 100.

@pullfrog pullfrog 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.

ℹ️ No critical issues. One ledger gap is noted inline.

Reviewed changes

I reviewed the full diff at 55926fe: the 11 features.* module switches, the merged holiday presets, the migration sections, views and tests.

  • App\Support\Features registry: DEFAULTS is used for upgrades and as the code fallback, and FRESH holds the seed values. Catalogue mode turns off the four circulation-side modules through enabled(), while stored() returns the saved switch for the settings forms. ConfigStore maps only the known keys. tests/feature-toggles-registry.unit.php passes locally (68/68).
  • Route gating: FeatureGateMiddleware sits first on every module route in web.php and on the z39 copy-cataloguing routes. The controllers check again with Features::requireOrRedirect(). tests/feature-toggles-routes.unit.php passes locally (25/25).
  • Service-level switches: CirculationPolicy::rulesActive() and PatronLedger::active() replace the old schemaReady() checks, including the SQL twins (lendableCopySql, debtBlockSql, debtThresholdSql). LibraryCalendar::fromDatabase() returns an empty calendar when its module is off. Supplier and RDA writes, plus the RDA export overrides, follow their switches. In maintenance, the attendance purge and the holiday extension are skipped.
  • Settings UI: a new admin-only Modules tab. The three Loans switches are saved inside the existing mayConfigure() block. Turning the calendar on or off runs extendPickupDeadlines() again.
  • Holiday presets: HolidayRules, HolidayCalculator and HolidayPresets store the selection in calendar.holiday_* and tag rows with regola_festivita. Apply is idempotent, legacy Italian rows are adopted, and maintenance runs ensureYears(). A new attiva flag lets manual yearly closures be switched on and off. tests/holiday-calculator.unit.php passes (145). I spot-checked US, UK, SE, NL, AR, AU-NSW and CA-ON dates for 2021–2028 (observed and substitute days, Midsommar, Argentine movable holidays) and they look correct.
  • Migration: the guarded regola_festivita/attiva columns and insert-if-missing features.* rows, using data probes for upgrades from RCs.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread app/Services/PatronLedger.php Resolved
@pullfrog

pullfrog Bot commented Oct 29, 2026

Copy link
Copy Markdown

Follow-up to the review: two more places where circulation_rules doesn't fully switch off. I checked both in the code at 55926fe.

⚠️ User CSV import still writes patron types when circulation_rules is off

PatronTypeImport::available() (app/Support/PatronTypeImport.php) checks only that the schema exists, not whether the module is on. UserImportController uses it in three places:

  • page() passes the patron types to utenti/import.php, which shows the "Tipologia" hint.
  • targetFields() offers the "tipologia" mapping column.
  • The import loop calls assign(), which runs UPDATE utenti SET tipologia_id = ?.

With patron type rules off and user import on, an imported CSV still assigns patron types without telling anyone. Unknown values produce "Tipologia lettore sconosciuta" warnings. When the module is switched back on, those readers suddenly fall under their type's rules. Every other entry point (PatronTypeAssignment, the user list filter) reads CirculationPolicy::types(), which is empty while the module is off.

Technical details
# PatronTypeImport ignores features.circulation_rules

## Affected sites
- app/Support/PatronTypeImport.php `available()`: schema-only check.
- app/Controllers/UserImportController.php: `page()` ($patronTypes), `targetFields()` (tipologia column), import loop `assign()`.
- app/Controllers/UsersController.php CSV export: tipologia columns gated on `schemaReady()` (they export empty values while off).

## Required outcome
- With circulation_rules off, the import neither offers nor writes `utenti.tipologia_id`; the stored value of existing users is untouched.

## Suggested approach
- Make `available()` also require `Features::enabled('circulation_rules')` (or `(new CirculationPolicy($db))->rulesActive()`), and switch the export's `schemaReady()` check to `rulesActive()`.

⚠️ Turning circulation_rules on or off leaves libri.copie_disponibili stale

DataIntegrity::recalculateBookAvailability() and recalculateAllBookAvailability() use CirculationPolicy::lendableCopySql(), and that now depends on the switch. But updateLoansSettings() doesn't recalculate when circulation_rules changes, even though it does re-check pickup deadlines when the calendar switch changes. The public catalogue reads the stored counter (FrontendController filters on l.copie_disponibili > 0 and shows it as "x / y Disponibili"). After the switch:

  • Switched off: a book whose only copy is consultation-only still shows 0 available, although the desk can now lend that copy.
  • Switched on: the consultation copy is still counted as available, so the catalogue offers a request that the policy then refuses with not_loanable.

Each book corrects itself only at its next loan event or at a manual integrity run.

Technical details
# Recalculate availability when circulation_rules flips

## Affected sites
- app/Controllers/SettingsController.php `updateLoansSettings()`: only the calendar flip triggers a follow-up.
- app/Support/DataIntegrity.php `recalculateAllBookAvailability()` / `recalculateBookAvailability()`: depend on `lendableCopySql()`.

## Required outcome
- After `features.circulation_rules` changes, the stored `copie_disponibili` / `stato` match the new lendable-copy rule.

## Suggested approach
- Record `Features::enabled('circulation_rules')` before `saveFeatureSwitches()`. If it changed, call `(new DataIntegrity($db))->recalculateAllBookAvailabilityBatched()`, or limit it to books with a `solo_consultazione = 1` copy, which are the only ones whose count changes.

Smaller items, for your judgement:

  • NotificationService (around line 2426): the loan-returned email still reads lateFineNetCents() without checking PatronLedger::active().
  • LibriApiController: still applies fornitore_filter while suppliers is off.
  • z39 plugin: /admin/books/import-unimarc and /admin/books/{id}/export.unimarc.xml have no marc gate, if they count as part of the MARC module.

Pullfrog  | View workflow run | via Pullfrog | Using claude-opus-5-5 | 𝕏

@pullfrog

pullfrog Bot commented Oct 29, 2026

Copy link
Copy Markdown

Second follow-up: holiday presets. I checked these against HolidayCalculator::forYear() at 55926fe.

⚠️ A holiday that falls on a weekend loses its own date when it is moved

For rules that move to a weekday (us, next_monday, pair, sunday_back), HolidayCalculator::dateOf() returns only the moved date, so only that day becomes a closure. For example, US Christmas 2027 gives only 2027-12-24, and AU/CA Christmas and Boxing Day 2027 give only 2027-12-27/2027-12-28. LibraryCalendar treats Saturday and Sunday as open unless they are set as weekly closed days. So a library that opens at weekends would be open on Christmas Day itself after ticking "Christmas". For the UK this is arguably right, because only the substitute day is the bank holiday. For a library's closures, though, both the real date and the weekday off are usually closed (SA and WA, for instance, list both Sat 25 Dec and Mon 27 Dec).

Technical details
# Observed holidays drop the actual date

## Affected sites
- app/Support/Holidays/HolidayCalculator.php `dateOf()` / `observe()`: return only the shifted date for weekend-substitute modes.
- app/Support/Holidays/HolidayPresets.php `expectedRows()`: one row per rule per year, built from `dateOf()`.

## Required outcome
- When a ticked holiday is moved off a weekend, the library is closed on the actual date as well as on the observed weekday (at least where the country's law keeps both, e.g. AU states; decide per country for UK/US).

## Suggested approach
- Let the calculator return both dates for observed rules, and have `expectedRows()` write a row for each under the same `regola_festivita`. `pruneStale`/`ensureRow` already key on (rule, date). Extend `tests/holiday-calculator.unit.php` with US 2027 Christmas, AU 2027 Christmas and WA Anzac 2026.

## Open questions for the human
- Should the UK keep the moved date only, matching bank-holiday law, or also close on the actual day?

ℹ️ Australian rule data

  • AU:easter_sunday is limited to ['ACT','NSW','QLD','VIC'], but SA, WA and NT also list Easter Sunday as a public holiday (SA, WA).
  • AU:anzac_day has no weekend substitute, but WA moves it to Monday (2026: Sat 25 and Mon 27 April). SA has no Anzac substitute, so this would need a WA-only rule.

Also minor: when a former Italian row is adopted, ensureRow() sets attiva = 1, so a legacy yearly closure the librarian had suspended between the migration and the first apply comes back on.

I spot-checked US, GB nations, IE, NL (Koningsdag), AR (law 27.399 movable holidays), SE/FI Midsummer and All Saints, CA (Victoria Day), the AU state Mondays and IT (San Francesco from 2026) for 2025–2028, and they match the official dates. Apply/untick/country-switch idempotency in HolidayPresets holds, and manual and ICS rows are never touched.

Pullfrog  | View workflow run | via Pullfrog | Using claude-opus-5-5 | 𝕏

… once they are back on

The legacy backfill marker is re-armed when the penalty is written with the module off, so the next maintenance run after fines are switched back on mirrors it, even in a library whose backfill was already recorded as done.
@fabiodalez-dev

fabiodalez-dev commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner Author

CodeRabbit skipped this PR (118 files, the limit is 100), so I reviewed the merged diff myself. I covered route gates, every channel per module, settings saves, migration defaults, the holiday rule table and the apply/ensureYears paths.

# Finding Severity Outcome
1 The fines probe in the migration ignored a configured debt threshold. A patron-type or rules-matrix fine was missed too. An RC that used only the debt block would lose it on upgrade. medium Fixed in 55926fe. The probe now covers debt_block_threshold, tipologie_lettore.multa_giornaliera/soglia_blocco_debito and regole_prestito.multa_giornaliera, each behind an information_schema guard. Five new cases in migration-0.9.0-feature-toggles.unit.php.
2 The ICS import dedupe counted holiday preset rows. An ICS closure on a ticked holiday was skipped, so unticking that holiday later reopened a day the file says is closed. low Fixed in 55926fe. The dedupe now ignores preset rows. Covered by test 13 in holidays-preset.spec.js.
3 Switching the closures calendar from Settings → Loans did not re-check pending pickup deadlines. low Fixed in 55926fe. A switch change runs LibraryCalendar::extendPickupDeadlines(). Covered by test 6 in feature-toggle-closures-calendar.spec.js.
4 A loss penalty assessed while fines were off was never mirrored once the backfill had completed (pullfrog's finding). medium Fixed in a2f57c9. Covered by section C2 in feature-toggles-services.unit.php.
5 With the visitor log off, the retention purge stops. — By design. The toggles plan says the switch also stops the purge. The first maintenance run after switching it back on purges anything past the retention.
6 Adopting a former Italian preset row sets attiva = 1. — Kept. Ticking a holiday means the library closes that day, and preset rows are managed only from the selection.
7 apply() (web) and ensureYears() (cron) have no unique key, so a concurrent pair could insert one duplicate holiday row. — Not changed in this PR. A duplicate closes the same day it already closes, and the next apply that unticks the rule removes it with the rest of the rule's rows. A unique key on (regola_festivita, data_inizio, ricorrente_annuale) would close the race, but it needs a dedupe step in the migration for rows that already exist. That is a schema change I would rather make on its own.
8 The return email reads lateFineNetCents() with fines off. — No effect. Nothing is charged while fines are off, so the value is 0.

Holiday dates I checked with no errors: Easter offsets, nth and last weekdays, Buß- und Bettag, Jeûne genevois, Midsommar, Victoria Day, the UK/US/AU weekend substitutes for 2021–2022, the Argentine movable holidays and the from/until years. One gap, not an error: the UK one-off extra bank holidays (2002, 2012, 2022, 2023) and the German national Reformationstag 2017 are not in the table, because overrides can move a holiday but not add a day. These are past years, so no future due date is affected.

@pullfrog pullfrog 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.

✅ No new issues found. The ledger gap from the previous review is fixed.

Reviewed changes

I reviewed the one commit added since 55926fe. It fixes the loss penalties that were lost while fines were off.

  • Re-armed the legacy backfill marker: when fines are off and prestiti.sanzione > 0, PatronLedger::chargeLossPenalty() now sets loans.legacy_penalties_backfilled back to '0'. backfillLegacyPenalties() already returns null while fines are off, so the marker stays '0' until the first maintenance run with fines on. That run mirrors the penalty under its penalty:<loan> key and marks the scan done again. There is no window where the scan is recorded as done before the penalty is mirrored.
  • Added test case C2: tests/feature-toggles-services.unit.php covers the whole sequence (marker already '1', penalty with fines off, fines on, maintenance writes exactly one ledger row). It also saves and restores the marker and cleans up the test loan.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@pullfrog pullfrog 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.

✅ No new issues found in the two new commits. The user import now respects the patron rules switch.

Reviewed changes

I reviewed the two commits added since a2f57c9 (cac20f9, 3960c19).

  • Evaluated selected holidays on demand: LibraryCalendar now receives the saved rules from HolidayPresets::calendarRules(), and isOpen() computes their dates for whatever year is asked, including an observed New Year that falls on 31 December. Once a selection is saved, loadClosures() skips rows with regola_festivita set, so stale or missing preset rows no longer affect due dates or fines. Every query method still goes through isOpen(), and closedThrough() / closedFrom() only look at one-off closures, so the long-closure jumps don't skip a holiday check. Before a selection exists (null), every row is read as before.
  • Serialized apply and upkeep: apply() and ensureYears() first take the same row lock (lockSelection(), a no-op upsert on calendar.holiday_rules). Upkeep then reads the selection with FOR UPDATE, and the row lookups use locking reads. A cron run inside an older REPEATABLE READ snapshot therefore can't bring back an unticked country's rows or duplicate the new ones. Because both paths take the locks in the same order, they can't deadlock each other. Both also call LibraryCalendar::reset() afterwards.
  • Gated PatronTypeImport::available() on circulation_rules: the check runs before the schema cache, so a running import stops assigning types as soon as the module is switched off. The CSV mapping drops tipologia as well. This fixes the user import gap from the first review.
  • Added coverage: unit tests for dynamic holidays in library-calendar.unit.php, for concurrent apply/upkeep, lock contention and rollback in holiday-presets-apply.unit.php, and for the importer switch in feature-toggles-services.unit.php. There is also an E2E test for a holiday beyond the preview years.

Not changed in these commits, and still open from the earlier follow-ups: weekend-substitute holidays close only the substitute day, not the actual date (this now applies to the dynamic evaluation too), and copie_disponibili is not recalculated when circulation_rules is switched.

Pullfrog  | Fix it ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review skipped: 399 files exceed the limit of 100.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Both modules are off on an upgraded install, so the 0.9.0 write checks
ran against modules that refuse to write. The test now switches them on
for those checks and also asserts the opposite: with the modules off a
save keeps the stored values and an import batch assigns no supplier.
The explicit section-to-test map stopped at the fines ledger, so the
three sections this branch adds to migrate_0.9.0-rc.1.sql failed the
runner as untested. Each now names its own behavioural test.

@pullfrog pullfrog 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.

ℹ️ No functional issues. One merge leftover is noted inline: a docblock is now in the wrong place.

Reviewed changes

I reviewed the two commits added since 23f23f2 (6112c06b, dc22152c) and how the conflicts were resolved in the three merges of test/0.9.0-coverage (c02a317f, f1c34f56, 83c8659d). The base branch's own changes are outside this PR's scope.

  • Switched the modules on in the cataloguing extras test: tests/cataloguing-extras-090.unit.php turns on suppliers and rda_types for its write checks. It also checks the reverse: with both off, a save keeps the stored values and assignSupplier() returns false.
  • Mapped the PR's migration sections: tests/migration-0.9.0-rc.1.unit.php now maps holiday presets, custom holidays and feature toggles to their own tests. Without this the runner failed those sections as untested. The section check passes locally (the sub-tests need a database).
  • Combined the base's locked calendar writes with the PR's paths: the base's commitChange() (named lock, one transaction, deadline move inside it) wraps the plain closure writes. The preset, yearly-switch and custom-holiday actions keep calendarChanged(). The PR's isPresetRow() guard is still on update and delete.
  • Kept the feature gates on the base's controller changes: the fines gate runs before the base's mayActOn() in PatronLedgerController. The stocktake gate comes before the base's action validation in InventoryController::close(). The suppliers gate comes before schemaMissing() in SuppliersController, and createForm now takes $db, with the route still gated. In CataloguingExtras, the isEmptyChoice() checks sit behind the module gates, and assignSupplier(): bool returns false while suppliers is off. PatronLedger::active() survives the base's switch to a WeakMap schema cache. php -l is clean, library-calendar.unit.php passes (170/170) and so does feature-toggles-routes.unit.php (25/25).

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

calendarChanged() had been placed between commitChange()'s docblock and
the method, so neither docblock attached to commitChange() and its
locking contract and callable type were lost to PHPStan.
The whole-year guard from the closures branch now also covers the two calendar writes this branch adds: applying a holiday preset and switching stored yearly closures back on both run through commitChange() and roll back, holiday selection included, when they would close every day of a year. The guard counts only active closures, and the user-import cancel route keeps the user_import feature gate.
…hind

custom-holidays: a calendar read from a lost connection now raises (loadClosures() never reads a failure as "no closures"), so the test asserts the exception instead of an open calendar. library-calendar-loans: the refused preset import restores the saved holiday selection and its rows even when the check fails, so a failing run cannot leak Ferragosto into the next suite.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes

This run covered one PR commit and four merges of test/0.9.0-coverage since 5cd3a38. Only 2ef276b3 needed conflict resolutions.

  • Routed holiday writes through commitChange(): importPreset and saveYearly now run under the calendar lock and transaction. They return yearCoverageError() when a preset adds or adopts rows, or when a yearly closure is switched back on. A refused change rolls back everything, including the saved holiday selection. Nesting is safe: HolidayPresets::apply goes through DbAtomic::run, which detects the outer transaction and leaves begin, commit and rollback to it.
  • Limited the whole-year guard to active closures: yearCoverageError() now only counts rows with attiva = 1 (when that column exists) and includes the weekly closed days. A switched-off yearly closure therefore no longer counts toward the guard.
  • Kept the PR filters in the base's stricter loadClosures(): the attiva and regola_festivita IS NULL selection filters still apply. A failed read now raises RuntimeException; before, it was treated as "no closures".
  • Updated calendar tests: custom-holidays.unit.php now expects the exception on a killed connection. library-calendar-loans.unit.php checks that a refused preset import or yearly switch leaves the selection unchanged, and puts the selection back even when that check fails.
  • Gated the new user-import cancel route: POST /admin/users/import/cancel from the base carries FeatureGateMiddleware('user_import'), like the other import routes.

Local checks: library-calendar.unit.php 180/180, feature-toggles-routes.unit.php 25/25, php -l clean on the touched files.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

When the stored holiday_country is not a country the calculator knows any more (a hand edit, a country dropped from the data), the selection reads as unsaved and the holiday card silently showed the default country as if nothing had ever been chosen, while the chosen holidays stopped being kept up to date. The selection now reports the stored code as unknown_country, the controller passes it to the view, and the card shows a translated warning naming it. What is applied to loans is unchanged.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes

This run covered one PR commit and one merge of test/0.9.0-coverage since f8ccfb0. For the merge, I checked only the conflict resolutions.

  • Warned about an unknown saved holiday country: HolidayPresets::readSelection() now returns unknown_country when a stored holiday_country is no longer a known country. The selection still counts as unsaved, and calendarRules() still returns null, so loans keep reading the stored closure rows. The calendar card shows an escaped warning that names the code, and the string is translated in all five locales. A new case in holiday-presets-apply.unit.php covers it and restores the stored selection in finally.
  • Resolved the base merge correctly: PatronLedger::schemaReady() now caches only a positive probe (from the base), and the PR's active() is kept. Mobile GET /me/movements keeps the debt_block threshold gate and adds the base's paging meta and next_cursor. debt-block.spec.js keeps the .env safeLoad() and adds the base's database-name guard. The fines-ledger migration test keeps the PR's features.fines / loss_penalties seed, plus the base's check that runs without a cache reset.
  • Checked the base's new upgrade backfill against the switches: the base adds PatronLedger::completeLegacyPenaltyBackfill(), which runs after the migrations in the Updater and also from maintenance. It goes through backfillLegacyPenalties(), which still does nothing unless the ledger is active and loss_penalties is on.

php -l is clean on the PHP files that were touched.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

With loss penalties off the backfill never completes and chargeLossPenalty() re-arms the marker, so debtBalanceCents() and debtBalanceSql() kept adding every unconverted prestiti.sanzione: patrons were refused loans for amounts the library chose not to mirror, while the balance shown to staff and patrons stayed under the threshold. legacyPenaltiesPending() now answers false when loss penalties are off, so the debt block reads the ledger alone in PHP and SQL.

@pullfrog pullfrog 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.

Important

The base merge adds a debt-block path that ignores the PR's loss_penalties switch. With loss mirroring off, every loss or damage amount counts toward the debt block permanently. See the inline comment.

Reviewed changes

I reviewed the merge of test/0.9.0-coverage (4eb5f51c, 43 base commits) since 6f6cdc0. For the merge I checked the conflict resolutions and how the base's changes interact with the PR's switches. The base branch's own work is outside this PR's scope.

  • Combined the debt-block probe with the fines switch: the new PatronLedger::activeOrFail() is Features::enabled('fines') && schemaReadyOrFail(). CirculationPolicy::debtBlockError() and LoanEligibility::debtBlockSql() use it together with debt_block, so a failed probe still throws instead of admitting the patron.
  • Kept the attiva filter in the base's reworked whole-year guard: yearCoverageError() now carries state from one ICS event to the next. The first read, in the $state === null branch, still counts only yearly closures with attiva = 1.
  • Adjusted calendar tests to the base: custom-holidays.unit.php accepts either "closed weekdays" or "closures not readable", because the weekdays are now read first. In library-calendar-loans.unit.php, preset cleanup now matches regola_festivita IS NOT NULL rather than the holiday names.

php -l is clean on the touched PHP files and the locale JSON files parse.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread app/Services/CirculationPolicy.php Resolved

@pullfrog pullfrog 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.

✅ No new issues found. The debt-block gap from the previous review is fixed, and no Pullfrog findings are still open.

Reviewed changes

I reviewed the one commit added since 4eb5f51 (4aef5ad).

  • Stopped unconverted legacy penalties from blocking while loss mirroring is off: PatronLedger::legacyPenaltiesPending() now returns false when loss_penalties is not effectively enabled. Its only callers are debtBalanceSql() and debtBalanceCents(), so the debt block in both CirculationPolicy::debtBlockError() and LoanEligibility::debtBlockSql() reads the ledger alone, matching the balance staff and patrons see. With loss_penalties on and the marker unset, unconverted prestiti.sanzione amounts still count until maintenance converts them, as the base intends.
  • Added coverage: tests/patron-ledger.unit.php checks that with loss_penalties overridden off and the marker unset, an 8.00 penalty over a 5.00 threshold blocks nobody in PHP or SQL. With the marker set, the block reads the ledger only. php -l is clean on both files.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@fabiodalez-dev

Copy link
Copy Markdown
Owner Author

This change shipped in 0.9.0-rc.1: the whole stack landed in main through the release PR #514 (https://github.com/fabiodalez-dev/Pinakes/releases/tag/v0.9.0-rc.1). I'm closing this one because its base branch was part of the stack, so GitHub cannot mark it as merged on its own.

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