feat(integrations): Stripe native integration + general integration framework#14
Merged
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…m source) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sconnect Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…elf-host integrations default-on Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… card Derive the Settings → Integrations catalog from integrationRegistry.catalog() via catalogForSettings() (single source of truth; drift test guards it). Add a client StripeConnectCard that paste-connects a restricted key (POST /api/integrations/stripe/connect), shows the scope checklist from the connector's credentialSpec help, surfaces inline errors, and renders status/last-sync/Disconnect when connected. Wire the card into the Stripe row, gated on caps.integrations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lift the CSV-import dedup+insert path into apps/web/src/lib/integrations/ingest.ts so the Stripe connector (next task) can reuse the same (companyId, externalId) dedup + in-batch Set + chunked insert. CSV import now builds its prepared rows then delegates dedup+insert to ingestRecords; categorization and account resolution stay in the route. Amounts stay strings across the money boundary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gory, gross+fee, exclusions) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ransactions Replace the C2.3 placeholder generators with real implementations that auto-paginate over Stripe balance_transactions (expand: data.source) and yield* the pure mapBalanceTransaction output per txn. incremental filters created > cursor.created (strict gt, unix seconds) so the high-water-mark txn isn't re-emitted; null cursor sweeps fully. Descriptor + validate unchanged. TDD: source.test.ts mocks getStripe, asserts charge->2 / payout->0 yields and the list params (created gt vs omitted). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ect + manual sync
Wire the connector source → account resolver → ingest core into real ingestion.
- accounts.ts: resolveAccountId(companyId, hint) — revenue/refund/dispute →
the company's income/revenue account; payment_processing_fees → an expense
account named "Payment processing fees" (find case-insensitive, else create
once, idempotent).
- sync.ts: runIntegrationSync(companyId, type, { mode }) — loads creds, reads
the cursor from integrations.metadata, iterates backfill/incremental, maps
each MappedRecord → IngestRow (amount as STRING, Decimal at the boundary,
source:"integration"), calls ingestRecords (dedups → re-run inserts 0),
advances the cursor to max record date, and read-modify-writes metadata.sync
(cursor/lastSyncAt/lastError/lastRecordCount) + lastSyncAt. Errors record
lastError and rethrow.
- sync/route.ts: POST gated exactly like connect → incremental sync.
Folds in two C1.5 review fixes on connect/route.ts:
- (A) connect no longer sets lastSyncAt — a SYNC owns it now.
- (B) one coherent integrations.metadata shape { livemode, sync? }; connect and
sync each merge (read-modify-write) so neither clobbers the other's fields.
Connect also kicks off a fire-and-forget backfill (not awaited).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ees account on multi-fee backfill) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… edit/delete) Guard test confirming that source:"integration" (Stripe-synced) rows: render their description text, show the "integration" SourcePill badge, and receive no Edit/Delete buttons — only manual rows get those affordances. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add runAllIntegrationSyncs() periodic driver: selects all active integrations across companies and runs an incremental runIntegrationSync for each, error-isolated (one failure is counted and does not stop the rest; the driver never throws). Register it as the "integration-sync" SYSTEM_JOB on an hourly (0 * * * *) UTC schedule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…S2322) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ogging
Wrap the outer db.select() in a try/catch so any DB error returns
{ synced: 0, failed: 0 } instead of propagating out of the SYSTEM_JOB's
run(). Replace console.error with structured logger (logger("integration-sync")).
Add regression test: db.select() rejection → resolves { synced:0, failed:0 }.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h UI Incremental sync now re-scans a 7-day trailing window (effective floor = min(cursor, now-7d)) so late mutations whose balance_transaction predates the cursor (refunds/disputes of old charges) are reliably re-ingested; idempotent via the existing (companyId, externalId) upsert. Backfill unchanged. Cursor made MONOTONIC: max(prevCursor, maxRecordDate) so a trailing re-scan of older records can never rewind the high-water mark (was a regression in C2.4). Surface connection health in the Stripe connect card: inline error indicator (reuses danger tokens + AlertCircle, role=alert) when metadata.sync.lastError is set, alongside the existing last-synced time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rror) live The connected Stripe row showed a generic inline Disconnect tile, so the StripeConnectCard's health view (last-sync + sync-error indicator) was never user-reachable, and the data layer didn't carry the last sync error. - Data: add `lastError` to `ConnectedIntegration`; project it from `metadata.sync.lastError` (written by runIntegrationSync) via `toConnectedIntegrations`, applied where the settings page derives the list from the verbatim `/api/integrations` rows. `lastSyncAt` stays sourced from the existing column. - Render: for a connected Stripe row, render `<StripeConnectCard connected />` (with last-sync + error + Disconnect) instead of the generic tile; wire Disconnect to `disconnectIntegration(id)`. Non-Stripe rows and the not-connected connect card are unchanged. Still gated on caps.integrations. - Test: integrations-tab reachability — lastError set -> role=alert renders; healthy -> formatted last-sync renders, no alert. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a context-only integrations domain whose only surface is an AI context contributor. It injects a "Connected data sources" section into the system prompt listing each ACTIVE integration with a relative last-sync time and the integration-sourced transaction count, so the assistant knows what's connected and how fresh it is. No new tools, handlers, prompt sections, or nav. core:true keeps it always-on and edition-agnostic; it adds nothing when no integration is connected. Graceful degradation: zero active integrations or any DB error returns [] and never throws out of sections(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ot per-bullet) Move the single integration-sourced transaction count from per-bullet to a section footer so the count is accurate regardless of number of integrations. Each bullet now shows only the integration label and last-sync time; the total appears once at the section level. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…of app registry Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… date-safe last-sync (M1/M2/M4)
M1: runIntegrationSync fires trackDataMutation("expenses") after inserts>0
(safe in fire-and-forget backfill); the manual sync route revalidates the
"accounts" + "expense-details" tags (request context, wrapped defensively).
M2: ingestRecords adds .onConflictDoNothing on (companyId, externalId) as a
DB-layer backstop so a concurrent backfill + hourly job can't abort a chunk.
M4: no change — fmtDate/formatDate already accept string|Date (confirmed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…type] (Next slug conflict with [id])
… SDK maxNetworkRetries handles 429s) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he Connections page Relocate the Integrations UI (Stripe connect + catalog) out of the Settings page and into the Connections page as a third, capability-gated tab (Connected MCPs · Your MCP · Integrations). The Integrations segment appears only when caps.integrations is on; ?tab=integrations falls back to "connected" when it is off. No behavior change to connect/sync/disconnect. The integrations data + mutation logic moves verbatim into a new client container under components/integrations/, which reads ?connect=stripe to auto-open the Stripe card. Old /settings?connect=stripe deep-links now redirect to /connections?tab=integrations&connect=stripe so the card still auto-opens. Files relocated under apps/web/src/components/integrations/ (integrations-tab, stripe-connect-card, integrations-data, plus the moved tests); new integrations-tab-container and connections-tabs unit test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ettings subtitle Post-smoke cleanup for the Settings→Connections move: - The relocated /connections integrations e2e asserted Plaid/QuickBooks coming-soon tiles, but no coming-soon connectors are registered today (only stripeConnector), so those tiles never render. Assert only the real Available tiles (Stripe + CSV Import); restoring coming-soon tiles is a separate tracked follow-up. - Settings subtitle no longer claims 'integrations' (that UI now lives on the Connections page). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gs when there is no Coming Soon - Order the catalog connectors-first ([...fromRegistry, ...STATIC_AVAILABLE]) so the live Stripe connector leads the Available list, CSV Import follows. - Show the 'Available' / 'Coming Soon' headings only when coming-soon connectors are actually registered. Today none are, so the list renders unlabelled (a single ungrouped set needs no header); the headings return automatically if coming-soon connectors are added later. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… Sync Three integration-UX improvements on the transactions list + connect card: 1. Source column shows the specific provider (e.g. Stripe), derived from the externalId prefix via the registry, instead of the generic 'integration'. 2. Stripe ingest now attributes a Vendor: the paying customer for charge/ refund/dispute rows (billing name → expanded customer name → email), and 'Stripe' for processing-fee rows. Connector expands data.source.customer. 3. Connected Stripe card gains a 'Sync now' button (design-system secondary Button) beside Disconnect — POSTs the incremental sync route and revalidates 'Last synced', reporting how many new transactions landed. Vendor populates on fresh ingest (idempotent re-sync won't rewrite existing rows). Source label is display-derived, so it applies to existing rows too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Workstream 3 — Stripe native integration + a general integration framework
Builds an integration framework (ingest-first, riding the foundation domain spine) with Stripe as the first connector. Cash-ledger ingest only; MRR/revenue-streams is a designed-for seam (deferred).
What it does
GET /v1/balance→ stored encrypted (integrationCredentials, AES-256-GCM viaSECRETS_ENCRYPTION_KEY).balance_transactions→ normalized into thetransactionsledger:reporting_category(nottype— disputes areadjustment).net; Decimal at the money boundary; zero-decimal currencies handled.(companyId, externalId)+onConflictDoNothing.integrationbadge.integrationscapability on by default (polling is outbound-only); cloud stays plan-gated.Framework
New app-level
integrationRegistry(apps/web/src/lib/integrations/) ofIntegrationConnectordescriptors withcredentialSpec+ an optionalsource(backfill/incremental). Outbound (actions) and inter-integration (dependsOn) are typed seams, unimplemented. The legacy engineProviderRegistryis deprecated.Schema
0014— newintegration_credentialstable (non-destructive; applies to PGlite + Postgres).Verification
next build✓; both editions.[id]vs[type]) that compiled but broke boot — fixed by relocating toconnect/[type]/sync/[type]/test/[type].Follow-ups (ship-as-v1, tracked)
Cosmetic double "Connected" badge; legacy generic-POST plan-gate consistency; tighten the catalog drift test to equality; disconnect-error toast; remove dead
withStripeBackoff; restore "Coming Soon" tiles if desired; replace the help-text scope parser with ascopes[]field.🤖 Generated with Claude Code