Merge branch 'dev' into feature/mail-templates

dev gained the delivery/rollback semantics for the dunning and SEPA mails while
the mail texts were being moved into templates here. Both services keep dev's
flow -- roll back only while nothing has gone out, log the event even when
archiving fails afterwards -- and render their content from the template.

The welcome mail follows the same rule now, including its archiving-failure
path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Marcel Peterkau
2026-08-28 20:50:51 +02:00
co-authored by Claude Opus 5
7 changed files with 726 additions and 99 deletions
+48
View File
@@ -219,3 +219,51 @@ def test_member_without_email_is_refused(tmp_path):
sender_email="verwaltung@example.org",
signature="Der Vorstand",
)
def test_archiving_failure_after_smtp_success_still_logs_the_sent_mail(tmp_path, monkeypatch):
import ccma.services.welcome_mail as welcome_mail_module
repository, member = _new_member_repository(tmp_path)
repository.save_email_settings(
delivery_mode="send",
smtp_host="mail.example.org",
smtp_port=587,
smtp_security="none",
smtp_username="",
smtp_password="",
imap_host="",
imap_port=993,
imap_security="ssl",
imap_username="",
imap_password="",
imap_drafts_folder="",
)
monkeypatch.setattr(
welcome_mail_module, "smtp_session", contextmanager(lambda settings: iter(["client"]))
)
monkeypatch.setattr(welcome_mail_module, "send_via_smtp", lambda client, content: None)
real_replace = welcome_mail_module.os.replace
def _flaky_replace(src, dst):
# Only the final move into the member file fails -- the mail is out by then.
if str(dst).endswith(".eml") and "Willkommen" in str(dst):
raise OSError("disk full")
return real_replace(src, dst)
monkeypatch.setattr(welcome_mail_module.os, "replace", _flaky_replace)
with pytest.raises(RepositoryError, match="zugestellt"):
generate_and_send_welcome_mail(
repository,
member.member_id,
delivery_mode="send",
sender_name="Verwaltung C3MA",
sender_email="verwaltung@example.org",
signature="Der Vorstand",
)
event = repository.get_events(member.member_id)[-1]
assert event.event_type == "welcome_email_sent"
assert event.data["archive_error"]
assert "document" not in event.references