Skip to content

Stop OwnerRez silently dropping owner revenue past 1000 bookings, and… - #617

Merged
smj1860 merged 1 commit into
mainfrom
claude/hostile-code-audit-rbz3f8
Aug 12, 2026
Merged

Stop OwnerRez silently dropping owner revenue past 1000 bookings, and…#617
smj1860 merged 1 commit into
mainfrom
claude/hostile-code-audit-rbz3f8

Conversation

@smj1860

@smj1860 smj1860 commented Aug 12, 2026

Copy link
Copy Markdown
Owner

… default crew sync to v2

INTEGRATION SYNC AUDIT — one real finding, in OwnerRez.

Both syncs upserted the whole booking set in ONE call and built the external_id -> id map from that call's returned rows. The WRITE is not capped; the returned REPRESENTATION is, at max_rows = 1000, with a 200 and no truncation signal — the same cap that applies to a read. So past 1000 bookings the map came back short, and selectOwnerRezBookingsToPostRevenue's closing .filter((b) => !!b.bookingId) silently dropped every booking missing from it.

The bookings themselves were written, so the calendar looked complete. Only the owner_transactions REVENUE rows were absent. No error, no log, nothing in the UI — an owner's P&L simply short by an amount that grows with portfolio size.

initial-sync is where it bites hardest: it fetches every booking for every property in one call, so an account with two years of history crosses 1000 on its FIRST sync — the one run whose entire job is to get the historical ledger right. incremental-sync has the same shape with a smaller blast radius.

Fixed by chunking the upsert so no single response can be truncated, at HALF of max_rows rather than at it, so a future change returning more columns or a lower server-side max_rows cannot quietly re-introduce it. A short chunk now throws rather than returning a partial map — a partial map is indistinguishable from success downstream, so failing loudly is the only way this class surfaces at all. The chunk loop is numeric, which is structurally exempt from the N+1 guardrail, and costs one round trip per 500 bookings.

Not triggered on live data (57 bookings), but this is CLAUDE.md's own stated target scale and structurally the same defect as the inventory truncation that silently duplicated items past property ~8.

The other two integrations are clean. iCal guards its tenant-supplied feed URLs with safeFetch (re-validating every redirect hop) and paginates the whole fan-out. Hospitable's one unbounded read is scoped to a single property AND a date window; a truncated result there leaves a stale Blocked row that self-heals on the next run.

CREW SYNC V2 — now the default, v1 retained.

The acceptance test (Phase 5b) passed, and NEXT_PUBLIC_CREW_SYNC_V2 is set in Vercel, so v2 has been the serving path since the first build after it was set. Inverted the client default from === 'true' to !== 'false', which closes a real hole rather than just flipping a switch: absent used to mean v1, so any environment not carrying the variable — a new preview, a rebuilt environment, a setting lost in a project migration — silently served crew the old path. NEXT_PUBLIC_ vars are inlined at BUILD time, so that state was invisible in config and observable only from behaviour.

The v1 branch stays in the tree, compiling and guardrail-covered. It is the rollback: set the variable to 'false' and rebuild, no code change. Commenting it out was considered and rejected — it is already unreachable when the flag is on, so commenting buys nothing while costing type-checking, lint, guardrail coverage, and a one-variable rollback. Phase 5d deletes it after the soak.

Updated the 2026-08-09 doc note, which claimed a redeploy was still pending and had been stale for three days, plus the Phase 5 status row.

Three guardrails caught things worth keeping:

  • error-reporting-coverage flagged the two log-only catch blocks the first version added. Removed both — the helper now carries the connection label in its thrown message, so catching to re-log and re-throw added no context.
  • supabase-error-handling flagged this file's own DOC COMMENT, which quoted the buggy const { … } = await supabase… verbatim. The scan reads raw source and cannot tell an illustration of a defect from the defect; rewritten as prose with a note explaining why.
  • n-plus-one-loops' pinned exception at initial-sync.ts:184 moved to :185 when the import landed. Repinned, along with the sync.ts entry citing it.

Canaried: unchunking the upsert fails three of the six new tests, including an end-to-end one that reproduces the original revenue gap through selectOwnerRezBookingsToPostRevenue rather than asserting it in a comment.

Claude-Session: https://claude.ai/code/session_014Zk7eKd7UeNCoWxHSVxAFR

Summary by CodeRabbit

  • New Features

    • Crew Sync v2 is now enabled by default, with a build-time rollback option available.
    • Large OwnerRez booking imports now process in batches to preserve complete booking records and mappings.
  • Bug Fixes

    • Prevented incomplete booking synchronization caused by response-size limits.
    • Improved error detection when booking data is truncated or unavailable.
  • Tests

    • Added coverage for large and small imports, empty inputs, truncation errors, and revenue posting.

