Update file metadata in manifest.json and change MinIO image source in docker-compose.yml

This commit is contained in:
k1nq
2026-09-23 09:23:13 +05:00
parent 8c2b89c80d
commit db4dd7572e
71 changed files with 31011 additions and 8308 deletions
+337
View File
@@ -0,0 +1,337 @@
"""Budget semantics for savings transfers, adjustments and archived history."""
from datetime import date
from uuid import uuid4
import pytest
from sqlalchemy.orm import Session
from app.budgeting.service import (
compute_period_state,
create_category,
fund,
get_or_create_period,
set_assignments,
)
from app.errors import AppError
from app.identity.models import User
from app.ledger.savings import adjust_balance, ensure_savings_resources
from app.ledger.service import create_account, create_expense, create_income, create_transfer
from app.spaces.service import create_space
def seed(db: Session):
user = User(email=f"{uuid4().hex}@example.com", password_hash="unused")
db.add(user)
db.flush()
space = create_space(db, user=user, name="BudgetSavings")
account = create_account(
db,
space_id=space.id,
user=user,
name="Debit",
kind="debit",
opened_on=date(2026, 5, 1),
opening_amount_kopecks=1_000_000,
)
return user, space, account
def savings_row(state: dict) -> dict:
return next(row for row in state["envelopes"] if row["name_key"] == "savings")
def test_transfer_to_savings_moves_pool_into_envelope(db: Session) -> None:
user, space, account = seed(db)
savings_account, _ = ensure_savings_resources(db, space_id=space.id)
before = compute_period_state(
db,
space_id=space.id,
period=get_or_create_period(db, space_id=space.id, year=2026, month=5),
)
assert before["pool"] == 0
assert savings_row(before)["available"] == 0
create_transfer(
db,
space_id=space.id,
user=user,
source_account_id=account.id,
target_account_id=savings_account.id,
amount_kopecks=250_000,
occurred_on=date(2026, 5, 10),
)
after = compute_period_state(
db,
space_id=space.id,
period=get_or_create_period(db, space_id=space.id, year=2026, month=5),
)
row = savings_row(after)
assert after["pool"] == -250_000
assert row["funded"] == 250_000
assert row["spent"] == 0
assert row["available"] == 250_000
assert after["income_total"] == 0
def test_transfer_back_from_savings_returns_pool(db: Session) -> None:
user, space, account = seed(db)
savings_account, _ = ensure_savings_resources(db, space_id=space.id)
create_transfer(
db,
space_id=space.id,
user=user,
source_account_id=account.id,
target_account_id=savings_account.id,
amount_kopecks=250_000,
occurred_on=date(2026, 5, 10),
)
create_transfer(
db,
space_id=space.id,
user=user,
source_account_id=savings_account.id,
target_account_id=account.id,
amount_kopecks=100_000,
occurred_on=date(2026, 5, 12),
)
state = compute_period_state(
db,
space_id=space.id,
period=get_or_create_period(db, space_id=space.id, year=2026, month=5),
)
row = savings_row(state)
assert state["pool"] == -150_000
assert row["funded"] == 150_000
assert row["available"] == 150_000
def test_adjustment_changes_only_envelope_available_not_pool(db: Session) -> None:
user, space, account = seed(db)
savings_account, _ = ensure_savings_resources(db, space_id=space.id)
create_transfer(
db,
space_id=space.id,
user=user,
source_account_id=account.id,
target_account_id=savings_account.id,
amount_kopecks=100_000,
occurred_on=date(2026, 5, 3),
)
adjust_balance(
db,
space_id=space.id,
user=user,
account_id=savings_account.id,
target_balance_kopecks=80_000,
occurred_on=date(2026, 5, 20),
reason="Процентная корректировка",
)
state = compute_period_state(
db,
space_id=space.id,
period=get_or_create_period(db, space_id=space.id, year=2026, month=5),
)
row = savings_row(state)
assert row["available"] == 80_000
assert row["funded"] == 100_000
assert state["income_total"] == 0
assert state["pool"] == -100_000
def test_adjustment_on_regular_account_keeps_pool_and_totals(db: Session) -> None:
user, space, account = seed(db)
create_income(
db,
space_id=space.id,
user=user,
account_id=account.id,
amount_kopecks=300_000,
occurred_on=date(2026, 5, 2),
)
adjust_balance(
db,
space_id=space.id,
user=user,
account_id=account.id,
target_balance_kopecks=1_000_000,
occurred_on=date(2026, 5, 21),
reason="Списание банка не отразилось",
)
state = compute_period_state(
db,
space_id=space.id,
period=get_or_create_period(db, space_id=space.id, year=2026, month=5),
)
assert state["income_total"] == 300_000
# The adjustment reconciles the account but never enters the budget pool.
assert state["pool"] == 300_000
assert state["uncategorized_expenses"] == 0
assert state["monthly_categories"] == []
def test_savings_envelope_available_uses_month_end_balance(db: Session) -> None:
user, space, account = seed(db)
savings_account, _ = ensure_savings_resources(db, space_id=space.id)
create_transfer(
db,
space_id=space.id,
user=user,
source_account_id=account.id,
target_account_id=savings_account.id,
amount_kopecks=50_000,
occurred_on=date(2026, 5, 28),
)
# A transfer in a later month must not affect the May envelope view.
create_transfer(
db,
space_id=space.id,
user=user,
source_account_id=account.id,
target_account_id=savings_account.id,
amount_kopecks=70_000,
occurred_on=date(2026, 6, 2),
)
may = compute_period_state(
db,
space_id=space.id,
period=get_or_create_period(db, space_id=space.id, year=2026, month=5),
)
june = compute_period_state(
db,
space_id=space.id,
period=get_or_create_period(db, space_id=space.id, year=2026, month=6),
)
assert savings_row(may)["available"] == 50_000
assert savings_row(may)["funded"] == 50_000
assert savings_row(june)["available"] == 120_000
assert savings_row(june)["funded"] == 70_000
def test_archived_category_history_stays_in_totals(db: Session) -> None:
user, space, account = seed(db)
category = create_category(db, space_id=space.id, user=user, kind="monthly", name="Transport")
set_assignments(
db,
space_id=space.id,
user=user,
period=get_or_create_period(db, space_id=space.id, year=2026, month=5),
items=[{"category_id": category.id, "planned_kopecks": 20_000}],
)
create_expense(
db,
space_id=space.id,
user=user,
account_id=account.id,
amount_kopecks=5_000,
occurred_on=date(2026, 5, 6),
category_id=category.id,
)
category.archived = True
db.flush()
state = compute_period_state(
db,
space_id=space.id,
period=get_or_create_period(db, space_id=space.id, year=2026, month=5),
)
rows = [row for row in state["monthly_categories"] if row["category_id"] == str(category.id)]
assert len(rows) == 1
row = rows[0]
assert row["archived"] is True
assert row["spent"] == 5_000
assert row["planned"] == 20_000
# An archived category refuses new assignments...
try:
set_assignments(
db,
space_id=space.id,
user=user,
period=get_or_create_period(db, space_id=space.id, year=2026, month=5),
items=[{"category_id": category.id, "planned_kopecks": 1}],
)
raise AssertionError("archived assignment must fail")
except AppError as exc:
assert exc.code == "BUDGET_CATEGORY_ARCHIVED"
# ...but its history still counts in the funding/spent totals.
summary_funded = sum(row["funded"] for row in state["monthly_categories"])
assert summary_funded == 0
def test_system_category_rejects_planning_funding_and_regular_expense(db: Session) -> None:
user, space, account = seed(db)
_, savings_category = ensure_savings_resources(db, space_id=space.id)
period = get_or_create_period(db, space_id=space.id, year=2026, month=5)
with pytest.raises(AppError) as exc:
set_assignments(
db,
space_id=space.id,
user=user,
period=period,
items=[{"category_id": savings_category.id, "planned_kopecks": 10_000}],
)
assert exc.value.code == "BUDGET_CATEGORY_SYSTEM"
with pytest.raises(AppError) as exc:
fund(
db,
space_id=space.id,
user=user,
period=period,
category_id=savings_category.id,
amount_kopecks=10_000,
kind="fund",
)
assert exc.value.code == "BUDGET_CATEGORY_SYSTEM"
with pytest.raises(AppError) as exc:
create_expense(
db,
space_id=space.id,
user=user,
account_id=account.id,
amount_kopecks=10_000,
occurred_on=date(2026, 5, 10),
category_id=savings_category.id,
)
assert exc.value.code == "BUDGET_CATEGORY_SYSTEM"
def test_archived_category_rejects_new_funding(db: Session) -> None:
user, space, _ = seed(db)
category = create_category(db, space_id=space.id, user=user, kind="monthly", name="Old")
category.archived = True
period = get_or_create_period(db, space_id=space.id, year=2026, month=5)
with pytest.raises(AppError) as exc:
fund(
db,
space_id=space.id,
user=user,
period=period,
category_id=category.id,
amount_kopecks=1_000,
kind="fund",
)
assert exc.value.code == "BUDGET_CATEGORY_ARCHIVED"
def test_blockers_carry_discriminated_data(db: Session) -> None:
user, space, account = seed(db)
create_expense(
db,
space_id=space.id,
user=user,
account_id=account.id,
amount_kopecks=5_000,
occurred_on=date(2026, 5, 6),
)
state = compute_period_state(
db,
space_id=space.id,
period=get_or_create_period(db, space_id=space.id, year=2026, month=5),
)
by_code = {blocker["code"]: blocker for blocker in state["close_blockers"]}
assert by_code["BUDGET_UNCATEGORIZED_EXPENSES"]["count"] == 1
assert "IMPORTS_UNRESOLVED" not in by_code
assert "IMPORT_UNRESOLVED_HOLDS" not in by_code