summaryrefslogtreecommitdiff
path: root/src/FundLab.Api/Persistence.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 21:18:35 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 21:18:35 +0800
commit877e03183b5acaff84526cb9fe978280cadd8a76 (patch)
tree69e81ebe38de13883bad392e9fd0fa61ecaaa9b5 /src/FundLab.Api/Persistence.fs
parent248b50c8fd057c87a852f8306fc4585905880abc (diff)
downloadfund-lab-877e03183b5acaff84526cb9fe978280cadd8a76.tar.gz
Backfill the missing cash debit for pre-3d-34 stock buys (3d-39)
Diffstat (limited to 'src/FundLab.Api/Persistence.fs')
-rw-r--r--src/FundLab.Api/Persistence.fs58
1 files changed, 49 insertions, 9 deletions
diff --git a/src/FundLab.Api/Persistence.fs b/src/FundLab.Api/Persistence.fs
index f18ad68..0f81aea 100644
--- a/src/FundLab.Api/Persistence.fs
+++ b/src/FundLab.Api/Persistence.fs
@@ -7149,6 +7149,13 @@ type FundRepository(connectionString: string) =
/// statement derives its amount straight from the same table the live hook reads, and
/// the (fund_id, reference_type, reference_id) unique key makes re-runs no-ops, so a
/// migrated database is unchanged and no movement is ever counted twice.
+ ///
+ /// Stock buys executed before the 3d-34 debit fix (2dedc30) never reduced
+ /// available_cash, so rebuilding only their ledger event would leave the fund
+ /// imbalanced by the cost. That batch runs last and also debits available_cash for the
+ /// funds still missing it, but only where the fund reconciles to zero before the batch
+ /// -- exactly the case where cash never paid for the buy. Funds whose debit already
+ /// happened are left untouched, so it stays idempotent.
member _.BackfillCashLedger() : int =
let statements =
[
@@ -7174,15 +7181,6 @@ type FundRepository(connectionString: string) =
"""
INSERT INTO cash_ledger_events
(id, fund_id, source_category, event_date, amount, reference_type, reference_id, note)
- SELECT gen_random_uuid(), t.fund_id, 'stock_buy',
- (t.executed_at AT TIME ZONE 'UTC')::date, -t.cost_cash,
- 'stock_buy', t.id::text, NULL
- FROM stock_trades t
- ON CONFLICT (fund_id, reference_type, reference_id) DO NOTHING
- """
- """
- INSERT INTO cash_ledger_events
- (id, fund_id, source_category, event_date, amount, reference_type, reference_id, note)
SELECT gen_random_uuid(), s.fund_id, 'stock_sell',
(s.executed_at AT TIME ZONE 'UTC')::date, s.proceeds,
'stock_sell', s.id::text, NULL
@@ -7239,6 +7237,48 @@ type FundRepository(connectionString: string) =
WHERE o.confirmed_invested_cash IS NOT NULL
ON CONFLICT (fund_id, reference_type, reference_id) DO NOTHING
"""
+ """
+ WITH candidate AS (
+ SELECT t.fund_id,
+ t.id::text AS reference_id,
+ (t.executed_at AT TIME ZONE 'UTC')::date AS event_date,
+ -t.cost_cash AS amount
+ FROM stock_trades t
+ WHERE NOT EXISTS (
+ SELECT 1
+ FROM cash_ledger_events e
+ WHERE e.fund_id = t.fund_id
+ AND e.reference_type = 'stock_buy'
+ AND e.reference_id = t.id::text
+ )
+ ),
+ inserted AS (
+ INSERT INTO cash_ledger_events
+ (id, fund_id, source_category, event_date, amount, reference_type, reference_id, note)
+ SELECT gen_random_uuid(), c.fund_id, 'stock_buy', c.event_date, c.amount,
+ 'stock_buy', c.reference_id, NULL
+ FROM candidate c
+ ON CONFLICT (fund_id, reference_type, reference_id) DO NOTHING
+ RETURNING fund_id, amount
+ ),
+ totals AS (
+ SELECT fund_id, SUM(amount) AS amount
+ FROM inserted
+ GROUP BY fund_id
+ ),
+ missing_debit AS (
+ SELECT f.id AS fund_id, t.amount
+ FROM funds f
+ JOIN totals t ON t.fund_id = f.id
+ WHERE f.initial_cash
+ + COALESCE((SELECT SUM(e.amount) FROM cash_ledger_events e WHERE e.fund_id = f.id), 0)
+ - (f.available_cash + f.reserved_cash) = 0
+ )
+ UPDATE funds f
+ SET available_cash = f.available_cash + m.amount
+ FROM missing_debit m
+ WHERE f.id = m.fund_id
+ """
]
use connection = new NpgsqlConnection(connectionString)