Skip to content

OUT-3574: daily cron to auto-refresh QBO refresh tokens - #239

Merged
SandipBajracharya merged 2 commits into
masterfrom
OUT-3574
May 6, 2026
Merged

OUT-3574: daily cron to auto-refresh QBO refresh tokens#239
SandipBajracharya merged 2 commits into
masterfrom
OUT-3574

Conversation

@SandipBajracharya

Copy link
Copy Markdown
Collaborator

Summary

  • Idle portals previously needed manual re-auth once their ~100-day refresh token lapsed. New daily Vercel cron at GET /api/quickbooks/refresh-tokens (06:00 UTC) sweeps any portal with qb_settings.sync_flag = true whose refresh token expires within 14 days and rotates it via the existing race-aware getRefreshedQbTokenInfo helper.
  • 14-day lead × daily run = comfortable headroom for extended outages (broken deploy across a long weekend, Intuit incident) before any portal actually expires. Refreshing earlier than strictly needed is the same shape of API call, so the conservative window costs nothing.
  • Per-run cap of 150 portals fits the non–Fluid-Compute 300 s function budget. Soonest-to-expire ordering means urgency is preserved if a backlog ever exceeds the cap.

Code sites

  • Selector: src/db/service/token.service.tsgetPortalsWithExpiringRefreshTokens (INNER JOIN on qb_settings.sync_flag = true)
  • Endpoint: src/app/api/quickbooks/refresh-tokens/{route.ts, refresh-tokens.controller.ts, refresh-tokens.service.ts}
  • Cron: vercel.json0 6 * * *

Out of scope (intentional)

  • Auto-suspending revoked portals — isSuspended is dead code today; revocation is logged + Sentry-captured + counted but doesn't mutate state.
  • IU notification on revocation — separate ticket.

Test plan

  • Unit tests for the loop driver, error isolation, and revocation counter
  • Integration test for the SQL interval predicate, soonest-first ordering, and end-to-end persistence
  • yarn tsc --noEmit clean; yarn lint:check shows only pre-existing warnings
  • Verify the cron run on Vercel after deploy (check Sentry + logs for scanned/refreshed summary)

🤖 Generated with Claude Code

…expiry

Idle portals previously needed manual re-auth once their ~100-day refresh
token lapsed. New daily cron at /api/quickbooks/refresh-tokens sweeps any
portal with sync_flag=true whose refresh token expires within 14 days and
rotates it via the existing race-aware getRefreshedQbTokenInfo helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Apr 30, 2026

Copy link
Copy Markdown

@vercel

vercel Bot commented Apr 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
quickbooks-sync Building Building Apr 30, 2026 9:29am
quickbooks-sync (dev) Ready Ready Preview, Comment Apr 30, 2026 9:29am

Request Review

@SandipBajracharya SandipBajracharya changed the title feat(OUT-3574): daily cron to auto-refresh QBO refresh tokens OUT-3574: daily cron to auto-refresh QBO refresh tokens Apr 30, 2026
@greptile-apps

greptile-apps Bot commented Apr 30, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a daily Vercel cron (GET /api/quickbooks/refresh-tokens, 06:00 UTC) that proactively rotates QBO refresh tokens expiring within 14 days for portals with sync_flag = true, capped at 150 portals per run ordered by urgency. The implementation reuses the existing race-aware getRefreshedQbTokenInfo helper and isolates per-portal errors so a single revocation or network blip doesn't abort the batch. Overall the code is well-structured and well-tested.

Confidence Score: 4/5

Safe to merge; two P2 findings worth addressing but neither blocks correctness under current Intuit TTL behaviour.

Only P2 findings present: ORDER BY uses tokenSetTime as a proxy for expiry order (works because Intuit uses a uniform TTL today, but would break if TTLs ever diverge), and sequential processing of 150 portals at ~2 s each sits right at the 300 s Vercel timeout with no safety margin. No P0/P1 issues found.

