Skip to content

account dropdowns in QB settings #254

Merged
SandipBajracharya merged 10 commits into
previewfrom
OUT-3275
May 22, 2026
Merged

account dropdowns in QB settings #254
SandipBajracharya merged 10 commits into
previewfrom
OUT-3275

Conversation

@SandipBajracharya
Copy link
Copy Markdown
Collaborator

No description provided.

SandipBajracharya and others added 4 commits May 20, 2026 14:36
…pers

Adds the server side of the settings dashboard account-ref editor: a single
/api/quickbooks/accounts route handling GET (lists income/expense/bank
accounts from QBO plus the portal's currently-selected refs) and PATCH
(validates each provided ref against QBO and updates qb_portal_connections
scoped by portalId).

- IntuitAPI.getAccountsForProductMapping runs three flat queries (no OR,
  parens, or IN — QBO's parser rejects all three on AccountType).
- _getAnAccount SELECT now includes AccountType so PATCH validation can
  read it; QBAccountRowSchema tightened to require AccountType.
- TokenService.updateAccountRefs validates AccountType per bucket in
  parallel before delegating to updateQBPortalConnection.
- Response uses an allowlist (QBPortalConnectionSelectSchema.pick) so
  token fields can't leak.
- patchFetcher helper mirrors postFetcher (throws on non-OK).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a third accordion section below Invoice Details that lets the user
pick which QBO accounts the integration uses for income, expense, and
bank. Skips the /accounts fetch entirely when QB isn't connected
(syncFlag off or no portal connection) and surfaces a "Connect to
QuickBooks" message inline instead.

- useOtherSettings hook backs the section via SWR; submits only changed
  fields through patchFetcher and mutates the cache on success.
- Custom dropdown component mirrors the QuickBooks-items dropdown
  pattern (button + click-outside-to-close panel) instead of a native
  <select>; no search box or sticky options since the lists are short.
- SettingAccordion wires the new section with the existing
  Cancel/Update button pattern, gated on syncFlag and showButton.
- useSettings opens the new "other-settings" item by default for
  un-enabled portals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- IntuitAPI.getAccountsForProductMapping: asserts the three queries are
  built without OR / parens / IN (QBO parser limits) and that the asset
  bucket queries the Bank type.
- AccountService.listAccountsForProductMapping: happy path + 404 when
  portal connection is missing; verifies SyncToken/Active are stripped.
- TokenService.updateAccountRefs: per-bucket AccountType validation,
  rejects mismatches and missing accounts, writes scoped by portalId.
- GET /api/quickbooks/accounts: 200 happy path with TEST_*_ACCOUNT_REF
  constants; 401 without token.
- PATCH /api/quickbooks/accounts: single-field update, wrong-AccountType
  rejection, empty-body 422, all-three-valid, tenant isolation.
- Extends QBAccountRowSchema fixtures in intuitAPI.responses.test.ts
  with AccountType after the schema tightening.
- Pins integration mock factories on globalThis (per the documented
  setupFile re-entry workaround) and adds getAccountsForProductMapping
  to the IntuitAPI mock default so existing tests don't break.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- TokenService.updateAccountRefs: guard the post-update row destructure
  against undefined so a zero-row WHERE produces an explicit 500 instead
  of a misleading 422 from SafePortalConnectionSchema.parse(undefined).
- Replace inline 'Income'/'Expense'/'Bank' magic strings with a local
  QBO_ACCOUNT_TYPE map. The existing AccountTypeObj enum holds
  lowercase routing keys, not QBO API values, so reusing it would
  mislead — keep the maps distinct, documented inline.
- IntuitAPI.getAccountsForProductMapping: reuse QB_ACCOUNT_COLUMNS in
  the SELECT clauses so the schema/SQL stay in sync; drop the "no
  response from QBO" throw (customQuery already throws on no-response;
  the remaining undefined-QueryResponse case is now treated as an
  empty bucket so the UI shows "No matching accounts" instead of
  "Could not load"); rename assetOtherRaw → assetRaw.
- OtherSettings dropdown: add aria-haspopup="listbox", aria-expanded,
  aria-labelledby on the trigger and role="listbox"/role="option"/
  aria-selected on the panel so screen readers can navigate it.
- Add regression test for the undefined-bucket → empty path.

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

linear-code Bot commented May 20, 2026

OUT-3275

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 20, 2026

Greptile Summary

This PR adds an "Other Settings" accordion panel in the QuickBooks settings page, backed by a new GET /api/quickbooks/accounts and PATCH /api/quickbooks/accounts endpoint. The GET endpoint lists income, expense, and bank accounts from QBO grouped by type; the PATCH validates each provided ref's AccountType against QBO before persisting it to qb_portal_connections.

  • API layer: AccountService fetches three parallel QBO queries and strips internal fields before returning. TokenService.updateAccountRefs validates each ref type against QBO's enum and scopes the UPDATE to the authenticated workspace. The controller uses SafePortalConnectionSchema.pick() to ensure token/secret columns are never returned in the response.
  • UI layer: useOtherSettings skips the fetch when QB is disconnected or sync is off, seeds dropdown state from the SWR response, and sends only changed fields on PATCH. OtherSettings renders three custom AccountSelect dropdowns with ARIA attributes and a stale-ID guard.

Confidence Score: 4/5

Safe to merge with minor polish — the new endpoint correctly validates account types against QBO and strips sensitive fields before responding.

The API and service layers are well-guarded: type validation against QBO, tenant scoping via workspaceId, and a SafePortalConnectionSchema allowlist on the response. The main gap is on the frontend — submit failures are caught and logged but no error state is returned to the UI, so users get no feedback when a PATCH fails. There is also a duplicate AccountOption type definition across two files. Neither of these affects correctness of the data path.

src/hook/useSettings.ts — submit error handling and AccountOption duplication; src/type/common.ts — duplicate AccountOption definition that could drift from the hook's copy

Important Files Changed

Filename Overview
src/app/api/quickbooks/accounts/accounts.controller.ts New controller; uses SafePortalConnectionSchema.pick() to strip sensitive token fields from the PATCH response, and delegates to AccountService / TokenService correctly.
src/app/api/quickbooks/accounts/accounts.service.ts New AccountService; maps QBAccountRowType to {id,name} before returning — no sensitive fields exposed. Error handling on missing portal connection is correct.
src/app/api/quickbooks/token/token.service.ts Adds updateAccountRefs; validates each ref's AccountType against QBO before writing, with tenant scoping via workspaceId WHERE clause. Payload is parsed twice (controller + service) — redundant but harmless defensive pattern.
src/hook/useSettings.ts Adds useOtherSettings; SWR fetch correctly skipped when disconnected. Submit errors are caught but only logged — no error state is propagated to the UI. AccountOption type is defined locally but is identical to the one in common.ts.
src/components/dashboard/settings/SettingAccordion.tsx Adds Other Settings accordion entry; button rendering uses hardcoded index === 2, consistent with existing index === 0/1 pattern for other sections.
src/components/dashboard/settings/sections/other/OtherSettings.tsx New AccountSelect dropdown component; handles disconnected/loading/error/empty states, includes ARIA attributes, and guards against stale IDs with a 'no longer in QuickBooks' label.
src/utils/intuitAPI.ts Adds _getAccountsForProductMapping with three parallel QBO queries; bound without wrapWithRetry because inner customQuery calls already retry. Income query adds AccountSubType to SELECT correctly.
src/type/common.ts Adds AccountOption, AccountsListResponse, and AccountRefsUpdateSchema; AccountOption is also re-declared in useSettings.ts (duplication).

Sequence Diagram

sequenceDiagram
    participant UI as OtherSettings (UI)
    participant Hook as useOtherSettings
    participant GETAPI as GET /api/quickbooks/accounts
    participant PATCHAPI as PATCH /api/quickbooks/accounts
    participant AS as AccountService
    participant TS as TokenService
    participant DB as QBPortalConnection (DB)
    participant QBO as QuickBooks API

    UI->>Hook: mount (checks syncFlag + portalConnectionStatus)
    Hook->>GETAPI: SWR fetch (authenticated)
    GETAPI->>AS: listAccountsForProductMapping()
    AS->>DB: getPortalTokens(workspaceId)
    DB-->>AS: connection row with current refs
    AS->>QBO: 3x customQuery (Income / Expense / Bank)
    QBO-->>AS: account rows per bucket
    AS-->>GETAPI: options + selected refs
    GETAPI-->>Hook: JSON (no secret fields)
    Hook-->>UI: options + settingState

    UI->>Hook: user selects account
    UI->>Hook: submit
    Hook->>PATCHAPI: PATCH changed fields only
    PATCHAPI->>TS: updateAccountRefs(payload)
    TS->>DB: getPortalTokens(workspaceId)
    TS->>QBO: getAnAccount() per changed ref
    QBO-->>TS: account rows
    TS->>TS: assert AccountType matches expected bucket
    TS->>DB: "updateQBPortalConnection WHERE portalId=workspaceId"
    DB-->>TS: updated row
    TS-->>PATCHAPI: full connection row
    PATCHAPI->>PATCHAPI: SafePortalConnectionSchema strips secrets
    PATCHAPI-->>Hook: safe portalConnection object
    Hook->>Hook: mutate SWR cache + update initialState
Loading

Reviews (1): Last reviewed commit: "fix(OUT-3275): address pre-push review" | Re-trigger Greptile

Comment thread src/hook/useSettings.ts Outdated
Comment thread src/hook/useSettings.ts
AccountOption was declared identically in src/type/common.ts and
src/hook/useSettings.ts. Drift risk: any addition to one shape would
silently diverge from the other across the client/server boundary.

Drop the local declaration in useSettings.ts; import the canonical
AccountOption from @/type/common. OtherSettings.tsx imports the type
directly from @/type/common too so each consumer reads the same source.

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

vercel Bot commented May 20, 2026

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

Project Deployment Actions Updated (UTC)
quickbooks-sync (dev) Ready Ready Preview, Comment May 22, 2026 5:36am

Request Review

…ocess

Per review feedback: variable names should answer "what is this value?",
not "what function ran to produce it?". Renamed in the controller +
service:

- `parsed` → `accountRefs`
- `data` → `accountsResponse`
- `payload` (local, not parameter) → `accountRefs`
- `conn` → `updatedConnection`
- `safe` → `safePortalConnection`

No behavior change. Tests + tsc + lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SandipBajracharya and others added 2 commits May 21, 2026 14:31
The PATCH endpoint is dashboard-only. Every account ref the dashboard
sends came from GET /api/quickbooks/accounts, which itself reads from
QBO. Re-fetching each ref to validate AccountType is redundant work
(up to 3 QBO API calls per save) defending against scenarios that
require either a dashboard bug or direct API misuse — neither of which
this layer is the right place to defend against.

- TokenService.updateAccountRefs: drop the per-ref getAnAccount +
  AccountType check; just write the refs scoped by portal_id and
  return the updated row.
- Lift the 404 into getPortalTokens itself (throws APIError(404) on
  missing connection) so callers don't need to try/catch and rewrap.
- accounts.service.ts: drop the now-redundant try/catch around
  getPortalTokens; the APIError it throws already has the right shape.
- accounts.controller.ts: inline SafePortalConnectionSchema.parse into
  the response.
- Tests: remove the now-stale "wrong AccountType" assertions (4 unit
  cases + 1 integration case); simplify the survivors to drop unused
  getAnAccount mocks.

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

Four of the five useSwrHelper callers in the settings dashboard pass the
same { suspense: true, revalidateOnMount: false } overrides. Move them
into the helper's defaults; callers can still override per-call.

useOtherSettings keeps an explicit { suspense: false, revalidateOnMount:
true } override because its loading/error state must stay inline to the
accordion section. Without it, SWR's suspense bubble would hit the
page-level loading.tsx and flash a full-screen spinner over the rest of
the dashboard on first open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Callers can now pass useSwrHelper<MyType>(url) and `data` is typed as
MyType | undefined instead of any. Default T = any keeps existing
call sites working without changes.

useOtherSettings adopts the generic and drops its `as ...` cast on
data?.options. The other consumers in this file (product mapping,
invoice detail) stay on the implicit any default and can be migrated
incrementally as they're touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renames the third settings accordion ("Other Settings" → "Account Mapping")
end-to-end: section header, accordion id, file/folder path, component, hook,
state/props types, handlers, and destructure aliases. Also updates the
missing-option placeholder copy to "Please select <label>" instead of
surfacing the opaque QBO ref id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@SandipBajracharya SandipBajracharya merged commit f455a10 into preview May 22, 2026
5 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.

1 participant