… default crew sync to v2

INTEGRATION SYNC AUDIT — one real finding, in OwnerRez.

Both syncs upserted the whole booking set in ONE call and built the
external_id -> id map from that call's returned rows. The WRITE is not capped;
the returned REPRESENTATION is, at max_rows = 1000, with a 200 and no
truncation signal — the same cap that applies to a read. So past 1000 bookings
the map came back short, and selectOwnerRezBookingsToPostRevenue's closing
`.filter((b) => !!b.bookingId)` silently dropped every booking missing from it.

The bookings themselves were written, so the calendar looked complete. Only
the owner_transactions REVENUE rows were absent. No error, no log, nothing in
the UI — an owner's P&L simply short by an amount that grows with portfolio
size.

initial-sync is where it bites hardest: it fetches every booking for every
property in one call, so an account with two years of history crosses 1000 on
its FIRST sync — the one run whose entire job is to get the historical ledger
right. incremental-sync has the same shape with a smaller blast radius.

Fixed by chunking the upsert so no single response can be truncated, at HALF
of max_rows rather than at it, so a future change returning more columns or a
lower server-side max_rows cannot quietly re-introduce it. A short chunk now
throws rather than returning a partial map — a partial map is
indistinguishable from success downstream, so failing loudly is the only way
this class surfaces at all. The chunk loop is numeric, which is structurally
exempt from the N+1 guardrail, and costs one round trip per 500 bookings.

Not triggered on live data (57 bookings), but this is CLAUDE.md's own stated
target scale and structurally the same defect as the inventory truncation
that silently duplicated items past property ~8.

The other two integrations are clean. iCal guards its tenant-supplied feed
URLs with safeFetch (re-validating every redirect hop) and paginates the whole
fan-out. Hospitable's one unbounded read is scoped to a single property AND a
date window; a truncated result there leaves a stale Blocked row that
self-heals on the next run.

CREW SYNC V2 — now the default, v1 retained.

The acceptance test (Phase 5b) passed, and NEXT_PUBLIC_CREW_SYNC_V2 is set in
Vercel, so v2 has been the serving path since the first build after it was
set. Inverted the client default from `=== 'true'` to `!== 'false'`, which
closes a real hole rather than just flipping a switch: absent used to mean v1,
so any environment not carrying the variable — a new preview, a rebuilt
environment, a setting lost in a project migration — silently served crew the
old path. NEXT_PUBLIC_ vars are inlined at BUILD time, so that state was
invisible in config and observable only from behaviour.

The v1 branch stays in the tree, compiling and guardrail-covered. It is the
rollback: set the variable to 'false' and rebuild, no code change. Commenting
it out was considered and rejected — it is already unreachable when the flag
is on, so commenting buys nothing while costing type-checking, lint, guardrail
coverage, and a one-variable rollback. Phase 5d deletes it after the soak.

Updated the 2026-08-09 doc note, which claimed a redeploy was still pending
and had been stale for three days, plus the Phase 5 status row.

Three guardrails caught things worth keeping:
  * error-reporting-coverage flagged the two log-only catch blocks the first
    version added. Removed both — the helper now carries the connection label
    in its thrown message, so catching to re-log and re-throw added no context.
  * supabase-error-handling flagged this file's own DOC COMMENT, which quoted
    the buggy `const { … } = await supabase…` verbatim. The scan reads raw
    source and cannot tell an illustration of a defect from the defect;
    rewritten as prose with a note explaining why.
  * n-plus-one-loops' pinned exception at initial-sync.ts:184 moved to :185
    when the import landed. Repinned, along with the sync.ts entry citing it.

Canaried: unchunking the upsert fails three of the six new tests, including an
end-to-end one that reproduces the original revenue gap through
selectOwnerRezBookingsToPostRevenue rather than asserting it in a comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Zk7eKd7UeNCoWxHSVxAFR
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
fieldstay Ready Ready Preview Aug 12, 2026 6:00pm

Request Review

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smj1860 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Crew Sync v2 is now enabled by default unless disabled at build time. OwnerRez initial and incremental syncs now use chunked booking upserts with complete ID mappings and explicit truncation errors. Tests cover large batches and revenue posting.

Changes

OwnerRez booking persistence

Layer / File(s) Summary
Chunked booking upsert helper
lib/inngest/functions/ownerrez/upsert-bookings.ts, unit/inngest/ownerrez-upsert-bookings.test.ts
Adds bounded booking upserts, complete external-ID mappings, explicit errors, and tests for large, small, empty, and truncated batches.
Sync workflow integration
lib/inngest/functions/ownerrez/initial-sync.ts, lib/inngest/functions/ownerrez/incremental-sync.ts, unit/guardrails/n-plus-one-loops.test.ts
Initial and incremental syncs use the shared helper while retaining mapping, logging, and affected-property processing. Guardrail line references are updated.

