v0.12.2
AlpineClubBookingsNZ v0.12.2
Release target: v0.12.2 on 2026-07-20. This patch public reference release
brings all changes landed after v0.12.1 into one supported tag. As with
v0.12.1, the version is a deliberate patch bump chosen by the owner even
though the range carries feature work — most additions are opt-in and flagged
off by default. Unlike v0.12.1, this release is not purely additive: it
lands the first destructive contract migration since the expand/migrate/contract
series began (legacy-structure contraction E13, the blue/green-safe subset of
#1939), a second breaking column-drop migration (Xero member-grouping
multi-select age tiers), and one changed default behaviour (admin post-login
landing). Both breaking migrations require the operator acknowledgement
ALLOW_BREAKING_BLUE_GREEN_MIGRATIONS=1. The canonical detailed entries remain
in CHANGELOG.md; this page is the release/operator overview.
Highlights
- Production Xero lock-date 503 fix. Retroactive (past-dated) booking
creation returned a 503 "Could not verify the Xero lock dates" whenever the
connected Xero organisation actually had lock dates set — the exact case
the guard exists for. xero-node's serializer coerces an MS-JSON/Date(...)/
payload into a JSDate, soparseXeroLockDate'svalue.slice(0,10)threw
and the guard failed closed; reconnecting could never fix it. The parser now
acceptsstring | Date. - Xero reliability follow-ups. A lock-date error taxonomy classifies the
guard's 503 asreconnect_required | rate_limited | transientwith
cause-specific, admin-only reason copy (member responses byte-identical); a
click-only live connection-health probe (GET /api/admin/xero/status?probe=1)
replaces the token-row-presence "Connected" chip; the finance-sync date parser
is nowDate-aware so SDK-coerced due-dates no longer drift into the
no-due-date aging bucket; and the Xero contact-create gate is shrunk to require
only name + email, with sparse members creating clean payloads. - Age-exempt (N/A) membership-type lifecycle. Membership types whose
allowed-age-tiers list "N/A (no age)" are now the single source for genuinely
age-exempt members, enforced through one shared helper at every ageTier write
site. On top of it: bulk membership-type assignment (up to 100 members) from
the members page with preview tokens, per-member audits, and a batched Xero
reconcile; Xero Setup import into membership types (or types + age tiers) that
never overwrites an existing current-season assignment and fully reports what
it skips; and an opt-in mode that lets subscription paid-detection look through
to per-type + tier fee item codes (default off = today's behaviour). - Xero member-grouping multi-select age tiers. A grouping rule can now target
any subset of age tiers (empty = all tiers) with specificity-based overlap
resolution and a fingerprint serializer proven byte-identical to the old one
for the migrated cases, plus a "Refresh from Xero" button and a "Last synced"
header. Its migration drops the old scalarageTiercolumn (breaking — see
below). - Admin post-login landing (behaviour change). After sign-in a member with
admin access and no set preference now lands on their admin area instead of the
member dashboard, applied by the application redirect resolver; a nullable
Member.postLoginLandingenum backs a profile toggle. No open-redirect
surface; a demoted admin with a stale preference still lands safely on
/dashboard. - Admin and booking UX. A Ctrl/Cmd-K admin feature-search palette; a
bookings-officer key-card row on the dashboard; a fixed admin booking-calendar
overflow (auto-expanding rows, "+N more" chip, greyed finished days); the
/financeand/lodgeshells now inherit the club theme;/admin/security
gains per-section Edit→Save gating, a persistable magic-link TTL, and a
stale-clobber fix; the member booking-edit panel now renders the required
minors-without-adult justification field (machine-readable error code); and an
in-progress stay's minimum-stay rule is evaluated against the whole contiguous
stay, surfaced as an advisory warning. - Legacy schema contraction E13 (destructive). Drops the dead
EntranceFee
andAgeTierXeroAcceptedContactGrouptables and an orphaned account-mapping
row — the blue/green-safe subset of #1939, re-verified as zero-reader against
thev0.12.1tag; the remainder is deferred to #2129/#2130/#2131. The #2130
runtime-prep also ships here (#2133): explicitselects stop the deployed
client naming the deferred columns, making next release's contract drop
blue/green-legal. - Docs and tests. The member-facing user guide is mirrored one-way to the
GitHub wiki by a push-triggered workflow (npm run docs:wiki-sync), and the
E2E seed fixtures were made relative/never-expiring so the suite stops going
stale at date boundaries.
Compatibility and operator impact
- Runtime requirements remain Node.js 24 LTS, npm 11+, and PostgreSQL 16.
- The release contains four migrations: two expand/additive and two breaking
contractmigrations. Both breaking migrations require the operator
acknowledgementALLOW_BREAKING_BLUE_GREEN_MIGRATIONS=1for this deploy — the
blue/green validator refuses a breaking migration without it. All four carry
old_code_compatible=yeswhere they have a ledger row.20260719170000_xero_grouping_age_tiers_multiselect(breakingcontract,
ledgered) backfills the scalarXeroContactGroupRule.ageTierinto a new
ageTiersarray (X → [X],null → []= "all tiers") and then drops the
scalar column.old_code_compatible=yesbut window-bounded and
admin-only: between migrate and cutover the old colour's
grouping/membership-admin reads still nameageTierand error with
column-does-not-exist. The live grouping sync fails closed (a re-grouping
edit errors and is retried post-cutover — no partial write, no money, no
booking capacity). Deploy with grouping/membership-admin traffic idle and cut
over promptly. No member is re-grouped in Xero by the migration.20260720120000_contract_drop_entrance_fee_and_agetier_xero_group
(destructivecontract, ledgered) drops the deadEntranceFeeand
AgeTierXeroAcceptedContactGrouptables and deletes the orphaned
entranceFeeAmountCentsaccount-mapping row.old_code_compatible=yes: an
independent drop-proof review re-verified zero readers against the
v0.12.1tag (the colour draining during this deploy) with no FK/cascade
trap, so the draining old colour keeps working. The acknowledgement is
required only because aDROPis breaking by class. Deliberately
kept/deferred (still read by the current runtime): theEntranceFeeCategory
enum,SeasonRate(the live public{{hut-fees}}embed),MembershipTypeAgeTier,
and theXeroItemCodeMapping.isMember/AgeTierSetting.xeroContactGroup*
columns — follow-ups #2129/#2130/#2131.20260719150000_add_post_login_landing(expand, ledgered) adds a
PostLoginLandingenum plus a nullableMember.postLoginLandingcolumn with
no default — a metadata-only catalog change even on the hotMembertable.
No existing member's landing changes on migrate; the changed admin default is
applied entirely by the application redirect resolver.20260719180000_add_use_fee_schedule_item_codes(expand, ledger-exempt)
adds a single flagged-off boolean on the cold single-row
MembershipLockoutSettingstable — additive with a constant default, so it
carries no ledger row under the same policy as v0.12.1's
add_login_security_setting.
- The one behaviour change is admin post-login landing: from the first login
after cutover, a member with any accessible admin area (including read-only
admins and finance-only viewers) lands on their admin area instead of
/dashboard, unless they set the new profile preference. It is applied by the
application, not by stored data. - Opt-in, no behaviour change until enabled: the new "use membership fee item
codes" subscription paid-detection mode (its migration only adds the flag), and
the age-exempt (N/A) membership types (config-only, no migration — effective
only when an admin sets a type's allowed age tiers to include or restrict to
N/A). - No migration makes a Xero, Stripe, or SES call, and no member is re-grouped in
Xero by any migration.
Upgrade and verification
Follow docs/UPGRADING.md from v0.12.1 to v0.12.2. In summary: take and
restore-test a fresh backup (mandatory this release — the E13 DROP TABLEs are
irreversible without it), review the four pending migrations against the safety
ledger, set ALLOW_BREAKING_BLUE_GREEN_MIGRATIONS=1 for the deploy, run the
grouping-drop deploy in a quiet admin window and cut over promptly, and complete
the post-upgrade actions.
After cutover, verify at minimum:
- Xero lock dates — creating a retroactive (past-dated) booking no longer
returns the 503 when the org has lock dates set; the admin Xero page shows the
real connection-health chip (and the Reconnect CTA when a token is revoked). - Member-grouping rules — each former single-tier rule shows that one tier
and each former "Any age" rule shows "all tiers"; "Refresh from Xero" triggers
no unexpected full regroup. - Post-login landing — admins (including read-only and finance-only) land on
their admin area on the first sign-in after cutover unless they set the new
profile preference; plain and demoted members still land on/dashboard. - Membership tooling — the members-page bulk "Set Membership Type" flow and
the Xero Setup import mapping modes work and never overwrite an existing
current-season assignment; the opt-in fee item-code mode is off unless enabled. - Age-exempt types — a type whose allowed tiers restrict to or include N/A
resolves holders toNOT_APPLICABLE, and N/A members stay non-bookable as
linked guests. - Booking edit — the member edit panel renders the required justification
field for a minors-without-adult edit; an in-progress stay can be extended
night-by-night with minimum-stay evaluated across the whole contiguous stay. - Legacy contraction — the deploy applied the E13 drops cleanly and the app
is healthy (the dropped tables had no readers).
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.