Skip to content

feat(mercury): pending transactions, kind/counterpartyId metadata, and test coverage#2452

Open
JSONbored wants to merge 3 commits into
we-promise:mainfrom
JSONbored:feat/mercury-sync-enhancements
Open

feat(mercury): pending transactions, kind/counterpartyId metadata, and test coverage#2452
JSONbored wants to merge 3 commits into
we-promise:mainfrom
JSONbored:feat/mercury-sync-enhancements

Conversation

@JSONbored

@JSONbored JSONbored commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Closes #2463.

Summary

Mercury Banking sync currently misses three data points available in every Mercury API payload:

  • Pending transactions are silently droppedstatus: "pending" entries were never imported, so Mercury pending charges don't appear until they post
  • Payment rail (kind) is not stored — Mercury returns kind: "ACH" | "Wire" | "Card" | "InternalTransfer" on every transaction; the field was unused
  • Counterparty ID is not stored — Mercury provides counterpartyId (a stable UUID) for deduplication and future analytics; the field was unused
  • Transaction::PENDING_PROVIDERS was missing "mercury" — even if pending data had been stored, the existing pending-detection SQL and Transaction#pending? would never check it

Changes

app/models/transaction.rb

Add "mercury" to PENDING_PROVIDERS so the existing reconciliation pipeline (pending→posted amount-match in Account::ProviderImportAdapter) activates for Mercury entries.

# Before
PENDING_PROVIDERS = %w[simplefin plaid lunchflow enable_banking akahu up].freeze

# After
PENDING_PROVIDERS = %w[simplefin plaid lunchflow enable_banking akahu up mercury].freeze

app/models/mercury_entry/processor.rb

Pass extra: to import_transaction with the following structure:

extra["mercury"]["pending"]         # true when status == "pending", false otherwise
extra["mercury"]["kind"]            # "ACH", "Wire", "Card", etc. — when present
extra["mercury"]["counterparty_id"] # Mercury counterparty UUID — when present

Pending transactions are now imported with the flag set. Only "failed" status continues to be skipped.

New test files — 30 cases

  • test/models/mercury_entry/processor_test.rb — sign convention, date fallback (postedAtcreatedAt), name priority (nickname → name → bankDescription), notes concatenation, pending flag, kind, counterpartyId, failed-skip, idempotency, merchant creation, no-linked-account guard
  • test/models/mercury_item/importer_test.rb — account discovery, no-duplicate unlinked records, balance update on linked accounts, transaction dedup (append-only new ids), sync window (90-day first sync, last_synced_at - 7d for subsequent syncs), 401 marks requires_update
  • test/models/mercury_account/processor_test.rb — balance update, CreditCard sign negation, cash_balance parity, no-linked-account no-op, transaction processing delegation

Test run

bin/rails test test/models/mercury_entry/processor_test.rb \
               test/models/mercury_item/importer_test.rb \
               test/models/mercury_account/processor_test.rb
# 30 runs, 56 assertions, 0 failures, 0 errors, 0 skips

bin/rails test test/models/mercury_item_test.rb \
               test/models/mercury_account_test.rb \
               test/models/provider/mercury_test.rb \
               test/controllers/mercury_items_controller_test.rb \
               test/models/transaction_test.rb
# 65 runs, 204 assertions, 0 failures, 0 errors, 0 skips

bin/rubocop app/models/transaction.rb app/models/mercury_entry/processor.rb \
            test/models/mercury_entry/processor_test.rb \
            test/models/mercury_item/importer_test.rb \
            test/models/mercury_account/processor_test.rb
# 5 files inspected, no offenses detected

Summary by CodeRabbit

  • New Features

    • Added Mercury provider support for pending transaction detection and enhanced metadata tracking.
    • Transactions now include complete metadata: pending status, transaction kind, and counterparty information.
  • Bug Fixes

    • Improved transaction deduplication logic to correctly identify and handle pending status updates.
    • Enhanced error handling during transaction imports with better logging and recovery.
  • Tests

    • Added comprehensive test coverage for Mercury transaction processing workflows.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

MercuryEntry::Processor gains extra/pending? helpers and structured error handling, passing a "mercury"-scoped metadata hash into import_adapter.import_transaction. Transaction::PENDING_PROVIDERS adds mercury, and three Account::ProviderImportAdapter pending-candidate queries are extended to match it. MercuryItem::Importer replaces set-based deduplication with an ID-indexed map supporting pending-to-posted status transitions.

Changes

Mercury Extra Metadata and Pending Support

Layer / File(s) Summary
Mercury extra metadata, error handling, and PENDING_PROVIDERS
app/models/transaction.rb, app/models/mercury_entry/processor.rb
Adds mercury to Transaction::PENDING_PROVIDERS, introduces private extra and pending? helpers in MercuryEntry::Processor, passes extra: into import_adapter.import_transaction, and wraps the import call with structured rescue for ArgumentError, ActiveRecord persistence errors, and generic exceptions.
Mercury pending detection in Account::ProviderImportAdapter
app/models/account/provider_import_adapter.rb
Extends the exact, fuzzy, and low-confidence pending-candidate queries to also match transactions.extra['mercury']['pending'] cast to boolean.
MercuryItem::Importer pending-to-posted transition
app/models/mercury_item/importer.rb
Replaces set-based duplicate filtering with an ID-indexed map; detects pending→non-pending status transitions on existing transactions, upserts the merged snapshot when new or updated items exist, and logs counts via DebugLogEntry.
Test suites: MercuryEntry, MercuryAccount, MercuryItem
test/models/mercury_entry/processor_test.rb, test/models/mercury_account/processor_test.rb, test/models/mercury_item/importer_test.rb
New test suites covering sign conversion, name/date resolution, notes formatting, extra metadata fields, failed-tx skip, idempotency, merchant creation, missing-account nil return, balance updates, CreditCard sign negation, depository cash_balance, account discovery, transaction dedup, sync-window date selection, and auth-error handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • we-promise/sure#2391: Adds up to Transaction::PENDING_PROVIDERS and extends Account::ProviderImportAdapter pending-matching to recognize extra['up']['pending'], the same pattern applied here for mercury.

Suggested reviewers

  • jjmata

Poem

🐇 Hop hop, the Mercury flows,
From "pending" to "posted" it goes,
Extra fields neatly stowed,
Idempotent roads bestowed,
No duplicate entry shall pose! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main changes: adding pending transaction support, kind/counterpartyId metadata, and test coverage to Mercury integration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@superagent-security

Copy link
Copy Markdown

Superagent didn't find any vulnerabilities or security issues in this PR.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 203cc9fedf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread app/models/transaction.rb
Comment thread app/models/mercury_entry/processor.rb

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/models/kraken_account/ledger_processor.rb`:
- Around line 33-35: The process_ledger_entry method is performing an exists?
database query for each ledger in the raw_ledgers.each loop, creating an N+1
query pattern. Before the loop that iterates through raw_ledgers, preload all
existing Kraken ledger external IDs into a Set or Hash for in-memory lookup.
Then replace the exists? query at line 79 with a lookup against this preloaded
collection instead of querying the database for each iteration.

In `@test/models/mercury_account/processor_test.rb`:
- Around line 49-56: The test method "returns nil without error when no linked
account" claims to verify a nil return value but only asserts that no exception
is raised using assert_nothing_raised. Capture the return value from the
MercuryAccount::Processor.new(mercury_account).process method call and add an
explicit assert_nil assertion to verify the return value is actually nil,
ensuring the test properly validates the return contract stated in the test
name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 990bf48d-3549-4b54-ad18-2cb47b1d9695

📥 Commits

Reviewing files that changed from the base of the PR and between fdcd0c7 and 203cc9f.

📒 Files selected for processing (12)
  • app/models/kraken_account/ledger_processor.rb
  • app/models/kraken_account/processor.rb
  • app/models/kraken_item/importer.rb
  • app/models/mercury_entry/processor.rb
  • app/models/provider/kraken.rb
  • app/models/transaction.rb
  • test/models/kraken_account/ledger_processor_test.rb
  • test/models/kraken_item/importer_test.rb
  • test/models/mercury_account/processor_test.rb
  • test/models/mercury_entry/processor_test.rb
  • test/models/mercury_item/importer_test.rb
  • test/models/provider/kraken_test.rb

Comment thread app/models/kraken_account/ledger_processor.rb Outdated
Comment thread test/models/mercury_account/processor_test.rb

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/models/mercury_item/importer.rb`:
- Around line 240-243: Replace the two Rails.logger.info calls in the
MercuryItem::Importer with DebugLogEntry.capture calls. The first call logs
transaction counts when storing new and updated transactions, and the second
logs when there are no new or updated transactions. Update both log statements
to use DebugLogEntry.capture with the same message content to ensure these
import diagnostics are properly captured in the support debug UI as per the
coding guidelines for provider sync/import diagnostics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c55dff3a-22bf-49be-b586-67abf4a02810

📥 Commits

Reviewing files that changed from the base of the PR and between 203cc9f and 6feb462.

📒 Files selected for processing (4)
  • app/models/account/provider_import_adapter.rb
  • app/models/kraken_account/ledger_processor.rb
  • app/models/mercury_item/importer.rb
  • test/models/mercury_account/processor_test.rb
✅ Files skipped from review due to trivial changes (1)
  • test/models/mercury_account/processor_test.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/models/kraken_account/ledger_processor.rb

Comment thread app/models/mercury_item/importer.rb Outdated

@jjmata jjmata left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Mercury additions are well thought out — especially the pending-to-posted transition logic that handles Mercury reusing the same transaction ID when a pending entry posts. The existing_by_id map approach is clean. A few observations:

Overlap with PR #2451 — This PR's diff includes KrakenAccount::LedgerProcessor, KrakenItem::Importer ledger fetching, and Provider::Kraken#get_ledgers — the same files changed in PR #2451. Importantly, this version of LedgerProcessor#process preloads existing external IDs into a Set upfront:

