Pending Transaction Reconciliation#
When a banking provider delivers a new booked (settled) transaction, Sure must determine whether it corresponds to an existing pending entry so it can update the record in place rather than create a duplicate. This reconciliation is handled centrally by Account::ProviderImportAdapter#import_transaction and applies to all providers (Plaid, SimpleFIN, Enable Banking, Akahu, Up, Mercury, Lunchflow).
Matching Strategies (Priority Order)#
The adapter applies three strategies in sequence, stopping at the first match :
1. Plaid pending_transaction_id (highest confidence)#
Plaid supplies an explicit linking ID that ties a pending entry to its posted counterpart. When pending_transaction_id is present, the adapter does a direct find_by(external_id: pending_transaction_id, source:) lookup — no heuristics needed .
2. Exact Amount Match (find_pending_transaction)#
For providers without explicit linking IDs (SimpleFIN, Enable Banking, etc.), find_pending_transaction searches for an existing pending entry that matches:
- Same account, source, amount (exact), and currency
- Date within an 8-day backward window (pending date ≤ posted date)
pendingflag set intransactions.extrafor any supported provider
The most recent candidate wins. On a successful match, the pending entry's external_id is updated to the posted transaction's ID, the pending flag is cleared, and the old external_id is written into extra["auto_claimed_pending_ids"] to prevent the stale pending record from being re-imported on the next sync .
3. Fuzzy Amount Match (suggestion only)#
When exact matching fails, find_pending_transaction_fuzzy and find_pending_transaction_low_confidence look for pending entries within ≤30% and 30–100% amount difference respectively, using a tighter 3-day window. These are designed for tip adjustments (e.g., a $50 authorization settling at $58 after tip). Unlike the exact-match path, fuzzy matches are not auto-applied — a suggestion is stored in extra["potential_posted_match"] on the pending entry for user review . To avoid false positives with recurring merchants, fuzzy matching requires either a unique merchant ID match or a unique name match, and returns nil if more than one candidate exists .
Same-ID Pending→Booked (Provider-Specific Bypass)#
Some ASPSPs (e.g., Revolut Italy via Enable Banking) reuse the same transaction_id for both the pending and booked states. In this case the adapter finds the entry by external_id from the start, so it skips the matching strategies above. When the incoming transaction is not pending but the stored entry still has a pending flag, the flag is cleared directly before saving .
Enable Banking: Importer-Level Pre-filtering#
Before entries reach the ProviderImportAdapter, the Enable Banking importer performs its own PDNG→BOOK deduplication. When the raw payload contains both PDNG and BOOK versions of a transaction, the importer uses dual-strategy matching to drop the pending row:
- Fingerprint match — both rows resolve to the same
compute_external_idvalue - Entry-reference cross-match — the pending and booked rows share an
entry_referenceeven when theirtransaction_idvalues differ
The compute_external_id method itself uses a two-tier strategy :
- ID-based:
"enable_banking_{transaction_id}"or"enable_banking_{entry_reference}"if available - Content-based: MD5 hash of
[date, amount, currency, credit_debit_indicator, creditor, debtor, remittance_info]— intentionally excluding status soPDNGandBOOKrows with the same logical content share the same fingerprint
The processor-level excluded-ID sets (auto_claimed_pending_ids + manual_merge) are pre-fetched per sync with two lateral-join queries and checked via O(1) Set lookups, preventing re-import of already-reconciled pending entries .
Known Edge Cases with Enable Banking#
The reconciliation logic was not originally designed for Enable Banking's behaviour, and several failure modes are tracked in issue #1470:
| Pattern | Root Cause | Status |
|---|---|---|
| Duplicate entries after settlement | Enable Banking assigns different transaction_id values to pending and booked versions; exact-amount match may also miss if another pending transaction has the same amount | Open |
| Names change between states | e.g., WWW.AMAZON pending → WWW.AMAZON*NW1GXXXX booked; no stable attribute to match on | Open |
syncs_include_pending not respected | Enable Banking importer originally fetched PDNG unconditionally; fix in progress via PR #2686 | Partially addressed |
| Pending sign flipped (DSK bank) | Pending transactions reported as income instead of expense | Open |
Workaround: Disable "Include pending transactions" in Settings → Self-Hosting to prevent pending entries from being imported at all. This eliminates duplicates but also removes real-time pending visibility .
Key Files#
| File | Purpose |
|---|---|
account/provider_import_adapter.rb | Central reconciliation logic: all three matching strategies, pending flag clearing, auto_claimed_pending_ids tracking |
enable_banking_account/transactions/processor.rb | Builds excluded-ID sets; dispatches per-transaction processing |
enable_banking_entry/processor.rb | compute_external_id (ID-based + content-based MD5); sets extra[:enable_banking][:pending] |
enable_banking_item/importer.rb | Importer-level PDNG→BOOK pre-filtering via fingerprint + entry-reference cross-match |