Skip to content

Releases: thatskiff33/AlpineClubBookingsNZ

v0.13.2

Choose a tag to compare

@thatskiff33 thatskiff33 released this 23 Jul 08:13
a41d913

AlpineClubBookingsNZ v0.13.2

Release target: v0.13.2 on 2026-07-23. This patch public reference release
brings all changes landed after v0.13.1 into one supported tag. It carries
five operator-relevant workstreams, one of which needs deliberate handling at
deploy time:

  1. The club-theme orphan-column expand→contract pair (#2187). The theme
    rebuild dropped from seven hand-picked brand colours to three seeds, leaving
    four former ClubTheme columns dead to code. This release ships both the
    additive expand migration that lets the new runtime stop naming them and
    the destructive contract migration that drops them — so the contract half
    needs the blue/green handling described below and is legal only once the new
    runtime has drained the previous colour
    .
  2. Config-bundle format version 2 (#2187). Configuration bundles now export
    at format version 2; a bundle exported by an older app (version 1) is refused
    on import with a clear message rather than importing stale colour columns.
  3. Guided provider setup wizards for Xero, Stripe, and Google sign-in
    (#2080/#2081/#2082/#2087).
    Provider credentials, already DB-only since
    v0.13.1 (#2079), are now captured through step-by-step in-app wizards.
  4. The opt-in AI help assistant (#2094). A new, default-off, spend-metered
    module that answers members' free-text questions grounded strictly in each
    page's curated help.
  5. Backup configuration re-homed to the encrypted store (#2095). The runtime
    now reads backup settings only from the in-app credential store; the
    legacy BACKUP_* environment variables are no longer honoured, and an
    unmigrated legacy config makes the nightly backup report FAILURE until the
    operator re-enters credentials in the new backup settings UI.

The canonical detailed entries remain in CHANGELOG.md; this page is the
release/operator overview.

⚠️ The ClubTheme orphan-column drop is destructive and must not run while a
pre-#2187 colour is still draining.
The four dropped columns
(brandCharcoal / brandRidge / brandMist / brandSnow) were named in the
SQL of every colour up to and including v0.13.1 — Prisma projects every
scalar of a model in an unnarrowed SELECT and in a mutation's implicit
RETURNING. Dropped while v0.13.1 is still the draining colour, the contract
migration breaks the old colour's theme reads and writes. The v0.13.2 runtime
names none of them, which is what makes the drop legal — but only after
v0.13.2 has replaced and drained v0.13.1.
Either defer the contract drop
until then (see below) or run it in a later quiet window with the breaking-
migration override. If the previous colour is still live, do not run the
drop.

Highlights

Club theming: three-seed substrate and the orphan-column drain (#2187)

  • Site Style now derives the whole theme from three seed colours instead of
    seven hand-picked ones.
    A vendored Radix colour generator turns one required
    accent plus two optional seeds into the full light/dark 12-step palette, with
    cross-colour text contrast guaranteed by construction, and the whole member and
    admin app — plus the public website — now paints from that generated palette.
  • 20260722140000_expand_club_theme_orphan_column_defaults — EXPAND
    (additive, blue/green safe).
    Adds a DB DEFAULT to each of the four former
    columns so the new runtime can INSERT a ClubTheme row without naming them,
    while the draining previous colour (which still writes explicit values) is
    unaffected. No data is rewritten and no column is dropped.
  • 20260722160000_contract_drop_club_theme_orphan_columns — CONTRACT
    (destructive).
    Drops brandCharcoal / brandRidge / brandMist /
    brandSnow. The four surfaces are derived from the generated substrate at
    render time (deriveBrandShims), so there is zero resolvable data loss, but the
    drop is irreversible without a restore. It carries a full rationale row in
    docs/BLUE_GREEN_MIGRATION_SAFETY.tsv
    (previous_expand_release = 20260722140000_expand_club_theme_orphan_column_defaults,
    old_code_compatible = yes) and refuses to run without the breaking-migration
    acknowledgement.
  • Configuration bundles move to format version 2. A bundle exported by an
    older app (version 1) is refused on import with a clear message rather than
    importing stale colour columns.

Guided provider setup wizards (#2080, #2081, #2082, #2087)

  • Xero (#2080/#2081). A reusable wizard shell walks a Full Admin through app
    creation, credential capture, and the OAuth connect, then completes setup with
    verified webhooks, item-code mappings, and member import, showing an amber
    badge on the setup page while webhook verification still needs attention.
  • Stripe (#2082). Captures its keys into the encrypted store and reads the
    publishable key at runtime, so changing keys takes effect immediately rather
    than needing a fresh build.
  • Google sign-in (#2087). Captures its OAuth credentials in-app, runs on a
    request-scoped NextAuth config, and keeps the module locked until a real Google
    round-trip verifies, so an unconfigured provider can never render a visibly
    broken sign-in path.
  • In all three, legacy provider env vars are detected, warned about, and ignored
    — re-enter the values in the wizard, then remove the env vars.

AI help assistant and the chat-style help widget (#2094)

  • The in-app help is now a chat-style widget on every surface (#2209, #2210).
    A single curated help corpus feeds a widget that answers page-specific
    questions and shows a full page guide, replacing the older scattered help UIs.
  • The AI help assistant module is opt-in and off by default (#2211, #2212).
    With it on, authenticated members can ask free-text questions that a paid AI
    model (Anthropic Claude Haiku 4.5) answers grounded strictly in the curated
    corpus
    . The club supplies its own Anthropic API key, held only in the
    encrypted vault (never an env var). A hard monthly spend cap (default
    NZ$10, integer cents) stops AI answers for the rest of the month once
    reached; the budget gate fails closed, reserving a conservative worst-case
    cost before each call and refusing to spend if it can no longer record usage.
    Enabling the module without a key is harmless — the ask box degrades to a
    structured fallback and curated page help still works. Members are told the
    answer may be wrong and that their question is sent to Anthropic (United
    States); the question text itself is never stored.

Backup configuration re-homed to the encrypted store (#2095)

  • Backup settings move into the encrypted credential store and out of the
    environment.
    The S3 access key and secret, destination bucket and region,
    retention window, restore-validation shadow database, and the on/off switch are
    managed from a new Admin → Integrations → Database Backups page
    (/admin/backups), with a Run backup now action that runs as a background
    job behind a database-level cross-process lock. The S3 secret and the
    restore-validation connection string are write-only and never echoed back.
  • The legacy BACKUP_* environment variables are no longer read. An install
    that configured backups through the old env vars upgrades to an empty store, so
    until the operator re-enters the backup settings the nightly backup reports a
    loud FAILURE rather than silently skipping
    — a stopped disaster-recovery path
    must alert, not disappear. Only BACKUP_CRON_SCHEDULE (cron-leader timing —
    when the nightly job fires) stays in the environment.

Hardening and fixes

  • Config-transfer column audit (#2178). A reverse-drift guard fails the build
    if a club-settings column is neither exported nor explicitly excluded,
    complementing the singleton-table audit (#2200).
  • Season-subscription charge-coverage reconcile (#2179). The guard that flips
    a stale NOT_REQUIRED seed subscription row now checks for an active
    charge-coverage claim (both at read and, relationally, at write) instead of a
    null-check on a to-many relation that was always true, so the reconcile stands
    aside when live coverage exists and proceeds when it does not.
  • Dependency advisories (#2196, #2224). Cleared newly published advisories in
    hono, fast-uri, dompurify, and sharp, and moved Next.js to 16.2.11 to
    clear a middleware-bypass and a Server-Actions denial-of-service advisory.

Compatibility and operator impact

  • Runtime requirements remain Node.js 24 LTS, npm 11+, and PostgreSQL 16.
  • The release contains six migrations: five additive/expand migrations plus one
    destructive contract migration.
    • Expand (no operator action):
      20260722120000_add_integration_wizard_progress (the wizard cursor for the
      guided setup shell, #2080), 20260722130000_add_xero_webhook_validation_receipt
      (the Xero webhook validation receipt sink, #2081),
      20260722140000_expand_club_theme_orphan_column_defaults (the theme
      orphan-column defaults, #2187), 20260722150000_add_backup_run (the backup-run
      ledger, #2095), and 20260723120000_add_ai_assistant (the AI assistant models,
      #2211). Each is additive and blue/green safe.
    • Contract (needs handling):
      20260722160000_contract_drop_club_theme_orphan_columns drops the four dead
      ClubTheme columns. The blue/green validator refuses it without
      ALLOW_BREAKING_BLUE_GREEN_MIGRATIONS=1 plus a non-empty
      BLUE_GREEN_MIGRATION_OVERRIDE_REASON. Because the paired expand migration
      ships in this same tag, the contract drop is legal only after the
      v0.13.2 runtime has replaced and drained v0.13.1
      . Either defer it
      mark it applied without running it (prisma migrate resolve --applied 20260722160000_contract_drop_club_theme_orphan_columns) until the expand-
      carrying...
Read more

v0.13.1

Choose a tag to compare

@thatskiff33 thatskiff33 released this 21 Jul 20:23
321f4ad

AlpineClubBookingsNZ v0.13.1

Release target: v0.13.1 on 2026-07-22. This patch public reference release
brings all changes landed after v0.13.0 into one supported tag. It carries two
lead items, both requiring operator action:

  1. Release B of the #2129/#2130 contract series — two destructive contract
    migrations that finish the expand/migrate/contract work E4 (#1930) and E8
    (#1934) began and that the v0.13.0 runtime-prep (Release A) made
    blue/green-legal. They make this release breaking: unlike v0.13.0 (whose
    four migrations were all expand / metadata-only), both drop schema and are
    irreversible — the deploy requires ALLOW_BREAKING_BLUE_GREEN_MIGRATIONS=1
    and a non-empty BLUE_GREEN_MIGRATION_OVERRIDE_REASON, and it is legal only
    on top of a deployed, drained, soaked v0.13.0
    .
  2. Encrypted DB-only provider credentials (#2079) — a Critical security/ops
    change.
    Xero credential resolution is hard-cut from env XERO_* to an
    encrypted IntegrationCredential store. On upgrade, an existing
    Xero-connected install enters a documented "needs re-entry" state and Xero work
    pauses
    until a Full Admin re-enters credentials in-app and reconnects OAuth.
    Its migration is additive/expand, so the release ships two contract
    migrations plus one expand migration
    .

The canonical detailed entries remain in CHANGELOG.md; this page is the
release/operator overview.

⚠️ Do not deploy this release until v0.13.0 (Release A) has been the live,
drained production colour long enough to soak.
These migrations drop a table
and three columns that the v0.12.2 colour still named in its SQL. Shipped
against a draining v0.12.2, they cause anonymous public 500s on every page
carrying {{hut-fees}}
(42P01 relation "SeasonRate" does not exist), admin
seasons pages 500, Xero item-code saves 500 (42703 column "isMember" does not exist), and age-tier saves plus the boot-time config self-heal failing on
every blue container start — none of it recoverable by rolling the app back.
The v0.13.0 colour names none of them, which is what makes this drop legal
now. If the live colour is v0.12.2 or earlier, stop.

Highlights

Release B — legacy contract drops (#2129 step 2, #2130 STEP 2)

  • 20260721120000_contract_drop_season_rateDROP TABLE "SeasonRate". The
    frozen member/non-member boolean-keyed nightly-rate table is dropped. Its rows
    were copied forward into MembershipTypeSeasonRate by the E4 re-key
    (20260717140000_pricing_rekey_by_membership_type), and nightly pricing, Xero
    hut-fee item codes, and the public {{hut-fees}} embed have all read the new
    table since #2129 step 1, so nothing user-visible changes. Release A removed the
    last application-runtime reader (the {{hut-fees}} embed); this PR removes the
    only survivors, which were seed-only and outside src/ (the include: { rates: true } read and rates: { create: … } write in
    e2e/setup/seed-second-lodge.ts, and createMissingSeasonRates plus its two
    call sites in prisma/seed.ts). Because the E4 fan-out that copied those rows
    forward was conditional on the install having a MEMBER_RATE-behaviour
    membership type and a NON_MEMBER-keyed type, the migration opens with a
    pre-drop coverage guard: a read-only anti-join that counts SeasonRate rows
    with no MembershipTypeSeasonRate counterpart for the same season and age tier
    and raises, aborting the transaction before the DROP TABLE, if any exist. A
    fork whose types never matched keeps its only copy of that pricing instead of
    losing it. docs/UPGRADING.md publishes the same check as a read-only operator
    pre-flight query — run it before you start.
  • 20260721130000_contract_drop_ismember_and_agetier_xero_columns. Deletes
    the orphaned legacy HUT_FEE item-code rows that carry no membershipTypeId
    (not resolvable for pricing by the current runtime — production held 16; the
    only paths that still touch them count or collect item codes in aggregate and
    name no dropped column), drops the old (category, ageTier, seasonType, isMember) unique index, drops XeroItemCodeMapping.isMember, and drops
    AgeTierSetting.xeroContactGroupId / xeroContactGroupName (their data moved
    into XeroContactGroupRule at E8, 20260716140000_xero_member_grouping). The
    still-live partial index XeroItemCodeMapping_hutfee_flat_unique is untouched.
    This is legal only because v0.12.2 narrowed the reads (#2133 STEP 1) and
    Release A narrowed the writes (#2130 STEP 1.5) on both models, so the draining
    colour names none of these columns in a SELECT or an implicit RETURNING.

Both migrations carry full rationale rows in
docs/BLUE_GREEN_MIGRATION_SAFETY.tsv
(previous_expand_release = 20260717140000_pricing_rekey_by_membership_type,
old_code_compatible = yes), and both refuse to run without the breaking-
migration acknowledgement. The #2130 select guard
(doomed-column-select-guard.test.ts) is kept even though its original columns
are gone — narrow selects remain the rule for both models.

Admin theming

  • The admin area now follows the club's saved site colours in light mode
    (#2144).
    A sweep of 1,410 class occurrences across the admin tree moved
    hard-coded light-grey ("slate") Tailwind colours onto the same semantic theme
    tokens the finance dashboard has used since #2137, so a club with a strongly
    non-default palette now sees it applied consistently across admin. Dark mode
    is visually unchanged for ~98% of occurrences
    (90.6% land on the exact token
    the existing .dark neutral remap already assigned — a provable dark-mode
    no-op — and a further 7.3% land on a token that renders identically today);
    ~2% genuinely change dark rendering, chiefly small deliberate dark-mode fixes on
    admin surfaces the remap never covered plus four occurrences on the public
    hut-leader instructions page (which renders under website-theme). In light
    mode there are two deliberate changes: the five grey text tints collapse onto
    the single AA-clamped text-muted-foreground tone (an accessibility
    improvement), and recessed panels use bg-muted while cards use bg-card
    (the finance precedent) so insets stay visibly recessed. A widened
    source-contract test now gates the whole admin tree (plus finance) against raw
    neutral classes, with a nine-entry per-file allowlist covering the deliberate
    literal-colour exclusions (print pages, signage letterboxes, code-preview panes,
    status chips, the member-import active-step border).

Encrypted DB-only provider credentials (#2079)

  • New encrypted credential store. The additive migration
    20260721210000_add_integration_credential creates one standalone
    IntegrationCredential table holding AES-256-GCM ciphertext under a key derived
    by real HKDF-SHA256 from the canonical getAuthSecret() resolver (fixed
    documented salt, versioned info labels), with a fresh random IV per encrypt and
    GCM AAD (provider:key:labelVersion) binding every ciphertext to its row. A
    secretSource field makes a silent AUTH_SECRETNEXTAUTH_SECRET flip
    diagnosable (still decrypts, flagged), while a changed secret value fails
    cleanly into "needs re-entry" rather than crashing.
  • Hard cutover of Xero resolution to the store.
    getOperationalXeroConfig(), getOperationalXeroEncryptionKey(), and the new
    getOperationalXeroWebhookKey() resolve from the store; env XERO_CLIENT_ID /
    XERO_CLIENT_SECRET / XERO_REDIRECT_URI / XERO_ENCRYPTION_KEY /
    XERO_WEBHOOK_KEY are no longer read for operation — legacy values are
    detected and flagged for removal in setup readiness, never silently honoured and
    never silently ignored. The webhook route stays fail-closed (missing/unreadable
    key rejects every delivery), and the OAuth redirect URI now derives from
    NEXTAUTH_URL.
  • Capture, cache, and gating. A 45 s cross-process credential cache
    (invalidated on write; negatives and DB errors never cached beyond the TTL) lets
    the cron-leader observe a web-slot write without a restart. An AUTH_SECRET
    strength gate
    hard-blocks credential capture on a weak secret (< 32 chars, or a
    placeholder — a blocklist that catches the 41-char .env.example literal),
    shows a passive amber readiness warning from day one, and imposes no boot-time
    enforcement
    . Writes go through a write-only, Full-Admin-only API (values never
    returned, metadata-only audit; area admins keep a metadata-only status GET), with
    an interim Xero Credentials entry section on /admin/xero/setup.
    Verify-reset: a client-credential write drops stored OAuth tokens; a
    webhook-key write re-arms verification without dropping tokens.
  • Truthful reconnect after upgrade. Tokens previously wrapped under the dropped
    XERO_ENCRYPTION_KEY become unreadable by design (no silent key import); a typed
    XeroTokenDecryptError maps to the reconnect state so the admin status panel,
    setup readiness, and finance-report messaging show "reconnect Xero" instead of a
    false "Connected". The credential entity is excluded from configuration export,
    the blue/green deploy script no longer hard-requires the dropped XERO_* vars
    (warn-only), docker-compose no longer plumbs them, and the .env examples were
    rewritten.

Compatibility and operator impact

  • Runtime requirements remain Node.js 24 LTS, npm 11+, and PostgreSQL 16.
  • The release contains three migrations: two destructive contract migrations
    (Release B) plus one additive/expand migration (#2079).
    The two Release B
    contract migrations are both old_code_compatible=yes and ledgered with
    previous_expand_release = 20260717140000_pricing_rekey_by_membership_type;
    20260721210000_add_integration_credential is a purely additive standalone
    table (no ledger row required — it matches no hot-table or breaking rege...
Read more

v0.13.0

Choose a tag to compare

@thatskiff33 thatskiff33 released this 21 Jul 12:32
0e83763

AlpineClubBookingsNZ v0.13.0

Release target: v0.13.0 on 2026-07-21. This minor public reference release
brings all changes landed after v0.12.2 into one supported tag. It touches
subscription billing
— the annual-subscription billing epic (#2151) changes how
the annual membership fee is deduped, voided, re-billed, and suppressed, and it
drops the old role-based subscription exemption in favour of DB-backed membership
types. An operator reading only this page must know the money paths change
this release; read the 0.13.0 changelog section and docs/UPGRADING.md before
deploying.

Unlike v0.12.2 (which carried two breaking contract migrations), this release
is not breaking: its four migrations are all expand / metadata-only and all
old_code_compatible=yes
, so the blue/green validator passes with no
ALLOW_BREAKING_BLUE_GREEN_MIGRATIONS override
and a normal deploy window is
enough. The canonical detailed entries remain in CHANGELOG.md; this page is the
release/operator overview.

Highlights

Annual-subscription billing epic (#2151)

  • Double-billing fix with void / re-bill (#2147). The billing preview and the
    in-transaction confirm now treat a season MemberSubscription as already billed
    when its status = PAID or it carries a non-null xeroInvoiceId
    (manually-marked-paid members with no invoice stay skipped), and coverage-based
    dedup counts only active (unreleased) claims. When the Xero sync observes a
    charge's invoice VOIDED/DELETED it atomically releases the coverage claim
    (releasedAt set, row kept for audit), marks the charge VOIDED (row kept),
    bumps MemberSubscription.voidGeneration, and clears the subscription's invoice
    link to NOT_INVOICED — the member becomes re-billable, and a post-void confirm
    derives a new idempotency key that folds in voidGeneration (byte-identical
    for never-voided subscriptions). A collapsed-by-default "Already invoiced"
    section in the preview shows the count and each suppressed member's Xero invoice
    number and status. Documented semantics change: a voided invoice previously read
    as UNPAID (booking lockout) and now reads as NOT_INVOICED (re-billable).
  • Billing-exception resolution provenance (#2148). A new nullable
    MembershipBillingException.resolvedVia column, backed by a
    MembershipBillingExceptionResolution enum (CONFIRM | PREVIEW_RECONCILE),
    records how an exception reached RESOLVED so a preview-reconciliation
    resolution is distinguishable from a confirm-run one. Existing and legacy
    resolved rows, and every open row, stay NULL — the "resolved before this
    column existed / not yet resolved" state.
  • Membership type is the sole subscription authority (#2149) — behaviour
    change.
    The role-based subscription exemption is removed from every
    derivation: membership type (subscriptionBehavior, plus age tier where the
    type is BASED_ON_AGE_TIER) is now the only authority on whether a member owes
    a subscription, and the login Role enum is a pure permission concept again. A
    fee-paying member who happens to hold role=ADMIN now shows their real
    subscription status everywhere. Five divergent derivations (booking gate,
    profile/subscription-status API, admin members list and its SQL filter variants,
    subscriptions list, CSV export, and the Xero-sync status check) are consolidated
    onto shared primitives. A data-only migration seeds two built-in types so the
    dropped exemption keeps a DB-backed NOT_REQUIRED fallback: ADMIN
    (NOT_REQUIRED, BLOCK_BOOKING) and LODGE (NOT_REQUIRED, MEMBER_RATE,
    so kiosk accounts still book on behalf of members).
  • Operator "already invoiced" family marker and refined family suppression
    (#2161 / #2167).
    For PER_FAMILY billing, a live legacy invoice suppresses a
    family's charge only when its holder's own resolved billing basis is
    PER_FAMILY; a PER_MEMBER-billed member's personal invoice no longer blocks
    the family fee (that member stays skipped per-member). To close the ambiguous
    legacy shapes that refinement re-opens, a new FamilyGroupSeasonInvoiceMarker
    table lets an operator mark a family "already invoiced": finance:edit-gated
    MARK/UNMARK actions on the billing route, an active marker suppressing the family
    regardless of basis, unmark releasing the row (releasedAt) while keeping it for
    audit, and a partial unique index enforcing one active marker per
    (familyGroupId, seasonYear). Suppression is fail-closed: it lifts only on a
    proven PER_MEMBER holder basis, with unresolvable bases carrying an
    "Unresolved basis" badge in the audit panel.

Xero and finance surfaces

  • VOIDED charges are fenced everywhere they could otherwise re-enqueue or
    re-invoice — enqueue/RETRY_CHARGE, invoice creation, the outbox failure handler,
    and the panel (no Retry button) — so a voided-then-retained charge cannot cause a
    second Xero write.
  • Four dependency security advisories cleared (#2173). npm audit began
    flagging one moderate and three high-severity advisories against two transitive
    packages (an axios/brace-expansion denial-of-service and prototype-pollution
    set), turning the required verify job red on main and every PR. The exact
    overrides pins were bumped (axios 1.16.11.18.1, brace-expansion
    5.0.65.0.7); xero-node still resolves ^1.7.7 and did not move, and no
    application code changed. The hardened axios form-serialization and body-limit
    paths are not on the Xero call path.

Admin UX

  • View-only banner rollout (#2142#2160#2168). The per-button "you cannot
    change this" explanation that reached almost nobody (a greyed-out button is
    skipped by the keyboard and shows no hover tooltip) is replaced by a short
    section-level banner shown once at the top of a read-only area — the pattern
    Booking Policies adopted in #2142, now rolled across most of the admin tree
    (#2160) and the member detail page as one page-level banner (#2168). About seven
    of every eight gated buttons are now explained by a banner instead of
    individually. Nothing about who can do what changed — every button is gated
    exactly as before.
  • Shared useSectionEditState hook (#2136). The canonical admin
    settings-section pattern (load read-only, per-section Edit reveals Save/Cancel,
    nothing auto-persists on toggle, Cancel reverts to the saved snapshot, Save
    persists once) is centralised into one hook, so Cancel restores every field and
    Save re-seeds both draft and snapshot from the save callback's return. Refactor
    only, adopted by the group discount, password policy, email sign-in link, and
    Google sign-in sections.
  • Quote-timing cards open with Edit (#2166), and the indicative-pricing toggle
    is staged behind Save (#2162).
    On Booking Policies → Public Booking Requests,
    the Quote Response Window / School Attendee Confirmation cards and the "Show
    indicative pricing" checkbox no longer write on the first keystroke or click —
    each now has its own Edit → change → Save → Cancel cycle, matching the rest of
    the area, and each save re-reads the stored settings and sends back only the
    fields it owns so it cannot clobber another admin's concurrent change.

Theming and print

  • Categorical teal on tokens, and a themed /finance body (#2137). The last
    literal-Tailwind categorical teal (waitlist-offered chip, audit-log family
    badge, family-group GROUP_CREATE badge) now reaches its hue through the
    --hue-* tokens (value-identical), the audit category badge map is de-duplicated
    into one shared module, and the /finance dashboard body now uses semantic
    surface tokens so it follows a strongly non-default club theme in light mode as
    well as dark.
  • Print / PDF dark mode no longer produces a blank page (#2146). Downloading a
    PDF or printing /finance or /admin/reports while browsing in dark mode used
    to render near-white ink on the forced-white print background. Print and PDF now
    always render the light colour scheme regardless of the browsing theme, across
    every printable surface (reports, chore roster, induction sign-off, lodge
    instructions).
  • Secondary text actually looks secondary (#2145), and bed-allocation labels
    no longer clip (#2150):
    the muted-foreground token is corrected so secondary
    text reads as secondary (falling back to identical-to-normal only where contrast
    requires it), and the bed-allocation board's label column gets its own 14rem
    width with two-line wrapping and a title fallback for long bed names.

Config transfer and hut fees

  • Config-transfer legacy-bundle compatibility window closed (#2131). One
    release after the E13 contraction, the importer rejects the legacy boolean-keyed
    bundle shapes (isMember columns, the pre-#1931 ENTRANCE_FEE category) at
    dry-run with a clear row-named error, and a HUT_FEE row with a blank
    membershipTypeKey is now a blocking error. v0.12.2 was the last release that
    could import the legacy shape
    — re-export archived bundles before upgrading
    (see docs/UPGRADING.md).
  • Public {{hut-fees}} embed re-sourced onto the authoritative rates (#2129,
    step 1).
    The embed was the last application-runtime reader of the frozen
    member/non-member SeasonRate table; it now reads MembershipTypeSeasonRate
    the same rows that price a booking — and renders a real per-lodge × season table
    with one column per active, publicly listed membership type. No schema change
    this release; SeasonRate still has one reader and two writers in seed code
    outside src/, so its DROP is deferred to a later release.
  • Blue/green runtime-prep for the legacy Xero column drops (#2130). This is
    step-1 prep only, with no schema change and no migration: every mutation on
    XeroItemCodeMapping and AgeTierSetting is narrowed with an explicit select
    (com...
Read more

v0.12.2

Choose a tag to compare

@thatskiff33 thatskiff33 released this 20 Jul 06:48
ff3ba8e

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 JS Date, so parseXeroLockDate's value.slice(0,10) threw
    and the guard failed closed; reconnecting could never fix it. The parser now
    accepts string | Date.
  • Xero reliability follow-ups. A lock-date error taxonomy classifies the
    guard's 503 as reconnect_required | rate_limited | transient with
    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 now Date-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 scalar ageTier column (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.postLoginLanding enum 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
    /finance and /lodge shells 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
    and AgeTierXeroAcceptedContactGroup tables and an orphaned account-mapping
    row — the blue/green-safe subset of #1939, re-verified as zero-reader against
    the v0.12.1 tag; the remainder is deferred to #2129/#2130/#2131. The #2130
    runtime-prep also ships here (#2133): explicit selects 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
    contract migrations.
    Both breaking migrations require the operator
    acknowledgement ALLOW_BREAKING_BLUE_GREEN_MIGRATIONS=1 for this deploy — the
    blue/green validator refuses a breaking migration without it. All four carry
    old_code_compatible=yes where they have a ledger row.
    • 20260719170000_xero_grouping_age_tiers_multiselect (breaking contract,
      ledgered
      ) backfills the scalar XeroContactGroupRule.ageTier into a new
      ageTiers array (X → [X], null → [] = "all tiers") and then drops the
      scalar column
      . old_code_compatible=yes but window-bounded and
      admin-only
      : between migrate and cutover the old colour's
      grouping/membership-admin reads still name ageTier and 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
      (destructive contract, ledgered) drops the dead EntranceFee and
      AgeTierXeroAcceptedContactGroup tables and deletes the orphaned
      entranceFeeAmountCents account-mapping row. old_code_compatible=yes: an
      independent drop-proof review re-verified zero readers against the
      v0.12.1 tag
      (the colour draining during this deploy) with no FK/cascade
      trap, so the draining old colour keeps working. The acknowledgement is
      required only because a DROP is breaking by class. Deliberately
      kept/deferred (still read by the current runtime): the EntranceFeeCategory
      enum, SeasonRate (the live public {{hut-fees}} embed), MembershipTypeAgeTier,
      and the XeroItemCodeMapping.isMember / AgeTierSetting.xeroContactGroup*
      columns — follow-ups #2129/#2130/#2131.
    • 20260719150000_add_post_login_landing (expand, ledgered) adds a
      PostLoginLanding enum plus a nullable Member.postLoginLanding column with
      no default — a metadata-only catalog change even on the hot Member table.
      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
      MembershipLockoutSettings table — 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:

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. *...
Read more

AlpineClubBookingsNZ v0.12.1

Choose a tag to compare

@thatskiff33 thatskiff33 released this 19 Jul 07:44
873f9a9

AlpineClubBookingsNZ v0.12.1

Release target: v0.12.1 on 2026-07-19. This patch public reference release
brings all changes landed after v0.12.0 into one supported tag. The version
is a deliberate patch bump chosen by the owner even though the range carries
feature work, because every addition is additive and flagged off by default —
nothing changes for an existing club until an admin opts in. The canonical
detailed entries remain in CHANGELOG.md; this page is the release/operator
overview.

Highlights

  • Optional sign-in methods behind a new admin Login & Security page
    (/admin/security): a per-club password-complexity policy (minimum length
    8–64, default 12; four character-class toggles, default off; fixed 128
    maximum) enforced only at password-set time, plus two module-flagged
    sign-in additions that never replace password login — email magic-link
    (single-use hashed token, club-set TTL) and Google OAuth (profile-initiated
    account linking only, resolving by pinned Google subject id, never by email
    match or auto-provisioning). Both new modules default off; an
    un-configured club is byte-identical to today.
  • Per-age-tier membership billing: membership types gain a Required based on
    age tier
    subscription behaviour that defers to the existing per-tier
    subscriptionRequiredForBooking flag (billing liability fixed by age at the
    start of the financial year), and annual membership fees gain the same
    flat vs per-tier shape the joining fee already carried (existing rows are
    the flat fallback; per-family fees stay flat-only). The membership-type
    editor adds an explicit "N/A (no age)" allowed-tier option.
  • Fees editor UX: the annual-fee editor replaces free-text Xero Account/Item
    inputs with searchable pickers (ACTIVE revenue accounts / sales-capable
    items) fed by the existing admin-gated proxy endpoints, falling back to
    manual entry on Xero disconnection, surfacing the resolved default account,
    and showing the fee-level proration rule — with no billing-math change.
  • Lobby Display polish: a six-board template pack (four built-ins + a
    two-board import bundle) so every display module is exercised, a guided
    visual builder at /admin/display/builder (ADR-004) over the unchanged
    save contract with the textarea editors retained as Advanced mode and no
    schema change, and night-columns rescoped as an honest permanent 3-night
    board. The Lobby Display module remains off by default.
  • A full documentation library: a docs foundation (style guide, audience-first
    hub, curated architecture diagrams, a coverage matrix, an advisory link-check
    CI workflow, and a screenshot harness), 65 operator guides in
    docs/guides/ across four batches, and a fifth batch of member-facing
    journey guides
    in docs/user-guide/, each written against the running
    seeded app — closing the coverage matrix to zero gaps.
  • A screenshot-forward README.md marketing front page with reproducible hero
    art and a booking-demo GIF harness, its former deep operational content
    relocated into the docs it duplicated. No runtime app code changes.
  • Fixes and hardening: membership-types editor close-on-save + dirty-guarded
    header X, /admin/display drill-down back-links (with a repo-wide
    normalisation), a dollar-quote-aware fix to the blue/green migration
    validator's session-clock gate, and a github/codeql-action bump to 4.37.0.

Compatibility and operator impact

  • Runtime requirements remain Node.js 24 LTS, npm 11+, and PostgreSQL 16.
  • The release contains five migrations: all expand/additive, no contract
    migration
    . Four have explicit safety-ledger rows in
    docs/BLUE_GREEN_MIGRATION_SAFETY.tsv, all old_code_compatible=yes:
    • 20260718130000_add_magic_link adds ClubModuleSettings.magicLink
      (flagged off, constant default) and a new empty MagicLinkToken table
      with an ON DELETE CASCADE FK to Member. The ADD COLUMN is catalog-only
      on the cold single-row settings table; the FK on a brand-new empty child
      table scans nothing. Additive; safe at any traffic level.
    • 20260719120000_add_google_oauth adds ClubModuleSettings.googleLogin
      (flagged off, constant default) and a nullable Member.googleSub with a
      unique index. Both ADD COLUMNs are catalog-only (the googleSub column
      takes no default, so even on the hot Member table it is metadata-only);
      the unique index builds over an all-NULL column (NULLs never collide) and
      briefly blocks Member writes — switch to CREATE UNIQUE INDEX CONCURRENTLY if Member is very large. No backfill, no provider call.
    • 20260719130000_add_based_on_age_tier_subscription_behavior registers the
      BASED_ON_AGE_TIER value on the MembershipTypeSubscriptionBehavior enum
      (ALTER TYPE ... ADD VALUE IF NOT EXISTS). Purely additive; no existing
      row changes and no old colour reads the new label before cutover.
    • 20260719140000_annual_fee_age_tier adds a nullable MembershipAnnualFee.ageTier
      (no default, no backfill — every existing row stays the flat NULL-tier
      row and resolves identically) plus index/constraint reshaping on the cold
      MembershipAnnualFee config table: the uniqueness re-keys from
      (membershipTypeId, effectiveFrom) to (membershipTypeId, ageTier, effectiveFrom) with a partial unique flat index, the effective-lookup
      index widens to include ageTier, and the single per-type GiST no-overlap
      EXCLUDE is split into two partial constraints (flat-vs-flat and same-tier
      overlaps still conflict; flat-vs-tier and cross-tier coexist). A raw-SQL
      CHECK forbids per-tier PER_FAMILY rows. Catalog-only ADD COLUMN plus
      brief-lock index/EXCLUDE swaps on a cold table; no DML.
  • The fifth migration, 20260718120000_add_login_security_setting, creates the
    empty single-row LoginSecuritySetting config table (password policy plus the
    magic-link TTL) with no FK and no hot-table or breaking SQL, so it needs no
    ledger row — the same policy under which several v0.12.0 additive migrations
    carried no row. An absent row falls through to the code defaults (minimum
    length 12, classes off, TTL 15), so an un-configured club behaves exactly as
    before.
  • One old-colour operator caveat. Because the old colour's annual-fee
    resolver does not filter by age tier, a per-age-tier annual-fee row is not
    invisible to it — once such a row falls in an active window the old colour can
    select it for a member of any tier (first match by effectiveFrom desc) and
    mis-price them at the wrong tier's amount. Do not create per-age-tier
    annual-fee rows until the cutover completes.
    Flat annual fees and existing
    rows are unaffected; per-tier joining fees already shipped in v0.12.0.
  • The two new sign-in modules default off, so magic-link and Google sign-in
    stay disabled through the migrate→cutover window until an admin enables the
    module (and, for Google, a member links their account). The
    password-complexity policy applies only at password-set time and never
    re-validates an existing password.
  • No migration makes a Xero, Stripe, or SES call, and no member is re-grouped
    in Xero. The annual-fee Xero pickers read only the existing admin-gated
    proxy endpoints server-side; no Xero secret reaches the browser bundle.

Upgrade and verification

Follow docs/UPGRADING.md from v0.12.0 to v0.12.1. In summary:
restore-test a fresh backup, review the five pending migrations against the
safety ledger, deploy in a normal window (no contract window is required this
release), and — critically — do not author any per-age-tier annual-fee rows
until cutover completes.

After cutover, verify at minimum:

  1. Login & Security — the admin security page shows the intended password
    policy (an un-configured club still accepts and rejects passwords exactly as
    before), and existing members can still sign in with their password.
  2. Optional sign-in modules — magic-link and Google OAuth remain disabled
    under Admin > Modules unless the club intends to use them; if enabling
    Google, per-club GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET are configured
    per CONFIGURATION.md and a member can link and then sign in.
  3. Per-age-tier billing — for any membership type set to Required based on
    age tier
    , the age-tier settings drive which tiers are billed, and exempt
    tiers get no subscription invoice; REQUIRED/NOT_REQUIRED types are
    unchanged.
  4. Annual fees — admin fee configuration and the public annual-fees embed
    show the same amounts as before; per-tier annual-fee rows are added only
    after cutover.
  5. Fees editor — the annual-fee Account/Item pickers list the expected Xero
    codes (or fall back to manual entry with the amber notice if Xero is
    disconnected), and the surfaced default account is correct.
  6. Lobby Display — the module stays off unless intended; if enabled, the
    template pack and the guided builder at /admin/display/builder work, and
    guest phones stay hidden unless both the member and the lodge opt in.
  7. Docs and README — the operator/member guides and the new README render
    correctly; these are documentation-only and carry no runtime risk.

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.

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...
Read more

AlpineClubBookingsNZ v0.11.0

Choose a tag to compare

@thatskiff33 thatskiff33 released this 12 Jul 22:50
6161e6a

Release target: v0.11.0 on 2026-07-13. This minor public reference release
brings all changes landed after v0.10.1 into one supported tag. The canonical
detailed entries remain in CHANGELOG.md; this page is the release/operator
overview.

Highlights

  • First-class multi-lodge scoping across bookings, capacity, rooms/beds,
    seasons/rates, instructions, member access, requests, waitlist, roster, kiosk,
    hut leaders, lockers, promos, work parties, and operational reporting.
  • Portable Configuration Export & Import with dry-run, explicit matching,
    category selection, merge/overwrite modes, bundle integrity warnings, and an
    automatic pre-apply backup attempt when backups are enabled. Operators must
    independently ensure a current, restore-tested database backup exists before
    applying an import; Configuration Export is not a database backup.
  • Declared partner relationships, admin partner management, partner invitation
    claim/cancellation, shareable-double placement, and capacity headroom bounded
    by both double-bed inventory and the lodge sleeping ceiling.
  • Admin recovery workflows for retroactive bookings, date overrides, explicit
    over-capacity admission, capacity holds, finished-stay restrictions, and
    per-action member-notification choices.
  • Safer bed allocation for large/minor groups, whole-stay moves, draft
    preservation, room continuity, clearer booking identity, and conflict
    feedback.
  • Finance and credit hardening for Internet Banking credit-note allocation,
    cancellation/refund restoration deduplication, Xero lock-date enforcement,
    and scoped reconciliation.
  • Editable access-role definitions, permission-aware setup navigation,
    committee contact routing, membership-type retirement/merge, and improved
    admin queue/filter/pagination visibility.
  • Restrained Alpine design system across public utility, member, and admin
    surfaces: editable brand tokens, semantic accessible states, responsive
    tables/forms, consistent alerts/status/occupancy, reduced motion, skip links,
    dark mode, and stronger empty/loading/filter/calendar patterns.
  • Eleven additional operational email templates are admin-editable while
    remaining always-send; admin-initiated member email suppression is explicit
    and audited where suppression is safe.
  • CI now treats the multi-lodge E2E suite and static/dead-code analysis as
    blocking release safeguards.

Compatibility and operator impact

  • Runtime requirements remain Node.js 24 LTS, npm 11+, and PostgreSQL 16.
  • The release contains 30 migrations. Twenty-six have explicit safety-ledger
    rows because they touch hot tables, contain contract/data operations, or need
    a documented deployment plan. The four without rows were checked directly
    against the validator and ledger policy and do not require rows:
    • 20260707115737_committee_contact_email_mode adds a new enum plus two
      columns to the cold, admin-written CommitteeAssignment table. The
      constant ROLE default preserves every old-colour insert/read assumption;
      there is no backfill, destructive SQL, or hot-table match.
    • 20260709120000_add_lodge_is_default adds a constant-default flag to the
      tiny Lodge settings table, backfills one row, adds a partial unique index,
      and replaces default_lodge_id() with an old-behaviour-compatible resolver.
      Its migration comments record the ordering dependency on the preceding
      timestamp repair; it contains no breaking/hot-table match.
    • 20260710120000_add_lodge_bed_type adds a new enum and constant-default
      bedType plus nullable bunkGroup to the cold lodge-bed inventory. Old
      colours see every existing/new omitted bed type as SINGLE; no row rewrite
      or destructive operation is involved.
    • 20260710140000_add_bed_allocation_second_occupant relaxes the bed-night
      uniqueness on the non-hot BedAllocation table, adds constant-default
      columns, backfills the denormalised type, and installs replacement/partial
      unique indexes. Old-colour inserts remain primary SINGLE occupants and
      therefore retain the former one-row-per-bed-night behaviour. Run during
      low bed-allocation admin activity for the index swap/backfill, but no
      breaking-migration override or ledger row is required.
  • The contract migrations that remove induction, finance-report, and legacy
    email-lodge fields require the quiet/drained old-colour window described in
    docs/UPGRADING.md and docs/BLUE_GREEN_MIGRATION_SAFETY.tsv.
  • The maximum sleeping-capacity fix can reduce bookable capacity when a lodge
    has more active beds than its configured LodgeSettings.capacity. Audit it
    before deployment with docs/CAPACITY_MODEL.md.
  • Configuration Export & Import is for configuration portability, not disaster
    recovery. Continue to use and restore-test pg_dump backups.

Upgrade and verification

Follow docs/UPGRADING.md from v0.10.1 to v0.11.0. In summary: restore-test
a fresh backup, review every pending migration against the safety ledger, audit
capacity and default-lodge intent, deploy in a quiet low-write window, cut over
promptly through the documented override path, then verify lodge scoping,
capacity, module/member access, booking/bed-allocation operational views,
finance/Xero reads, email lodge identity, and the application theme.

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.

Publication evidence

  • Release commit: 6161e6a5824dd03626edbf41d6f3924ae030e2df
  • Release PR: #1854 (merge commit; all required checks green)
  • Main CI: run 29211672529 — verify, migration drift, static analysis, secret scanning, Docker image security, and GHCR publish passed
  • Main E2E: run 29211672504 — Playwright E2E and E2E multi-lodge passed
  • Main CodeQL: run 29211672220 — passed
  • App image: ghcr.io/thatskiff33/alpineclubbookingsnz-app:6161e6a5824dd03626edbf41d6f3924ae030e2df
    • OCI index digest: sha256:9fcc4659c5a3c794e067e1dc4b59a749aa6d65fb5f4bda4a9abf6f5d4a5887bb
  • Migrate image: ghcr.io/thatskiff33/alpineclubbookingsnz-migrate:6161e6a5824dd03626edbf41d6f3924ae030e2df
    • OCI index digest: sha256:73528bac0698394aed71c1f8907cb49a005de27957ad35ae6d665c465111b358

Maintainability follow-up

  • No release-blocking follow-up remains. Continue tracking the non-blocking hotspots documented in docs/MAINTENANCE.md.

This release does not publish an npm package because package.json remains private.

AlpineClubBookingsNZ v0.10.1

Choose a tag to compare

@thatskiff33 thatskiff33 released this 07 Jul 06:51
331cb7e

Tag: v0.10.1 · Previous: v0.10.0 (2026-07-07) · Merge commit: 331cb7e27.

What this release is

A patch release: four payment/booking-recovery hardening fixes and one operator
cleanup script on top of v0.10.0. No new features, and no behaviour changes
outside the raced/edge shapes the fixes close.

No database migrations, no schema changes, no post-upgrade actions.
docs/BLUE_GREEN_MIGRATION_SAFETY.tsv gains no rows and either app color can
serve throughout the deploy. The standard procedure still applies: back up the
database before deploying (docs/UPGRADING.md, "v0.10.0 → v0.10.1").

Changes

  • Quote re-send vs concurrent decline race closed — an admin re-sending a
    booking-request quote while the member declines can no longer resurrect a
    DECLINED/CANCELLED request or send its quote email; the losing re-send
    rolls back with a 409. (#1508, issue #1504)
  • Recovery replays converged with their inline Stripe bodies — the
    refund-request and booking-modification recovery replays previously sent a
    different reason under the same idempotency key, so Stripe rejected the
    replay with idempotency_error and recovery retried to exhaustion instead of
    converging (safe-failing; never a double refund). Shared per-path body
    builders now guarantee byte-identical replays. (#1509, issue #1507)
  • Refund-appeal allocation plan frozen — the approve route computes the
    per-transaction Stripe refund allocation once, uses it inline, and persists
    the same slices to the recovery operation on inline failure, so a replay
    re-requests exactly the original slices under the original idempotency keys.
    Supersedes the completed-refund remainder heuristic; in the exotic mixed
    Stripe + Internet-Banking appeal shape the route now refunds the plannable
    Stripe portion inline and logs any shortfall (net Stripe money unchanged; the
    IB portion still settles via credit note). (#1513, issue #1510)
  • Aggregate per-invoice cap on never-settled Internet-Banking credit mint
    multiple never-settled IB payments matched to a single invoice can no longer
    collectively mint account credit above that invoice's cash. No current app
    flow produces the aggregate shape, so real-flow mint amounts are unchanged.
    (#1512, issue #1505)
  • New operator script npm run payments:backfill-cancel-flattened — a
    one-off, idempotent, dry-run-by-default backfill restoring the stored
    Payment.status on rows the pre-#1489 cancel defect flattened to FAILED
    on cancelled bookings. The read path already compensates, so this is
    cosmetic-only for stored rows; it makes no Xero and no Stripe calls. See
    "Backfill cancel-flattened payment statuses" in docs/MAINTENANCE.md.
    (#1511, issue #1506)

Optional post-upgrade cleanup

If your fork ever ran a pre-v0.10.0 build, booking cancellations may have
flattened captured (PARTIALLY_)REFUNDED payments' stored status to
FAILED. You can restore the stored rows with
npm run payments:backfill-cancel-flattened — dry-run first, review the
report, then re-run with --apply. Detail in docs/MAINTENANCE.md.

Validation evidence

  • Release PR #1514 CI fully green at head f9c396c75 (tree identical to the
    merge commit): verify (lint, typecheck, tests, build), Playwright E2E,
    Migration drift check, Static analysis gate, CodeQL, dependency-review,
    gitleaks (full-repo + PR-diff), docker-image-security.
  • Local on the release branch: npm run lint 0 errors (5 pre-existing
    warnings, unchanged from the v0.10.0 cut); npm run typecheck pass (banner
    alpine-club-bookings-nz@0.10.1).
  • npm run db:check-drift not run — no schema change in this release
    (git diff v0.10.0..v0.10.1 -- prisma/ is empty).
  • An independent review agent verified changelog completeness (all 5
    first-parent merges since v0.10.0 covered, no direct commits), the
    no-migrations claim, the version bump, and the UPGRADING/MAINTENANCE
    cross-references before merge.

Images

Published by main CI for the merge commit:

  • ghcr.io/thatskiff33/alpineclubbookingsnz-app:331cb7e2784fedc862e6feca712d7ba3dcf13193
  • ghcr.io/thatskiff33/alpineclubbookingsnz-migrate:331cb7e2784fedc862e6feca712d7ba3dcf13193

Maintainability follow-ups (non-blocking)

Unchanged from v0.10.0 — the current known-hotspots list in
docs/MAINTENANCE.md (large-file split queue headed by
src/lib/xero-operation-outbox.ts, queued for PR-b of #1272's replay-stack
co-location).

AlpineClubBookingsNZ v0.10.0

Choose a tag to compare

@thatskiff33 thatskiff33 released this 07 Jul 02:45
0182cd7

Tag: v0.10.0 · Previous: v0.9.0 (2026-06-27) · Merge commit: 0182cd7ea.

What this release is

A large quality-and-hardening wave layered on top of the public booking,
membership, and finance capabilities, followed by a remediation wave
(epic #1348) that closed the post-wave audit findings and a live-feedback
admin-UX wave (epic #1438). The public deployment shape is unchanged.

Downstream forks: read docs/UPGRADING.md and the Migration/deployment notes
in CHANGELOG.md before deploying, and back up your database first.
This
release contains two destructive/behaviour migrations and other hot-table
migrations.

Post-upgrade actions you must take

These are the actions a fork operator has to take after upgrading. Full detail
and the exact migration names are in docs/UPGRADING.md (v0.9.0 → v0.10.0).

(a) Re-enable your capability modules — some features switch OFF on upgrade

Migration 20260627120000_core_module_defaults_off changes the default for the
high-risk capability modules — kiosk, chores, finance dashboard, waitlist,
Xero integration, bed allocation, and Internet Banking payments
— to off,
and switches them off on any fork whose Admin > Modules row was never saved
by an admin (the untouched singleton default row). If you never opened and saved
Module settings, these features will turn off on upgrade.

Action: after upgrading, open Admin > Modules and re-enable the modules
you use once their provider/setup is ready. Rows an admin already saved are
preserved; general-purpose modules stay on.

(b) Complete or export in-flight inductions first — unfinished ones are deleted

Migration 20260702100000_induction_workflow_types moves inductions to a
single-Pass flow and, doing so, deletes in-flight (DRAFT/IN_PROGRESS)
per-item induction results and clears their self-assessment answers
. Completed
historical inductions are kept.

Action: before upgrading, finish or export any inductions a member has
started but not completed. There is no recovery after the migration runs.

(c) Audit membership access roles only if you ran intermediate main

The contract migration 20260630120000_rename_member_role_to_user collapses
the legacy Member.role values (MEMBER/ASSOCIATE/LIFE) into USER and
recreates the role enum. It assumes no live deployment used the intermediate
Access-Roles window.

Action: if your fork deployed a main build between 2026-06-28 and
2026-06-30
, run npm run db:audit-access-role-cleanup after upgrading and
resolve anything it flags. Forks that upgraded straight from the v0.9.0 tag
never entered that window and can skip this.

(d) The three verified-safe migration items — you do NOT need to re-audit

These are already recorded as blue/green-safe in
docs/BLUE_GREEN_MIGRATION_SAFETY.tsv; no fork action required:

  • ClubTheme sub-AA gold theme bump is conditional — it only rewrites a
    persisted theme still on the old sub-AA default; a theme you have customised is
    left as-is.
  • BookingGuestNight backfill is automatic — it runs during migration and
    old code ignores the table.
  • Access-role backfills are old-code-compatible — old code keeps reading
    Member.role/financeAccessLevel while the new access-role tables are added.

(e) AgeTier NOT_APPLICABLE — deploy in a quiet window, or defer the backfill

Migrations 20260707000000_add_age_tier_not_applicable (enum add) and
20260707000100_backfill_org_age_tier_not_applicable (backfill) introduce a
NOT_APPLICABLE age tier for organisation-type members. The backfill is
old_code_compatible=no: a pre-v0.10.0 app color cannot read the new enum
value, so old-color reads of the flipped rows can error while both colors are
live (the classic blue/green enum-backfill hazard).

Action (per the owner decision on epic #1438, 2026-07-07): deploy both
migrations in a quiet window and cut over promptly, or defer the backfill
migration until the old color has fully drained, then run it (the UPDATE is
idempotent and safe to run late). The enum-add migration is a plain expand and is
safe either way. Detail and the ledger caveat are in docs/UPGRADING.md and
docs/BLUE_GREEN_MIGRATION_SAFETY.tsv.

Behaviour changes to note

  • Capability modules default off (see (a)).
  • Booking Officer / on-behalf booking scope widened — Booking Officers can
    open booking detail; bookings:edit holders can create and quote on behalf of
    members; an admin's own bookings still go through normal member payment paths.
  • Email preferences enforced on reminder and chores sends.
  • Non-member hold policy is admin-toggleable.
  • Cancellation policy — tiered credit restore (owner decision D7). A member
    who paid entirely with account credit and cancels inside the 0%-refund tier now
    gets nothing back, the same as a card payer; a captured-but-partially-refunded
    cancel is tiered on the remaining value (#1493). See the committee heads-up
    draft (docs/releases/v0.10.0-committee-headsup-draft.md) — brief your
    committee before wider rollout.

Removed / changed routes

  • The standalone POST /api/bookings/cancel route was removed earlier in the
    line; cancellation goes through the consolidated cancel path. Forks calling it
    directly should move to the current booking cancel flow.

Validation evidence (fill in at tag time)

  • npm ci — clean install on the release commit.
  • npm run lint — result.
  • npm run typecheck — result.
  • npm test / build / Playwright E2E — from the release PR's green CI run.
  • npm run db:check-drift against a shadow database — in sync.
  • Migration-drift and static-analysis gates — green on the release PR.

Deferred to post-approval (owner-gated)

  • Create the annotated v0.10.0 tag on the merged commit and publish this
    GitHub release (checklist step 6).
  • Image names / commit SHA / GHCR references — record at tag time.
  • Non-blocking maintainability follow-ups — carry the current
    docs/MAINTENANCE.md known-hotspots list.

AlpineClubBookingsNZ v0.9.0

Choose a tag to compare

@thatskiff33 thatskiff33 released this 27 Jun 01:20
e9752c1

Release commit: e9752c19ed4a1e7b68d7b778148ec0e8d1a75e51

  • Release classification: minor public reference release. The change set since
    0.8.0 adds public join flows, module controls, induction, locker,
    finance-dashboard, provider-recovery, and security hardening while preserving
    the existing public deployment shape.
  • Added group-booking join flows and APIs, including organiser-owned join
    codes, member self-add, non-member email verification, organiser management,
    organiser cancellation cleanup, public join pages, member dashboard context,
    and protected route/API coverage for group joinability.
  • Added group-booking settlement options for both each-pays-own and
    organiser-pays modes. Organisers can collect one combined Stripe payment or
    one Internet Banking/Xero invoice for joined bookings, while joiners remain
    linked to their own child bookings for capacity, status, and audit purposes.
  • Added lodge induction and sign-off workflows with induction templates,
    section/item results, assigned signers, self-assessment capture, member
    sign-off records, route access hardening, and nomination settings support for
    deployments that require induction before membership completion.
  • Added member locker administration and allocation, including API validation,
    unique locker names, dashboard/member context, and admin controls that can be
    disabled through Admin Modules.
  • Added database-backed Admin Modules toggles for group bookings, lockers,
    induction, work parties, promo codes, hut leaders, communications, and
    skifield conditions, keeping deploy-time .env capabilities as the outer
    operator gate.
  • Added member category and profile metadata support, including Life and
    Associate member categories, title, gender, occupation, life-member date,
    comments, configurable member-field visibility, CSV import/export hardening,
    and refreshed member edit/detail screens.
  • Added subscription booking lockout controls so clubs can block bookings for
    members with unpaid annual subscriptions, configure the lockout behavior in
    admin, and align the subscription year with either Xero's financial year or
    an explicit local override.
  • Reworked the finance dashboard to use the single operational Xero connection
    already used by bookings, payments, and subscriptions. Finance-specific Xero
    OAuth routes, token storage, and finance Xero usage metering were removed,
    while finance reports gained revenue reconciliation, chart-of-accounts
    snapshots, KPI cards, trend/mix charts, balance-sheet, cash, costs, working
    capital, pricing-sensitivity, and booking metric views.
  • Added Whakapapa/skifield condition widgets and admin cache controls with
    cached report payloads, freeze windows, public endpoint handling, and module
    gating for deployments that do not expose mountain-condition content.
  • Fixed image upload/runtime storage and visual-editor behavior, including
    read-only root filesystem upload handling, image resizing, admin toolbar and
    alignment tests, photo-gallery token rendering, and safer upload trace
    redaction.
  • Improved email/provider recovery visibility with token-email recovery
    actions, undeliverable admin-alert escalation, waitlist-offer email failure
    surfacing, Xero amount-mismatch repair alerts, missing Xero refund credit-note
    reporting, stale Xero operation/inbound-event recovery, exhausted payment
    recovery health signals, and the consolidated operator queue.
  • Hardened security and idempotency boundaries, including source-scoped
    processed webhook event claims, SES SNS SignatureVersion 2 enforcement,
    Xero token refresh leases, payment-link/client-secret ownership tests,
    group-join response neutralisation, mixed-method route boundary coverage,
    public rate-limit proxy assumptions, and high-severity dependency refreshes.
  • Migration/deployment notes:
    • 20260615110000_add_lodge_induction_signoff creates induction template,
      result, signer, and settings tables plus Member.requiresInduction; run
      during low membership-admin traffic before enabling induction-gated flows.
    • 20260616120000_induction_assigned_signers_and_self_assessment adds
      induction self-assessment fields and assigned-signer records; avoid active
      induction edits during cutover.
    • 20260618120000_add_group_booking adds group-booking and join staging
      tables for shareable join codes; open new group joins only after the new
      runtime is live.
    • 20260619120000_add_booking_organiser_settled adds
      Booking.organiserSettled for organiser-pays child bookings; run during
      low booking traffic and do not create organiser-pays joins until old app
      colors have drained.
    • 20260619130000_add_group_booking_settlement and
      20260620120000_add_group_settlement_internet_banking add combined group
      settlement records for Stripe and Internet Banking/Xero settlement.
    • 20260620121500_add_whakapapa_report_cache and
      20260620133000_add_whakapapa_cache_frozen_until add cached skifield
      report payloads and freeze-window controls.
    • 20260620145000_add_lockers and 20260622100000_harden_locker_names add
      member locker allocation and then enforce unique, bounded locker names;
      resolve duplicate locker names before the hardening migration.
    • 20260621150000_scope_processed_webhook_event_idempotency replaces the
      global webhook-event idempotency key with a (source, eventId) key so
      Stripe, Xero, and SES events cannot collide across providers.
    • 20260621160000_add_xero_token_refresh_lease adds the operational Xero
      token refresh lease used to prevent parallel refresh-token rotation.
    • 20260622120000_add_module_toggles adds Admin Modules activation booleans
      for the newly modularised features, all defaulting on for upgraded installs.
    • 20260623120000_add_member_status_fields,
      20260623130000_add_member_gender_title, and
      20260626120000_member_field_visibility_and_categories add the new member
      metadata/category fields and settings; avoid assigning new enum categories
      until the new runtime is serving traffic.
    • 20260626120000_add_membership_lockout_settings adds the singleton
      subscription booking-lockout settings row used by admin controls.
    • 20260626120000_add_chart_of_accounts_finance_snapshot_type adds the
      finance chart-of-accounts snapshot type used by revenue reconciliation.
    • 20260626121000_drop_finance_xero_storage_and_usage drops the retired
      finance-specific Xero token and usage tables after the runtime has moved to
      the single operational Xero connection.