Crew Sync v2 rollout

Layer / File(s) Summary
Default flag and rollout documentation
lib/dexie/context.tsx, docs/CREW_SYNC_V2_PHASES.md
Crew Sync v2 is enabled unless NEXT_PUBLIC_CREW_SYNC_V2 is false. The documentation records the rollout state and rollback procedure.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OwnerRezSync
  participant upsertBookingsReturningIds
  participant Supabase
  OwnerRezSync->>upsertBookingsReturningIds: submit booking rows
  upsertBookingsReturningIds->>Supabase: upsert bounded chunks and select IDs
  Supabase-->>upsertBookingsReturningIds: return external booking IDs and database IDs
  upsertBookingsReturningIds-->>OwnerRezSync: return merged ID mapping
Loading

Possibly related PRs

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: preventing OwnerRez revenue loss for accounts with more than 1,000 bookings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/hostile-code-audit-rbz3f8

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/CREW_SYNC_V2_PHASES.md`:
- Around line 39-55: Synchronize the remaining Phase 5 documentation with the
current v2-live state: update the flag-default statements near the rollout
instructions, reconcile the 5a quota-check status everywhere, revise the
operator steps around the 5b acceptance test so they no longer require enabling
or promoting an already-live default, and mark the 5e guardrail section as
completed. Preserve historical notes only when clearly labeled as superseded,
leaving one unambiguous current rollout procedure.

In `@unit/inngest/ownerrez-upsert-bookings.test.ts`:
- Around line 47-53: The test fixtures in
unit/inngest/ownerrez-upsert-bookings.test.ts at lines 47-53 and 107-108 use
three as any casts; replace them with explicit types. Define a typed
OwnerRezBookingRow fixture builder and use it for both row sets, and type the
mocked client against the minimal contract consumed by
upsertBookingsReturningIds, narrowing that function’s parameter if necessary.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f70667f7-4477-4e3d-937b-dc6a362fc2ce

📥 Commits

Reviewing files that changed from the base of the PR and between e83ba85 and c3c64fb.

📒 Files selected for processing (7)
  • docs/CREW_SYNC_V2_PHASES.md
  • lib/dexie/context.tsx
  • lib/inngest/functions/ownerrez/incremental-sync.ts
  • lib/inngest/functions/ownerrez/initial-sync.ts
  • lib/inngest/functions/ownerrez/upsert-bookings.ts
  • unit/guardrails/n-plus-one-loops.test.ts
  • unit/inngest/ownerrez-upsert-bookings.test.ts

Comment on lines +39 to +55
| 5 | Rollout, acceptance test, old-code deletion, convention + guardrail | 🟡 **In progress** — 5e (convention + `unit/guardrails/crew-sync-coverage.test.ts`) done 2026-07-29. 5b two-device acceptance test passed 2026-08-12; flag set in Vercel and client default inverted to on the same day, so **v2 is the serving path**. Still open: 5a Realtime quota check, 5c soak, 5d deletion of the v1 path |

> **2026-08-09flag flipped, redeploy still required.** `NEXT_PUBLIC_*` is
> inlined at BUILD time (see the note at `lib/dexie/context.tsx`'s
> `CREW_SYNC_V2` and `clientInlinedOnly: true` in `lib/env.ts`), so setting the
> var in Vercel does nothing to a bundle that was already built. v2 turns on at
> the next **rebuild**, not the next restart. Until then v1 is still serving.
> **2026-08-12v2 is the DEFAULT and is live.** `NEXT_PUBLIC_CREW_SYNC_V2`
> is set to `true` in Vercel and many builds have shipped since it was set, so
> v2 has been the serving path for some time — the 2026-08-09 note this
> replaces ("redeploy still required") was already stale when written against a
> branch that redeployed on every push.
>
> Everything else on the DB side is verified live in production (2026-08-09):
> migration `20260725191358` in the ledger, `notify_crew_sync` present,
> 12 broadcast triggers attached, RLS policy `crew_receive_own_sync_broadcasts`
> on `realtime.messages`, and the trigger's `crew_members.user_id` join matches
> both the client's `crew:${userId}` topic and the policy's `auth.uid()`.
> `realtime.send()` was probed end to end and does land a row.
> The client default is now inverted (`!== 'false'`), so v2 serves even where
> the variable is absent. That closes a real hole: `NEXT_PUBLIC_*` is inlined
> at BUILD time, so an environment missing the variable silently served v1 with
> nothing in config to show it. Rollback is `NEXT_PUBLIC_CREW_SYNC_V2=false`
> plus a rebuild — still a build-time change, not a runtime toggle.
>
> Still open before the soak: 5b (two-device acceptance test — never run), then
> 5c. Do NOT do 5d yet: the v1 `postgres_changes` code is the rollback path, and
> rolling back is another rebuild.
>
> Expect one deliberate behaviour change: v1 had a `postgres_changes` channel on
> `property_assets`; v2 has no trigger for it (`SAFETY_POLL_ONLY` in
> `unit/guardrails/crew-sync-coverage.test.ts`). Asset edits reach the device on
> screen-open or the safety poll rather than in ~2 s.
>
> Watch the Realtime DB pool (2 → 15, per 5a). Private-channel RLS
> authorization takes a connection on every join AND every reconnect, v2 is
> all-private-channel, and the reconnect jitter is 5–35 s — so a Realtime node
> restart puts the whole fleet through that pool inside 30 seconds.
> 5b (two-device acceptance test) passed 2026-08-12. Still open: 5a's Realtime
> quota check, the 5c soak, and 5d deletion of the v1 path — which stays in the
> tree, compiling and guardrail-covered, until then.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Synchronize the Phase 5 status and instructions.

The new status says v2 is live and the client default is on, but this document still contains conflicting rollout state:

  • Lines 37-38 say the flag defaults off.
  • Lines 695-700 mark the 5a quota check complete, while Lines 39 and 53-55 list 5a as open.
  • Lines 701-702 still instruct the operator to set the flag and promote after acceptance.
  • Lines 750-763 still describe the 5e guardrail as work to add, although Line 39 marks 5e complete.

Update or label these statements so the document has one current rollout procedure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/CREW_SYNC_V2_PHASES.md` around lines 39 - 55, Synchronize the remaining
Phase 5 documentation with the current v2-live state: update the flag-default
statements near the rollout instructions, reconcile the 5a quota-check status
everywhere, revise the operator steps around the 5b acceptance test so they no
longer require enabling or promoting an already-live default, and mark the 5e
guardrail section as completed. Preserve historical notes only when clearly
labeled as superseded, leaving one unambiguous current rollout procedure.

