Refactor mock campaign send reporting

This commit is contained in:
2026-07-21 12:54:08 +02:00
parent b7653b58f4
commit ef513816f8
2 changed files with 330 additions and 82 deletions

View File

@@ -299,6 +299,237 @@ def _mock_sent_folder(config: Any) -> str:
return "Sent" return "Sent"
def _mock_campaign_version(
session: Session,
*,
tenant_id: str,
campaign_id: str,
version_id: str | None,
) -> tuple[Campaign, CampaignVersion]:
campaign = (
session.query(Campaign)
.filter(Campaign.id == campaign_id, Campaign.tenant_id == tenant_id)
.one_or_none()
)
if not campaign:
raise MockCampaignSendError("Campaign not found or not accessible")
wanted_version_id = version_id or campaign.current_version_id
if not wanted_version_id:
raise MockCampaignSendError("Campaign has no current version")
version = session.get(CampaignVersion, wanted_version_id)
if not version or version.campaign_id != campaign.id:
raise MockCampaignSendError(
"Campaign version not found or not part of campaign"
)
return campaign, version
def _mock_mailbox_for_run(*, send: bool, clear_mailbox: bool) -> Any | None:
mailbox = _require_mock_mailbox() if send or clear_mailbox else _mock_mailbox()
if clear_mailbox and mailbox is not None:
mailbox.clear_records()
return mailbox
def _build_mock_campaign_run(
session: Session,
*,
tenant_id: str,
campaign: Campaign,
version: CampaignVersion,
mailbox: Any | None,
send: bool,
include_warnings: bool,
include_needs_review: bool,
append_sent: bool,
check_files: bool,
) -> tuple[Any, Any, _MockSendBatch]:
files = files_integration()
raw_json = version.raw_json if isinstance(version.raw_json, dict) else {}
assert_server_safe_campaign_paths(
raw_json,
managed_files_available=files.available,
)
with files.prepared_campaign_snapshot(
session,
tenant_id=tenant_id,
campaign_id=campaign.id,
raw_json=raw_json,
include_bytes=True,
prefix="govoplan-mock-send-",
) as prepared:
prepared_raw = load_campaign_json(prepared.path)
config = load_campaign_config_from_json(
session,
tenant_id=tenant_id,
raw_json=prepared_raw,
campaign_id=campaign.id,
)
validation_report = validate_campaign_config(
config,
campaign_file=prepared.path,
check_files=check_files,
)
build_result = build_campaign_messages(
config,
campaign_file=prepared.path,
write_eml=False,
)
files.annotate_built_messages_with_managed_files(
build_result.built_messages,
prepared.managed_files_by_local_path,
)
send_batch = _mock_send_batch(
config=config,
built_messages=build_result.built_messages,
mailbox=mailbox,
send=send,
include_warnings=include_warnings,
include_needs_review=include_needs_review,
append_sent=append_sent,
)
return validation_report, build_result, send_batch
def _mock_validation_payload(validation_report: Any) -> dict[str, Any]:
payload = validation_report.model_dump(mode="json")
payload.update(
{
"ok": validation_report.ok,
"error_count": validation_report.error_count,
"warning_count": validation_report.warning_count,
}
)
return payload
def _mock_build_payload(build_result: Any) -> dict[str, Any]:
report = build_result.report
payload = report.model_dump(mode="json")
payload.update(
{
"built_count": report.built_count,
"queueable_count": report.queueable_count,
"needs_review_count": report.needs_review_count,
"blocked_count": report.blocked_count,
"warning_count": report.warning_count,
"ready_count": report.ready_count,
"messages": [_message_payload(message) for message in report.messages],
}
)
return payload
def _mock_send_steps(
*,
validation_report: Any,
validation_payload: dict[str, Any],
build_report: Any,
send_batch: _MockSendBatch,
send: bool,
append_sent: bool,
) -> list[dict[str, Any]]:
return [
{
"key": "validate",
"label": "Validate campaign JSON",
"status": "ok" if validation_report.ok else "needs_review",
"summary": validation_payload,
},
{
"key": "build",
"label": "Build messages",
"status": "ok" if build_report.queueable_count else "needs_review",
"summary": {
"built": build_report.built_count,
"queueable": build_report.queueable_count,
"needs_review": build_report.needs_review_count,
"blocked": build_report.blocked_count,
},
},
{
"key": "send",
"label": "Mock SMTP delivery",
"status": (
"skipped"
if not send
else "ok"
if send_batch.failed_count == 0
else "needs_review"
),
"summary": {
"attempted": send_batch.attempted_count,
"sent": send_batch.sent_count,
"failed": send_batch.failed_count,
"skipped": send_batch.skipped_count,
},
},
{
"key": "imap",
"label": "Mock IMAP Sent append",
"status": (
"skipped"
if not send or not append_sent
else "ok"
if send_batch.imap_failed_count == 0
else "needs_review"
),
"summary": {
"appended": send_batch.imap_appended_count,
"failed": send_batch.imap_failed_count,
},
},
]
def _mock_campaign_send_response(
*,
campaign: Campaign,
version: CampaignVersion,
mailbox: Any | None,
validation_report: Any,
validation_payload: dict[str, Any],
build_result: Any,
build_payload: dict[str, Any],
send_batch: _MockSendBatch,
send: bool,
include_warnings: bool,
include_needs_review: bool,
append_sent: bool,
) -> dict[str, Any]:
return {
"campaign_id": campaign.id,
"version_id": version.id,
"version_number": version.version_number,
"send_requested": send,
"include_warnings": include_warnings,
"include_needs_review": include_needs_review,
"append_sent": append_sent,
"steps": _mock_send_steps(
validation_report=validation_report,
validation_payload=validation_payload,
build_report=build_result.report,
send_batch=send_batch,
send=send,
append_sent=append_sent,
),
"validation": validation_payload,
"build": build_payload,
"send": {
"attempted_count": send_batch.attempted_count,
"sent_count": send_batch.sent_count,
"failed_count": send_batch.failed_count,
"skipped_count": send_batch.skipped_count,
"imap_appended_count": send_batch.imap_appended_count,
"imap_failed_count": send_batch.imap_failed_count,
"results": send_batch.results,
},
"mailbox": {
"messages": mailbox.list_records(limit=200) if mailbox is not None else []
},
}
def run_mock_campaign_send( def run_mock_campaign_send(
session: Session, session: Session,
*, *,
@@ -320,87 +551,38 @@ def run_mock_campaign_send(
mailbox only when send=True. mailbox only when send=True.
""" """
campaign = session.query(Campaign).filter(Campaign.id == campaign_id, Campaign.tenant_id == tenant_id).one_or_none() campaign, version = _mock_campaign_version(
if not campaign:
raise MockCampaignSendError("Campaign not found or not accessible")
wanted_version_id = version_id or campaign.current_version_id
if not wanted_version_id:
raise MockCampaignSendError("Campaign has no current version")
version = session.get(CampaignVersion, wanted_version_id)
if not version or version.campaign_id != campaign.id:
raise MockCampaignSendError("Campaign version not found or not part of campaign")
mailbox = _require_mock_mailbox() if send or clear_mailbox else _mock_mailbox()
if clear_mailbox and mailbox is not None:
mailbox.clear_records()
files = files_integration()
assert_server_safe_campaign_paths(
version.raw_json if isinstance(version.raw_json, dict) else {},
managed_files_available=files.available,
)
with files.prepared_campaign_snapshot(
session, session,
tenant_id=tenant_id, tenant_id=tenant_id,
campaign_id=campaign.id, campaign_id=campaign_id,
raw_json=version.raw_json if isinstance(version.raw_json, dict) else {}, version_id=version_id,
include_bytes=True, )
prefix="govoplan-mock-send-", mailbox = _mock_mailbox_for_run(send=send, clear_mailbox=clear_mailbox)
) as prepared: validation_report, build_result, send_batch = _build_mock_campaign_run(
prepared_raw = load_campaign_json(prepared.path) session,
config = load_campaign_config_from_json(session, tenant_id=tenant_id, raw_json=prepared_raw, campaign_id=campaign.id) tenant_id=tenant_id,
validation_report = validate_campaign_config(config, campaign_file=prepared.path, check_files=check_files) campaign=campaign,
build_result = build_campaign_messages(config, campaign_file=prepared.path, write_eml=False) version=version,
files.annotate_built_messages_with_managed_files(build_result.built_messages, prepared.managed_files_by_local_path) mailbox=mailbox,
send=send,
send_batch = _mock_send_batch( include_warnings=include_warnings,
config=config, include_needs_review=include_needs_review,
built_messages=build_result.built_messages, append_sent=append_sent,
mailbox=mailbox, check_files=check_files,
send=send, )
include_warnings=include_warnings, validation_payload = _mock_validation_payload(validation_report)
include_needs_review=include_needs_review, build_payload = _mock_build_payload(build_result)
append_sent=append_sent, return _mock_campaign_send_response(
) campaign=campaign,
version=version,
validation_json = validation_report.model_dump(mode="json") mailbox=mailbox,
validation_json.update({"ok": validation_report.ok, "error_count": validation_report.error_count, "warning_count": validation_report.warning_count}) validation_report=validation_report,
build_report = build_result.report validation_payload=validation_payload,
build_json = build_report.model_dump(mode="json") build_result=build_result,
build_json.update({ build_payload=build_payload,
"built_count": build_report.built_count, send_batch=send_batch,
"queueable_count": build_report.queueable_count, send=send,
"needs_review_count": build_report.needs_review_count, include_warnings=include_warnings,
"blocked_count": build_report.blocked_count, include_needs_review=include_needs_review,
"warning_count": build_report.warning_count, append_sent=append_sent,
"ready_count": build_report.ready_count, )
"messages": [_message_payload(message) for message in build_report.messages],
})
return {
"campaign_id": campaign.id,
"version_id": version.id,
"version_number": version.version_number,
"send_requested": send,
"include_warnings": include_warnings,
"include_needs_review": include_needs_review,
"append_sent": append_sent,
"steps": [
{"key": "validate", "label": "Validate campaign JSON", "status": "ok" if validation_report.ok else "needs_review", "summary": validation_json},
{"key": "build", "label": "Build messages", "status": "ok" if build_report.queueable_count else "needs_review", "summary": {"built": build_report.built_count, "queueable": build_report.queueable_count, "needs_review": build_report.needs_review_count, "blocked": build_report.blocked_count}},
{"key": "send", "label": "Mock SMTP delivery", "status": "skipped" if not send else ("ok" if send_batch.failed_count == 0 else "needs_review"), "summary": {"attempted": send_batch.attempted_count, "sent": send_batch.sent_count, "failed": send_batch.failed_count, "skipped": send_batch.skipped_count}},
{"key": "imap", "label": "Mock IMAP Sent append", "status": "skipped" if not send or not append_sent else ("ok" if send_batch.imap_failed_count == 0 else "needs_review"), "summary": {"appended": send_batch.imap_appended_count, "failed": send_batch.imap_failed_count}},
],
"validation": validation_json,
"build": build_json,
"send": {
"attempted_count": send_batch.attempted_count,
"sent_count": send_batch.sent_count,
"failed_count": send_batch.failed_count,
"skipped_count": send_batch.skipped_count,
"imap_appended_count": send_batch.imap_appended_count,
"imap_failed_count": send_batch.imap_failed_count,
"results": send_batch.results,
},
"mailbox": {"messages": mailbox.list_records(limit=200) if mailbox is not None else []},
}

View File

@@ -0,0 +1,66 @@
from __future__ import annotations
import unittest
from types import SimpleNamespace
from govoplan_campaign.backend.dev.mock_campaign import (
_MockSendBatch,
_mock_send_steps,
)
class MockCampaignStepTests(unittest.TestCase):
def test_preview_marks_delivery_steps_as_skipped(self):
validation = SimpleNamespace(ok=True)
build = SimpleNamespace(
built_count=2,
queueable_count=2,
needs_review_count=0,
blocked_count=0,
)
steps = _mock_send_steps(
validation_report=validation,
validation_payload={"ok": True},
build_report=build,
send_batch=_MockSendBatch(results=[]),
send=False,
append_sent=True,
)
self.assertEqual(
[step["status"] for step in steps],
["ok", "ok", "skipped", "skipped"],
)
def test_delivery_failures_require_review(self):
validation = SimpleNamespace(ok=False)
build = SimpleNamespace(
built_count=1,
queueable_count=0,
needs_review_count=1,
blocked_count=0,
)
batch = _MockSendBatch(
results=[],
failed_count=1,
imap_failed_count=1,
)
steps = _mock_send_steps(
validation_report=validation,
validation_payload={"ok": False},
build_report=build,
send_batch=batch,
send=True,
append_sent=True,
)
self.assertEqual(
[step["status"] for step in steps],
["needs_review", "needs_review", "needs_review", "needs_review"],
)
if __name__ == "__main__":
unittest.main()