AlpineClubBookingsNZ v0.12.0
Release target: v0.12.0 on 2026-07-18. This minor public reference release
brings all changes landed after v0.11.0 into one supported tag. The canonical
detailed entries remain in CHANGELOG.md; this page is the release/operator
overview.
Highlights
- Lobby Display: a new flagged-off module for per-lodge lobby screens with
admin-authored layouts and templates (room cards, night columns, status
board), a lodge notice, guest name-granularity control, registered display
devices, and a two-sided (member and lodge) opt-in before any guest
phone number is shown — and only ever for adult members; youth and child
phone numbers are never shown. Off by default; nothing changes until
enabled. - Exclusive whole-lodge holds: a school/group booking request can ask for sole
occupancy, and an admin can place a whole-lodge hold that blocks all other
bookings for those nights, enforced through capacity checks, bed allocation,
status guards, and advisory locks. - Multi-lodge operation is core: the
multiLodgemodule flag is removed,
lodge routes are un-gated, and the admin home becomes a lodge hub. The
vestigialClubModuleSettings.multiLodgecolumn is retired but not yet
dropped (the drop is deferred to a future contract migration). Single-lodge
clubs see no operational change. - Authoritative fee schedules and membership billing: database-held season
rates keyed by membership type, per-type/per-tier joining fees, annual-fee
invoice components, durable subscription billing, family billing modes,
manual mark-paid provenance, and public fee presentation behind a
double-opt-in embed. Day-one amounts are backfilled from existing
configuration and the legacy tables are retained. - Database-first club identity and configuration: club identity, lodge
address, capacity, age tiers, and email settings resolve database-first with
config-file fallback, a boot-time self-heal backfills missing values without
overwriting admin edits, andCONFIG_BUNDLE_IMPORT_PATHcan auto-import a
configuration bundle at boot into an empty database for disaster recovery
or cloning. Age tiers can run as a contiguous subset (fewer than four). - Rule-based Xero member grouping with a server-persisted dry-run that must be
fresh before any bulk re-sync, plus Xero/finance hardening: settlement apply
retry, write durability, credit deallocation, entrance-fee dedup with a
single-active-link index, transactional season billing, and a webhook
processing lease so redelivered events reprocess safely. - Booking settlement and lifecycle correctness: NZ date-only enforcement,
corrected lock topology, closed cancel and double-charge races, truly
terminal request/hold states, single-sourced deferred-payment state, and
completion at the end of the checkout day (enabling priced, capacity-checked
checkout-day extensions). Split-booking settlement, payment amounts, and
admin presentation are corrected. - View-only admin access roles are enforced across admin editors, routes, and
action buttons: read the data, mutate nothing. - Member lifecycle administration: surfaced deletion requests, member merge,
duplicate-capture auto-refund template and history event, book-on-behalf for
non-members, and CSV import of already-cancelled members. - The legacy standalone committee directory and its admin CRUD are removed in
favour of the member-linked roles/assignments system from v0.11.0 — this
release's one contract migration. - Performance, load, and accessibility: admin bookings pagination, pool
sizing, a k6 load harness, load-stability fixes, accessible form errors and
UI states. CI adds Semgrep static analysis; the attack surface is
documented.
Compatibility and operator impact
- Runtime requirements remain Node.js 24 LTS, npm 11+, and PostgreSQL 16.
- The release contains 25 migrations: 24 expand/additive and one contract.
Fifteen have explicit safety-ledger rows in
docs/BLUE_GREEN_MIGRATION_SAFETY.tsvbecause they touch hot-table-matching
SQL, contain data operations, or need a documented deployment plan. (One
further ledger change is a documentation-only correction to the pre-v0.11.0
20260708230000_add_member_credit_note_allocationrow.) The ten without
rows were checked directly against the validator and ledger policy and do
not require rows:20260714130000_add_oswald_club_theme_fontadds theOSWALDvalue to the
ClubThemeFontenum (ALTER TYPE ... ADD VALUE IF NOT EXISTS). Purely
additive; no table, row, or hot-table SQL is involved.20260715130000_xero_entrance_fee_single_active_linkcreates one partial
unique index onXeroObjectLinkguaranteeing at most one ACTIVE
entrance-fee invoice link per member — a last-line DB guard behind the
existing mint idempotency key. Recorded in
prisma/partial-unique-indexes.tsv; no data change and no destructive
SQL.20260716120000_add_booking_request_versionadds
BookingRequest.version INTEGER NOT NULL DEFAULT 0for the held-request
conversion fences. A constant-default ADD COLUMN is catalog-only on
PostgreSQL 11+, and old colours neither read nor write the column.20260717120000_add_admin_member_delete_request_prefadds
NotificationPreference.adminMemberDeleteRequestdefaultingtrue, so
existing admins keep receiving delete-request alerts. Additive only.20260717160000_add_club_identity_settingscreates the empty,
all-nullableClubIdentitySettingssingleton. An absent or empty row is
fully functional through the DB →club.json→ hard-default fallback
chain; no seed values are written by SQL.20260717160100_add_lodge_addressadds nullableLodge.addressand
backfills the previously hardcoded contact-page address onto the existing
default lodge only — a single-row update on a tiny cold config table.20260717160200_add_book_now_settingsadds constant-default Book Now
columns to thePublicContentSettingssingleton plus a SET NULL FK to
PageContent. The defaults preserve today's behaviour exactly (button
shown, booking-flow target).20260717180000_genericise_starter_lodge_copyis a data-only UPDATE of
the starter privacy/terms/FAQ page copy, guarded on an exact match of the
full previous shipped HTML, so any page an admin has edited is left
completely untouched.20260717210000_xero_member_grouping_dry_runcreates the cold
XeroMemberGroupingDryRunprovenance table that enforces dry-run
freshness before a bulk re-sync. DB-only and idempotent; zero Xero calls.20260717220000_add_club_identity_facebook_urladds nullable
ClubIdentitySettings.facebookUrl. A NULL value falls through to
club.json, and the boot-time config self-heal backfills it without
overwriting an admin edit.
- The one contract migration,
20260714140000_drop_committee_member, drops
the legacy standalone committee directory table. Its expand predecessor,
20260629130000_add_committee_roles_assignments, shipped inv0.11.0
(deployed 2026-07-13) and backfilled roles/assignments while the table still
existed, so the drop loses no data beyond the retired directory itself — no
assignment or contact data lives only in the dropped table — and the
required expand release is fully drained. The old colour's admin committee
CRUD routes still read the table, so between migrate and cutover those
admin-only routes error with relation-does-not-exist; public committee and
contact surfaces read from assignments on both colours and are unaffected.
Deploy with old-colour admin traffic idle or drained, cut over promptly,
and use
ALLOW_BREAKING_BLUE_GREEN_MIGRATIONS=1with a non-empty override reason
acknowledging this window. - Two expand migrations carry window-bounded old-colour caveats documented in
their ledger rows.20260717170000_joining_fee_modelre-keys the
entrance-fee Xero item-code mappings fromENTRANCE_FEEtoJOINING_FEE;
once that runs, the old colour resolves both the item code and the
amount of a new entrance-fee invoice from the legacy flat mappings, so
during the migrate→cutover window it can mint a wrong per-category amount,
or — if the flat amount is unset — mark the operation SUCCEEDED and
silently never create the invoice. Operations queued before the window
carry frozen amount/item payloads and replay safely. Membership approvals
and entrance-fee minting must therefore be fully idle on the old colour
from migrate until cutover.20260716140000_xero_member_groupingconverges
grouping rules, so a membership-type rule save on the draining old colour
should be avoided. Deploy both in a quiet window and cut over promptly. - No migration makes a Xero, Stripe, or SES call. No member is re-grouped in
Xero until an admin runs the dry-run and bulk re-sync in
docs/XERO_MEMBER_GROUPING_RUNBOOK.md. - The new fee model backfills day-one amounts from the existing configuration
and retains the legacySeasonRateboolean keys and flat Xero mapping
amounts, so both colours price season and annual fees identically during
cutover; entrance/joining fees carry the old-colour window caveat above. CONFIG_BUNDLE_IMPORT_PATHis for disaster recovery and cloning only: it
imports a configuration bundle at boot only when the database holds no
non-seed configuration, and is not a database backup. Continue to use and
restore-testpg_dumpbackups.
Upgrade and verification
Follow docs/UPGRADING.md from v0.11.0 to v0.12.0. In summary:
restore-test a fresh backup, review every pending migration against the safety
ledger, schedule a quiet low-write window, confirm the committee contract
window, deploy through the documented override path, and cut over promptly.
After cutover, verify at minimum:
- Club identity and configuration — admin club identity (name, short
name, hut-leader label, Facebook URL), lodge address, capacity, age tiers,
and email settings show the expected values now held in the database (the
boot-time self-heal backfills from the effective config without
overwriting admin edits). - Fee schedules — admin fee configuration (season rates by membership
type, joining fees, annual fees and components, subscription billing
settings) and the public join/fees pages show the same amounts the club
charged before the upgrade. - Lobby Display — the module remains disabled unless the club intends to
use it; if enabling, followdocs/lobby-display/operating.mdand confirm
guest phones stay hidden unless both the member and the lodge opt in
(and only adult members' phones ever show; youth/child are never shown). - Exclusive holds — if the club uses school/group requests, confirm a
request can flag exclusivity and an admin whole-lodge hold blocks other
bookings for its nights. - Config bundle auto-import —
CONFIG_BUNDLE_IMPORT_PATHis unset on the
production deployment unless deliberately used for DR/cloning; when set,
it imports only into an empty, non-configured database. - Xero member grouping — only the migration's backfilled tier rules are
active (runbook pre-check), and a fresh dry-run is run and reviewed before
any bulk re-sync is initiated. - View-only access — a view-only admin access role can read but not
mutate: spot-check action buttons and editors across bookings, members,
finance, and content surfaces.
This release does not publish an npm package (package.json remains private).
The supported artefacts are the Git tag/GitHub release and the repository's
container-image release workflow.
Release engineering evidence
- Release PR: #2036 (merge commit
9734c0f8), cut frommain@ea021fa9. - Two independent adversarial reviews (factual accuracy; operator deployability) — all findings fixed in-PR (
1b10d7ea). Carry-forward: #2038. - CI fully green on the PR (verify, migration drift, Playwright E2E, E2E multi-lodge, static analysis gate, CodeQL, Semgrep, gitleaks, dependency review) plus a local CI-faithful gate (Node 24.18, LF clone): lint, typecheck, full test suite, production build, migration-safety coverage (189 migrations).
- Container images: published by the
Publish GHCR imagesworkflow frommainat the merge commit SHA. CHANGELOG.mdis canonical; readdocs/UPGRADING.md(v0.11.0 → v0.12.0) before deployment.