src/db/service/token.service.ts (ORDER BY semantics) and src/app/api/quickbooks/refresh-tokens/refresh-tokens.service.ts (batch timeout headroom)

Important Files Changed

Filename Overview
src/db/service/token.service.ts Adds getPortalsWithExpiringRefreshTokens with a correct SQL interval predicate and INNER JOIN on qb_settings; ORDER BY tokenSetTime is a proxy for expiry order that only holds when all portals share the same XRefreshTokenExpiresIn TTL.
src/app/api/quickbooks/refresh-tokens/refresh-tokens.service.ts Well-structured batch loop with per-portal error isolation; sequential processing leaves almost no headroom against the 300 s Vercel function timeout at the 150-portal cap.
src/app/api/quickbooks/refresh-tokens/refresh-tokens.controller.ts Correctly guards against missing CRON_SECRET env var using an explicit !cronSecret check before the Bearer comparison to prevent the "Bearer undefined" bypass.
src/app/api/quickbooks/refresh-tokens/route.ts Minimal route file; sets maxDuration = 300, uses withErrorHandler, delegates to the controller. No issues.
test/integration/quickbooks/refreshTokens/expiringSweep.test.ts Good end-to-end integration coverage: auth rejection, SQL interval predicate, soonest-first ordering, DB persistence, and all three exclusion paths (soft-deleted, no-settings, syncFlag=false).
test/unit/api/quickbooks/refresh-tokens/refresh-tokens.service.test.ts Comprehensive unit tests cover the loop driver, error isolation, revocation counting, and empty-batch case; hoisted mock for QBReconnectRequiredError avoids DB-import issues cleanly.
vercel.json Adds daily 06:00 UTC cron entry; missing trailing newline at end of file.

Sequence Diagram

sequenceDiagram
    participant Vercel as Vercel Scheduler (06:00 UTC)
    participant Route as GET /api/quickbooks/refresh-tokens
    participant Controller as refreshExpiringTokensCron
    participant Service as refreshExpiringTokens
    participant DB as PostgreSQL
    participant Intuit as Intuit OAuth API

    Vercel->>Route: GET (Authorization: Bearer CRON_SECRET)
    Route->>Controller: withErrorHandler(refreshExpiringTokensCron)
    Controller->>Controller: Validate Bearer token
    Controller->>Service: refreshExpiringTokens()
    Service->>DB: getPortalsWithExpiringRefreshTokens(14 days, 150)
    DB-->>Service: portals[] (expiring soonest-first, up to 150)
    loop for each portal
        Service->>Intuit: getRefreshedQbTokenInfo(portalId, row)
        Intuit-->>Service: new access_token + refresh_token
        Service->>DB: UPDATE qb_portal_connections SET tokens, tokenSetTime
        alt QBReconnectRequiredError
            Service->>Service: reconnectRequired++, Sentry.captureException
        else other error
            Service->>Service: errored++, Sentry.captureException
        end
    end
    Service-->>Controller: scanned/refreshed/reconnectRequired/errored
    Controller-->>Route: NextResponse.json success + summary
    Route-->>Vercel: 200 OK
Loading

Reviews (1): Last reviewed commit: "feat(OUT-3574): daily cron to auto-refre..." | Re-trigger Greptile

Comment thread src/db/service/token.service.ts
Comment thread src/app/api/quickbooks/refresh-tokens/refresh-tokens.service.ts
Comment thread vercel.json
Intuit calls go through withRetry, so a flaky/rate-limited window can push
per-portal latency to 5+ s. 150 × 2 s sat exactly on the 300 s Vercel cap
with no room for jitter. 120 finishes in ~120 s at typical 1 s/portal and
still gets ~60 portals through at a pessimistic 5 s/portal before timeout.
Any deferred portals roll into the 14-day lead window, no token expires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@priosshrsth priosshrsth left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm

@SandipBajracharya
SandipBajracharya merged commit 2726950 into master May 6, 2026
4 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