Skip to content

Commit a84f109

Browse files
committed
update
1 parent 890504b commit a84f109

4 files changed

Lines changed: 137 additions & 14 deletions

File tree

manager_for_ynab/auto_approve/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
@dataclass(frozen=True)
3131
class Transaction:
3232
id: str
33-
matched_transaction_id: str
33+
matched_transaction_id: str | None
3434
plan_id: str
3535
account_name: str
3636
payee_name: str
@@ -96,7 +96,7 @@ async def auto_approve(
9696
found_txns = [txn for txns in txns_by_plan.values() for txn in txns]
9797
total_txns = len(found_txns)
9898

99-
_print(f"Found {total_txns} matched transaction(s) to approve.", quiet=quiet)
99+
_print(f"Found {total_txns} transaction(s) to approve.", quiet=quiet)
100100
if found_txns:
101101
print_found_txns(found_txns, quiet=quiet)
102102

@@ -139,6 +139,7 @@ def build_updates(
139139
ynab.SaveTransactionWithIdOrImportId(id=txn_id, approved=True)
140140
for txn in txns
141141
for txn_id in (txn.id, txn.matched_transaction_id)
142+
if txn_id is not None
142143
)
143144
return grouped
144145

@@ -170,7 +171,7 @@ def print_found_txns(found_txns: list[Transaction], *, quiet: bool) -> None:
170171
if quiet:
171172
return
172173

173-
table = Table(title="Matched Transactions To Approve")
174+
table = Table(title="Transactions To Approve")
174175
table.add_column("Date")
175176
table.add_column("Account")
176177
table.add_column("Payee")

manager_for_ynab/auto_approve/auto_approve.sql

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,27 @@ FROM transactions
1010
WHERE
1111
transactions.deleted = 0
1212
AND transactions.approved = 0
13-
AND transactions.matched_transaction_id IS NOT NULL
14-
-- matched pairs reference each other
15-
-- so keep one stable row per pair
16-
AND transactions.id < transactions.matched_transaction_id
13+
AND (
14+
(
15+
transactions.matched_transaction_id IS NOT NULL
16+
-- matched pairs reference each other
17+
-- so keep one stable row per pair
18+
AND transactions.id < transactions.matched_transaction_id
19+
)
20+
OR EXISTS (
21+
SELECT 1
22+
FROM scheduled_transactions
23+
WHERE
24+
scheduled_transactions.plan_id = transactions.plan_id
25+
AND scheduled_transactions.deleted = 0
26+
AND scheduled_transactions.account_name
27+
= transactions.account_name
28+
AND scheduled_transactions.payee_name = transactions.payee_name
29+
AND scheduled_transactions.amount = transactions.amount
30+
AND DATE(scheduled_transactions.date_next)
31+
= DATE(transactions."date", '+1 month')
32+
)
33+
)
1734
ORDER BY
1835
transactions."date" ASC
1936
, transactions.account_name ASC

testing/seed.sql

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,72 @@ CREATE TABLE subtransactions (
514514
)
515515
;
516516

517+
CREATE TABLE scheduled_transactions (
518+
id TEXT PRIMARY KEY
519+
, plan_id TEXT
520+
, account_id TEXT
521+
, account_name TEXT
522+
, amount INT
523+
, amount_formatted TEXT
524+
, amount_currency REAL
525+
, category_id TEXT
526+
, category_name TEXT
527+
, date_first TEXT
528+
, date_next TEXT
529+
, deleted BOOLEAN
530+
, flag_color TEXT
531+
, flag_name TEXT
532+
, frequency TEXT
533+
, memo TEXT
534+
, payee_id TEXT
535+
, payee_name TEXT
536+
, transfer_account_id TEXT
537+
)
538+
;
539+
540+
INSERT INTO scheduled_transactions (
541+
id
542+
, plan_id
543+
, account_id
544+
, account_name
545+
, amount
546+
, amount_formatted
547+
, amount_currency
548+
, category_id
549+
, category_name
550+
, date_first
551+
, date_next
552+
, deleted
553+
, flag_color
554+
, flag_name
555+
, frequency
556+
, memo
557+
, payee_id
558+
, payee_name
559+
, transfer_account_id
560+
) VALUES (
561+
'sched-solo'
562+
, 'plan-1'
563+
, NULL
564+
, 'Checking'
565+
, -7000
566+
, '-$7.00'
567+
, -7.0
568+
, NULL
569+
, NULL
570+
, '2026-04-21'
571+
, '2026-05-21'
572+
, 0
573+
, NULL
574+
, NULL
575+
, 'monthly'
576+
, NULL
577+
, NULL
578+
, 'Solo'
579+
, NULL
580+
)
581+
;
582+
517583
INSERT INTO transactions (
518584
id
519585
, plan_id

tests/auto_approve/test.py

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def test_fetch_auto_approve_transactions_filters_expected_rows(db):
2929
found = await fetch_auto_approve_transactions(con)
3030

3131
assert {plan_id: [txn.id for txn in txns] for plan_id, txns in found.items()} == {
32-
"plan-1": ["pair-a-1"],
32+
"plan-1": ["pair-a-1", "unmatched"],
3333
"plan-2": ["pair-b-1"],
3434
}
3535

@@ -69,6 +69,28 @@ def test_build_updates_groups_by_plan_and_updates_both_ids():
6969
assert all(txn.approved is True for txns in updates.values() for txn in txns)
7070

7171

72+
def test_build_updates_approves_unmatched_scheduled_transactions():
73+
txns_by_plan = {
74+
"plan-1": [
75+
Transaction(
76+
id="txn-1",
77+
matched_transaction_id=None,
78+
plan_id="plan-1",
79+
account_name="Checking",
80+
payee_name="Apple",
81+
amount_formatted="-$21.76",
82+
date="2026-04-20",
83+
)
84+
]
85+
}
86+
87+
updates = build_updates(txns_by_plan)
88+
89+
assert {plan_id: [txn.id for txn in txns] for plan_id, txns in updates.items()} == {
90+
"plan-1": ["txn-1"],
91+
}
92+
93+
7294
@patch.dict("os.environ", {_ENV_TOKEN: ""})
7395
@pytest.mark.asyncio
7496
async def test_run_requires_token(db):
@@ -105,6 +127,15 @@ def _expected_auto_approve_result(updated_count: int) -> AutoApproveResult:
105127
amount_formatted="-$4.50",
106128
date="2026-04-20",
107129
),
130+
Transaction(
131+
id="unmatched",
132+
matched_transaction_id=None,
133+
plan_id="plan-1",
134+
account_name="Checking",
135+
payee_name="Solo",
136+
amount_formatted="-$7.00",
137+
date="2026-04-21",
138+
),
108139
Transaction(
109140
id="pair-b-1",
110141
matched_transaction_id="pair-b-2",
@@ -177,9 +208,13 @@ async def test_auto_approve_for_real_returns_updated_count(
177208
ynab_api_client.assert_called_once_with(ynab_configuration.return_value)
178209
sync.assert_called_once_with("token", db, False, quiet=True)
179210
assert [plan_id for plan_id, _ in updates] == ["plan-1", "plan-2"]
180-
assert [txn.id for txn in updates[0][1].transactions] == ["pair-a-1", "pair-a-2"]
211+
assert [txn.id for txn in updates[0][1].transactions] == [
212+
"pair-a-1",
213+
"pair-a-2",
214+
"unmatched",
215+
]
181216
assert [txn.id for txn in updates[1][1].transactions] == ["pair-b-1", "pair-b-2"]
182-
assert result == _expected_auto_approve_result(2)
217+
assert result == _expected_auto_approve_result(3)
183218

184219

185220
@patch.dict("os.environ", {_ENV_TOKEN: "token"})
@@ -194,7 +229,7 @@ async def test_run_dry_run_does_not_update_transactions(sync, db, capsys):
194229
sync.assert_called_once_with("token", db, False, quiet=False)
195230
assert "** Refreshing SQLite DB **" in out
196231
assert "** Done **" in out
197-
assert "Found 2 matched transaction(s) to approve." in out
232+
assert "Found 3 transaction(s) to approve." in out
198233
assert "Use --for-real to actually approve transactions." in out
199234

200235

@@ -217,7 +252,7 @@ async def test_run_quiet_suppresses_all_output(sync, db, capsys):
217252
async def test_run_no_matching_transactions(sync, db, capsys):
218253
with sqlite3.connect(db) as con:
219254
con.execute(
220-
"UPDATE transactions SET approved = 1 WHERE matched_transaction_id IS NOT NULL"
255+
"UPDATE transactions SET approved = 1 WHERE matched_transaction_id IS NOT NULL OR id = 'unmatched'"
221256
)
222257

223258
ret = await run(("--sqlite-export-for-ynab-db", str(db)))
@@ -227,7 +262,7 @@ async def test_run_no_matching_transactions(sync, db, capsys):
227262
sync.assert_called_once_with("token", db, False, quiet=False)
228263
assert "** Refreshing SQLite DB **" in out
229264
assert "** Done **" in out
230-
assert "Found 0 matched transaction(s) to approve." in out
265+
assert "Found 0 transaction(s) to approve." in out
231266

232267

233268
@patch.dict("os.environ", {_ENV_TOKEN: "token"})
@@ -248,5 +283,9 @@ async def test_run_for_real_updates_transactions_grouped_by_plan(
248283
ynab_api_client.assert_called_once_with(ynab_configuration.return_value)
249284
sync.assert_called_once_with("token", db, False, quiet=False)
250285
assert [plan_id for plan_id, _ in updates] == ["plan-1", "plan-2"]
251-
assert [txn.id for txn in updates[0][1].transactions] == ["pair-a-1", "pair-a-2"]
286+
assert [txn.id for txn in updates[0][1].transactions] == [
287+
"pair-a-1",
288+
"pair-a-2",
289+
"unmatched",
290+
]
252291
assert [txn.id for txn in updates[1][1].transactions] == ["pair-b-1", "pair-b-2"]

0 commit comments

Comments
 (0)