fix(jobs): delegate recurring-transaction sync gate to Sync.for_family#1975
fix(jobs): delegate recurring-transaction sync gate to Sync.for_family#1975galuis116 wants to merge 2 commits into
Conversation
`IdentifyRecurringTransactionsJob#family_has_incomplete_syncs?` hand-rolled the list of provider `*_items` associations it polled — plaid, simplefin, lunchflow, enable_banking, sophtron — missing nine other `Syncable` provider concerns on `Family`: coinbase, binance, kraken, coinstats, snaptrade, mercury, brex, indexa_capital, ibkr. When a sync on any of those nine was in flight, the debounce gate fell through and `RecurringTransaction::Identifier` ran against a partial dataset; the follow-up re-enqueue then hit the `find_or_initialize_by` upsert path and inherited the stale `occurrence_count`. Same drift pattern that bolted sophtron on as the 5th entry (we-promise#591) was already an iteration of. The maintainers' own `Sync.for_family` (sync.rb:61) already enumerates every `*_items` association via `Family.reflect_on_all_associations(:has_many)` filtered by inclusion of `Syncable` — exactly the helper the gate should delegate to so the list cannot drift again. - Add `Sync.any_incomplete_for?(family)` class method that wraps `for_family(family).incomplete.exists?`. - Rewrite `family_has_incomplete_syncs?` to delegate. 14 lines → 1. - New test file `test/jobs/identify_recurring_transactions_job_test.rb` covers in-flight Coinbase + Mercury (gate fires), idle (identifier runs), missing family, and superseded-by-newer-schedule. - `test/models/sync_test.rb` gets 2 new tests pinning `any_incomplete_for?` against a provider `_items` sync and a family-itself sync. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughReplaces IdentifyRecurringTransactionsJob's hand-rolled incomplete-sync checks with a delegation to new Sync.any_incomplete_for?(family). Adds unit tests for the Sync method and job tests verifying debounce behavior for various in-flight provider syncs, missing family, and cache superseding. ChangesSync debounce gate unification
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Clean and correct refactor. A few notes: Root cause: The hand-rolled list covering only 5 of 14
Tests: The Coinbase test (creates a Generated by Claude Code |
… test env) `Rails.cache` is `ActiveSupport::Cache::NullStore` in the Rails test env, so the previous test's `Rails.cache.write(cache_key, @scheduled_at + 10, ...)` was a no-op and `Rails.cache.read(cache_key)` returned `nil`. The supersession short-circuit `return if latest_scheduled && latest_scheduled > scheduled_at` then fell through, the job proceeded to invoke `RecurringTransaction::Identifier`, and the Mocha `.expects(:identify_recurring_patterns).never` failed in CI. Switch to `Rails.cache.stubs(:read).with(cache_key).returns(...)` — the same idiom `test/models/provider/twelve_data_test.rb:186-197` already uses for the cache layer. Add an `assert_nil` on the bare `perform` return so Minitest's assertion counter sees an explicit assertion (silences the "missing assertions" warning). No production-code change. Behavior under test is unchanged; only the test mechanism for simulating "newer scheduled run already in cache" is fixed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
IdentifyRecurringTransactionsJob#family_has_incomplete_syncs?(app/jobs/identify_recurring_transactions_job.rb:45-60) was the debounce gate that delayedRecurringTransaction::Identifieruntil in-flight provider syncs settled. The gate hand-rolled the list of provider*_itemsassociations it polled —plaid_items,simplefin_items,lunchflow_items,enable_banking_items,sophtron_items— and silently missed nine otherSyncableprovider concerns onFamily(coinbase_items,binance_items,kraken_items,coinstats_items,snaptrade_items,mercury_items,brex_items,indexa_capital_items,ibkr_items). When a sync on any of those nine was in flight, the gate fell through, the identifier ran against a partial dataset, and the resultingRecurringTransactionrow got a staleoccurrence_count; the late-arriving sync then re-scheduled the job and the follow-up run upserted viafind_or_initialize_byinheriting the stale row.The maintainers' own
Sync.for_family(app/models/sync.rb:61-75) already enumerates every*_itemsassociation viaFamily.reflect_on_all_associations(:has_many)filtered by inclusion of theSyncablemodule — exactly the helper this gate should delegate to so the list cannot drift again. The file has had a single edit since creation (Apr 19, 2026, #591 bolting on Sophtron as the 5th entry); this PR closes the regression class so the next provider integration doesn't reopen it.Fixes #1974.
Fix
Sync.any_incomplete_for?(family)class method onSyncthat wrapsfor_family(family).incomplete.exists?— mirrors the existingSync.clean/Sync.for_familyclass-method idiom and is reusable from any future caller that needs the same gate.IdentifyRecurringTransactionsJob#family_has_incomplete_syncs?to delegate. 14 lines → 1, plus a docstring explaining the drift the delegation closes.The reflection-based discovery in
family_syncable_associations(sync.rb:81-88) was already trusted bySync.for_familyitself — same contract, same trust gate, no new abstraction.Tests
New file:
test/jobs/identify_recurring_transactions_job_test.rb(5 tests)coinbase_items)Added to
test/models/sync_test.rb(2 tests)Sync.any_incomplete_for?returns true for an in-flight Mercury-item sync; flips to false once the sync completesSync.any_incomplete_for?returns true for a sync directly on theFamilyrowThe existing test
for_family includes syncable provider item associations from family reflections(sync_test.rb:217) was the precedent that reflection-based discovery is the right pattern; the new tests extend that same shape to theincompletepredicate.Files changed
app/jobs/identify_recurring_transactions_job.rb— delegate the gate; 14-line hand-rolled list deleted, replaced with 1-line call.app/models/sync.rb— newSync.any_incomplete_for?class method (8 lines incl. docstring).test/jobs/identify_recurring_transactions_job_test.rb— new file, 5 tests.test/models/sync_test.rb— 2 new tests pinningSync.any_incomplete_for?.CI / pre-PR checks
The standard CI gate from
CLAUDE.md(bin/rails test,bin/rubocop -f github -a,bin/brakeman --no-pager) is required. No*.erbchanged (erb_lint no-op). No API endpoint changed (rswag regeneration not required).[Allow edits from maintainers] is enabled on this PR.
Summary by CodeRabbit
Refactor
Tests