feat(focus): restaurant picker + server-side 90-day background backfill - #564
Conversation
✅ Deploy Preview for easyshifthq ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Updates to Preview Branch (feat/focus-focuslink-datafeed) ↗︎
Tasks are run on every commit but only new migration files are pushed.
View logs for this Workflow Run ↗︎. |
|
Warning Review limit reached
Next review available in: 14 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis PR adds a Focus POS restaurant picker flow and server-side background backfill. A new ChangesFocus picker and background sync
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Wizard as FocusSetupWizard
participant Hook as useFocusConnection
participant EdgeFn as focus-list-restaurants
participant Handler as handleListRestaurants
participant Focus as Focus POS API
Wizard->>Hook: listRestaurants(restaurantId, apiKey, apiSecret, environment)
Hook->>EdgeFn: invoke focus-list-restaurants
EdgeFn->>Handler: handleListRestaurants(req, deps)
Handler->>Handler: auth.getUser() + role check
Handler->>Handler: SSRF guard on base URL
Handler->>Focus: GET /api/restaurants (Basic auth)
Focus-->>Handler: restaurants JSON or error
Handler-->>EdgeFn: 200 {success, restaurants|error}
EdgeFn-->>Hook: response
Hook-->>Wizard: FocusRestaurantOption[]
Wizard->>Wizard: auto-select or render picker
sequenceDiagram
participant Cron as pg_cron
participant EdgeFn as focus-backfill-sync
participant Handler as handleBackfillSync
participant Batch as processBackfillBatch
participant DB as focus_connections
Cron->>EdgeFn: net.http_post every 5 min
EdgeFn->>Handler: handleBackfillSync(req, deps)
Handler->>DB: select backfilling Lynk connections
loop each connection within budget
Handler->>Batch: processBackfillBatch(config, opts)
Batch-->>Handler: {syncCursor, status, daysProcessed}
Handler->>DB: CAS update (id, restaurant_id, sync_cursor)
end
Handler-->>EdgeFn: {processed, errors, elapsedMs}
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7937f16f65
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const result: TransactionSyncResult = await deps.processDayTransactions( | ||
| { supabase: deps.supabase, fetchDatafeed: deps.fetchDatafeed }, | ||
| config, | ||
| targetDate, | ||
| { skipUnifiedSalesSync: true }, | ||
| ); |
There was a problem hiding this comment.
Sync backfilled days into unified_sales
When initial backfill runs through either focus-sync-data or the new 5-minute cron, every day is handed to processDayTransactions with skipUnifiedSalesSync: true here. Neither caller invokes sync_focus_transactions_to_unified_sales for the processed range afterward, and the 6-hour safety-net RPC only resyncs the last few days, so once the cursor advances the older backfilled orders/items/payments never reach unified_sales or daily aggregates and won't appear in P&L unless manually resynced. Run the range RPC for each successful batch before advancing or marking the backfill done.
Useful? React with 👍 / 👎.
| for (const voidedCheckId of deletedCheckIds) { | ||
| const { error: delError } = await deps.supabase | ||
| .from('focus_orders') | ||
| .delete() | ||
| .eq('restaurant_id', config.restaurantId) | ||
| .eq('business_date', businessDate) | ||
| .eq('focus_check_id', voidedCheckId); |
There was a problem hiding this comment.
Remove unified_sales rows for deleted checks
When a later datafeed contains a DeleteRecord for a previously synced check, this path deletes the focus_orders row before the unified-sales RPC runs. The RPC cleanup only iterates existing focus_orders, so the deleted check has no iteration that can remove its existing focus-* sale/tip/discount rows or reaggregate the day, leaving voided revenue in reports. Delete the matching unified_sales rows and refresh aggregates as part of handling the DeleteRecord.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
docs/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md (2)
165-178: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAlign the custom-range flow with the binding resolution.
This section still instructs implementers to background custom ranges with
scheduleBackground/waitUntil, but §8.2 explicitly makes custom range synchronous. Keeping both instructions in the same plan will send the implementation in opposite directions.🤖 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/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md` around lines 165 - 178, The custom-range guidance is inconsistent with the binding resolution because it still tells implementers to use scheduleBackground/waitUntil even though custom range handling is meant to be synchronous. Update the plan section that describes the custom-range flow so it no longer references backgrounding for custom ranges and instead aligns with the synchronous behavior already defined in the custom-range rules, keeping the instructions consistent with the surrounding focus-picker and background-sync plan.
191-194: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMake the backfill batch cap consistent.
maxDays: 30here conflicts with §8.3’smaxDays: 7for cron backfill. Please pick one limit and use it consistently across the plan and tests.🤖 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/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md` around lines 191 - 194, Make the backfill batch cap consistent across the plan and its tests: the current maxDays value conflicts with the cron backfill limit described in the plan’s backfill section. Update the backfill configuration in the plan so the same maxDays limit is used everywhere, and align any test expectations or references that mention the alternate value. Use the backfill cron section and related test descriptions as the source of truth when updating the maxDays setting.supabase/functions/_shared/focusSyncDataHandler.ts (1)
102-138: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAdd the missing CAS chain level here
update(...).eq(...).eq(...).eq('sync_cursor', ...)needs a 4-step chain, matchingfocusBackfillSyncHandler.ts.CasEq3currently exposes onlyselect(), so the helper's type surface doesn't match the actual call shape.🤖 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 `@supabase/functions/_shared/focusSyncDataHandler.ts` around lines 102 - 138, The CAS helper types in focusSyncDataHandler are missing one step in the update chain, so the declared surface does not match the actual .update(...).eq(...).eq(...).eq('sync_cursor', ...) usage. Update the chain types around CasUpdateResult, CasEq1, CasEq2, and CasEq3 so the helper supports the 4-step CAS flow used by the sync logic, matching the shape already used in focusBackfillSyncHandler and allowing select() only after the extra eq() level.
🧹 Nitpick comments (8)
src/hooks/useFocusConnection.tsx (1)
111-117: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winUse
staleTime: 30_000here (src/hooks/useFocusConnection.tsx:111-117)
refetchIntervalalready runs independently ofstaleTime, sostaleTime: 0just adds extra refetches on focus/remount. A 30s stale window keeps the 8s backfill poll and matches the hook guidelines.Suggested change
- staleTime: 0, + staleTime: 30_000,🤖 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 `@src/hooks/useFocusConnection.tsx` around lines 111 - 117, Update useFocusConnection’s useQuery options so the query stays fresh for 30 seconds instead of forcing immediate staleness; in useFocusConnection.tsx, change the staleTime setting in the hook’s query config to 30_000 while keeping refetchInterval unchanged. This reduces extra focus/remount refetches without affecting the backfill polling behavior.Source: Coding guidelines
tests/unit/focusTransactionSyncHandler.test.ts (1)
166-535: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider prefixing critical-path test descriptions with
CRITICAL:.This suite covers financial transaction persistence, batched upserts, and unified_sales sync — tests like "returns { status: 'error' } when focus_orders upsert fails" or the RPC-params tests are good candidates. As per coding guidelines,
tests/unit/**/*.test.{ts,tsx}: "Prefix critical business logic tests with 'CRITICAL:' in test description."🤖 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 `@tests/unit/focusTransactionSyncHandler.test.ts` around lines 166 - 535, Prefix the critical-path test cases in processDayTransactions with CRITICAL: in their description strings, especially the ones covering focus_orders upsert failure, items/payments batch upserts, unified_sales RPC params, and voided-check deletes. Update the relevant it(...) titles in focusTransactionSyncHandler.test.ts so they clearly identify financial/business-critical behavior while leaving the test logic and assertions unchanged.Source: Coding guidelines
supabase/functions/_shared/focusSyncDataHandler.ts (2)
438-485: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winIncremental sync failures aren't persisted, unlike backfill failures.
The backfill branch persists
connection_status='error'/last_errorwhenbatchResult.status === 'error'(Lines 402-406), but the incremental branch here only refreshes timestamps regardless ofstatus. A recurring incremental failure (Lynk auth issue, license expiry, etc.) won't surface viaconnection_status/last_errorfor the frontend/ops to notice.♻️ Proposed fix — persist error state on incremental failures too
const nowIso = now.toISOString(); + const incUpdatePayload: Record<string, unknown> = { + last_sync_time: nowIso, + updated_at: nowIso, + }; + if (status === 'error') { + incUpdatePayload.connection_status = 'error'; + incUpdatePayload.last_error = r1.status === 'error' ? r1.error : r2.error; + incUpdatePayload.last_error_at = nowIso; + } const { error: casIncErr } = await deps.serviceClient .from('focus_connections') - .update({ - last_sync_time: nowIso, - updated_at: nowIso, - }) + .update(incUpdatePayload) .eq('id', connRow.id) .eq('restaurant_id', restaurantId) .eq('sync_cursor', readCursor) .select();🤖 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 `@supabase/functions/_shared/focusSyncDataHandler.ts` around lines 438 - 485, The incremental path in focusSyncDataHandler should persist failures the same way the backfill branch does: after processDayTransactions returns r1/r2, if status is error, update focus_connections via deps.serviceClient to set connection_status to error and last_error from the failure result before returning. Keep the existing timestamp refresh for success/empty cases, and preserve the current CAS behavior around the sync_cursor/readCursor update.
297-316: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win
decrypt()call unwrapped in try/catch.Unlike the legacy path's decrypt (wrapped to catch
FocusAuthError), this Lynk-path decrypt at Line 305 isn't guarded. A decrypt failure (e.g. corrupted ciphertext or key rotation) will propagate uncaught out ofhandleSyncDatainstead of returning a clean JSON error, potentially violating the edge-function convention of consistent error responses.As per coding guidelines,
supabase/functions/**/*.tsshould "Use try-catch and return consistent JSON error responses in Edge Functions."♻️ Proposed fix
const encSvc = await getEncryptionService(); - const apiSecret = await encSvc.decrypt(connRow.api_secret_encrypted); + let apiSecret: string; + try { + apiSecret = await encSvc.decrypt(connRow.api_secret_encrypted); + } catch { + return jsonError(500, 'Failed to decrypt Focus POS API credentials'); + }🤖 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 `@supabase/functions/_shared/focusSyncDataHandler.ts` around lines 297 - 316, The Lynk-path secret decryption in focusSyncDataHandler’s handleSyncData flow is not protected, so a decrypt failure can escape instead of returning a consistent Edge Function JSON error. Wrap the getEncryptionService()/encSvc.decrypt(connRow.api_secret_encrypted) block in try/catch, handle the failure the same way the legacy path handles FocusAuthError, and return a jsonError response with an appropriate status/message so the Lynk path matches the existing error-response convention.Source: Coding guidelines
supabase/functions/_shared/focusLynkClient.ts (1)
229-254: 🩺 Stability & Availability | 🔵 TrivialPer-day network latency can exceed the caller's backfill budget.
Each
fetchDatafeedcall can take up to ~60s worst case (30s POST + 30s blob GET), butprocessBackfillBatchonly checksbudgetMsbefore starting a day (not during). WithbudgetMsset to 12s (manual sync) or ~50s per connection (cron), one slow Focus/Azure response can make a single invocation run far longer than its nominal budget, risking edge-function execution-time limits being hit mid-write.Consider tightening
TIMEOUT_MSfor this use case or sharing a single deadline-basedAbortControlleracross both requests bounded by the caller's remaining budget, so worst-case latency per day stays proportional to the allotted slice.Also applies to: 325-343
🤖 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 `@supabase/functions/_shared/focusLynkClient.ts` around lines 229 - 254, The fetchDatafeed path in focusLynkClient should not use a fixed per-request timeout that can exceed the caller’s remaining backfill budget. Update the POST /api/lynk/sync flow and the related blob GET path it feeds (the same network pattern referenced by fetchDatafeed) to use a shared deadline-based AbortController or otherwise derive timeouts from the remaining budget passed through processBackfillBatch, so each day’s work stays within the caller’s slice. Keep the existing fetch/deps.fetch structure and syncUrl handling, but make TIMEOUT_MS or request cancellation budget-aware rather than independently allowing ~30s per request.supabase/functions/_shared/focusBulkSyncHandler.ts (2)
386-393: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winFailed connections never get
last_sync_timebumped, risking round-robin starvation.On any
processConnectionthrow (now including the new Lynk incomplete-credentials/sync-failure paths), the catch block only logs and records the error — it never updateslast_sync_time. Since rows are selectedORDER BY last_sync_time ASC NULLS FIRST LIMIT 5, a persistently failing connection will keep being re-selected ahead of healthy connections on every 6h run, starving the round-robin.focusBackfillSyncHandler.tsalready solves this exact problem with a best-effort fire-and-forgetlast_sync_timewrite in its catch block.🤖 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 `@supabase/functions/_shared/focusBulkSyncHandler.ts` around lines 386 - 393, The error path in focusBulkSyncHandler’s processConnection catch block is not advancing last_sync_time, so failing connections keep getting picked first by the ORDER BY last_sync_time ASC NULLS FIRST query. Update the catch block in focusBulkSyncHandler to perform the same best-effort fire-and-forget last_sync_time bump used by focusBackfillSyncHandler after logging/pushing the error, while keeping the existing error handling and result.errors behavior intact.
196-231: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winLynk sync failures don't persist
connection_status, unlike the legacy path and the sibling backfill cron.When Lynk credentials are incomplete (Line 197) or
processDayTransactionsfails for both recent days (Lines 228-231),processConnectionjust throws. The legacy path persistsconnection_status: 'error'onFocusAuthError(Lines 241-249), andfocusBackfillSyncHandler.tsexplicitly writesconnection_status='error'+last_erroron Lynk backfill failures. Here, the failure is only logged/pushed into the responseerrors[]array — thefocus_connectionsrow still reflects a healthy state, so the UI pollingconnection_status(per PR objectives) won't surface repeated incremental-sync failures to the user.♻️ Suggested fix: persist error state for Lynk incremental failures
const failed = results.find((r) => r.status === 'error'); if (failed?.status === 'error') { - throw new Error(failed.error ?? 'Focus transaction incremental sync failed'); + const message = failed.error ?? 'Focus transaction incremental sync failed'; + await deps.serviceClient + .from('focus_connections') + .update({ + connection_status: 'error', + last_error: message, + updated_at: new Date().toISOString(), + }) + .eq('id', row.id) + .eq('restaurant_id', row.restaurant_id); + throw new Error(message); }🤖 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 `@supabase/functions/_shared/focusBulkSyncHandler.ts` around lines 196 - 231, The Lynk incremental sync path in processConnection currently throws on missing credentials or when processDayTransactions fails, but it never updates the focus_connections row to an error state. Update this branch to persist connection_status: 'error' and a meaningful last_error before returning/throwing, mirroring the legacy FocusAuthError handling and the focusBackfillSyncHandler failure path. Use the existing symbols processConnection, processDayTransactions, and the focus_connections update logic so UI polling on connection_status reflects incremental-sync failures.tests/unit/focusSyncDataHandler.test.ts (1)
631-631: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace repeated
as anycasts with a properly-typed fixture.Static analysis flags
no-explicit-anyat all these call sites whereMOCK_CONNECTION_LYNK_BACKFILL/MOCK_CONNECTION_LYNK_INCREMENTALare cast toanywhen passed toserviceClientOpts.connection. This suggests theMockConnectiontype used bymakeServiceClientMockdoesn't yet include the Lynk fields (api_key,api_secret_encrypted,environment). Extending that type once would eliminate all ~17 casts.Also applies to: 644-644, 655-655, 676-676, 687-687, 701-701, 728-728, 746-746, 769-769, 784-784, 799-799, 821-821, 835-835, 849-849, 861-861, 873-873, 886-886
🤖 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 `@tests/unit/focusSyncDataHandler.test.ts` at line 631, The repeated `as any` casts on `serviceClientOpts.connection` indicate the shared mock type is missing the Lynk-specific fields. Update the `MockConnection` type used by `makeServiceClientMock` to include the `api_key`, `api_secret_encrypted`, and `environment` properties so `MOCK_CONNECTION_LYNK_BACKFILL` and `MOCK_CONNECTION_LYNK_INCREMENTAL` can be passed without casts. Then remove all the `as any` call sites in `focusSyncDataHandler.test.ts` and keep the fixtures strongly typed.Source: Linters/SAST tools
🤖 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/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md`:
- Around line 7-8: The live verification step currently hard-codes a
machine-specific absolute path, which leaks a private filesystem layout and
breaks portability. Update the verification note in this plan to use the same
repo-relative or env-var-based form used elsewhere in the document, and keep the
`FOCUS_API_KEY`/`FOCUS_API_SECRET`/`FOCUS_RESTAURANT_GUID` guidance intact
without exposing any values.
In
`@docs/superpowers/specs/2026-07-02-focus-picker-and-background-sync-design.md`:
- Around line 191-194: The cron backfill cap is inconsistent between this
section and the later binding resolution, so align the `processBackfillBatch`
limit with the final `maxDays` choice. Update the spec text near
`processBackfillBatch` to use the same cron value as §8.3, and make sure any
related references to `BUDGET_MS`, per-restaurant budget, and the cron batch
behavior in the surrounding design stay consistent.
- Around line 212-214: Update the Connection query in the relevant React Query
hook so polling stops as soon as a connection enters error, even if it is still
is_active and initial_sync_done is false. Adjust the conditional refetchInterval
logic to also check the connection status before returning 8000, and otherwise
return false; keep staleTime unchanged. Use the existing query setup and data
fields in the Connection query function/hook to locate the change.
- Around line 169-178: The custom-range flow still uses the obsolete
backgrounding path, which conflicts with the updated design and makes the
behavior ambiguous. Remove the scheduleBackground/waitUntil wording from the
custom range section and update the flow to be synchronous instead. Keep the
range-processing logic centered on processDateRangeTransactions and the
custom-range handling in focusTransactionSyncHandler or focusBackfillBatch, but
describe it as executing inline and returning the normal response path rather
than backgrounded:true.
In `@package.json`:
- Line 98: Upgrade the fast-xml-parser dependency in package.json from 4.5.6 to
a fixed 5.5.6+ release, since it parses untrusted Focus XML and the current
version is in the affected CVE range. Then verify
supabase/functions/_shared/focusDatafeedParser.ts still keeps DTD/entity
handling disabled when constructing or using the parser, and adjust the parser
configuration there if needed.
In `@supabase/functions/_shared/focusBackfillSyncHandler.ts`:
- Around line 356-376: The best-effort write to focus_connections currently
handles only the resolved error case in the `.then()` callback and can still
produce an unhandled rejection if the promise rejects before resolving. Update
the fire-and-forget update in `focusBackfillSyncHandler` to fail safely by
adding a `.catch()` on the Supabase chain (or wrapping the async write in a
try/catch helper) so network/runtime failures are swallowed and only logged,
preserving the intended never-throw behavior for this error-state write.
In `@supabase/functions/_shared/focusDatafeedParser.ts`:
- Around line 112-116: The XMLParser configuration in focusDatafeedParser should
disable entity processing for this external feed. Update the XMLParser options
in the parser setup to set processEntities to false unless entity/DOCTYPE
support is explicitly required, keeping the existing ignoreAttributes,
parseTagValue, and trimValues behavior unchanged.
In `@supabase/functions/_shared/focusTransactionSyncHandler.ts`:
- Around line 302-319: `processDayTransactions` and
`processDateRangeTransactions` are swallowing
`sync_focus_transactions_to_unified_sales` RPC failures by only calling
`console.warn`, so callers still see success even when `unified_sales` is out of
sync. Update the return shape in these functions to surface the RPC outcome (for
example a warning/failure flag or message) and propagate it from the shared sync
block that calls `deps.supabase.rpc('sync_focus_transactions_to_unified_sales',
...)`. Add tests for both the single-day and range paths to assert the
RPC-failure case is reported to the caller instead of being silently ignored.
In `@supabase/functions/focus-backfill-sync/index.ts`:
- Around line 35-48: The auth check in focus-backfill-sync is fail-open when
SUPABASE_SERVICE_ROLE_KEY is missing because serviceRoleKey becomes an empty
string and handleBackfillSync can accept an empty Bearer token. Change the flow
in the main try block and/or handleBackfillSync so missing
SUPABASE_SERVICE_ROLE_KEY is treated as a misconfiguration and immediately
rejected with a non-2xx response, before createClient or timingSafeEqual is
used. Keep the fix centered around handleBackfillSync, serviceRoleKey, and the
Authorization Bearer parsing so only a real configured secret can pass.
In `@supabase/migrations/20260701130000_focus_transactions_unified_sales.sql`:
- Around line 92-263: The sync logic in the per-check loop fixes item-level
orphans, but it still leaves whole-check rows behind when a `focus_orders` check
is voided or deleted, and the later aggregation still relies on `synced_at >=
v_sync_start`, which misses those removed checks. Add a check-level cleanup in
the same migration before the aggregation step to delete `unified_sales` rows
for `pos_system = 'focus'` whose `external_order_id` no longer matches any
`focus_orders` record in the processed date range, while preserving
`parent_sale_id IS NULL` so split rows stay intact. Then restore the aggregation
behavior to the `focus_daily_reports` approach from
`20260627150000_focus_sync_hardening.sql`, or otherwise ensure deleted dates are
explicitly re-aggregated.
In `@supabase/migrations/20260701140000_focus_transactions_cron.sql`:
- Line 13: The schedule comment is misleading because the cron expression
described by the text does not match the actual interval used by the focus
transactions job. Update the comment near the cron setup in the migration so it
accurately reflects the intended schedule for the focus transactions cron,
keeping it consistent with the cron expression used by the scheduling logic and
avoiding any mention of the wrong frequency.
In `@supabase/migrations/20260701150000_focus_transactions_integrity.sql`:
- Around line 12-24: In the previous migration file
(20260701130000_focus_transactions_unified_sales.sql), the cleanup logic for
unified_sales table (around lines 92-261) only handles pruning rows for checks
that exist in focus_orders. Add an explicit DELETE statement that removes
orphaned unified_sales rows where the corresponding focus_orders record no
longer exists, using the natural key columns (restaurant_id, business_date,
focus_check_id) in a NOT IN or LEFT JOIN condition to identify and delete rows
whose referenced focus_orders have been deleted.
In `@supabase/tests/46_focus_transactions_schema.sql`:
- Around line 124-188: The current checks in the focus transaction schema test
only confirm that SELECT and ALL policies exist on the transaction tables, but
they do not verify the actual policy predicates. Update the assertions around
pg_policies for focus_orders, focus_order_items, and focus_payments to inspect
the policy condition fields (such as qual and with_check) and confirm they match
the migration contract, specifically the user_restaurants membership and role IN
('owner', 'manager') logic.
- Around line 289-297: The card_last4 test only covers an overlong value, so the
PCI boundary in the focus_payments checks may still miss short or non-numeric
fragments. Update the SQL in the focus_transactions_schema test around the
throws_ok cases for focus_payments.card_last4 to add assertions for a too-short
value and a non-digit value, alongside the existing 5-digit case, so the
constraint is validated against all non-4-digit inputs.
In `@tests/unit/focusSyncB6.test.tsx`:
- Around line 203-222: The test name in FocusSync B6 is misleading because it
claims the destructive toast path is covered, but the body only checks the
disabled Sync button and that triggerManualSync is not called. Either rename the
test around renderFocusSync to reflect the disabled-button guard, or adjust the
test to exercise handleSync’s missing-date branch and assert mockToast is called
so the toast behavior is actually covered.
---
Outside diff comments:
In `@docs/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md`:
- Around line 165-178: The custom-range guidance is inconsistent with the
binding resolution because it still tells implementers to use
scheduleBackground/waitUntil even though custom range handling is meant to be
synchronous. Update the plan section that describes the custom-range flow so it
no longer references backgrounding for custom ranges and instead aligns with the
synchronous behavior already defined in the custom-range rules, keeping the
instructions consistent with the surrounding focus-picker and background-sync
plan.
- Around line 191-194: Make the backfill batch cap consistent across the plan
and its tests: the current maxDays value conflicts with the cron backfill limit
described in the plan’s backfill section. Update the backfill configuration in
the plan so the same maxDays limit is used everywhere, and align any test
expectations or references that mention the alternate value. Use the backfill
cron section and related test descriptions as the source of truth when updating
the maxDays setting.
In `@supabase/functions/_shared/focusSyncDataHandler.ts`:
- Around line 102-138: The CAS helper types in focusSyncDataHandler are missing
one step in the update chain, so the declared surface does not match the actual
.update(...).eq(...).eq(...).eq('sync_cursor', ...) usage. Update the chain
types around CasUpdateResult, CasEq1, CasEq2, and CasEq3 so the helper supports
the 4-step CAS flow used by the sync logic, matching the shape already used in
focusBackfillSyncHandler and allowing select() only after the extra eq() level.
---
Nitpick comments:
In `@src/hooks/useFocusConnection.tsx`:
- Around line 111-117: Update useFocusConnection’s useQuery options so the query
stays fresh for 30 seconds instead of forcing immediate staleness; in
useFocusConnection.tsx, change the staleTime setting in the hook’s query config
to 30_000 while keeping refetchInterval unchanged. This reduces extra
focus/remount refetches without affecting the backfill polling behavior.
In `@supabase/functions/_shared/focusBulkSyncHandler.ts`:
- Around line 386-393: The error path in focusBulkSyncHandler’s
processConnection catch block is not advancing last_sync_time, so failing
connections keep getting picked first by the ORDER BY last_sync_time ASC NULLS
FIRST query. Update the catch block in focusBulkSyncHandler to perform the same
best-effort fire-and-forget last_sync_time bump used by focusBackfillSyncHandler
after logging/pushing the error, while keeping the existing error handling and
result.errors behavior intact.
- Around line 196-231: The Lynk incremental sync path in processConnection
currently throws on missing credentials or when processDayTransactions fails,
but it never updates the focus_connections row to an error state. Update this
branch to persist connection_status: 'error' and a meaningful last_error before
returning/throwing, mirroring the legacy FocusAuthError handling and the
focusBackfillSyncHandler failure path. Use the existing symbols
processConnection, processDayTransactions, and the focus_connections update
logic so UI polling on connection_status reflects incremental-sync failures.
In `@supabase/functions/_shared/focusLynkClient.ts`:
- Around line 229-254: The fetchDatafeed path in focusLynkClient should not use
a fixed per-request timeout that can exceed the caller’s remaining backfill
budget. Update the POST /api/lynk/sync flow and the related blob GET path it
feeds (the same network pattern referenced by fetchDatafeed) to use a shared
deadline-based AbortController or otherwise derive timeouts from the remaining
budget passed through processBackfillBatch, so each day’s work stays within the
caller’s slice. Keep the existing fetch/deps.fetch structure and syncUrl
handling, but make TIMEOUT_MS or request cancellation budget-aware rather than
independently allowing ~30s per request.
In `@supabase/functions/_shared/focusSyncDataHandler.ts`:
- Around line 438-485: The incremental path in focusSyncDataHandler should
persist failures the same way the backfill branch does: after
processDayTransactions returns r1/r2, if status is error, update
focus_connections via deps.serviceClient to set connection_status to error and
last_error from the failure result before returning. Keep the existing timestamp
refresh for success/empty cases, and preserve the current CAS behavior around
the sync_cursor/readCursor update.
- Around line 297-316: The Lynk-path secret decryption in focusSyncDataHandler’s
handleSyncData flow is not protected, so a decrypt failure can escape instead of
returning a consistent Edge Function JSON error. Wrap the
getEncryptionService()/encSvc.decrypt(connRow.api_secret_encrypted) block in
try/catch, handle the failure the same way the legacy path handles
FocusAuthError, and return a jsonError response with an appropriate
status/message so the Lynk path matches the existing error-response convention.
In `@tests/unit/focusSyncDataHandler.test.ts`:
- Line 631: The repeated `as any` casts on `serviceClientOpts.connection`
indicate the shared mock type is missing the Lynk-specific fields. Update the
`MockConnection` type used by `makeServiceClientMock` to include the `api_key`,
`api_secret_encrypted`, and `environment` properties so
`MOCK_CONNECTION_LYNK_BACKFILL` and `MOCK_CONNECTION_LYNK_INCREMENTAL` can be
passed without casts. Then remove all the `as any` call sites in
`focusSyncDataHandler.test.ts` and keep the fixtures strongly typed.
In `@tests/unit/focusTransactionSyncHandler.test.ts`:
- Around line 166-535: Prefix the critical-path test cases in
processDayTransactions with CRITICAL: in their description strings, especially
the ones covering focus_orders upsert failure, items/payments batch upserts,
unified_sales RPC params, and voided-check deletes. Update the relevant it(...)
titles in focusTransactionSyncHandler.test.ts so they clearly identify
financial/business-critical behavior while leaving the test logic and assertions
unchanged.
🪄 Autofix (Beta)
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
Run ID: 0177592d-dfa2-4d47-b386-bd7725b877a2
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpublic/logos/focus.svgis excluded by!**/*.svg
📒 Files selected for processing (54)
docs/superpowers/plans/2026-07-01-focus-pos-transactions-plan.mddocs/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.mddocs/superpowers/specs/2026-07-01-focus-pos-transactions-design.mddocs/superpowers/specs/2026-07-02-focus-picker-and-background-sync-design.mdpackage.jsonsrc/components/FocusSync.tsxsrc/components/IntegrationLogo.tsxsrc/components/pos/FocusSetupWizard.tsxsrc/components/pos/SyncComponents.tsxsrc/hooks/useFocusConnection.tsxsupabase/config.tomlsupabase/functions/_shared/focusBackfillBatch.tssupabase/functions/_shared/focusBackfillSyncHandler.tssupabase/functions/_shared/focusBulkSyncHandler.tssupabase/functions/_shared/focusDatafeedParser.tssupabase/functions/_shared/focusListRestaurantsHandler.tssupabase/functions/_shared/focusLynkClient.tssupabase/functions/_shared/focusSaveConnectionHandler.tssupabase/functions/_shared/focusSyncDataHandler.tssupabase/functions/_shared/focusTestConnectionHandler.tssupabase/functions/_shared/focusTransactionSyncHandler.tssupabase/functions/deno.jsonsupabase/functions/focus-backfill-sync/index.tssupabase/functions/focus-bulk-sync/index.tssupabase/functions/focus-list-restaurants/index.tssupabase/functions/focus-save-connection/index.tssupabase/functions/focus-sync-data/index.tssupabase/functions/focus-test-connection/index.tssupabase/migrations/20260630120000_focus_focuslink_api.sqlsupabase/migrations/20260701120000_focus_transactions.sqlsupabase/migrations/20260701130000_focus_transactions_unified_sales.sqlsupabase/migrations/20260701140000_focus_transactions_cron.sqlsupabase/migrations/20260701150000_focus_transactions_integrity.sqlsupabase/migrations/20260701160000_focus_transactions_security.sqlsupabase/migrations/20260702120000_focus_backfill_cron.sqlsupabase/tests/45_focus_focuslink_api.sqlsupabase/tests/46_focus_transactions_schema.sqlsupabase/tests/47_focus_transactions_unified_sales.sqlsupabase/tests/48_focus_backfill_cron.sqltests/fixtures/focus-datafeed-sample.xmltests/unit/focusBackfillBatch.test.tstests/unit/focusBackfillSyncHandler.test.tstests/unit/focusBulkSyncHandler.test.tstests/unit/focusDatafeedParser.test.tstests/unit/focusListRestaurantsHandler.test.tstests/unit/focusLynkClient.test.tstests/unit/focusPosRegistration.test.tsxtests/unit/focusSaveConnectionHandler.test.tstests/unit/focusSetupWizard.test.tsxtests/unit/focusSyncB6.test.tsxtests/unit/focusSyncDataHandler.test.tstests/unit/focusTestConnectionHandler.test.tstests/unit/focusTransactionSyncHandler.test.tstests/unit/useFocusConnection.test.tsx
| **Live verify:** `source "/Users/josedelgado/Documents/Cold Stone Setup/focus-creds.env"` | ||
| (`FOCUS_API_KEY`/`FOCUS_API_SECRET`/`FOCUS_RESTAURANT_GUID`) — **never echo/print/commit the values**. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use a repo-relative verification path.
The hard-coded /Users/.../focus-creds.env only works on one machine and leaks a private filesystem layout into the repo. Please replace it with the env-var/repo-relative form used elsewhere in the doc.
🤖 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/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md`
around lines 7 - 8, The live verification step currently hard-codes a
machine-specific absolute path, which leaks a private filesystem layout and
breaks portability. Update the verification note in this plan to use the same
repo-relative or env-var-based form used elsewhere in the document, and keep the
`FOCUS_API_KEY`/`FOCUS_API_SECRET`/`FOCUS_RESTAURANT_GUID` guidance intact
without exposing any values.
| - **Custom range (new)**: if body has `{ startDate, endDate }` (ISO `YYYY-MM-DD`, ≤ 90 days, `start ≤ end`): | ||
| run it in the background via an injectable `deps.scheduleBackground(promise)`: | ||
| - index.ts passes `(p) => (globalThis as any).EdgeRuntime?.waitUntil?.(p) ?? p`. | ||
| - tests pass a collector (`(p)=>{ tasks.push(p) }`) and await it, so the range logic is fully tested. | ||
| - Range logic = `processDateRangeTransactions(deps, config, startDate, endDate, { maxDays: 90 })` | ||
| (new small helper in `focusTransactionSyncHandler.ts` or `focusBackfillBatch.ts`): iterate the explicit | ||
| date list, `processDayTransactions` each, then one `sync_focus_transactions_to_unified_sales(start,end)`. | ||
| Respond immediately `{ backgrounded:true, mode:'custom' }`. | ||
| - Note: custom-range uses `waitUntil` (not the durable cron), acceptable because ranges are small and | ||
| user-retriggerable; the important 90-day backfill is on the durable cron. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Remove the obsolete backgrounding path here.
This section still routes custom range through scheduleBackground/waitUntil, but §8.2 explicitly removes that path and makes custom range synchronous. Leaving both versions in the doc makes the intended behavior ambiguous.
🧰 Tools
🪛 LanguageTool
[style] ~174-~174: This is not the usual sequence for adjectives that have no special emphasis.
Context: ...rtDate, endDate, { maxDays: 90 }) (new small helper infocusTransactionSyncHandler....
(EN_ADJ_ORDER)
🤖 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/superpowers/specs/2026-07-02-focus-picker-and-background-sync-design.md`
around lines 169 - 178, The custom-range flow still uses the obsolete
backgrounding path, which conflicts with the updated design and makes the
behavior ambiguous. Remove the scheduleBackground/waitUntil wording from the
custom range section and update the flow to be synchronous instead. Keep the
range-processing logic centered on processDateRangeTransactions and the
custom-range handling in focusTransactionSyncHandler or focusBackfillBatch, but
describe it as executing inline and returning the normal response path rather
than backgrounded:true.
| - Connection query: add a **conditional `refetchInterval`** so progress updates while backfilling: | ||
| `refetchInterval: (q) => q.state.data && !q.state.data.initial_sync_done && q.state.data.is_active ? 8000 : false`. | ||
| Keep `staleTime: 30000`. (No manual caching — pure React Query, per CLAUDE.md.) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Stop polling errored connections here too.
This refetch interval keeps firing while is_active and !initial_sync_done, even if the connection has already entered error. That leaves the UI polling a dead sync until the later §8.5 rewrite lands.
🤖 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/superpowers/specs/2026-07-02-focus-picker-and-background-sync-design.md`
around lines 212 - 214, Update the Connection query in the relevant React Query
hook so polling stops as soon as a connection enters error, even if it is still
is_active and initial_sync_done is false. Adjust the conditional refetchInterval
logic to also check the connection status before returning 8000, and otherwise
return false; keep staleTime unchanged. Use the existing query setup and data
fields in the Connection query function/hook to locate the change.
| "date-fns": "^3.6.0", | ||
| "date-fns-tz": "^3.2.0", | ||
| "embla-carousel-react": "^8.6.0", | ||
| "fast-xml-parser": "^4.5.6", |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
set -euo pipefail
printf '\n== files ==\n'
git ls-files | rg '(^|/)(package\.json|focusDatafeedParser\.ts|tests/fixtures/focus-datafeed-sample\.xml|fast-xml-parser|xmlparser|xml-parser)' || true
printf '\n== search fast-xml-parser ==\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' 'fast-xml-parser|XMLParser|DOCTYPE|entity' .
printf '\n== package.json slice ==\n'
nl -ba package.json | sed -n '85,110p'Repository: toyiyo/nimble-pnl
Length of output: 50375
🏁 Script executed:
set -euo pipefail
echo '== focusDatafeedParser.ts =='
fd -a 'focusDatafeedParser.ts' . || true
echo '== focus-datafeed-sample.xml =='
fd -a 'focus-datafeed-sample.xml' . || true
echo '== package.json dependency context =='
nl -ba package.json | sed -n '90,105p'
echo '== fast-xml-parser usage search =='
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' 'fast-xml-parser|XMLParser|doctype|DOCTYPE|entity' .Repository: toyiyo/nimble-pnl
Length of output: 424
🏁 Script executed:
set -euo pipefail
echo '== focusDatafeedParser.ts matches =='
rg -n -C 4 'XMLParser|fast-xml-parser|doctype|DOCTYPE|entity' supabase/functions/_shared/focusDatafeedParser.ts
echo '== focusDatafeedParser.ts excerpt =='
sed -n '1,220p' supabase/functions/_shared/focusDatafeedParser.ts
echo '== package.json excerpt =='
sed -n '90,105p' package.json
echo '== package-lock fast-xml-parser excerpt =='
rg -n -C 2 '"fast-xml-parser"|node_modules/fast-xml-parser|resolved": ".*fast-xml-parser' package-lock.jsonRepository: toyiyo/nimble-pnl
Length of output: 8346
🌐 Web query:
fast-xml-parser 4.5.6 entity shadowing DoS advisory 4.1.3 5.3.5 CVE-2026-33036
💡 Result:
The vulnerability identified as CVE-2026-33036 is an XML entity expansion Denial of Service (DoS) flaw in the fast-xml-parser library [1][2]. It functions as an incomplete fix or bypass for the previous CVE-2026-26278 [3][1]. Vulnerability Summary: Versions 4.0.0-beta.3 through 5.5.5 are affected [1][2]. The issue stems from the replaceEntitiesValue function in OrderedObjParser.js, which only applies expansion limits (such as maxTotalExpansions and maxExpandedLength) to DOCTYPE-defined entities [3][1]. Numeric character references (e.g., &#NNN;, &#xHH;) and standard XML entities are processed via a separate code path that lacks these expansion counters [3][2]. This allows attackers to bypass configured limits by using large quantities of numeric entity references, leading to excessive memory allocation, high CPU consumption, and potential process crashes [3][1]. Related Advisories and Issues: - CVE-2026-33036: Numeric entity expansion bypass (Fixed in 5.5.6) [1][2]. - CVE-2026-26278: Original DoS through entity expansion in DOCTYPE (Affected: 4.1.3 to 4.5.4, 5.0.0 to 5.3.6; Fixed in 4.5.4, 5.3.6) [4][5]. - CVE-2026-25896: Entity encoding bypass via regex injection in DOCTYPE entity names (Affected: 4.1.3 to 5.3.4; Fixed in 4.5.4, 5.3.5) [6][7][8]. - CVE-2026-33349: Improper validation of specified quantity in input when limits are set to 0 (Fixed in 5.5.7) [9]. It is recommended to upgrade to the latest version of fast-xml-parser (at least 5.5.7 or higher) to address these and subsequent related security issues [9].
Citations:
- 1: https://osv.dev/vulnerability/CVE-2026-33036
- 2: https://www.tenable.com/cve/CVE-2026-33036
- 3: GHSA-8gc5-j5rx-235r
- 4: GHSA-jmr7-xgp7-cmfj
- 5: https://nvd.nist.gov/vuln/detail/CVE-2026-26278
- 6: GHSA-m7jm-9gc2-mpf2
- 7: https://nvd.nist.gov/vuln/detail/CVE-2026-25896
- 8: https://app.opencve.io/cve/CVE-2026-25896
- 9: https://security.snyk.io/vuln/SNYK-JS-FASTXMLPARSER-15699647
package.json:98 — upgrade fast-xml-parser off 4.5.6. This parser consumes untrusted Focus XML, and 4.5.6 is in the affected range for CVE-2026-33036. Move to a fixed release (5.5.6+) and keep DTD/entity handling disabled in supabase/functions/_shared/focusDatafeedParser.ts.
🤖 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 `@package.json` at line 98, Upgrade the fast-xml-parser dependency in
package.json from 4.5.6 to a fixed 5.5.6+ release, since it parses untrusted
Focus XML and the current version is in the affected CVE range. Then verify
supabase/functions/_shared/focusDatafeedParser.ts still keeps DTD/entity
handling disabled when constructing or using the parser, and adjust the parser
configuration there if needed.
| -- 20-22: SELECT policy — member can read (all three tables) | ||
| -- We verify by checking the policy rows in pg_policies. | ||
| -- ───────────────────────────────────────────────────────────────────── | ||
| SELECT ok( | ||
| EXISTS ( | ||
| SELECT 1 FROM pg_policies | ||
| WHERE schemaname = 'public' | ||
| AND tablename = 'focus_orders' | ||
| AND cmd = 'SELECT' | ||
| ), | ||
| 'focus_orders has a SELECT policy' | ||
| ); | ||
|
|
||
| SELECT ok( | ||
| EXISTS ( | ||
| SELECT 1 FROM pg_policies | ||
| WHERE schemaname = 'public' | ||
| AND tablename = 'focus_order_items' | ||
| AND cmd = 'SELECT' | ||
| ), | ||
| 'focus_order_items has a SELECT policy' | ||
| ); | ||
|
|
||
| SELECT ok( | ||
| EXISTS ( | ||
| SELECT 1 FROM pg_policies | ||
| WHERE schemaname = 'public' | ||
| AND tablename = 'focus_payments' | ||
| AND cmd = 'SELECT' | ||
| ), | ||
| 'focus_payments has a SELECT policy' | ||
| ); | ||
|
|
||
| -- ───────────────────────────────────────────────────────────────────── | ||
| -- 23-25: FOR ALL policy exists on each transaction table | ||
| -- ───────────────────────────────────────────────────────────────────── | ||
| SELECT ok( | ||
| EXISTS ( | ||
| SELECT 1 FROM pg_policies | ||
| WHERE schemaname = 'public' | ||
| AND tablename = 'focus_orders' | ||
| AND cmd = 'ALL' | ||
| ), | ||
| 'focus_orders has a FOR ALL (owner/manager) policy' | ||
| ); | ||
|
|
||
| SELECT ok( | ||
| EXISTS ( | ||
| SELECT 1 FROM pg_policies | ||
| WHERE schemaname = 'public' | ||
| AND tablename = 'focus_order_items' | ||
| AND cmd = 'ALL' | ||
| ), | ||
| 'focus_order_items has a FOR ALL (owner/manager) policy' | ||
| ); | ||
|
|
||
| SELECT ok( | ||
| EXISTS ( | ||
| SELECT 1 FROM pg_policies | ||
| WHERE schemaname = 'public' | ||
| AND tablename = 'focus_payments' | ||
| AND cmd = 'ALL' | ||
| ), | ||
| 'focus_payments has a FOR ALL (owner/manager) policy' | ||
| ); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Assert the policy predicate, not just its existence.
These checks only prove that a SELECT/ALL policy exists; a permissive policy with the wrong USING clause would still pass. Please verify the user_restaurants membership and role IN ('owner', 'manager') expressions from the migration contract via pg_policies.qual/with_check.
🤖 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 `@supabase/tests/46_focus_transactions_schema.sql` around lines 124 - 188, The
current checks in the focus transaction schema test only confirm that SELECT and
ALL policies exist on the transaction tables, but they do not verify the actual
policy predicates. Update the assertions around pg_policies for focus_orders,
focus_order_items, and focus_payments to inspect the policy condition fields
(such as qual and with_check) and confirm they match the migration contract,
specifically the user_restaurants membership and role IN ('owner', 'manager')
logic.
…st coverage
Security (CodeRabbit major × 2):
- focusDatafeedParser: add processEntities:false to XMLParser to prevent XXE/DoS
via crafted DTD entities in Focus datafeed XML (fast-xml-parser v5 option)
- focus-backfill-sync/index.ts: fail closed on missing SUPABASE_URL/SERVICE_KEY
to prevent Bearer timingSafeEqual('','') pass-through on misconfigured deploy
Stability (CodeRabbit minor × 3):
- focusBackfillSyncHandler: add .catch() to fire-and-forget error-state write
so an unexpected promise rejection is logged rather than silently dropped
- focusBulkSyncHandler: persist connection_status='error' + last_error when Lynk
incremental sync fails (matches legacy portal path and backfill cron behavior)
- focusBulkSyncHandler: bump last_sync_time in catch block so failed connections
don't monopolize round-robin ORDER BY last_sync_time ASC NULLS FIRST
Error persistence (CodeRabbit minor):
- focusSyncDataHandler: persist connection_status='error'+last_error when Lynk
incremental processDayTransactions fails; mirrors backfill error path
Test coverage (4 new tests, github-code-quality, CodeRabbit nit × 2):
- focusListRestaurantsHandler.test.ts: remove unused beforeEach import (lint fix)
- focusSyncB6.test.tsx: rename test title to match actual assertions (no toast)
- 46_focus_transactions_schema.sql: plan(36→38) + 2 new card_last4 cases
(too-short 3-digit and non-digit '12AB') to fully enforce PCI constraint
- focusBulkSyncHandler.test.ts: 2 new tests for error persistence + starvation fix
- focusSyncDataHandler.test.ts: 2 new tests for incremental error persistence
+ import focusLynkClient to control fetchDatafeed in error scenario
Doc fixes (CodeRabbit nit):
- 20260701140000_focus_transactions_cron.sql: fix comment '*/6' → '0 */6'
Full suite: 5314 tests pass, 0 TS errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Review triage complete (Phase 9d, commit Fixed (commit def8101)
DeclinedCodex P1 ( The backfill design deliberately skips the per-day RPC (CPU budget). The 6-hour Codex P1 ( The TypeScript layer deletes CodeRabbit nit ( The code comment at line 60 and 81 explains why CodeRabbit ( Intentional: the data is durably written to CodeRabbit ( The bulk-sync handler wraps the entire CodeRabbit ( Valid observation; the CodeRabbit ( The 4-step chain CodeRabbit ( Valid style suggestion but the CodeRabbit (plan + spec docs): The planning docs describe the pre-binding state; §8 binding resolutions supersede them. The implementation is correct (synchronous custom range, maxDays=7 in cron). Fixing historical docs doesn't affect correctness. CodeRabbit ( Checking |
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fill Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…om-range, budgets, a11y) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… review-hardened) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ds, Focus API call, response shaping
Adds the shared handler for the focus-list-restaurants edge function (task A1).
- POST body: restaurantId + apiKey + apiSecret + optional environment
- Auth: JWT → 401; role owner|manager → 403 (same FOCUS_ALLOWED_ROLES as test-connection)
- SSRF: isSafeUrl(baseUrl, FOCUSPOS_HOST_RE) guard before any outbound call
- sandboxBaseUrl from deps only — never from request body (§8.6)
- Focus-side failures (401/403/404/other) → HTTP 200 {success:false, error:<friendly>}
- Network errors → HTTP 200 {success:false}
- Success → HTTP 200 {success:true, restaurants:[{restaurant_guid, restaurant_name}]}
- Blank/missing restaurant_name defaults to the GUID (§4.1 step 8)
- Filters out items without a string restaurant_guid
- apiKey/apiSecret never logged on any code path (28-test assertion suite)
- 28 Vitest tests: RED → GREEN; full suite 5173 tests passing, zero regressions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntry (A2) Thin Deno entry that mirrors focus-save-connection: CORS pre-flight, userClient (JWT forwarded) + serviceClient (service role), passes globalThis.fetch and FOCUS_API_SANDBOX_URL env var as injectable deps. All business logic lives in focusListRestaurantsHandler (A1). config.toml gains [functions.focus-list-restaurants] verify_jwt=false. 5173 Vitest tests pass, zero regressions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…assthrough, refetchInterval polling
- Export FocusRestaurantOption type {restaurant_guid, restaurant_name} (design §4.2)
- Add listRestaurantsMutation calling focus-list-restaurants edge fn; handles all
three error shapes (transport reject, HTTP error, Focus-side data.error message)
- Fix triggerManualSync to accept and spread options ({startDate,endDate}) into the
invoke body; was silently dropping custom range (design §8.5 / Frontend critical #1)
- Add __focusRefetchInterval exported helper (polls 8000ms while backfilling; returns
false on done/inactive/error/null; design §8.5 / Frontend major #1)
- Wire refetchInterval into useQuery with the helper; staleTime stays 30000
- 16 new Vitest tests (34 total); zero regressions (5189 passed)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cker in setup wizard Implements design §4.3 + §8.5 (A4): step machine instructions→credentials→select→done. Removes RESTAURANT_GUID_PATTERN and the GUID text input; "Find my restaurant(s)" button calls listRestaurants and advances to a select step with auto-select-when-one and a shadcn Select for multiple results. aria-current="step" moved to role="listitem". Done step copy updated to background 90-day import framing. 18 new Vitest tests (40 total, 5207 suite total, zero regressions). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Budget/day-cap loop, cursor advance, initialSyncDone flag, injectable clock+processDayTransactions — fully tested with zero regressions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ransactions (B2) Replace per-row await loops in upsertItems/upsertPayments with a single array upsert per check (§8.4), collapsing hundreds of sequential DB round-trips per day into two. Add processDateRangeTransactions export (iterates explicit date list, skipUnifiedSalesSync each day, one RPC for the full range) with injectable processDayTransactions for testability. Add DateRangeSyncDeps type. 15 new Vitest tests (41 total in file); full suite 5245 tests, zero regressions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…custom-range + error status
- Lynk backfill path delegates to processBackfillBatch({budgetMs:12_000, maxDays:5})
for instant feedback; the durable 5-min cron finishes the rest server-side.
- Cursor write uses CAS (§8.1): .eq('sync_cursor', readCursor).select() guards
concurrent pg_cron ticks from clobbering each other.
- On batch error: persists connection_status='error' + last_error + last_error_at
so the frontend polling stops and the operator sees the stall (§8.3).
- Response includes backgrounded=true while initial_sync_done=false.
- Custom range (body startDate+endDate, Lynk only): validates both required,
start≤end, span≤14 days (§8.2 — synchronous, no waitUntil); calls
processDateRangeTransactions; returns {daysSynced, status}.
- Bad range (span>14, start>end, unparseable, missing one field) → HTTP 400.
- Portal path CAS-updated to use the same 3-eq chain for multi-tenant safety (§8.1).
- 17 new Vitest tests (44 total in file); full suite 5262 tests, zero regressions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ckfill engine)
New focusBackfillSyncHandler.ts: timing-safe Bearer gate, queries only
is_active+initial_sync_done=false+api_key IS NOT NULL (Lynk backfill rows),
round-robin LIMIT 5, processBackfillBatch per restaurant (maxDays=7,
per-restaurant budget from remaining wall time capped at 50s), CAS write
(§8.1), error status write on batch failure (§8.3), 2s inter-restaurant
sleep, isolated errors continue loop. Returns {processed,errors,elapsedMs}.
Thin focus-backfill-sync/index.ts mirrors focus-bulk-sync entry pattern.
config.toml: [functions.focus-backfill-sync] verify_jwt=false.
Migration 20260702120000_focus_backfill_cron.sql: idempotent unschedule
guard, */5 * * * * schedule, current_setting missing_ok GUC for graceful
auth on unset keys (§8.6). pgTAP 48_focus_backfill_cron.sql asserts cron
job exists on */5 * * * * schedule. 24 Vitest tests; full suite 5286
tests, zero regressions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add skip guard at the top of the isLynkPath block in processConnection:
if (!row.initial_sync_done) return { unchanged cursor/flag } so the 6-h
bulk-sync never races the 5-min focus-backfill-sync cron on sync_cursor.
Test-first (design §8.7): 4 new Vitest tests assert —
• backfilling Lynk row: processDayTransactions NOT called, cursor unchanged
• skipped row still counted as processed (no error surfaced)
• incremental Lynk row (initial_sync_done=true): still processed (2 calls)
• portal backfilling row (no api_key): still processed via processReportDay
Also mocks focusTransactionSyncHandler + focusLynkClient at module level
so future Lynk-path tests in this file don't require real network calls.
Full suite: 5290 tests, zero regressions (5286 before + 4 new).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ive progress, a11y Design §8.5 / Frontend critical #2 + major #4: FocusSync.tsx: - Remove dead state: syncProgress, totalDaysSynced, syncResult, SyncResult interface - Remove SyncProgressDisplay and SyncResults renders (no progress loop) - handleSync makes ONE call: recent/initial → triggerManualSync(restaurantId); custom → triggerManualSync(restaurantId, {startDate, endDate}) in yyyy-MM-dd - Toast changed to background message: "Running in the background. You can leave this page; it keeps going." SyncComponents.tsx — InitialSyncPendingAlert: - Collapse to single message path (drop hasProgress bifurcation) - New copy: "Importing your last 90 days in the background (N of 90). No need to keep this page open." - Wrap progress count in <span role="status" aria-live="polite" aria-atomic="true"> - Add Progress bar with aria-label="Sync progress" aria-valuemin/max - Remove dead "First sync pending" / "Click Sync Now to continue" branches 15 new Vitest tests (TDD: RED → GREEN); full suite 5305 tests, zero regressions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fix useQuery generic - Replace direct palette colors (green-100/700, text-primary, text-green-600) with semantic tokens (text-foreground, border-border/40, bg-muted/*) - Align typography to design scale: text-[14px]/[13px]/[12px] replacing text-sm/text-xs/text-2xl - Apply canonical card container style (rounded-xl border border-border/40 bg-muted/30) to ConnectionStatus and SyncResults - Apply canonical primary button style to SyncButton (h-9 px-4 rounded-lg bg-foreground) - Add aria-hidden to decorative icons in SyncProgressDisplay and HowSyncingWorksInfo - Fix useFocusConnection useQuery<FocusConnection | null> explicit generic so TypeScript resolves connection type correctly (was resolving to unknown due to refetchInterval cast) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dlers, unify timestamps Reuse: - focusListRestaurantsHandler: import FOCUS_ALLOWED_ROLES from focusReportClient instead of re-defining an identical Set locally Efficiency: - focusBackfillBatch: hoist todayInTz() call above the loop (timezone + now are constant per call; no need to rebuild Intl.DateTimeFormat on each iteration) - focusBackfillSyncHandler + focusSyncDataHandler: capture a single nowIso constant per update block so last_sync_time, updated_at, and last_error_at are guaranteed to share the same timestamp Simplification: - FocusSetupWizard: replace 'no-restaurants' string sentinel with a dedicated listEmpty boolean state (string overloaded two different meanings) - FocusSetupWizard: extract inline async Sync Now click handler to named handleSyncNow, consistent with handleFindRestaurants/handleSaveAndConnect/handleRetry - SyncComponents: extract capitalize() helper for config.dataLabel (3 call sites) Tests: 5305 passed, 0 regressions; typecheck: 0 errors Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e, UI copy - Thread FOCUS_API_SANDBOX_URL through BackfillSyncDeps / BulkSyncDeps / SyncDataDeps + index.ts entry points (Codex critical: sandbox connections were silently falling back to the production URL — focusApiBaseUrl §6) - Inspect CAS .select() result in focusBackfillSyncHandler and focusSyncDataHandler: skip incrementing processed / return stale data when 0 rows (another tick already won — spec §8.1; security + performance major) - Remove dead Lynk backfill block in focusBulkSyncHandler (lines 214-231); B5 skip guard at line 187 made it unreachable — security/maintainability/ sound-logic major - Fix SyncModeSelector description "max 90 days" → "max 14 days" to match the MAX_CUSTOM_RANGE_DAYS=14 server-side cap (sound-logic major) - Fix useFocusConnection staleTime: 0 (was 30000) so 8s refetchInterval actually fires network requests during backfill (performance minor) - Remove dead serviceClient variable in focus-list-restaurants/index.ts (OCR-rules / maintainability minor) - Merge duplicate focusLynkClient imports in focusBackfillSyncHandler (maintainability minor) - 2 new Vitest tests: CAS miss not counted as processed; sandboxBaseUrl threaded to txConfig.baseUrl; 5307 tests green, 0 TS errors Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tom-range sync Custom-range sync is processed synchronously by the edge function, so 'Import started' was misleading — the sync is already done when the toast appears. Use 'Sync complete' for custom mode, keep 'Import started' for recent/initial background kicks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…parsing error
- focus-backfill-sync/index.ts: escape cron schedule comment (*/5 was ending
block comment prematurely, causing ESLint parse failure)
- focusTransactionSyncHandler.test.ts: replace `as any` casts with typed
alternatives (TransactionSupabaseDeps, FetchDatafeedFn, Record<string,unknown>[],
unknown[], { error?: string }) — zero no-explicit-any errors in all new files
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…handleSyncNow, thread staleTime fix - focusBackfillBatch: add 'inprogress' to BackfillBatchResult.status union; first-day inprogress now surfaces without a completed-day status - focusBackfillSyncHandler: thread sandboxBaseUrl to txConfig.baseUrl; skip restaurant when CAS misses (0 rows updated = stale cursor); write error status on batch failure; add missing test coverage for new paths - FocusSetupWizard: add isSyncingNow guard in handleSyncNow to prevent concurrent sync kicks; add finally block to always reset flag - SyncComponents/useFocusConnection: staleTime 0 so 8s refetchInterval fires reliably during backfill (was 30000) - 15 new Vitest tests; full suite 5312 tests, zero regressions Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…st coverage
Security (CodeRabbit major × 2):
- focusDatafeedParser: add processEntities:false to XMLParser to prevent XXE/DoS
via crafted DTD entities in Focus datafeed XML (fast-xml-parser v5 option)
- focus-backfill-sync/index.ts: fail closed on missing SUPABASE_URL/SERVICE_KEY
to prevent Bearer timingSafeEqual('','') pass-through on misconfigured deploy
Stability (CodeRabbit minor × 3):
- focusBackfillSyncHandler: add .catch() to fire-and-forget error-state write
so an unexpected promise rejection is logged rather than silently dropped
- focusBulkSyncHandler: persist connection_status='error' + last_error when Lynk
incremental sync fails (matches legacy portal path and backfill cron behavior)
- focusBulkSyncHandler: bump last_sync_time in catch block so failed connections
don't monopolize round-robin ORDER BY last_sync_time ASC NULLS FIRST
Error persistence (CodeRabbit minor):
- focusSyncDataHandler: persist connection_status='error'+last_error when Lynk
incremental processDayTransactions fails; mirrors backfill error path
Test coverage (4 new tests, github-code-quality, CodeRabbit nit × 2):
- focusListRestaurantsHandler.test.ts: remove unused beforeEach import (lint fix)
- focusSyncB6.test.tsx: rename test title to match actual assertions (no toast)
- 46_focus_transactions_schema.sql: plan(36→38) + 2 new card_last4 cases
(too-short 3-digit and non-digit '12AB') to fully enforce PCI constraint
- focusBulkSyncHandler.test.ts: 2 new tests for error persistence + starvation fix
- focusSyncDataHandler.test.ts: 2 new tests for incremental error persistence
+ import focusLynkClient to control fetchDatafeed in error scenario
Doc fixes (CodeRabbit nit):
- 20260701140000_focus_transactions_cron.sql: fix comment '*/6' → '0 */6'
Full suite: 5314 tests pass, 0 TS errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…test files Matches the repo convention (focusSaveConnectionHandler.test.ts et al.) for Supabase client mocks; removes 60 lint errors introduced when review-fix commits added mock coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
def8101 to
f8dc402
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
supabase/functions/_shared/focusBulkSyncHandler.ts (1)
400-411: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftSkip
sync_cursoron Lynk backfill rows. This write can clobber a newer cursor fromfocus-backfill-sync: the Lynk backfill branch returnsrow.sync_cursorunchanged, so if the cron advances between the read and this update, bulk sync regresses the row back to the stale value. Omitsync_cursor/initial_sync_donefrom this path or add a CAS onsync_cursor.🤖 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 `@supabase/functions/_shared/focusBulkSyncHandler.ts` around lines 400 - 411, The update in focusBulkSyncHandler should not blindly write sync_cursor and initial_sync_done for Lynk backfill rows, since that can overwrite a newer cursor from focus-backfill-sync. In the bulk sync persistence path, use the existing identifiers row.sync_cursor and newSyncCursor to either omit sync_cursor/initial_sync_done from this update when the Lynk backfill branch is returning the cursor unchanged, or add a compare-and-swap guard on sync_cursor before calling .update(...).eq('id', row.id).eq('restaurant_id', row.restaurant_id).
🤖 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.
Outside diff comments:
In `@supabase/functions/_shared/focusBulkSyncHandler.ts`:
- Around line 400-411: The update in focusBulkSyncHandler should not blindly
write sync_cursor and initial_sync_done for Lynk backfill rows, since that can
overwrite a newer cursor from focus-backfill-sync. In the bulk sync persistence
path, use the existing identifiers row.sync_cursor and newSyncCursor to either
omit sync_cursor/initial_sync_done from this update when the Lynk backfill
branch is returning the cursor unchanged, or add a compare-and-swap guard on
sync_cursor before calling .update(...).eq('id', row.id).eq('restaurant_id',
row.restaurant_id).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8ac4e24f-0bb3-48f0-b770-d1bdb475d573
📒 Files selected for processing (31)
docs/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.mddocs/superpowers/specs/2026-07-02-focus-picker-and-background-sync-design.mdsrc/components/FocusSync.tsxsrc/components/pos/FocusSetupWizard.tsxsrc/components/pos/SyncComponents.tsxsrc/hooks/useFocusConnection.tsxsupabase/config.tomlsupabase/functions/_shared/focusBackfillBatch.tssupabase/functions/_shared/focusBackfillSyncHandler.tssupabase/functions/_shared/focusBulkSyncHandler.tssupabase/functions/_shared/focusDatafeedParser.tssupabase/functions/_shared/focusListRestaurantsHandler.tssupabase/functions/_shared/focusSyncDataHandler.tssupabase/functions/_shared/focusTransactionSyncHandler.tssupabase/functions/focus-backfill-sync/index.tssupabase/functions/focus-bulk-sync/index.tssupabase/functions/focus-list-restaurants/index.tssupabase/functions/focus-sync-data/index.tssupabase/migrations/20260701140000_focus_transactions_cron.sqlsupabase/migrations/20260702120000_focus_backfill_cron.sqlsupabase/tests/46_focus_transactions_schema.sqlsupabase/tests/48_focus_backfill_cron.sqltests/unit/focusBackfillBatch.test.tstests/unit/focusBackfillSyncHandler.test.tstests/unit/focusBulkSyncHandler.test.tstests/unit/focusListRestaurantsHandler.test.tstests/unit/focusSetupWizard.test.tsxtests/unit/focusSyncB6.test.tsxtests/unit/focusSyncDataHandler.test.tstests/unit/focusTransactionSyncHandler.test.tstests/unit/useFocusConnection.test.tsx
✅ Files skipped from review due to trivial changes (2)
- supabase/migrations/20260701140000_focus_transactions_cron.sql
- docs/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md
🚧 Files skipped from review as they are similar to previous changes (23)
- supabase/tests/48_focus_backfill_cron.sql
- supabase/functions/focus-sync-data/index.ts
- supabase/migrations/20260702120000_focus_backfill_cron.sql
- supabase/functions/focus-bulk-sync/index.ts
- tests/unit/focusBackfillSyncHandler.test.ts
- supabase/functions/focus-backfill-sync/index.ts
- tests/unit/focusSyncB6.test.tsx
- supabase/config.toml
- supabase/functions/focus-list-restaurants/index.ts
- tests/unit/focusSyncDataHandler.test.ts
- supabase/functions/_shared/focusBackfillSyncHandler.ts
- src/components/FocusSync.tsx
- supabase/functions/_shared/focusTransactionSyncHandler.ts
- src/components/pos/SyncComponents.tsx
- tests/unit/focusListRestaurantsHandler.test.ts
- supabase/functions/_shared/focusListRestaurantsHandler.ts
- tests/unit/focusTransactionSyncHandler.test.ts
- supabase/functions/_shared/focusBackfillBatch.ts
- tests/unit/focusBackfillBatch.test.ts
- supabase/tests/46_focus_transactions_schema.sql
- src/components/pos/FocusSetupWizard.tsx
- tests/unit/focusSetupWizard.test.tsx
- supabase/functions/_shared/focusSyncDataHandler.ts
Backfill days are written with skipUnifiedSalesSync:true, but both unified_sales crons only re-sync the last 2 business days — so backfilled history (days 3..90) would never reach unified_sales / P&L. processBackfillBatch now syncs its own processed date range via sync_focus_transactions_to_unified_sales after the loop, and a full 90-day window on completion as a reconciliation backstop. Non-fatal on RPC error (order rows are durably written). +4 tests. Also adds no-explicit-any eslint-disable header to 3 mock-heavy test files (repo convention) to clear 60 lint errors surfaced by the review-fix test coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review re-triage — post-rebase (commit
|
| Finding | Resolution |
|---|---|
Codex P1 — focusBackfillBatch.ts — Sync backfilled days into unified_sales |
Real bug, fixed. Backfill writes with skipUnifiedSalesSync:true, and both unified_sales crons only re-sync the last 2 business days — so backfilled history (days 3..90) never reached unified_sales/P&L. processBackfillBatch now calls sync_focus_transactions_to_unified_sales for its processed range after the loop, plus a full 90-day window on completion as a reconciliation backstop. Non-fatal on RPC error (order rows are durably written). +4 tests. |
❌ Declined — false positive (with evidence)
| Finding | Reason |
|---|---|
Codex P1 — focusTransactionSyncHandler.ts:284 — Remove unified_sales rows for deleted checks |
The RPC's Step 2 orphan-delete removes unified_sales rows via external_item_id NOT IN (SELECT … FROM focus_order_items WHERE business_date = …) (migration 20260701130000, lines 116/205/254). A voided check (its items CASCADE-deleted from focus_order_items) therefore has its sale/tip/discount rows orphan-deleted when the RPC runs for that date. The P1 fix above ensures that RPC now runs for backfilled ranges too. |
✅ Already addressed (verified in current code)
- CodeRabbit Security Major —
focus-backfill-sync/index.tsfail-open Bearer → the entry fails closed: returns 500 ifSUPABASE_SERVICE_ROLE_KEY/SUPABASE_URLis empty, before any auth compare. - CodeRabbit Minor —
focusBackfillSyncHandler.tsfire-and-forget update lacks.catch()→ the best-effort error-state write already has a.then(...).catch(...)chain. - github-code-quality —
focusListRestaurantsHandler.test.ts:18unusedbeforeEach→ already removed (import { describe, it, expect, vi }).
❌ Declined — by design (with rationale)
- CodeRabbit Major —
focusTransactionSyncHandler.ts:319unified_sales RPC failure swallowed, returns ok → Incremental path: the 6-hour transaction cron re-syncs the last 2 business days, so a transient RPC failure self-heals while thefocus_orders/items/paymentsrows are durably written. Backfill path: the new completion full-window sync (above) is the backstop. Making it fatal would strand durably-written order rows on a transient RPC blip.
⏭️ Out of scope — already-merged #563 code
CodeRabbit comments on supabase/migrations/20260701130000|140000|150000_*.sql, supabase/tests/46_focus_transactions_schema.sql, focusDatafeedParser.ts, and package.json target code already merged to main via #563 — not in this PR's diff. If any are real, they belong in a follow-up against main, not here.
📝 Nits (read, not actioned)
- Design/plan doc suggestions (
*-design.md,*-plan.md) — these are point-in-time design records; kept as-authored. focusSyncB6.test.tsx:222test-title wording — the test still validates the behavior via its assertions.
Re-verified locally on main: typecheck 0 · lint (changed files) 0 errors · 5,324 tests pass · build ✓.
…bbit Major, 9d) The B5 skip guard returned row.sync_cursor unchanged, but the persistence path still wrote it back — racing focus-backfill-sync: if the 5-min cron advanced the cursor between bulk-sync's read and this write, bulk-sync regressed it to the stale value (and spuriously bumped last_sync_time, perturbing the round-robin). processConnection now returns skipped:true and the caller writes nothing for skipped rows (still counted as processed). Test updated to assert no write. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up fix (commit
|
|
Summary
focus-list-restaurantsedge function,focusListRestaurantsHandler.ts,listRestaurantsmutation, and a step-by-step picker UI (credentials → select → done). Auto-selects single results; shows a labeled<Select>for multiple; falls back to "(name unavailable)" for nameless entries. Eliminates the UX dead-end that blocked non-technical operators.focus-backfill-syncedge function on a 5-minute pg_cron, replacing the old single-day-per-click model. Lynk backfill now advancessync_cursorup to 7 days per 5-min tick with a wall-clock budget (80 s).focus-bulk-synccedes Lynk backfilling to the new cron (skip guard). Manual "Sync Now" kicks a small 5-day batch and returns immediately. Custom date-range sync (≤14 days) is synchronous. Progress is polled passively viarefetchInterval(8 s while backfilling → stops on done/inactive/error)..eq('sync_cursor', readCursor)prevents concurrent-tick clobbering across cron and manual paths. CAS misses produce zero retries (accepted trade-off).skipUnifiedSalesSyncper-day during initial backfill with one unified-sales RPC at range end.FOCUS_API_SANDBOX_URLnow threaded to all three handler dep objects and their index.ts entry points — previously silently fell back to the prod URL.aria-live="polite"progress region;<Progress>witharia-label/valuemin/max.Test plan
npm run test— 5310 pass, 0 failnpm run typecheck— 0 errorsnpm run build— producesdist/without errorsnpm run test:db— 1497/1497 pass;focus-backfill-synccron job confirmed incron.jobon*/5 * * * *npm run test:e2e— 144–146 pass, 3 pre-existing failures (payroll/scheduling, unrelated to this branch)FOCUS_API_SANDBOX_URLenv var is wired in staging edge function configDesign doc
docs/superpowers/specs/2026-07-02-focus-picker-and-background-sync-design.md🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes