Skip to content

feat(connections): Mercury bank feed — setup, callback, inbox posting - #364

Merged
jfrench9 merged 4 commits into
mainfrom
feature/mercury-connection
Sep 12, 2026
Merged

feat(connections): Mercury bank feed — setup, callback, inbox posting#364
jfrench9 merged 4 commits into
mainfrom
feature/mercury-connection

Conversation

@jfrench9

Copy link
Copy Markdown
Member

Summary

The Mercury bank feed in the app: connect Mercury from the marketplace, and post the bank lines it captures from the inbox. Pairs with robosystems #1381 (the provider) and #1382 (the classify loop), both merged, over @robosystems/client 1.17.0. The provider is off in production until Mercury's compliance review clears the production client, so nothing new shows in the marketplace until the API flag is on.

Changes

  • Connections marketplace (connections/content.tsx, components/MercurySetupForm.tsx): a Mercury setup form with the backfill start date, a treasury toggle, and — only where the API advertises it (optional_config carries api_key, self-hosted deployments) — a personal read-only token in place of the OAuth sign-in. The two backend refusals are mapped to plain instructions: "sever QuickBooks first" and "initialize a chart of accounts first". The Mercury partner page (mercury.com/partner/robosystems, the referral link) is linked from the marketplace row and the form for anyone not yet banking there.
  • OAuth callback (connections/mercury-callback/page.tsx): the registered redirect target; mirrors the QuickBooks page without a realm, with a decline-at-Mercury path and a no-graph timeout.
  • Connection card (components/ConnectionCard.tsx): "Mercury" and "External" labels; the fiscal-calendar bootstrap state shows for Mercury as well as QuickBooks; the subtitle reads "Organization" for a bank feed.
  • Inbox (ledger/inbox/EventBlockDetailModal.tsx): a "Post to account" picker on bank-feed lines (bank_transaction, bank_fee, external_transfer): the chart's active accounts minus the bank leg, preselected with the feed's suggestion and its source. Approve carries the choice into the commit as metadata_patch.classified_element_id; Classify records it without posting; an unclassified line cannot be approved from the UI (the API refuses it too). A split recorded over MCP is shown and posted as is. Preview reflects the chosen account. Other event types are unchanged — this is what a bookkeeper posts from a bank line, so the picker never touches the amount or the bank leg.
  • Dependency: @robosystems/client ^1.17.0.

Deploy Notes

  • API first: needs a RoboSystems release carrying #1381 and #1382 (mercury provider, mercury_config, transition_to: 'classified'). Against an older API the marketplace simply never lists Mercury and the picker never renders (bank-feed event types do not exist), so a mis-ordered deploy degrades to today's behavior rather than breaking.
  • @robosystems/client 1.16.0 → 1.17.0: additive types only; no existing call site changed shape.
  • No env var, no CloudFormation change. The Mercury provider is gated server-side by CONNECTION_MERCURY_ENABLED.

Testing

  • npm run test:all green (format, lint, typecheck; 57 files, 423 tests). npm run build green (new route).
  • New tests: connections/__tests__/MercurySetupForm.test.tsx (create + OAuth redirect, api-key mode hidden on hosted, api-key connect skips OAuth, both refusal mappings, the partner link) and ledger/inbox/__tests__/EventBlockDetailModal.test.tsx (tree flattening, bank-leg exclusion, suggestion preselect, approve carries the patch, unclassified refused, classify records without posting, MCP split shown as-is, non-bank events untouched).
  • Not run: a browser pass against the Mercury sandbox; that is the whole-loop run owed after the API deploy.

🤖 Generated with Claude Code

https://claude.ai/code/session_0188CbjDiNBtxdEYjSJ5Y7mX

…rtner link

The Mercury provider in the connections marketplace: a setup form (backfill
start, treasury toggle, and — only where the deployment advertises it — a
personal read-only token in place of OAuth), the `/connections/mercury-callback`
redirect target, the provider label on the card, and the fiscal-calendar
bootstrap state for a Mercury-only tenant. The QuickBooks-conflict and
no-chart refusals from the backend are mapped to plain instructions. The
Mercury partner page (`mercury.com/partner/robosystems`) is linked from the
marketplace row and the form.

Types follow `@robosystems/client` 1.17.0 (`provider: 'mercury'`,
`mercury_config`); the bump lands with that release.

Claude-Session: https://claude.ai/code/session_0188CbjDiNBtxdEYjSJ5Y7mX
A bank-feed line (Mercury) arrives captured with a suggested account and
posts only once classified (robosystems #1382). The detail modal now
shows a "Post to account" picker for bank_transaction / bank_fee /
external_transfer events: the chart's active accounts minus the bank leg,
preselected with the feed's suggestion and its source. Approve carries
the choice into the commit as `metadata_patch.classified_element_id`;
Classify records it without posting; an unclassified line cannot be
approved from the UI. A split recorded over MCP is shown and posted as
is. Preview reflects the chosen account. Other event types are untouched.

Claude-Session: https://claude.ai/code/session_0188CbjDiNBtxdEYjSJ5Y7mX
…e classify transition

`provider: 'mercury'`, `mercury_config`, and `transition_to: 'classified'`
are generated-tier additions in 1.17.0; the setup form and the inbox
picker on this branch type against them.

Claude-Session: https://claude.ai/code/session_0188CbjDiNBtxdEYjSJ5Y7mX
@github-actions github-actions Bot added the change:normal Normal change: automated review documented on the pull request before merge label Sep 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Change classification: normal — platform or application code changed.

A normal change carries a documented automated review on this pull request before merge, in place of a second human approver. The change-classification job requests that review and passes once it has been posted. Recorded by the change-classification job.

@jfrench9

Copy link
Copy Markdown
Member Author

@claude please review this PR

@claude

claude Bot commented Sep 12, 2026

Copy link
Copy Markdown

Claude finished @jfrench9's task in 2m 54s —— View job


Reviewing PR #364

Todo list:

  • Gather context (PR body, diff, CLAUDE.md conventions)
  • Review MercurySetupForm.tsx + connections/content.tsx
  • Review mercury-callback/page.tsx
  • Review ConnectionCard.tsx changes
  • Review EventBlockDetailModal.tsx posting-to-account picker
  • Review new tests
  • Post final review feedback

Summary

This is a well-scoped, well-tested PR that follows the app's established conventions closely (error mapping via friendlyError, page.tsx/content.tsx split, refreshKey-style optimistic patterns, colocated tests). Didn't run npm run test:all myself — no node_modules installed in this checkout — but the code matches the described test coverage and I traced the logic manually against it.

Findings

1. Stale/invalid classifiedElementId isn't guarded against (low-moderate) — EventBlockDetailModal.tsx:187-197

classifiedElementId is seeded from metadata.classified_element_id (a prior classification) or suggested_element_id, independent of whether that id actually appears in the loaded accounts list:

setClassifiedElementId(prior || suggestedElementId)
...
const options = flattenAccountTree(...).filter((account) => account.id !== event?.resourceElementId)
setAccounts(options)

If that id belongs to an account that's since been deactivated (or is otherwise absent from options — e.g. it happens to equal the bank leg being excluded), the <select> shows "Choose an account…" (no option matches value), which reads as unclassified — but classifiedElementId is still truthy, so needsClassification is false and Approve will silently succeed, posting a classified_element_id that isn't a valid choice from the visible list. Worth either validating that the seeded id is in options once loaded (falling back to '' if not) or disabling Approve until the select value is confirmed present in the options.

2. Mercury callback swallows raw error messages — connections/mercury-callback/page.tsx:92-100

} catch (err) {
  console.error('Mercury callback error:', err)
  setError(err instanceof Error ? err.message : 'Failed to process Mercury callback')

This exactly mirrors qb-callback/page.tsx, so it's not a regression introduced here, but per CLAUDE.md's error-mapping convention ("Write surfaces predating this still pass raw err.message through; convert them as they're touched"), a raw SDK error here would still show the "<label> failed: {...}" JSON envelope to the user on any non-success:false failure path (e.g. a network error, or an unexpected exception from oauthCallback). Since this is a brand-new surface (not one predating friendlyError), it'd be worth routing through friendlyError(err.message) here rather than copying the pre-existing QB gap forward. Minor, since the common failure paths (response.data.success === false, access_denied, missing code/state) are already handled with clean copy above it.

3. No Mercury logo asset — ConnectionCard.tsx

PROVIDER_IMAGES only has a quickbooks entry, so Mercury connection cards render without a logo (the {image && <Image .../>} block simply skips). Likely deliberate/deferred (no asset shipped yet) rather than an oversight, but flagging in case a logo was expected to land with this PR. Fix this →

What looks solid

  • flattenAccountTree correctly drops inactive accounts, excludes the bank leg (resourceElementId), and sorts by code — verified against the test tree.
  • classificationPatch/needsClassification/hasSplit gating is consistent across preview, approve, and classify — non-bank event types and MCP-set splits correctly bypass the picker/patch entirely.
  • MercurySetupForm's error-message overrides are ordered correctly (chart-required checked before the QuickBooks-conflict message, since the chart-required copy also mentions "QuickBooks").
  • api_key mode correctly gates on optional_config from the provider list and skips OAuth entirely when a token is supplied.
  • Dependency bump (@robosystems/client 1.16.0 → 1.17.0) is clean and additive-only in both package.json and package-lock.json.
  • Good test coverage on both new files, including the "unclassified line can't be approved" and "MCP split shown as-is" cases called out in the PR description.

Review findings on #364: a classification seeded from the feed's
suggestion or a prior choice could name an account that is no longer on
the chart (retired since, or the bank leg itself). The select then showed
"Choose an account…" while Approve would still post it. The seeded id is
now checked against the loaded options and cleared when absent, so the
refusal covers it. The Mercury callback page maps SDK errors through
`friendlyError` instead of showing the raw envelope.

Claude-Session: https://claude.ai/code/session_0188CbjDiNBtxdEYjSJ5Y7mX
@jfrench9

Copy link
Copy Markdown
Member Author

Addressed:

  • Finding 1 — the seeded classifiedElementId is now checked against the loaded options once the chart arrives and cleared when absent (retired account, or the bank leg), so the select's "Choose an account…" and the Approve refusal agree. Test added: a suggestion that names a retired account reads as unclassified and Approve is refused.
  • Finding 2 — the callback page routes SDK errors through friendlyError rather than copying the QuickBooks page's raw pass-through.
  • Finding 3 — deliberate. The logo comes from Mercury's Media Kit under the partnership's brand terms; it lands as its own asset drop rather than a placeholder here.

@jfrench9
jfrench9 merged commit e93f902 into main Sep 12, 2026
7 checks passed
@jfrench9
jfrench9 deleted the feature/mercury-connection branch September 12, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:normal Normal change: automated review documented on the pull request before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant