from datetime import date import pytest from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st from sqlalchemy.orm import Session from app.errors import AppError from app.identity.models import User from app.ledger.models import Posting from app.ledger.service import ( account_balance, create_account, create_expense, create_income, create_refund, create_transfer, get_account, ) from app.spaces.service import create_space def seed(db: Session): user = User(email="owner@example.test", password_hash="unused-in-domain-tests") db.add(user) db.flush() space = create_space(db, user=user, name="Personal") return user, space @given(amount=st.integers(min_value=1, max_value=10**12)) @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) def test_transfer_postings_conserve_money(db: Session, amount: int) -> None: db.rollback() user, space = seed(db) source = create_account( db, space_id=space.id, user=user, name="Debit", kind="debit", opened_on=date(2026, 1, 1), opening_amount_kopecks=amount, ) target = create_account( db, space_id=space.id, user=user, name="Cash", kind="cash", opened_on=date(2026, 1, 1), ) operation = create_transfer( db, space_id=space.id, user=user, source_account_id=source.id, target_account_id=target.id, amount_kopecks=amount, occurred_on=date(2026, 1, 2), ) db.flush() postings = db.query(Posting).filter_by(operation_id=operation.id).all() assert len(postings) == 2 assert sum(item.amount_kopecks for item in postings) == 0 assert account_balance(db, source) == 0 assert account_balance(db, target) == amount db.rollback() def test_credit_expense_is_negative_balance_and_refund_is_capped(db: Session) -> None: user, space = seed(db) card = create_account( db, space_id=space.id, user=user, name="Credit", kind="credit", opened_on=date(2026, 2, 1), ) expense = create_expense( db, space_id=space.id, user=user, account_id=card.id, amount_kopecks=12_345, occurred_on=date(2026, 2, 2), ) assert account_balance(db, card) == -12_345 create_refund( db, space_id=space.id, user=user, parent_operation_id=expense.id, amount_kopecks=2_345, occurred_on=date(2026, 2, 3), ) assert account_balance(db, card) == -10_000 with pytest.raises(AppError, match="Refund exceeds"): create_refund( db, space_id=space.id, user=user, parent_operation_id=expense.id, amount_kopecks=10_001, occurred_on=date(2026, 2, 4), ) def test_cross_space_lookup_fails_closed(db: Session) -> None: user, first = seed(db) other = create_space(db, user=user, name="Other") account = create_account( db, space_id=first.id, user=user, name="Cash", kind="cash", opened_on=date(2026, 1, 1), ) with pytest.raises(AppError) as exc: get_account(db, space_id=other.id, account_id=account.id) assert exc.value.code == "LEDGER_ACCOUNT_NOT_FOUND" def test_idempotency_key_prevents_duplicate_income(db: Session) -> None: user, space = seed(db) account = create_account( db, space_id=space.id, user=user, name="Debit", kind="debit", opened_on=date(2026, 1, 1), ) first = create_income( db, space_id=space.id, user=user, account_id=account.id, amount_kopecks=50_000, occurred_on=date(2026, 1, 5), request_key="retry-1", ) second = create_income( db, space_id=space.id, user=user, account_id=account.id, amount_kopecks=50_000, occurred_on=date(2026, 1, 5), request_key="retry-1", ) assert first.id == second.id assert account_balance(db, account) == 50_000