Comment on lines +47 to +53
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { client: { from } as any, calls }
}

const rows = (n: number) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Array.from({ length: n }, (_, i) => ({ external_id: `e${i}` })) as any

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -nP '\bas\s+any\b|\bany\b|`@ts-ignore`' unit/inngest/ownerrez-upsert-bookings.test.ts

Repository: smj1860/fieldstay

Length of output: 506


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- file outline ---'
ast-grep outline unit/inngest/ownerrez-upsert-bookings.test.ts
printf '%s\n' '--- relevant test sections ---'
sed -n '1,125p' unit/inngest/ownerrez-upsert-bookings.test.ts
printf '%s\n' '--- related booking row types and test-client patterns ---'
rg -n "OwnerRezBookingRow|ownerrez.*booking|create.*Test|mock.*from|as any" --glob '*.{ts,tsx}' .

Repository: smj1860/fieldstay

Length of output: 19440


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- upsert implementation ---'
sed -n '1,115p' lib/inngest/functions/ownerrez/upsert-bookings.ts
printf '%s\n' '--- OwnerRez booking types and revenue selector ---'
sed -n '540,675p' lib/integrations/providers/ownerrez.ts
printf '%s\n' '--- Supabase client type declarations ---'
rg -n "export type Supabase|SupabaseClient|createServerClient|Database" lib unit types --glob '*.{ts,tsx}' 2>/dev/null | head -120

Repository: smj1860/fieldstay

Length of output: 21402


Replace the three as any casts with explicit types.

Use a typed OwnerRezBookingRow fixture builder for both row sets. Type the mock against the minimal client contract used by upsertBookingsReturningIds, or narrow that function’s parameter to the contract.

📍 Affects 1 file
  • unit/inngest/ownerrez-upsert-bookings.test.ts#L47-L53 (this comment)
  • unit/inngest/ownerrez-upsert-bookings.test.ts#L107-L108
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@unit/inngest/ownerrez-upsert-bookings.test.ts` around lines 47 - 53, The test
fixtures in unit/inngest/ownerrez-upsert-bookings.test.ts at lines 47-53 and
107-108 use three as any casts; replace them with explicit types. Define a typed
OwnerRezBookingRow fixture builder and use it for both row sets, and type the
mocked client against the minimal contract consumed by
upsertBookingsReturningIds, narrowing that function’s parameter if necessary.

Source: Coding guidelines

@smj1860
smj1860 merged commit 461318e into main Aug 12, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants