Date: 2026-02-09Documentation Index
Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
Use this file to discover all available pages before exploring further.
Related Spec: FA-20 (Plaid Bank Feed Integration)
Branch:
cursor/plaid-balances-transactions-7b69
Executive Summary
After thorough review of the Plaid integration code against the Plaid API documentation and the database schema, 7 critical bugs were identified that collectively prevent balances and transactions from being posted. All bugs have been fixed in this branch. The root cause is a schema-code mismatch: the edge functions were written assuming columns and constraints that don’t exist on the actual database tables. The code was never validated against the live schema after the initial migration.Bugs Found & Fixed
Bug 1: Missing status Column on fa_bank_statement_lines (CRITICAL)
Impact: Every transaction insert/update fails silentlyLocation:
supabase/functions/plaid-sync/index.ts lines 311, 355, 376Location:
supabase/functions/plaid-webhook/index.ts line 229
Problem:The
plaid-sync edge function wrote status: normalized.is_pending ? 'pending' : 'posted' and the webhook handler wrote status: 'removed', but the fa_bank_statement_lines table has no status column. The original table creation (migration 20251127) defines only: id, statement_id, line_number, transaction_date, post_date, description, amount, transaction_type, check_number, reference_number, is_matched, matched_at, custom_fields. Subsequent migrations added organization_id, teller_transaction_id, and plaid_transaction_id but never a status column.
When Supabase receives an upsert with an unknown column, the operation fails with a Postgres error.
Fix:Moved the pending/posted/removed status into
custom_fields.plaid_status. This follows the existing pattern where custom_fields JSONB stores provider-specific metadata.
Bug 2: transaction_type CHECK Constraint Violation (CRITICAL)
Impact: Every transaction insert fails with constraint violationLocation:
supabase/functions/_shared/plaid-client.ts function mapTransactionType()
Problem:The
mapTransactionType() function returned rich types ('check', 'ach', 'pos', 'fee', 'interest', 'transfer') based on Plaid transaction characteristics, but fa_bank_statement_lines.transaction_type originally had a CHECK constraint limited to CHECK (transaction_type IN ('debit', 'credit')). Any value other than ‘debit’ or ‘credit’ caused a constraint violation.
Fix:Expanded the CHECK constraint on
fa_bank_statement_lines.transaction_type to support the full set of types: 'debit', 'credit', 'check', 'ach', 'wire', 'pos', 'fee', 'interest', 'transfer'. The mapTransactionType() function now returns these richer types directly. Additional provider-specific metadata (e.g., check numbers) is stored in custom_fields.check_number.
Bug 3: Column Name Mismatch current_balance vs balance (CRITICAL)
Impact: Balance not saved when connecting Plaid accountsLocation:
supabase/functions/plaid-exchange-token/index.ts line 211
Problem:The token exchange function wrote
current_balance: currentBalance || 0 but the actual column on fa_bank_accounts is named balance (not current_balance). This means the initial balance from Plaid was silently dropped.
Fix:Changed to
balance: currentBalance || 0.
Bug 4: Missing Required NOT NULL Fields (CRITICAL)
Impact: Account creation fails with NOT NULL constraint violationLocation:
supabase/functions/plaid-exchange-token/index.ts lines 203-222
Problem:The
fa_bank_accounts table requires:
account_number VARCHAR(50) NOT NULLgl_account_id UUID NOT NULL REFERENCES fa_accounts(id)
last_four: lastFour (which is not a column on fa_bank_accounts) and completely omitted gl_account_id.
Fix:
- Set
account_numbertolastFour || '0000'(the last 4 digits of the bank account, or a placeholder) - Auto-discover a GL cash account by looking for active accounts with “cash” in the name, falling back to any active account
Bug 5: account_type CHECK Constraint Violation (CRITICAL)
Impact: Account creation fails for credit, investment, and loan accountsLocation:
supabase/functions/plaid-exchange-token/index.ts function mapPlaidAccountType()
Problem:The original mapper returned values like
'credit_card', 'money_market', 'investment', 'loan', 'other' but the CHECK constraint only allows 'checking' | 'savings' | 'payroll' | 'reserve'.
Fix:All Plaid types now map to one of the four allowed values:
- depository/checking → checking
- depository/savings → savings
- depository/money market → savings
- credit → checking (safe default)
- loan → reserve
- investment → reserve
Bug 6: Frontend Response Field Name Mismatch (HIGH)
Impact: Frontend always shows 0 transactions synced even when sync succeedsLocation:
src/platform/banking/providers/plaid/usePlaidSync.ts lines 73-79
Problem:The edge function returns snake_case JSON keys (
transactions_added, transactions_modified, transactions_removed, has_more) but the frontend hook tried to read camelCase properties (transactionsAdded, transactionsModified, etc.). This meant the result always showed 0 changes.
Fix:Added fallback to read both snake_case and camelCase:
data?.transactions_added ?? data?.transactionsAdded ?? 0.
Bug 7: Balance Never Updated After Sync (HIGH)
Impact: Bank balance card always shows initial balance, never refreshesLocation:
supabase/functions/plaid-sync/index.ts
Problem:The sync function only updated
plaid_cursor and plaid_last_synced_at after syncing transactions, but never fetched or updated the account balance. Plaid’s /transactions/sync endpoint doesn’t include balance data — you must separately call /accounts/get to get current balances.
Fix:After syncing transactions, the function now calls
getAccounts() with the access token to fetch fresh balance data, then updates fa_bank_accounts.balance. The balance is also included in the sync response for the frontend to display.
Recommendations for Improvement
All 10 recommendations have been implemented. See commits on branch cursor/plaid-balances-transactions-7b69.
1. Add a status Column to fa_bank_statement_lines — IMPLEMENTED
Migration 20260209120000_plaid-schema-improvements.sql adds the column with CHECK constraint. Edge functions now write directly to status:
- Filtering out pending/removed transactions in reconciliation
- Proper bank rec matching on only posted transactions
- Querying removed transactions for audit purposes
2. Expand account_type CHECK Constraint — IMPLEMENTED
Migration adds money_market, credit_card, loan, investment, other. The plaid-exchange-token mapper now uses these values directly:
3. Expand transaction_type for Bank Statement Lines — IMPLEMENTED
Migration widens the column to TEXT and adds check, ach, wire, pos, fee, interest, transfer. mapTransactionType() now returns these richer types:
4. Auto-Sync on Webhook (Instead of Just Logging) — IMPLEMENTED
Theplaid-webhook handler now performs a full incremental sync when SYNC_UPDATES_AVAILABLE is received and plaid_sync_enabled is true. The autoSyncTransactions() function handles the full flow including balance refresh and sync logging:
5. Plaid Balance Refresh Should Use /accounts/balance/get — IMPLEMENTED
The getBalances() function has been added to plaid-client.ts and is now used by plaid-sync, plaid-webhook auto-sync, and plaid-scheduled-sync. Benefits:
- Faster (real-time balance from the bank)
- Doesn’t count against the same rate limits
- Returns
availableandcurrentbalances
6. GL Account Association Needs User Selection — IMPLEMENTED
After Plaid multi-account connection,BankAccountsPage refreshes the list and shows a toast prompting the user to review GL account assignments. Users click Edit on each account to change the GL mapping. The auto-discovery still works as a sensible default:
- After Plaid connection, present a dialog asking the user to select/create a GL account for each bank account
- Store the mapping in
fa_bank_accounts.gl_account_id - The
MultiAccountSetupDialogalready exists for Teller but doesn’t appear to be used for Plaid connections
7. Scheduled Balance Sync — IMPLEMENTED
Newplaid-scheduled-sync edge function that iterates all connected Plaid accounts, refreshes balances via /accounts/balance/get, and optionally syncs transactions. Recommended cron: 0 */4 * * *:
8. Add updated_at to fa_bank_statement_lines — IMPLEMENTED
Migration adds updated_at TIMESTAMPTZ DEFAULT now() column and set_fa_bank_statement_lines_updated_at trigger using the shared update_updated_at_column() function. Now tracks when Plaid transactions were last modified.
9. Error Recovery and Retry Queue — IMPLEMENTED
Newfa_plaid_sync_log table tracks every sync attempt with status, error codes, retry count, and timing. All sync paths (manual, webhook, scheduled) now create log entries. The table supports a retry queue pattern via status='retrying' and next_retry_at:
- A
fa_plaid_sync_logtable tracking sync attempts, results, and errors - Automatic retry with exponential backoff for transient failures
- User notification when sync has been failing for >24 hours
10. Plaid Products: Add auth for Account Verification — READY
The infrastructure already supports requesting auth product. The PlaidLinkOptions.products field accepts string[], and the plaid-create-link-token edge function passes it through. To enable, pass products={['transactions', 'auth']} to usePlaidLink or PlaidConnectButton. Benefits:
- Full account and routing number verification
- Micro-deposit verification as fallback
- Direct deposit setup from Plaid (for HR-PAY-03)
hr_employee_bank_accounts.verification_method = 'plaid' flow.
Testing Recommendations
- Schema validation test: Add a test that verifies the columns written by each edge function actually exist on the target table
- Constraint validation test: Verify that all mapped enum values satisfy CHECK constraints
- End-to-end sandbox test: Connect a Plaid sandbox account, sync transactions, verify they appear in
fa_bank_statement_lines, verify balance updates onfa_bank_accounts - Webhook integration test: Simulate
SYNC_UPDATES_AVAILABLEandTRANSACTIONS_REMOVEDwebhooks, verify correct handling
Files Changed
| File | Changes |
|---|---|
supabase/functions/_shared/plaid-client.ts | Fixed mapTransactionType() to only return ‘debit’/‘credit’ |
supabase/functions/plaid-sync/index.ts | Removed status field, fixed transaction_type, added balance fetch, sequential line numbers |
supabase/functions/plaid-exchange-token/index.ts | Fixed column names, added required fields, fixed account type mapping |
supabase/functions/plaid-webhook/index.ts | Fixed TRANSACTIONS_REMOVED handler to use custom_fields |
src/platform/banking/providers/plaid/usePlaidSync.ts | Fixed snake_case response parsing, added balance display |