from __future__ import annotations from datetime import date from xml.etree import ElementTree import pytest import ccma.services.documents as document_module from ccma.services.documents import DocumentError, DocumentService, _replace_xml_placeholders from ccma.services.housekeeper import Housekeeper from ccma.storage.repository import MemberRepository, RepositoryError def test_placeholder_replacement_crosses_formatted_odf_spans() -> None: source = b""" Hello {{member.full_name}}! """ rendered = _replace_xml_placeholders(source, {"member.full_name": "Ada Lovelace"}) text = "".join(ElementTree.fromstring(rendered).itertext()) assert "Hello Ada Lovelace!" in text assert "{{" not in text def test_unknown_placeholder_is_reported() -> None: source = b"""

{{member.unknown}}

""" with pytest.raises(DocumentError, match="member.unknown"): _replace_xml_placeholders(source, {}) def test_default_templates_are_seeded_without_overwriting_store_version(tmp_path) -> None: repository = MemberRepository(tmp_path) repository.initialize() template = repository.root / "templates" / "Mitglied.fodt" template.write_text("custom", encoding="utf-8") repository.initialize() assert template.read_text(encoding="utf-8") == "custom" assert {path.name for path in (repository.root / "templates").iterdir()} == { "Forderung.fodt", "Mahnung.fodt", "Mitglied.fodt", } def test_templates_are_filtered_by_available_context(tmp_path) -> None: repository = MemberRepository(tmp_path) repository.initialize() service = DocumentService(repository) member_templates = { item.path.name for item in service.compatible_templates(has_claim=False, has_reminder=False) } claim_templates = { item.path.name for item in service.compatible_templates(has_claim=True, has_reminder=False) } reminder_templates = { item.path.name for item in service.compatible_templates(has_claim=True, has_reminder=True) } assert member_templates == {"Mitglied.fodt"} assert claim_templates == {"Forderung.fodt", "Mitglied.fodt"} assert reminder_templates == {"Forderung.fodt", "Mahnung.fodt", "Mitglied.fodt"} def test_generated_reminder_pdf_is_stored_audited_and_linked(tmp_path, monkeypatch) -> None: repository = MemberRepository(tmp_path) repository.initialize() member = repository.create_member(first_name="Ada", last_name="Lovelace") member.status = "active" member.accepted_at = "2026-01-01" member.membership_started_at = "2026-01-01" repository.save_member(member) Housekeeper(repository).run(today=date(2026, 1, 1)) claim = repository.get_contributions(member.member_id).claims[0] claim_id = str(claim["claim_id"]) reminder = repository.create_reminder_draft( member.member_id, claim_id, level=1, name="Zahlungserinnerung", payment_deadline_days=14, ) def fake_convert(source, output_directory): destination = output_directory / f"{source.stem}.pdf" destination.write_bytes(b"%PDF-1.7\ntest") return destination monkeypatch.setattr(document_module, "_convert_to_pdf", fake_convert) service = DocumentService(repository) template = next(item for item in service.list_templates() if item.path.name == "Mahnung.fodt") generated = service.generate( template, member.member_id, output_name="Mahnung.pdf", claim_id=claim_id, reminder_id=str(reminder["reminder_id"]), ) assert generated.path.read_bytes().startswith(b"%PDF-") assert generated.relative_path == "documents/Mahnung.pdf" stored = repository.get_contributions(member.member_id) updated = next(item for item in stored.reminders if item["reminder_id"] == reminder["reminder_id"]) assert updated["status"] == "generated" assert updated["document"]["path"] == generated.relative_path assert updated["document"]["sha256"] == generated.sha256 assert repository.get_events(member.member_id)[-1].event_type == "reminder_document_generated" with pytest.raises(RepositoryError, match="innerhalb"): repository.register_reminder_document( member.member_id, claim_id, str(reminder["reminder_id"]), relative_path="../outside.pdf", sha256="invalid", template="Mahnung.fodt", )