mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 02:56:39 +07:00
fix: restrict transaction splits to one parent category, show subcategory breakdown
Раньше сплит позволял смешивать вообще любые категории. Реальная задача — разложить одну покупку по подкатегориям ОДНОЙ базовой категории (100 € на продукты: сколько на сладости, сколько на уход). А раз Dashboard и рейтинг категорий сворачивают подкатегории в родителя, эта разбивка нигде не была видна. - _build_splits() требует, чтобы все категории сплита были одной родительской категорией или её прямыми подкатегориями. - category_rollup.py отдаёт для каждого родительского тотала список category-источников, которые в него скинулись (children) — пусто, если источник один. - Dashboard и рейтинг категорий: строка с непустым children раскрывается по клику, показывая вклад каждой подкатегории. - Форма транзакции: выбор категории при сплите — сначала базовая (верхний уровень), затем строки сплита выбирают саму базовую или её подкатегорию.
This commit is contained in:
@@ -66,7 +66,9 @@ async def test_backup_import_rejects_transaction_with_unknown_tag_id(client: Asy
|
||||
|
||||
async def test_backup_roundtrip_preserves_transaction_splits(client: AsyncClient, account_id, categories):
|
||||
groceries = categories["Groceries"]["id"]
|
||||
shopping = categories["Shopping"]["id"]
|
||||
sweets = (
|
||||
await client.post("/categories", json={"name": "Sweets", "kind": "expense", "color": "#7a869a", "parent_id": groceries})
|
||||
).json()["id"]
|
||||
created = await client.post(
|
||||
"/transactions",
|
||||
json=_txn(
|
||||
@@ -75,7 +77,7 @@ async def test_backup_roundtrip_preserves_transaction_splits(client: AsyncClient
|
||||
category_id=None,
|
||||
splits=[
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": shopping, "amount": "30.00", "note": "household chemicals"},
|
||||
{"category_id": sweets, "amount": "30.00", "note": "candy and snacks"},
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -89,5 +91,5 @@ async def test_backup_roundtrip_preserves_transaction_splits(client: AsyncClient
|
||||
|
||||
refetched = next(t for t in (await client.get("/transactions")).json()["items"] if t["id"] == txn_id)
|
||||
splits_by_note = {s["note"]: s for s in refetched["splits"]}
|
||||
assert splits_by_note["household chemicals"]["category_id"] == shopping
|
||||
assert splits_by_note["candy and snacks"]["category_id"] == sweets
|
||||
assert refetched["category"] is None
|
||||
|
||||
@@ -135,11 +135,17 @@ async def test_a_subcategory_keeps_its_own_budget_separate(client: AsyncClient,
|
||||
|
||||
|
||||
async def test_budget_status_counts_a_split_transactions_share(client: AsyncClient, account_id, categories):
|
||||
"""A receipt split between Groceries and Shopping must count its 70.00
|
||||
slice toward the Groceries budget, not the full 100.00 or nothing."""
|
||||
"""A receipt split between Groceries and one of its own subcategories
|
||||
(Sweets) must count each line toward the right budget: the full 100.00
|
||||
rolls up into the Groceries budget, but only the 30.00 Sweets line
|
||||
counts toward a budget set on Sweets itself."""
|
||||
groceries = categories["Groceries"]["id"]
|
||||
shopping = categories["Shopping"]["id"]
|
||||
sweets_resp = await client.post(
|
||||
"/categories", json={"name": "Sweets", "kind": "expense", "color": "#7a869a", "parent_id": groceries}
|
||||
)
|
||||
sweets = sweets_resp.json()["id"]
|
||||
await client.post("/budgets", json={"category_id": groceries, "monthly_limit": "100.00"})
|
||||
await client.post("/budgets", json={"category_id": sweets, "monthly_limit": "50.00"})
|
||||
await client.post(
|
||||
"/transactions",
|
||||
json=txn_payload(
|
||||
@@ -149,11 +155,12 @@ async def test_budget_status_counts_a_split_transactions_share(client: AsyncClie
|
||||
date="2026-08-10",
|
||||
splits=[
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": shopping, "amount": "30.00"},
|
||||
{"category_id": sweets, "amount": "30.00"},
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
resp = await client.get("/budgets/status", params={"year": 2026, "month": 8})
|
||||
item = resp.json()["items"][0]
|
||||
assert money(item["spent"]) == Decimal("70.00")
|
||||
items = {item["category_id"]: item for item in resp.json()["items"]}
|
||||
assert money(items[groceries]["spent"]) == Decimal("100.00")
|
||||
assert money(items[sweets]["spent"]) == Decimal("30.00")
|
||||
|
||||
@@ -127,14 +127,20 @@ async def test_more_than_eight_expense_categories_roll_up_into_other(client: Asy
|
||||
assert money(other[0]["amount"]) == Decimal("10.00")
|
||||
|
||||
|
||||
async def test_split_transaction_counts_each_split_toward_its_own_category(
|
||||
async def test_split_transaction_rolls_up_into_one_slice_with_a_children_breakdown(
|
||||
client: AsyncClient, account_id, categories
|
||||
):
|
||||
"""A hypermarket receipt split between Groceries and Shopping must show
|
||||
up as two separate slices on the donut, not one lump under whichever
|
||||
category happened to be picked."""
|
||||
"""A hypermarket receipt split between Groceries and one of its own
|
||||
subcategories (Sweets) must show up as a single Groceries slice on the
|
||||
donut with the full 100.00 total — a split's categories always share one
|
||||
parent (see routes/transactions.py's _build_splits), so it never
|
||||
fragments into separate top-level slices — but the slice must expose a
|
||||
children breakdown so the Groceries/Sweets split is still visible."""
|
||||
groceries = categories["Groceries"]["id"]
|
||||
shopping = categories["Shopping"]["id"]
|
||||
sweets_resp = await client.post(
|
||||
"/categories", json={"name": "Sweets", "kind": "expense", "color": "#7a869a", "parent_id": groceries}
|
||||
)
|
||||
sweets = sweets_resp.json()["id"]
|
||||
await client.post(
|
||||
"/transactions",
|
||||
json=_txn(
|
||||
@@ -144,7 +150,7 @@ async def test_split_transaction_counts_each_split_toward_its_own_category(
|
||||
date="2026-08-01",
|
||||
splits=[
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": shopping, "amount": "30.00"},
|
||||
{"category_id": sweets, "amount": "30.00"},
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -154,5 +160,25 @@ async def test_split_transaction_counts_each_split_toward_its_own_category(
|
||||
breakdown = {row["name"]: row for row in body["spending_by_category"]}
|
||||
|
||||
assert money(body["spent"]) == Decimal("100.00")
|
||||
assert money(breakdown["Groceries"]["amount"]) == Decimal("70.00")
|
||||
assert money(breakdown["Shopping"]["amount"]) == Decimal("30.00")
|
||||
assert "Sweets" not in breakdown
|
||||
assert money(breakdown["Groceries"]["amount"]) == Decimal("100.00")
|
||||
|
||||
children = {child["name"]: child for child in breakdown["Groceries"]["children"]}
|
||||
# The 70.00 line was filed directly on Groceries itself (no
|
||||
# subcategory), so it shows up as a child entry with Groceries' own id
|
||||
# and name too — same as any genuine subcategory would.
|
||||
assert money(children["Groceries"]["amount"]) == Decimal("70.00")
|
||||
assert money(children["Sweets"]["amount"]) == Decimal("30.00")
|
||||
|
||||
|
||||
async def test_a_single_category_slice_has_no_children_breakdown(client: AsyncClient, account_id, categories):
|
||||
"""A category funded by exactly one source (the common case — no
|
||||
splitting, no subcategories involved) shouldn't show a redundant
|
||||
one-item breakdown of itself."""
|
||||
groceries = categories["Groceries"]["id"]
|
||||
await client.post("/transactions", json=_txn(account_id, amount="50.00", category_id=groceries, date="2026-08-01"))
|
||||
|
||||
resp = await client.get("/dashboard/summary", params={"year": 2026, "month": 8})
|
||||
breakdown = {row["name"]: row for row in resp.json()["spending_by_category"]}
|
||||
|
||||
assert breakdown["Groceries"]["children"] == []
|
||||
|
||||
78
backend/tests/test_reports.py
Normal file
78
backend/tests/test_reports.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Long-range reports: the category ranking (which categories cost the
|
||||
most over an arbitrary period) and the single-category spending detail —
|
||||
both split-aware via services/category_rollup.py, same as the Dashboard
|
||||
breakdown (see test_dashboard.py).
|
||||
"""
|
||||
from decimal import Decimal
|
||||
|
||||
from httpx import AsyncClient
|
||||
|
||||
from tests.helpers import money, txn_payload as _txn
|
||||
|
||||
|
||||
async def test_ranking_rolls_up_a_split_into_one_item_with_children(client: AsyncClient, account_id, categories):
|
||||
groceries = categories["Groceries"]["id"]
|
||||
sweets = (
|
||||
await client.post("/categories", json={"name": "Sweets", "kind": "expense", "color": "#7a869a", "parent_id": groceries})
|
||||
).json()["id"]
|
||||
await client.post(
|
||||
"/transactions",
|
||||
json=_txn(
|
||||
account_id,
|
||||
amount="100.00",
|
||||
category_id=None,
|
||||
date="2026-08-01",
|
||||
splits=[
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": sweets, "amount": "30.00"},
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
resp = await client.get("/reports/category-ranking", params={"kind": "expense"})
|
||||
items = {item["name"]: item for item in resp.json()["items"]}
|
||||
|
||||
assert "Sweets" not in items
|
||||
assert money(items["Groceries"]["amount"]) == Decimal("100.00")
|
||||
children = {child["name"]: child for child in items["Groceries"]["children"]}
|
||||
assert money(children["Groceries"]["amount"]) == Decimal("70.00")
|
||||
assert money(children["Sweets"]["amount"]) == Decimal("30.00")
|
||||
|
||||
|
||||
async def test_ranking_item_with_a_single_source_has_no_children(client: AsyncClient, account_id, categories):
|
||||
groceries = categories["Groceries"]["id"]
|
||||
await client.post("/transactions", json=_txn(account_id, amount="50.00", category_id=groceries, date="2026-08-01"))
|
||||
|
||||
resp = await client.get("/reports/category-ranking", params={"kind": "expense"})
|
||||
items = {item["name"]: item for item in resp.json()["items"]}
|
||||
|
||||
assert items["Groceries"]["children"] == []
|
||||
|
||||
|
||||
async def test_category_spending_report_counts_a_subcategorys_split_share(client: AsyncClient, account_id, categories):
|
||||
"""A top-level category's own report folds in both its plain
|
||||
transactions and any split lines landing on it or its subcategories."""
|
||||
groceries = categories["Groceries"]["id"]
|
||||
sweets = (
|
||||
await client.post("/categories", json={"name": "Sweets", "kind": "expense", "color": "#7a869a", "parent_id": groceries})
|
||||
).json()["id"]
|
||||
await client.post(
|
||||
"/transactions",
|
||||
json=_txn(
|
||||
account_id,
|
||||
amount="100.00",
|
||||
category_id=None,
|
||||
date="2026-08-01",
|
||||
splits=[
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": sweets, "amount": "30.00"},
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
parent_report = (await client.get("/reports/category-spending", params={"category_id": groceries})).json()
|
||||
assert money(parent_report["total_amount"]) == Decimal("100.00")
|
||||
assert parent_report["transaction_count"] == 1
|
||||
|
||||
child_report = (await client.get("/reports/category-spending", params={"category_id": sweets})).json()
|
||||
assert money(child_report["total_amount"]) == Decimal("30.00")
|
||||
@@ -403,10 +403,47 @@ async def test_a_transfer_whose_destination_account_was_deleted_can_still_be_lis
|
||||
assert (await client.delete(f"/transactions/{txn_id}")).status_code == 204
|
||||
|
||||
|
||||
async def _subcategory(client: AsyncClient, parent_id: int, name: str) -> int:
|
||||
resp = await client.post(
|
||||
"/categories", json={"name": name, "kind": "expense", "color": "#7a869a", "parent_id": parent_id}
|
||||
)
|
||||
return resp.json()["id"]
|
||||
|
||||
|
||||
async def test_create_split_transaction_across_categories(client: AsyncClient, account_id, categories):
|
||||
"""The hypermarket-receipt case: one purchase, part groceries, part
|
||||
shopping — category_id stays empty on the row itself, and its splits
|
||||
carry the categorization instead."""
|
||||
"""The hypermarket-receipt case: one purchase, part everyday groceries,
|
||||
part sweets — a split's categories must share one parent (see
|
||||
_build_splits), so both lines here are Groceries itself and one of its
|
||||
own subcategories. category_id stays empty on the row itself, and its
|
||||
splits carry the categorization instead."""
|
||||
groceries = categories["Groceries"]["id"]
|
||||
sweets = await _subcategory(client, groceries, "Sweets")
|
||||
resp = await client.post(
|
||||
"/transactions",
|
||||
json=_txn(
|
||||
account_id,
|
||||
amount="100.00",
|
||||
category_id=None,
|
||||
splits=[
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": sweets, "amount": "30.00", "note": "candy and snacks"},
|
||||
],
|
||||
),
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
assert body["category"] is None
|
||||
splits = {s["category"]["name"]: s for s in body["splits"]}
|
||||
assert money(splits["Groceries"]["amount"]) == Decimal("70.00")
|
||||
assert money(splits["Sweets"]["amount"]) == Decimal("30.00")
|
||||
assert splits["Sweets"]["note"] == "candy and snacks"
|
||||
|
||||
|
||||
async def test_create_split_rejects_categories_from_different_parents(client: AsyncClient, account_id, categories):
|
||||
"""The whole point of a split is dividing one purchase across the
|
||||
subcategories of ONE parent — Groceries and Shopping are two unrelated
|
||||
top-level categories, so mixing them must be rejected even though both
|
||||
are individually valid expense categories."""
|
||||
groceries = categories["Groceries"]["id"]
|
||||
shopping = categories["Shopping"]["id"]
|
||||
resp = await client.post(
|
||||
@@ -417,22 +454,36 @@ async def test_create_split_transaction_across_categories(client: AsyncClient, a
|
||||
category_id=None,
|
||||
splits=[
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": shopping, "amount": "30.00", "note": "household chemicals"},
|
||||
{"category_id": shopping, "amount": "30.00"},
|
||||
],
|
||||
),
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
assert body["category"] is None
|
||||
splits = {s["category"]["name"]: s for s in body["splits"]}
|
||||
assert money(splits["Groceries"]["amount"]) == Decimal("70.00")
|
||||
assert money(splits["Shopping"]["amount"]) == Decimal("30.00")
|
||||
assert splits["Shopping"]["note"] == "household chemicals"
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
async def test_create_split_rejects_two_different_parents_subcategories(client: AsyncClient, account_id, categories):
|
||||
groceries = categories["Groceries"]["id"]
|
||||
shopping = categories["Shopping"]["id"]
|
||||
sweets = await _subcategory(client, groceries, "Sweets")
|
||||
electronics = await _subcategory(client, shopping, "Electronics")
|
||||
resp = await client.post(
|
||||
"/transactions",
|
||||
json=_txn(
|
||||
account_id,
|
||||
amount="100.00",
|
||||
category_id=None,
|
||||
splits=[
|
||||
{"category_id": sweets, "amount": "70.00"},
|
||||
{"category_id": electronics, "amount": "30.00"},
|
||||
],
|
||||
),
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
async def test_create_split_rejects_category_id_alongside_splits(client: AsyncClient, account_id, categories):
|
||||
groceries = categories["Groceries"]["id"]
|
||||
shopping = categories["Shopping"]["id"]
|
||||
sweets = await _subcategory(client, groceries, "Sweets")
|
||||
resp = await client.post(
|
||||
"/transactions",
|
||||
json=_txn(
|
||||
@@ -441,7 +492,7 @@ async def test_create_split_rejects_category_id_alongside_splits(client: AsyncCl
|
||||
category_id=groceries,
|
||||
splits=[
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": shopping, "amount": "30.00"},
|
||||
{"category_id": sweets, "amount": "30.00"},
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -460,7 +511,7 @@ async def test_create_split_rejects_a_single_split(client: AsyncClient, account_
|
||||
|
||||
async def test_create_split_rejects_amounts_not_summing_to_total(client: AsyncClient, account_id, categories):
|
||||
groceries = categories["Groceries"]["id"]
|
||||
shopping = categories["Shopping"]["id"]
|
||||
sweets = await _subcategory(client, groceries, "Sweets")
|
||||
resp = await client.post(
|
||||
"/transactions",
|
||||
json=_txn(
|
||||
@@ -469,7 +520,7 @@ async def test_create_split_rejects_amounts_not_summing_to_total(client: AsyncCl
|
||||
category_id=None,
|
||||
splits=[
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": shopping, "amount": "20.00"},
|
||||
{"category_id": sweets, "amount": "20.00"},
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -479,7 +530,7 @@ async def test_create_split_rejects_amounts_not_summing_to_total(client: AsyncCl
|
||||
async def test_create_split_rejects_on_transfer(client: AsyncClient, account_id, categories):
|
||||
other = (await client.post("/accounts", json={"name": "Savings", "type": "savings"})).json()
|
||||
groceries = categories["Groceries"]["id"]
|
||||
shopping = categories["Shopping"]["id"]
|
||||
sweets = await _subcategory(client, groceries, "Sweets")
|
||||
resp = await client.post(
|
||||
"/transactions",
|
||||
json=_txn(
|
||||
@@ -490,7 +541,7 @@ async def test_create_split_rejects_on_transfer(client: AsyncClient, account_id,
|
||||
transfer_account_id=other["id"],
|
||||
splits=[
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": shopping, "amount": "30.00"},
|
||||
{"category_id": sweets, "amount": "30.00"},
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -499,7 +550,7 @@ async def test_create_split_rejects_on_transfer(client: AsyncClient, account_id,
|
||||
|
||||
async def test_update_can_convert_a_transaction_into_a_split_and_back(client: AsyncClient, account_id, categories):
|
||||
groceries = categories["Groceries"]["id"]
|
||||
shopping = categories["Shopping"]["id"]
|
||||
sweets = await _subcategory(client, groceries, "Sweets")
|
||||
created = await client.post("/transactions", json=_txn(account_id, amount="100.00", category_id=groceries))
|
||||
txn_id = created.json()["id"]
|
||||
|
||||
@@ -509,7 +560,7 @@ async def test_update_can_convert_a_transaction_into_a_split_and_back(client: As
|
||||
"category_id": None,
|
||||
"splits": [
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": shopping, "amount": "30.00"},
|
||||
{"category_id": sweets, "amount": "30.00"},
|
||||
],
|
||||
},
|
||||
)
|
||||
@@ -525,7 +576,7 @@ async def test_update_can_convert_a_transaction_into_a_split_and_back(client: As
|
||||
|
||||
async def test_update_rejects_splits_not_summing_to_the_new_amount(client: AsyncClient, account_id, categories):
|
||||
groceries = categories["Groceries"]["id"]
|
||||
shopping = categories["Shopping"]["id"]
|
||||
sweets = await _subcategory(client, groceries, "Sweets")
|
||||
created = await client.post(
|
||||
"/transactions",
|
||||
json=_txn(
|
||||
@@ -534,7 +585,7 @@ async def test_update_rejects_splits_not_summing_to_the_new_amount(client: Async
|
||||
category_id=None,
|
||||
splits=[
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": shopping, "amount": "30.00"},
|
||||
{"category_id": sweets, "amount": "30.00"},
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -555,10 +606,7 @@ async def test_update_of_unrelated_field_tolerates_a_split_whose_category_was_de
|
||||
state — the split's own category was never re-submitted, so nothing here
|
||||
needs it to still exist."""
|
||||
groceries = categories["Groceries"]["id"]
|
||||
extra = await client.post(
|
||||
"/categories", json={"name": "Deletable", "kind": "expense", "color": "#123456", "sort_order": 99}
|
||||
)
|
||||
extra_id = extra.json()["id"]
|
||||
deletable = await _subcategory(client, groceries, "Deletable")
|
||||
created = await client.post(
|
||||
"/transactions",
|
||||
json=_txn(
|
||||
@@ -567,12 +615,12 @@ async def test_update_of_unrelated_field_tolerates_a_split_whose_category_was_de
|
||||
category_id=None,
|
||||
splits=[
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": extra_id, "amount": "30.00"},
|
||||
{"category_id": deletable, "amount": "30.00"},
|
||||
],
|
||||
),
|
||||
)
|
||||
txn_id = created.json()["id"]
|
||||
assert (await client.delete(f"/categories/{extra_id}")).status_code == 204
|
||||
assert (await client.delete(f"/categories/{deletable}")).status_code == 204
|
||||
|
||||
resp = await client.patch(f"/transactions/{txn_id}", json={"description": "renamed"})
|
||||
assert resp.status_code == 200
|
||||
@@ -584,7 +632,7 @@ async def test_category_filter_matches_transactions_split_into_that_category(
|
||||
client: AsyncClient, account_id, categories
|
||||
):
|
||||
groceries = categories["Groceries"]["id"]
|
||||
shopping = categories["Shopping"]["id"]
|
||||
sweets = await _subcategory(client, groceries, "Sweets")
|
||||
await client.post(
|
||||
"/transactions",
|
||||
json=_txn(
|
||||
@@ -593,18 +641,18 @@ async def test_category_filter_matches_transactions_split_into_that_category(
|
||||
category_id=None,
|
||||
splits=[
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": shopping, "amount": "30.00"},
|
||||
{"category_id": sweets, "amount": "30.00"},
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
resp = await client.get("/transactions", params={"category_id": shopping})
|
||||
resp = await client.get("/transactions", params={"category_id": sweets})
|
||||
assert resp.json()["total"] == 1
|
||||
|
||||
|
||||
async def test_delete_transaction_cascades_its_splits(client: AsyncClient, account_id, categories):
|
||||
groceries = categories["Groceries"]["id"]
|
||||
shopping = categories["Shopping"]["id"]
|
||||
sweets = await _subcategory(client, groceries, "Sweets")
|
||||
created = await client.post(
|
||||
"/transactions",
|
||||
json=_txn(
|
||||
@@ -613,7 +661,7 @@ async def test_delete_transaction_cascades_its_splits(client: AsyncClient, accou
|
||||
category_id=None,
|
||||
splits=[
|
||||
{"category_id": groceries, "amount": "70.00"},
|
||||
{"category_id": shopping, "amount": "30.00"},
|
||||
{"category_id": sweets, "amount": "30.00"},
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -623,5 +671,5 @@ async def test_delete_transaction_cascades_its_splits(client: AsyncClient, accou
|
||||
|
||||
# If the split rows outlived their parent, this budget/dashboard-style
|
||||
# category filter would still find a (now-orphaned) match.
|
||||
resp = await client.get("/transactions", params={"category_id": groceries})
|
||||
resp = await client.get("/transactions", params={"category_id": sweets})
|
||||
assert resp.json()["total"] == 0
|
||||
|
||||
Reference in New Issue
Block a user