existing_external_ids = account.entries
  .where(source: "kraken")
  .where("external_id LIKE 'kraken_ledger_%'")
  .pluck(:external_id)
  .to_set

…whereas PR #2451's version does a per-entry account.entries.exists?(...) call — one DB query per ledger entry. This PR's version is better for performance. Whichever order these merge, the per-entry query version shouldn't land. The two PRs need to be coordinated.

DebugLogEntry over Rails.logger for Mercury importer — The old importer used Rails.logger.info for transaction storage; this PR upgrades to DebugLogEntry.capture, which is consistent with the project's debug logging guidelines. Good.

extra["mercury"]["kind"] vs. Sure transaction kinds — The Mercury kind field (e.g., "externalTransfer") is stored as raw metadata in extra, not mapped to Sure's Transaction#kind enum. That's the safe choice for now, but a future follow-up could map known Mercury kinds to funds_movement where appropriate (similar to what PR #2460 does for Up's transferAccount).


Generated by Claude Code

JSONbored added a commit to JSONbored/sure-upstream that referenced this pull request Jun 23, 2026
Address review feedback (jjmata):

- N+1: LedgerProcessor#process_ledger_entry ran `account.entries.exists?(...)`
  per ledger entry (up to ~10k per sync). Load the existing Kraken external IDs
  once into a Set and test membership in memory (newly created IDs are added so
  the same run stays idempotent) — same pattern as we-promise#2452.
- Tests: the idempotency test now asserts the first pass actually creates the
  entry (assert_difference) before asserting the second is a no-op; add a guard
  asserting the second (all-skipped) pass issues a single bulk external_id pluck,
  not one query per entry.

No behavior change to imported entries.
…d test coverage

- Transaction::PENDING_PROVIDERS: add "mercury" so the existing pending
  reconciliation pipeline (pending→posted amount matching in
  ProviderImportAdapter) activates for Mercury entries

- MercuryEntry::Processor: pass extra: to import_transaction with
    extra["mercury"]["pending"]        = true/false (status == "pending")
    extra["mercury"]["kind"]           = ACH / Wire / Card / etc.
    extra["mercury"]["counterparty_id"] = Mercury counterparty UUID
  Pending transactions are now imported with the flag set rather than
  being silently ignored; failed transactions continue to be skipped

- Tests (new files, 30 cases):
  - test/models/mercury_entry/processor_test.rb  — sign convention,
    date fallback, name priority, notes concat, pending flag, kind,
    counterpartyId, failed skip, idempotency, merchant creation,
    no-linked-account guard
  - test/models/mercury_item/importer_test.rb    — account discovery,
    no-duplicate unlinked records, balance update on linked accounts,
    transaction dedup (append-only new ids), sync window (90-day first
    sync, last_synced_at-7d subsequent), 401 marks requires_update
  - test/models/mercury_account/processor_test.rb — balance update,
    CreditCard sign negation, cash_balance parity, no-linked-account
    no-op, transaction processing delegation
…1, nil assertion

- provider_import_adapter: add mercury to all three find_pending_transaction*
  SQL predicates so Mercury pending entries are found and claimed when the
  posted version arrives (exact, fuzzy, and low-confidence paths)

- mercury_item/importer: replace append-only dedup with an upsert-by-id
  that replaces the stored raw payload when status changes from pending to
  non-pending; prevents pending flag from persisting indefinitely when Mercury
  reuses the same transaction ID for the posted version

- kraken_account/ledger_processor: preload all existing kraken ledger
  external_ids into a Set before the loop; replaces per-entry exists? query
  (N+1) with an in-memory Set#include? lookup

- test/models/mercury_account/processor_test: capture return value from
  process and add assert_nil to enforce the nil contract stated in the test name
@JSONbored JSONbored force-pushed the feat/mercury-sync-enhancements branch from cb16c6b to a828f3e Compare June 23, 2026 05:12
@JSONbored

Copy link
Copy Markdown
Contributor Author

Thanks @jjmata — good catch on the overlap. Fixed:

Overlap with #2451 (resolved): this branch was accidentally stacked on #2451 — it carried the three Kraken commits plus a stray edit to KrakenAccount::LedgerProcessor inside a Mercury commit. I rebased it onto main, dropping all of that, so #2452 is now Mercury-only (mercury_*, account/provider_import_adapter.rb, transaction.rb + Mercury tests). Confirmed the two PRs now share zero files, so they merge cleanly in any order.

The good Set-based idempotency you flagged now lives solely in #2451, which I updated to the same approach (single bulk pluck scoped to the kraken_ledger_ prefix) — the per-entry exists? is gone there too. So whichever merges first, the per-entry version won't land.

DebugLogEntry: 👍 thanks.

extra["mercury"]["kind"] → Sure kinds: agreed it's the safe choice for now; left as raw metadata. Happy to do the funds_movement mapping for known Mercury kinds (e.g. externalTransfer) as a follow-up, mirroring #2460's Up transferAccount handling.

rubocop clean; Mercury suite green (30 runs) on the rebased base.

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.

feat(mercury): import pending transactions and store kind/counterpartyId metadata

2 participants