Skip to content

AlpineClubBookingsNZ v0.12.0

Choose a tag to compare

@thatskiff33 thatskiff33 released this 18 Jul 07:12
9734c0f

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 multiLodge module flag is removed,
    lodge routes are un-gated, and the admin home becomes a lodge hub. The
    vestigial ClubModuleSettings.multiLodge column 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, and CONFIG_BUNDLE_IMPORT_PATH can 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.tsv because 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_allocation row.) The ten without
    rows were checked directly against the validator and ledger policy and do
    not require rows:
    • 20260714130000_add_oswald_club_theme_font adds the OSWALD value to the
      ClubThemeFont enum (ALTER TYPE ... ADD VALUE IF NOT EXISTS). Purely
      additive; no table, row, or hot-table SQL is involved.
    • 20260715130000_xero_entrance_fee_single_active_link creates one partial
      unique index on XeroObjectLink guaranteeing 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_version adds
      BookingRequest.version INTEGER NOT NULL DEFAULT 0 for 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_pref adds
      NotificationPreference.adminMemberDeleteRequest defaulting true, so
      existing admins keep receiving delete-request alerts. Additive only.
    • 20260717160000_add_club_identity_settings creates the empty,
      all-nullable ClubIdentitySettings singleton. 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_address adds nullable Lodge.address and
      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_settings adds constant-default Book Now
      columns to the PublicContentSettings singleton plus a SET NULL FK to
      PageContent. The defaults preserve today's behaviour exactly (button
      shown, booking-flow target).
    • 20260717180000_genericise_starter_lodge_copy is 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_run creates the cold
      XeroMemberGroupingDryRun provenance table that enforces dry-run
      freshness before a bulk re-sync. DB-only and idempotent; zero Xero calls.
    • 20260717220000_add_club_identity_facebook_url adds 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 in v0.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=1 with 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_model re-keys the
    entrance-fee Xero item-code mappings from ENTRANCE_FEE to JOINING_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_grouping converges
    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 legacy SeasonRate boolean 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_PATH is 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-test pg_dump backups.

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:

  1. 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).
  2. 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.
  3. Lobby Display — the module remains disabled unless the club intends to
    use it; if enabling, follow docs/lobby-display/operating.md and 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).
  4. 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.
  5. Config bundle auto-importCONFIG_BUNDLE_IMPORT_PATH is unset on the
    production deployment unless deliberately used for DR/cloning; when set,
    it imports only into an empty, non-configured database.
  6. 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.
  7. 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 from main @ 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 images workflow from main at the merge commit SHA.
  • CHANGELOG.md is canonical; read docs/UPGRADING.md (v0.11.0 → v0.12.0) before deployment.