PR 1914 completes issue #1337 after the stage-1 marker writer fully replaced old API replicas. The shared paid-group transaction now refuses insertion when the locked payment carries bookingRefundStartedAt. Migration 0193 adds a group-scoped database guard covering active INSERT and inactive-to-active or booking-group UPDATE paths, including writers outside the shared helper. Seated-group replay remains authoritative before refusal, and the trigger excludes solo payment rows.
fix/1337-group-close-guard-enable at e3f310656, pushed as draft PR 1914 before evidence source was added
Generated
Reviewer brief
Outcome and scope
Refund-first and seat-first orderings now converge without losing money or seats
The application probes durable group rows under payment-row then slot locks before honoring the marker. A seated group replays successfully and cannot be refunded. An unseated marked payment is refused before any seat write. The database trigger enforces the same boundary for direct INSERT and inactive-to-active or group-id UPDATE paths. Twelve PostgreSQL regressions, API typecheck, migration-policy tests, fresh schema convergence, and two independent final reviews passed.
The rolling-deploy prerequisite is satisfied
Stage 1 merged in PR 1913 and production run 32834391227 successfully applied migration 0192 and completed Deploy production applications before this branch was published. That Fly deployment step replaced and drained the old API replicas. The run later failed while staging the PR evidence frontend because Vercel returned a protected-deployment 302; the base release run 32826688178 had already failed at the same unrelated step. The completed API deployment is not rolled back by that later frontend failure.
Claim map
Evidence coverage
Each material changed behavior or state maps to direct evidence or an explicit gap.
Behavior or claim
Evidence
Status
A group cannot gain an active seat after its payment starts compensation
Application marker check, database trigger, direct INSERT regression, inactive-to-active UPDATE regression, and refund-first lock-wait regression
Covered
An already seated group is never refunded because a marker exists
Application ordering keeps the in-lock seated-row replay ahead of marker refusal, with existing seat-first concurrency coverage
Covered
Unexpected writers cannot bypass the application helper
0193 BEFORE INSERT OR UPDATE OF status, booking_group_id trigger and direct SQL-path regressions
Covered
The database guard does not overblock solo payments or inactive group history
Friend-customer payment scoping and a PostgreSQL solo-row regression; cancelled rows insert before an attempted guarded activation
Covered
The complete production release, including evidence and CRM/widget frontend promotion, is green
Gap outside this PR's API behavior. The API deployment succeeded, but a pre-existing PR evidence frontend protection failure stopped later frontend stages.
Gap
Observed change
Review evidence
Evidence is grouped by the reviewer question each item addresses.
Concurrency contract
Table
Both transaction orders preserve the authoritative outcome
Payment-row then slot-lock order serializes the decision. The seated-row probe runs before marker refusal, so whichever state commits first determines the safe result.
Winning transaction
Waiting transaction observes
Result
Seat write commits first
Committed active rows under both locks
Replay success, no refund
Compensation marker commits first
No active rows and non-null bookingRefundStartedAt
Committed seats replay; only a proven unseated state can remain refundable
Direct or future writer bypasses helper
Database trigger joins booking group to marked group payment
Active INSERT or activation UPDATE fails with check_violation
Application guard
Code
Seated replay precedes marker refusal under the same lock order
The transaction already locked the captured payment row and then the slot. It first replays a committed group. Only an unseated payment reaches marker refusal.
Authoritative decision at code head e3f310656.After
0193 guards active INSERT and activation or group-id UPDATE
Only reservations tied to a payment with non-empty friend customer ids and a committed refund marker are rejected. Cancelled and no-show history remains writable, while any transition into a seat-holding status is checked.
Complete enforcement shape in migration 0193.After
IF NEW.booking_group_id IS NOT NULL
AND NEW.status IN ('confirmed', 'waitlisted', 'pending_approval', 'completed')
AND EXISTS (
SELECT 1
FROM payments
WHERE booking_group_id = NEW.booking_group_id
AND COALESCE(friend_customer_ids_csv, '') <> ''
AND booking_refund_started_at IS NOT NULL
)
THEN
RAISE EXCEPTION 'group booking compensation has already started'
USING ERRCODE = 'check_violation';
END IF;
CREATE TRIGGER reservations_booking_refund_guard
BEFORE INSERT OR UPDATE OF status, booking_group_id ON reservations
FOR EACH ROW
EXECUTE FUNCTION reject_group_booking_after_refund_started();
Behavioral regression matrix
Table
PostgreSQL coverage exercises the enforcement boundaries
The same real-database suite contains the adjacent seat-write invariants, so the new guard is checked alongside existing ban, replay, capacity, and ordinary insertion behavior.
Scenario
Expected result
Observed
Marked group, direct active INSERT
check_violation, no seat
Pass
Marked group, cancelled row activated
check_violation, row remains inactive
Pass
Writer waits behind refund decision
Application refusal after lock release
Pass
Retry after marker with earlier cutoff time
Refusal despite otherwise bookable cutoff
Pass
Marked solo payment
Active INSERT remains outside group trigger
Pass
Unmarked ordinary group
Seats insert normally
Pass
Confidence
Validation
Pass
API typecheck
bun run typecheck in packages/api exited 0 after the final guard and tests.
Pass
PostgreSQL enforcement suite
12 tests passed with 0 failures and 37 assertions after recreating the local database and migrating through 0193.
Pass
Fresh migration chain and schema convergence
One empty database migrated through 0193 and a separate empty database received drizzle-kit push. assert-migration-chain reported 193 migrations applied and matching schemas, including the raw trigger/function allowlist.
Pass
Migration-path policy
scripts/ci-migration-paths.test.ts passed 2 tests with 10 assertions.
Pass
Independent final correctness and security reviews
Both reviewers found no remaining group financial-integrity, tenant/group crossover, replay, error-path, lock-order, trigger-scope, migration, or rolling-compatibility gap after stage-1 deployment.
Warn
Exact-head CI
Pending on draft PR 1914. Local focused proof passed before this evidence source was authored.
Limits
Known issues, risks, and gaps
The group invariant is closed; remaining limits are explicit scope and unrelated release infrastructure
Solo bookings remain outside the group handoff The application marker decision is entered only for paid groups, and the trigger requires a payment with non-empty friend customer ids. A PostgreSQL regression proves a marked solo payment is not blocked. This PR does not claim to change solo compensation semantics.
The database guard depends on payment group identity The trigger joins reservations.booking_group_id to payments.booking_group_id and then verifies a non-empty friend customer list. Modern paid-group checkout always persists both. Legacy or tampered rows without group identity are already rejected by the surrounding booking flow rather than guessed.
Production frontend staging currently fails after API deployment Run 32834391227 deployed the API successfully, satisfying the replica-drain gate, then failed because the PR evidence Vercel deployment returned HTTP 302 even with the trusted OIDC header. Base run 32826688178 failed identically. This does not roll back the API, but the overall production workflow remains red until that separate protection configuration is corrected.