mirror of
https://github.com/we-promise/sure.git
synced 2026-08-05 16:42:18 +00:00
* fix(goals): match manual_save pledges by contribution delta, not full balance Closes #2177 * fix(goals): clarify depository-only contribution; lowercase test names Address review: document that valuation_contribution's delta is only consumed for goal-linked Depository accounts, so the positive-delta guard is correct and no liability paydown sign case can arise. Lowercase NOT in two test names to match project convention. * test(goals): pin pledge matching through the reconciliation manager Follow-up to #2178, which matched manual_save pledges by contribution delta but only unit-tested the matcher with an injected delta. The delta derivation itself (the balances-table lookup and the nil-to-0 fallback in valuation_contribution) had no coverage, and reconciliation_manager_test never touched pledges. Two manager-level tests pin the wiring end to end: a 2000 -> 2150 reconcile matches an open 150 pledge, and a same-balance reconcile (zero delta) leaves it open, closing the <= 0 boundary. Also documents the staleness window on valuation_contribution: the prior balance reads the balances table, which recomputes async after the valuation saves, so a second same-day reconcile racing that sync derives a stale delta and self-heals on the next save. * fix(goals): derive same-day re-reconcile delta from the prior valuation The balances table recomputes asynchronously after a reconcile, so a second same-day reconcile racing that sync derived its delta from the pre-first-reconcile row. An overstated delta could wrongly close a larger open pledge — and a wrong match, unlike a miss, never self-heals. Prefer the valuation's own pre-save amount as the prior balance when the reconcile updates an existing valuation; fall back to the balances row, then 0. Same-date re-reconciles are immune to the race; only the cross-date window remains, and only in the miss direction. --------- Co-authored-by: galuis116 <galuis116@gmail.com>
171 lines
6.4 KiB
Ruby
171 lines
6.4 KiB
Ruby
require "test_helper"
|
|
|
|
class Account::ReconciliationManagerTest < ActiveSupport::TestCase
|
|
include BalanceTestHelper
|
|
|
|
setup do
|
|
@account = accounts(:investment)
|
|
@manager = Account::ReconciliationManager.new(@account)
|
|
end
|
|
|
|
test "new reconciliation" do
|
|
create_balance(account: @account, date: Date.current, balance: 1000, cash_balance: 500)
|
|
|
|
result = @manager.reconcile_balance(balance: 1200, date: Date.current)
|
|
|
|
assert_equal 1200, result.new_balance
|
|
assert_equal 700, result.new_cash_balance # Non cash stays the same since user is valuing the entire account balance
|
|
assert_equal 1000, result.old_balance
|
|
assert_equal 500, result.old_cash_balance
|
|
assert_equal true, result.success?
|
|
end
|
|
|
|
test "updates existing reconciliation without date change" do
|
|
create_balance(account: @account, date: Date.current, balance: 1000, cash_balance: 500)
|
|
|
|
# Existing reconciliation entry
|
|
existing_entry = @account.entries.create!(name: "Test", amount: 1000, date: Date.current, entryable: Valuation.new(kind: "reconciliation"), currency: @account.currency)
|
|
|
|
result = @manager.reconcile_balance(balance: 1200, date: Date.current, existing_valuation_entry: existing_entry)
|
|
|
|
assert_equal 1200, result.new_balance
|
|
assert_equal 700, result.new_cash_balance # Non cash stays the same since user is valuing the entire account balance
|
|
assert_equal 1000, result.old_balance
|
|
assert_equal 500, result.old_cash_balance
|
|
assert_equal true, result.success?
|
|
end
|
|
|
|
test "updates existing reconciliation with date and amount change" do
|
|
create_balance(account: @account, date: 5.days.ago, balance: 1000, cash_balance: 500)
|
|
create_balance(account: @account, date: Date.current, balance: 1200, cash_balance: 700)
|
|
|
|
# Existing reconciliation entry (5 days ago)
|
|
existing_entry = @account.entries.create!(name: "Test", amount: 1000, date: 5.days.ago, entryable: Valuation.new(kind: "reconciliation"), currency: @account.currency)
|
|
|
|
# Should update and change date for existing entry; not create a new one
|
|
assert_no_difference "Valuation.count" do
|
|
# "Update valuation from 5 days ago to today, set balance from 1000 to 1500"
|
|
result = @manager.reconcile_balance(balance: 1500, date: Date.current, existing_valuation_entry: existing_entry)
|
|
|
|
assert_equal true, result.success?
|
|
|
|
# Reconciliation
|
|
assert_equal 1500, result.new_balance # Equal to new valuation amount
|
|
assert_equal 1000, result.new_cash_balance # Get non-cash balance today (1200 - 700 = 500). Then subtract this from new valuation (1500 - 500 = 1000)
|
|
|
|
# Prior valuation
|
|
assert_equal 1000, result.old_balance # This is the balance from the old valuation, NOT the date we're reconciling to
|
|
assert_equal 500, result.old_cash_balance
|
|
end
|
|
end
|
|
|
|
test "handles date conflicts" do
|
|
create_balance(account: @account, date: Date.current, balance: 1000, cash_balance: 1000)
|
|
|
|
# Existing reconciliation entry
|
|
@account.entries.create!(
|
|
name: "Test",
|
|
amount: 1000,
|
|
date: Date.current,
|
|
entryable: Valuation.new(kind: "reconciliation"),
|
|
currency: @account.currency
|
|
)
|
|
|
|
# Doesn't pass existing_valuation_entry, but reconciliation manager should recognize its the same date and update the existing entry
|
|
assert_no_difference "Valuation.count" do
|
|
result = @manager.reconcile_balance(balance: 1200, date: Date.current)
|
|
|
|
assert result.success?
|
|
assert_equal 1200, result.new_balance
|
|
end
|
|
end
|
|
|
|
test "dry run does not persist account" do
|
|
create_balance(account: @account, date: Date.current, balance: 1000, cash_balance: 500)
|
|
|
|
assert_no_difference "Valuation.count" do
|
|
@manager.reconcile_balance(balance: 1200, date: Date.current, dry_run: true)
|
|
end
|
|
|
|
assert_difference "Valuation.count", 1 do
|
|
@manager.reconcile_balance(balance: 1200, date: Date.current)
|
|
end
|
|
end
|
|
|
|
test "reconciliation matches an open manual_save pledge by contribution delta" do
|
|
account = accounts(:depository)
|
|
manager = Account::ReconciliationManager.new(account)
|
|
create_balance(account: account, date: Date.current, balance: 2000, cash_balance: 2000)
|
|
|
|
pledge = goal_pledges(:open_transfer).goal.goal_pledges.create!(
|
|
account: account,
|
|
amount: 150,
|
|
currency: "USD",
|
|
kind: "manual_save"
|
|
)
|
|
|
|
result = manager.reconcile_balance(balance: 2150, date: Date.current)
|
|
|
|
assert result.success?
|
|
assert pledge.reload.status_matched?
|
|
end
|
|
|
|
test "reconciliation to the same balance leaves manual_save pledges open" do
|
|
account = accounts(:depository)
|
|
manager = Account::ReconciliationManager.new(account)
|
|
create_balance(account: account, date: Date.current, balance: 2000, cash_balance: 2000)
|
|
|
|
pledge = goal_pledges(:open_transfer).goal.goal_pledges.create!(
|
|
account: account,
|
|
amount: 150,
|
|
currency: "USD",
|
|
kind: "manual_save"
|
|
)
|
|
|
|
result = manager.reconcile_balance(balance: 2000, date: Date.current)
|
|
|
|
assert result.success?
|
|
assert_not pledge.reload.status_matched?
|
|
end
|
|
|
|
test "second same-day reconcile derives its delta from the valuation it updates, not the stale balance row" do
|
|
account = accounts(:depository)
|
|
manager = Account::ReconciliationManager.new(account)
|
|
create_balance(account: account, date: Date.current, balance: 2000, cash_balance: 2000)
|
|
|
|
goal = goal_pledges(:open_transfer).goal
|
|
first_pledge = goal.goal_pledges.create!(
|
|
account: account,
|
|
amount: 150,
|
|
currency: "USD",
|
|
kind: "manual_save"
|
|
)
|
|
|
|
assert manager.reconcile_balance(balance: 2150, date: Date.current).success?
|
|
assert first_pledge.reload.status_matched?
|
|
|
|
# The post-reconcile balance sync is async and hasn't run, so the
|
|
# balances row still says $2,000. The second save of $150 (2150 → 2300)
|
|
# must not be read as a $300 contribution off that stale row — that
|
|
# would wrongly close the larger pledge, and a wrong match never
|
|
# self-heals.
|
|
oversized_pledge = goal.goal_pledges.create!(
|
|
account: account,
|
|
amount: 300,
|
|
currency: "USD",
|
|
kind: "manual_save"
|
|
)
|
|
second_pledge = goal.goal_pledges.create!(
|
|
account: account,
|
|
amount: 150,
|
|
currency: "USD",
|
|
kind: "manual_save"
|
|
)
|
|
|
|
assert manager.reconcile_balance(balance: 2300, date: Date.current).success?
|
|
|
|
assert_not oversized_pledge.reload.status_matched?
|
|
assert second_pledge.reload.status_matched?
|
|
end
|
|
end
|