Add asset records, claims, and credit workflows

This commit is contained in:
Marcel Peterkau
2026-06-26 23:03:06 +02:00
parent 30b6d253b2
commit d1dab793a6
11 changed files with 2060 additions and 10 deletions
+138
View File
@@ -1,7 +1,9 @@
import json
from decimal import Decimal
import pytest
from ccma.domain.contributions import claim_balance
from ccma.storage.repository import (
MemberRepository,
RepositoryError,
@@ -238,3 +240,139 @@ def test_organization_sender_data_is_stored_centrally(tmp_path) -> None:
organization = repository.get_configuration()["organization"]
assert organization["street"] == "Testweg 1"
assert organization["iban"] == "DE89370400440532013000"
def test_repository_creates_asset_record_and_events(tmp_path) -> None:
repository = MemberRepository(tmp_path / "store")
repository.initialize()
asset = repository.create_asset(
label="Clubraumschlüssel A12",
category="key",
inventory_number="KEY-A12",
deposit_amount_default="25",
)
asset_dir = repository.assets_root / asset.asset_id
assert (asset_dir / "asset.json").is_file()
assert (asset_dir / "events.jsonl").is_file()
assert (asset_dir / "files").is_dir()
loaded = repository.get_asset(asset.asset_id)
assert loaded.label == "Clubraumschlüssel A12"
assert loaded.deposit_amount_default == "25.00"
def test_asset_can_be_assigned_and_returned_to_single_member(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
member = repository.create_member(first_name="Ada", last_name="Lovelace")
other = repository.create_member(first_name="Grace", last_name="Hopper")
asset = repository.create_asset(label="Transponder 01")
repository.assign_asset(asset.asset_id, member.member_id)
assigned = repository.get_asset(asset.asset_id)
assert assigned.status == "issued"
assert assigned.current_holder_member_id == member.member_id
assert [item.asset_id for item in repository.list_member_assets(member.member_id)] == [asset.asset_id]
with pytest.raises(RepositoryError, match="bereits einem Mitglied zugeordnet"):
repository.assign_asset(asset.asset_id, other.member_id)
repository.return_asset(asset.asset_id)
returned = repository.get_asset(asset.asset_id)
assert returned.status == "available"
assert returned.current_holder_member_id == ""
assert repository.list_member_assets(member.member_id) == []
def test_asset_assignment_is_audited_on_asset_and_member(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
member = repository.create_member(first_name="Key", last_name="Holder", member_number="0042")
asset = repository.create_asset(label="Clubraumschlüssel")
repository.assign_asset(asset.asset_id, member.member_id)
repository.return_asset(asset.asset_id)
asset_events = [event.event_type for event in repository.get_asset_events(asset.asset_id)]
member_events = [event.event_type for event in repository.get_events(member.member_id)]
assert asset_events == ["asset_created", "asset_issued", "asset_returned"]
assert "asset_assigned" in member_events
assert "asset_returned" in member_events
def test_asset_deposit_cannot_change_while_issued(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
member = repository.create_member(first_name="Ada", last_name="Lovelace")
asset = repository.create_asset(label="Clubraumschlüssel", deposit_amount_default="25")
repository.assign_asset(asset.asset_id, member.member_id)
issued = repository.get_asset(asset.asset_id)
issued.deposit_amount_default = "35"
with pytest.raises(RepositoryError, match="Kaution kann nur geändert werden"):
repository.save_asset(issued)
def test_asset_deposit_can_change_when_not_issued(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
asset = repository.create_asset(label="Clubraumschlüssel", deposit_amount_default="25")
asset.deposit_amount_default = "35"
repository.save_asset(asset)
updated = repository.get_asset(asset.asset_id)
assert updated.deposit_amount_default == "35.00"
def test_manual_asset_claim_is_linked_to_member_and_asset(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
member = repository.create_member(first_name="Ada", last_name="Lovelace")
asset = repository.create_asset(label="Clubraumschlüssel")
repository.assign_asset(asset.asset_id, member.member_id)
result = repository.create_manual_claim(
member.member_id,
title="Kaution Clubraumschlüssel",
amount="25.00",
due_date="2026-06-26",
description="Kaution für Schlüssel",
claim_type="asset_deposit",
references={"asset_id": asset.asset_id},
)
claim = result["claim"]
loaded_claim = repository.get_contributions(member.member_id).claims[0]
assert claim["claim_id"] == loaded_claim["claim_id"]
assert loaded_claim["origin"]["asset_id"] == asset.asset_id
assert repository.get_asset_events(asset.asset_id)[-1].event_type == "asset_claim_created"
def test_negative_claim_can_be_settled_with_credit(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
member = repository.create_member(first_name="Ada", last_name="Lovelace")
asset = repository.create_asset(label="Clubraumschlüssel")
repository.assign_asset(asset.asset_id, member.member_id)
claim = repository.create_manual_claim(
member.member_id,
title="Kautionsrückzahlung",
amount="-25.00",
due_date="2026-06-26",
claim_type="asset_refund",
references={"asset_id": asset.asset_id},
)["claim"]
repository.record_credit(
member.member_id,
str(claim["claim_id"]),
credit_date="2026-06-26",
amount="25.00",
allocation_amount="25.00",
reference="Bar ausgezahlt",
)
data, loaded_claim = repository.get_claim(member.member_id, str(claim["claim_id"]))
assert claim_balance(data, loaded_claim) == Decimal("0.00")
assert data.credits[0]["amount"] == "25.00"
+25
View File
@@ -1,5 +1,6 @@
def test_ui_modules_import_without_creating_root_window() -> None:
import ccma.app # noqa: F401
import ccma.ui.asset_tab # noqa: F401
import ccma.ui.claim_tab # noqa: F401
import ccma.ui.main_window # noqa: F401
import ccma.ui.member_tab # noqa: F401
@@ -130,6 +131,21 @@ def test_member_table_sort_uses_display_values() -> None:
assert [member.member_id for member in _sort_members(members, "status", False)] == ["2", "1", "3"]
def test_asset_table_filter_and_sort_use_status_and_holder_label() -> None:
from ccma.domain.models import Asset
from ccma.ui.work_tabs import _asset_table_value, _filter_assets, _selected_asset_filter
assets = [
Asset("1", "Clubraumschlüssel", status="issued", current_holder_member_id="member-1"),
Asset("2", "Beamer", status="available"),
]
assert _selected_asset_filter("Alle") == "all"
assert _selected_asset_filter("AUSGEGEBEN") == "issued"
assert [asset.asset_id for asset in _filter_assets(assets, "available")] == ["2"]
assert _asset_table_value(assets[0], "holder", "0001 · Ada") == "0001 · Ada"
def test_claim_table_sort_uses_due_date_by_raw_value() -> None:
from ccma.domain.models import ContributionData
from ccma.ui.member_tab import _claim_sort_value
@@ -139,3 +155,12 @@ def test_claim_table_sort_uses_due_date_by_raw_value() -> None:
newer = {"title": "Neu", "due_date": "2025-07-31", "amount": "50.00"}
assert _claim_sort_value(data, older, "due") < _claim_sort_value(data, newer, "due")
def test_negative_claims_are_labeled_as_credit() -> None:
from ccma.domain.contributions import claim_status
from ccma.domain.models import ContributionData
data = ContributionData()
claim = {"claim_id": "claim-1", "title": "Rueckzahlung", "amount": "-25.00"}
assert claim_status(data, claim) == "credit"