feat(scripts): import the matched split history as settled
ci / lint-test (push) Successful in 46s

Adds --write to the matcher. Wrote 1,242 split rows across 657 transactions.

Imported settled, and that is the whole design. These obligations were
discharged years ago on a platform we no longer run, and their residual is
already carried by transaction 2348. Writing them unsettled would re-open
roughly $40k of debts that were paid. ACTIVE_OBLIGATION keeps settled splits
out of every owed figure while myShare/mySplitOf still count them, which is
exactly the asymmetry this needs: the import exists to correct historical
SPEND, not to move a balance.

Effect: $35,259 leaves my historical spend -- $13,088 in 2024, $22,117 in
2025 -- because a $200 grocery shop that was always half hers no longer reads
as $200 of mine. Balances are byte-identical before and after (Molina
-1226.72/145, Sonu 5428.08/419), which is the assertion that matters.

Shares are written as the CSV computed them, so a 50/50 row can land as
50.01/49.99. That is faithful rather than tidy; no transaction exceeds 100%.

Rehearsed on the 37-row Rome file first (24 rows) and verified before the full
run -- both the balances and one split read back through the API.
This commit is contained in:
2026-07-28 12:02:57 +10:00
parent c9b000a428
commit b4a116c134
+34
View File
@@ -221,6 +221,10 @@ def main() -> int:
ap.add_argument("--verbose", action="store_true") ap.add_argument("--verbose", action="store_true")
ap.add_argument("--file", help="only this CSV (substring match)") ap.add_argument("--file", help="only this CSV (substring match)")
ap.add_argument("--window", type=int, default=5, help="date tolerance in days") ap.add_argument("--window", type=int, default=5, help="date tolerance in days")
ap.add_argument(
"--write", action="store_true",
help="actually insert the splits (settled=true). Without this, nothing is written.",
)
args = ap.parse_args() args = ap.parse_args()
paths = sorted(glob.glob(os.path.join(DUMP_DIR, "*SplitMyExpenses*.csv"))) paths = sorted(glob.glob(os.path.join(DUMP_DIR, "*SplitMyExpenses*.csv")))
@@ -324,6 +328,36 @@ def main() -> int:
print(f"Of the matched, {already} already carry a split and would be left alone;") print(f"Of the matched, {already} already carry a split and would be left alone;")
print(f"{len(rep.matched) - already} would gain one.") print(f"{len(rep.matched) - already} would gain one.")
if args.write:
# Imported as settled: these obligations were discharged on a platform
# we no longer run, and the residual is already carried by transaction
# 2348. Writing them unsettled would re-open ~$40k of debts that were
# paid years ago. See ACTIVE_OBLIGATION in analytics-sql.ts -- settled
# rows stay in spend and leave every owed figure, which is exactly the
# point: this import exists to correct historical SPEND.
SETTLED_ON = "2026-01-09" # the carryover's date
written = 0
for row, tx in rep.matched:
if tx["has_split"]:
continue
payer_share = round(100 - row.ower_share, 2)
pairs = [(row.ower, row.ower_share)]
if payer_share > 0:
pairs.append((row.payer, payer_share))
for pid, share in pairs:
cur.execute(
"""
INSERT INTO transaction_splits
(transaction_id, participant_id, share_percent, settled, settled_at)
VALUES (%s, %s, %s, true, %s)
ON CONFLICT (transaction_id, participant_id) DO NOTHING
""",
(tx["id"], pid, share, SETTLED_ON),
)
written += cur.rowcount
conn.commit()
print(f"\nWROTE {written} split rows (settled=true, settled_at={SETTLED_ON}).")
if args.verbose: if args.verbose:
print("\n--- ambiguous ---") print("\n--- ambiguous ---")
for row, cands in rep.ambiguous[:40]: for row, cands in rep.ambiguous[:40]: