diff --git a/backend/alembic/versions/20260922_0003_savings_system.py b/backend/alembic/versions/20260922_0003_savings_system.py index c8004cd..0f54733 100644 --- a/backend/alembic/versions/20260922_0003_savings_system.py +++ b/backend/alembic/versions/20260922_0003_savings_system.py @@ -29,9 +29,7 @@ def _check_constraints(bind, table: str) -> list[dict]: def _has_unique_constraint(bind, table: str, name: str) -> bool: inspector = sa.inspect(bind) - return any( - (item.get("name") or "") == name for item in inspector.get_unique_constraints(table) - ) + return any((item.get("name") or "") == name for item in inspector.get_unique_constraints(table)) def _normalized_sqltext(expression: str) -> str: @@ -96,9 +94,7 @@ def upgrade() -> None: ), ) if not _has_column(bind, "operations", "previous_category_id"): - op.add_column( - "operations", sa.Column("previous_category_id", sa.Uuid(), nullable=True) - ) + op.add_column("operations", sa.Column("previous_category_id", sa.Uuid(), nullable=True)) _replace_check_constraint(bind, "operations", "type_valid", NEW_OPERATION_TYPES) if not _has_unique_constraint(bind, "categories", "uq_categories_system_pair"): @@ -172,12 +168,8 @@ def downgrade() -> None: ) # The preflight proved these system resources have no dependent history. - bind.execute( - sa.text("DELETE FROM categories WHERE system_key = 'savings'") - ) - bind.execute( - sa.text("DELETE FROM accounts WHERE system_key = 'savings'") - ) + bind.execute(sa.text("DELETE FROM categories WHERE system_key = 'savings'")) + bind.execute(sa.text("DELETE FROM accounts WHERE system_key = 'savings'")) if _has_unique_constraint(bind, "categories", "uq_categories_system_pair"): with op.batch_alter_table("categories") as batch_op: batch_op.drop_constraint("uq_categories_system_pair", type_="unique") diff --git a/backend/app/analytics/service.py b/backend/app/analytics/service.py index 401f9f6..dfe3249 100644 --- a/backend/app/analytics/service.py +++ b/backend/app/analytics/service.py @@ -39,10 +39,7 @@ def category_spend( .scalar() ) totals[expense.category_id] += expense.amount_kopecks - int(refunded or 0) - categories = { - item.id: item - for item in db.query(Category).filter_by(space_id=space_id).all() - } + categories = {item.id: item for item in db.query(Category).filter_by(space_id=space_id).all()} return [ { "category_id": category_id, diff --git a/backend/app/ledger/models.py b/backend/app/ledger/models.py index ae1bd44..bc25d18 100644 --- a/backend/app/ledger/models.py +++ b/backend/app/ledger/models.py @@ -28,9 +28,7 @@ class Account(Base, PkMixin, TimestampMixin): __tablename__ = "accounts" __table_args__ = ( - CheckConstraint( - "kind IN ('cash', 'debit', 'credit', 'savings')", name="kind_valid" - ), + CheckConstraint("kind IN ('cash', 'debit', 'credit', 'savings')", name="kind_valid"), CheckConstraint("status IN ('active', 'archived')", name="status_valid"), CheckConstraint("currency = 'RUB'", name="currency_rub_only"), UniqueConstraint("space_id", "id", name="uq_accounts_space_id"), diff --git a/backend/app/ledger/router.py b/backend/app/ledger/router.py index ab72a38..3ab3379 100644 --- a/backend/app/ledger/router.py +++ b/backend/app/ledger/router.py @@ -346,9 +346,7 @@ def savings_transfer(operation_id: uuid.UUID, db: Db, context: Context, _csrf: C @router.post("/operations/{operation_id}/savings-transfer/revert") -def savings_transfer_revert( - operation_id: uuid.UUID, db: Db, context: Context, _csrf: Csrf -) -> dict: +def savings_transfer_revert(operation_id: uuid.UUID, db: Db, context: Context, _csrf: Csrf) -> dict: from app.ledger import savings operation = savings.revert_savings_conversion( diff --git a/backend/app/ledger/savings.py b/backend/app/ledger/savings.py index 381957f..6795994 100644 --- a/backend/app/ledger/savings.py +++ b/backend/app/ledger/savings.py @@ -22,17 +22,13 @@ SAVINGS_ACCOUNT_NAME = "Накопления" def get_savings_account(db: Session, *, space_id: uuid.UUID) -> Account | None: return ( - db.query(Account) - .filter_by(space_id=space_id, system_key=SAVINGS_SYSTEM_KEY) - .one_or_none() + db.query(Account).filter_by(space_id=space_id, system_key=SAVINGS_SYSTEM_KEY).one_or_none() ) def _savings_category(db: Session, *, space_id: uuid.UUID) -> Category | None: return ( - db.query(Category) - .filter_by(space_id=space_id, system_key=SAVINGS_SYSTEM_KEY) - .one_or_none() + db.query(Category).filter_by(space_id=space_id, system_key=SAVINGS_SYSTEM_KEY).one_or_none() ) @@ -143,9 +139,7 @@ def adjust_balance( "Credit account balances cannot be adjusted with a non-negative target", ) if target_balance_kopecks < 0: - raise bad_request( - "LEDGER_ADJUST_TARGET_NEGATIVE", "Target balance cannot be negative" - ) + raise bad_request("LEDGER_ADJUST_TARGET_NEGATIVE", "Target balance cannot be negative") from app.ledger.service import account_balance, ensure_period_open_for_date ensure_period_open_for_date(db, space_id=space_id, day=occurred_on) @@ -277,9 +271,7 @@ def convert_expense_to_savings( "operation_id": str(operation.id), "savings_account_id": str(savings_account.id), "amount_kopecks": operation.amount_kopecks, - "previous_category_id": ( - str(previous_category_id) if previous_category_id else None - ), + "previous_category_id": (str(previous_category_id) if previous_category_id else None), }, ) return operation diff --git a/backend/tests/test_adapter_contracts.py b/backend/tests/test_adapter_contracts.py index b6f54fb..d5c8700 100644 --- a/backend/tests/test_adapter_contracts.py +++ b/backend/tests/test_adapter_contracts.py @@ -546,8 +546,19 @@ class TestAlfaSavingsRule: def savings_statement_rows(self) -> list[list[object]]: rows = [list(row) for row in synthetic_alfa_rows()] rows[9] = [ - None, None, None, None, None, None, None, None, None, None, - "Расходы", None, "416,90 RUR", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "Расходы", + None, + "416,90 RUR", ] savings_row = [ "03.09.2026", diff --git a/backend/tests/test_import_savings.py b/backend/tests/test_import_savings.py index 603a43d..1752233 100644 --- a/backend/tests/test_import_savings.py +++ b/backend/tests/test_import_savings.py @@ -163,9 +163,7 @@ def test_revert_removes_created_savings_transfer(db: Session) -> None: assert db.query(Operation).filter_by(space_id=space.id).count() == 0 assert db.query(SourceRecordLink).filter_by(space_id=space.id).count() == 0 assert document.status == "ready" - source = ( - db.query(SourceRecord).filter_by(parse_run_id=document.active_parse_run_id).one() - ) + source = db.query(SourceRecord).filter_by(parse_run_id=document.active_parse_run_id).one() # Repeated parse gives the same decision: the hint survives revert. assert source.review_decision == "savings_transfer" @@ -350,9 +348,9 @@ def test_unresolved_hold_counts_and_month_filter(db: Session) -> None: unresolved = service.unresolved_holds(db, space_id=space.id, document=document) assert len(unresolved) == 2 - assert service.unresolved_hold_counts( - db, space_id=space.id, document_ids=[document.id] - ) == {document.id: 2} + assert service.unresolved_hold_counts(db, space_id=space.id, document_ids=[document.id]) == { + document.id: 2 + } may = service.parse_month_filter("2026-05") assert may == (2026, 5) may_holds = [h for h in unresolved if h.occurred_on and h.occurred_on.month == 5] diff --git a/backend/tests/test_review_queue_batch.py b/backend/tests/test_review_queue_batch.py index c6792b1..1f09f90 100644 --- a/backend/tests/test_review_queue_batch.py +++ b/backend/tests/test_review_queue_batch.py @@ -58,9 +58,7 @@ def category_ids(client: TestClient) -> dict[str, str]: ) assert response.status_code == 200, response.text return { - item["systemKey"]: item["id"] - for item in response.json() - if item["systemKey"] is not None + item["systemKey"]: item["id"] for item in response.json() if item["systemKey"] is not None } diff --git a/backend/tests/test_savings.py b/backend/tests/test_savings.py index 811369e..2f31122 100644 --- a/backend/tests/test_savings.py +++ b/backend/tests/test_savings.py @@ -97,9 +97,7 @@ def test_adjust_balance_creates_single_signed_posting(db: Session) -> None: assert len(postings) == 1 assert postings[0].amount_kopecks == -25_000 assert account_balance(db, account) == 475_000 - event = ( - db.query(AuditEvent).filter_by(event_type="account.balance_adjusted").one() - ) + event = db.query(AuditEvent).filter_by(event_type="account.balance_adjusted").one() assert event.detail["old_balance_kopecks"] == 500_000 assert event.detail["new_balance_kopecks"] == 475_000 assert event.detail["delta_kopecks"] == -25_000 @@ -235,9 +233,7 @@ def test_convert_expense_to_savings_and_revert_roundtrip(db: Session) -> None: assert account_balance(db, account) == 500_000 - 12_345 assert account_balance(db, savings_account) == 12_345 - reverted = revert_savings_conversion( - db, space_id=space.id, user=user, operation_id=expense.id - ) + reverted = revert_savings_conversion(db, space_id=space.id, user=user, operation_id=expense.id) assert reverted.type == "expense" assert reverted.counter_account_id is None assert reverted.category_id == category.id @@ -263,9 +259,7 @@ def test_convert_rejects_income_refund_transfer_and_double_conversion( occurred_on=date(2026, 3, 2), ) with pytest.raises(AppError) as exc: - convert_expense_to_savings( - db, space_id=space.id, user=user, operation_id=income.id - ) + convert_expense_to_savings(db, space_id=space.id, user=user, operation_id=income.id) assert exc.value.code == "LEDGER_SAVINGS_CONVERT_INVALID" refundable_expense = create_expense( @@ -285,9 +279,7 @@ def test_convert_rejects_income_refund_transfer_and_double_conversion( occurred_on=date(2026, 3, 4), ) with pytest.raises(AppError) as exc: - convert_expense_to_savings( - db, space_id=space.id, user=user, operation_id=refund.id - ) + convert_expense_to_savings(db, space_id=space.id, user=user, operation_id=refund.id) assert exc.value.code == "LEDGER_SAVINGS_CONVERT_INVALID" expense = create_expense( @@ -300,9 +292,7 @@ def test_convert_rejects_income_refund_transfer_and_double_conversion( ) convert_expense_to_savings(db, space_id=space.id, user=user, operation_id=expense.id) with pytest.raises(AppError) as exc: - convert_expense_to_savings( - db, space_id=space.id, user=user, operation_id=expense.id - ) + convert_expense_to_savings(db, space_id=space.id, user=user, operation_id=expense.id) assert exc.value.code == "LEDGER_SAVINGS_CONVERT_INVALID" transfer = create_transfer( @@ -315,9 +305,7 @@ def test_convert_rejects_income_refund_transfer_and_double_conversion( occurred_on=date(2026, 3, 6), ) with pytest.raises(AppError) as exc: - convert_expense_to_savings( - db, space_id=space.id, user=user, operation_id=transfer.id - ) + convert_expense_to_savings(db, space_id=space.id, user=user, operation_id=transfer.id) assert exc.value.code == "LEDGER_SAVINGS_CONVERT_INVALID" @@ -341,9 +329,7 @@ def test_convert_rejects_expense_with_confirmed_refund(db: Session) -> None: ) with pytest.raises(AppError) as exc: - convert_expense_to_savings( - db, space_id=space.id, user=user, operation_id=expense.id - ) + convert_expense_to_savings(db, space_id=space.id, user=user, operation_id=expense.id) assert exc.value.code == "LEDGER_SAVINGS_CONVERT_HAS_REFUNDS" @@ -365,14 +351,10 @@ def test_convert_rejects_closed_period_and_cross_space_operation(db: Session) -> period = get_or_create_period(db, space_id=space.id, year=2026, month=3) close_period(db, space_id=space.id, user=user, period=period) with pytest.raises(AppError) as exc: - convert_expense_to_savings( - db, space_id=space.id, user=user, operation_id=expense.id - ) + convert_expense_to_savings(db, space_id=space.id, user=user, operation_id=expense.id) assert exc.value.code == "BUDGET_PERIOD_CLOSED" with pytest.raises(AppError) as exc: - convert_expense_to_savings( - db, space_id=other_space.id, user=user, operation_id=expense.id - ) + convert_expense_to_savings(db, space_id=other_space.id, user=user, operation_id=expense.id) assert exc.value.code == "LEDGER_OPERATION_NOT_FOUND" @@ -490,6 +472,4 @@ def test_operations_on_other_spaces_are_isolated(db: Session) -> None: occurred_on=date(2026, 3, 14), ) convert_expense_to_savings(db, space_id=space.id, user=user, operation_id=expense.id) - assert ( - db.query(Operation).filter_by(space_id=other_space.id).count() == 0 - ) + assert db.query(Operation).filter_by(space_id=other_space.id).count() == 0