Don’t re-create pending SimpleFIN transactions when pending sync is disabled (#2835)

* fix(simplefin): skip pending entries in processor when pending is disabled

When SIMPLEFIN_INCLUDE_PENDING/syncs_include_pending is off, pending rows
already stored in raw_transactions_payload were still (re)created as
entries on every sync - including ones the user manually deleted - because
the setting only affected the API request, not reprocessing of the stored
payload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(simplefin): add rake task to prune stale pending rows from payload store

raw_transactions_payload accumulates transactions across syncs and is
never pruned, so pending rows fetched before pending inclusion was
disabled keep getting re-imported. This one-time maintenance task removes
them (dry-run by default; scope by item_id/account_id).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(simplefin): address PR #2835 review feedback on pending detection

Fix epoch-zero pending check treating non-numeric posted strings (e.g.
"unavailable") as pending via String#to_i coercion; compare against
explicit zero representations instead, matching posted_date. Dedupe the
prune_pending rake task's copy of this logic by delegating to a new
public SimplefinEntry::Processor.pending? class method. Also close a
test gap where SIMPLEFIN_INCLUDE_PENDING env var precedence over the
Setting wasn't actually exercised.

* test(simplefin): cover pending-guard precedence and add rake task tests

Add the missing mirror case for pending_enabled? precedence (env var
disabling pending over a permissive Setting) and add test coverage for
the prune_pending rake task, which previously had none: dry_run safety
default, correct pruning via the shared Processor.pending? predicate
(including the malformed-posted regression), and that it never touches
Entry/Transaction rows.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
PrplHaz4
2026-07-29 18:13:02 -05:00
committed by Juan José Mata
parent 5ca49392c4
commit 5fb8f4f81a
4 changed files with 342 additions and 21 deletions

View File

@@ -142,6 +142,99 @@ class SimplefinEntry::ProcessorTest < ActiveSupport::TestCase
assert_equal true, sf["pending"], "expected pending flag to be true when posted==0 and/or pending=true"
end
test "skips pending transactions when pending inclusion is disabled" do
Setting.stubs(:syncs_include_pending).returns(false)
tx = {
id: "tx_pending_disabled_1",
amount: "-30.00",
currency: "USD",
payee: "Test Store",
description: "Auth hold",
posted: Date.current.to_s,
transacted_at: (Date.current - 1).to_s,
pending: true
}
# Clear the env var so this only exercises the Setting fallback branch of pending_enabled?
with_env_overrides SIMPLEFIN_INCLUDE_PENDING: nil do
assert_no_difference "@account.entries.count" do
SimplefinEntry::Processor.new(tx, simplefin_account: @simplefin_account).process
end
end
end
test "still imports posted transactions when pending inclusion is disabled" do
Setting.stubs(:syncs_include_pending).returns(false)
tx = {
id: "tx_posted_disabled_1",
amount: "-30.00",
currency: "USD",
payee: "Test Store",
description: "Settled",
posted: Date.current.to_s,
transacted_at: (Date.current - 1).to_s,
pending: false
}
with_env_overrides SIMPLEFIN_INCLUDE_PENDING: nil do
assert_difference "@account.entries.count", 1 do
SimplefinEntry::Processor.new(tx, simplefin_account: @simplefin_account).process
end
end
end
test "SIMPLEFIN_INCLUDE_PENDING env var takes precedence over Setting" do
# Setting says "skip pending", but the env var (mirrored via the boot-time config it
# populates) says "include pending" - env var must win, matching
# SimplefinItem::Importer#fetch_accounts_data's effective_pending resolution.
Setting.stubs(:syncs_include_pending).returns(false)
Rails.configuration.x.simplefin.stubs(:include_pending).returns(true)
tx = {
id: "tx_pending_env_override_1",
amount: "-30.00",
currency: "USD",
payee: "Test Store",
description: "Auth hold",
posted: Date.current.to_s,
transacted_at: (Date.current - 1).to_s,
pending: true
}
with_env_overrides SIMPLEFIN_INCLUDE_PENDING: "1" do
assert_difference "@account.entries.count", 1 do
SimplefinEntry::Processor.new(tx, simplefin_account: @simplefin_account).process
end
end
end
test "SIMPLEFIN_INCLUDE_PENDING env var disabling pending takes precedence over a permissive Setting" do
# Mirror of the test above: this is the actual real-world guard scenario the PR
# fixes - a self-hoster sets SIMPLEFIN_INCLUDE_PENDING=0 while the Setting (UI
# toggle) still says "include pending". The env var must win and skip the row.
Setting.stubs(:syncs_include_pending).returns(true)
Rails.configuration.x.simplefin.stubs(:include_pending).returns(false)
tx = {
id: "tx_pending_env_disable_1",
amount: "-30.00",
currency: "USD",
payee: "Test Store",
description: "Auth hold",
posted: Date.current.to_s,
transacted_at: (Date.current - 1).to_s,
pending: true
}
with_env_overrides SIMPLEFIN_INCLUDE_PENDING: "0" do
assert_no_difference "@account.entries.count" do
SimplefinEntry::Processor.new(tx, simplefin_account: @simplefin_account).process
end
end
end
test "infers pending when posted is explicitly 0 and transacted_at present (no explicit pending flag)" do
# Some SimpleFIN banks indicate pending by sending posted=0 + transacted_at, without pending flag
t_epoch = (Date.current - 1).to_time.to_i
@@ -163,4 +256,27 @@ class SimplefinEntry::ProcessorTest < ActiveSupport::TestCase
sf = entry.transaction.extra.fetch("simplefin")
assert_equal true, sf["pending"], "expected pending to be inferred from posted=0 + transacted_at present"
end
test "does not treat a non-numeric posted value as epoch-zero pending" do
# Regression: `posted_val.to_i.zero?` would also match malformed strings like
# "unavailable" (String#to_i coerces non-numeric input to 0), wrongly flagging a
# settled transaction as pending. Only literal 0 / "0" should count as epoch-zero.
tx = {
id: "tx_malformed_posted_1",
amount: "-11.00",
currency: "USD",
payee: "Test Store",
description: "Settled",
memo: "",
posted: "unavailable",
transacted_at: (Date.current - 1).to_s
# Note: NO pending flag set
}
SimplefinEntry::Processor.new(tx, simplefin_account: @simplefin_account).process
entry = @account.entries.find_by!(external_id: "simplefin_tx_malformed_posted_1", source: "simplefin")
sf = entry.transaction.extra.fetch("simplefin")
assert_equal false, sf["pending"], "expected a non-numeric posted value to not be inferred as pending"
end
end