fix(campaign): mark excluded delivery as skipped
This commit is contained in:
@@ -89,6 +89,7 @@ class BuildStatus(StrEnum):
|
||||
class SendStatus(StrEnum):
|
||||
DRAFT = "draft"
|
||||
QUEUED = "queued"
|
||||
SKIPPED = "skipped"
|
||||
|
||||
|
||||
class CampaignMeta(StrictModel):
|
||||
|
||||
@@ -78,6 +78,7 @@ class JobQueueStatus(StrEnum):
|
||||
|
||||
class JobSendStatus(StrEnum):
|
||||
NOT_QUEUED = "not_queued"
|
||||
SKIPPED = "skipped"
|
||||
QUEUED = "queued"
|
||||
CLAIMED = "claimed"
|
||||
SENDING = "sending"
|
||||
|
||||
@@ -513,6 +513,12 @@ def _message_draft(
|
||||
eml_path: str | None = None,
|
||||
eml_size: int | None = None,
|
||||
) -> MessageDraft:
|
||||
if validation_status == MessageValidationStatus.EXCLUDED:
|
||||
# Exclusion is a completed validation decision, not a pending delivery.
|
||||
# Keep both transport projections explicit so reports never imply that
|
||||
# SMTP or IMAP work is still expected for this row.
|
||||
send_status = SendStatus.SKIPPED
|
||||
imap_status = ImapStatus.SKIPPED
|
||||
if imap_status is None:
|
||||
imap_status = _imap_initial_status(config) if build_status == BuildStatus.BUILT else ImapStatus.SKIPPED
|
||||
return MessageDraft(
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""mark untouched excluded jobs as skipped delivery
|
||||
|
||||
Revision ID: d8b3e2c1f4a5
|
||||
Revises: c7a2f91e4b60
|
||||
Create Date: 2026-07-22 11:00:00.000000
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "d8b3e2c1f4a5"
|
||||
down_revision = "c7a2f91e4b60"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Only normalize rows with no recorded transport effect. Unexpected
|
||||
# historical delivery evidence must remain intact for audit/reconciliation.
|
||||
op.get_bind().execute(
|
||||
sa.text(
|
||||
"UPDATE campaign_jobs "
|
||||
"SET send_status = 'skipped', imap_status = 'skipped' "
|
||||
"WHERE validation_status = 'excluded' "
|
||||
"AND send_status = 'not_queued' "
|
||||
"AND imap_status IN ('not_requested', 'pending', 'skipped')"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.get_bind().execute(
|
||||
sa.text(
|
||||
"UPDATE campaign_jobs "
|
||||
"SET send_status = 'not_queued', imap_status = 'not_requested' "
|
||||
"WHERE validation_status = 'excluded' "
|
||||
"AND send_status = 'skipped' "
|
||||
"AND imap_status = 'skipped'"
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""mark untouched excluded jobs as skipped delivery
|
||||
|
||||
Revision ID: d8b3e2c1f4a5
|
||||
Revises: c7a2f91e4b60
|
||||
Create Date: 2026-07-22 11:00:00.000000
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "d8b3e2c1f4a5"
|
||||
down_revision = "c7a2f91e4b60"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Only normalize rows with no recorded transport effect. Unexpected
|
||||
# historical delivery evidence must remain intact for audit/reconciliation.
|
||||
op.get_bind().execute(
|
||||
sa.text(
|
||||
"UPDATE campaign_jobs "
|
||||
"SET send_status = 'skipped', imap_status = 'skipped' "
|
||||
"WHERE validation_status = 'excluded' "
|
||||
"AND send_status = 'not_queued' "
|
||||
"AND imap_status IN ('not_requested', 'pending', 'skipped')"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.get_bind().execute(
|
||||
sa.text(
|
||||
"UPDATE campaign_jobs "
|
||||
"SET send_status = 'not_queued', imap_status = 'not_requested' "
|
||||
"WHERE validation_status = 'excluded' "
|
||||
"AND send_status = 'skipped' "
|
||||
"AND imap_status = 'skipped'"
|
||||
)
|
||||
)
|
||||
@@ -35,7 +35,7 @@ from govoplan_campaign.backend.campaign.validation import validate_campaign_conf
|
||||
from govoplan_campaign.backend.messages.builder import build_campaign_messages
|
||||
from govoplan_campaign.backend.messages.models import MessageDraft
|
||||
from govoplan_campaign.backend.sending.execution import create_execution_snapshot, profile_transport_revisions
|
||||
from govoplan_campaign.backend.campaign.models import CampaignConfig
|
||||
from govoplan_campaign.backend.campaign.models import CampaignConfig, SendStatus
|
||||
from govoplan_campaign.backend.integrations import files_integration, mail_integration
|
||||
from govoplan_campaign.backend.path_security import assert_server_safe_campaign_paths
|
||||
|
||||
@@ -406,7 +406,11 @@ def _job_from_message(
|
||||
build_status=message.build_status.value if hasattr(message.build_status, "value") else str(message.build_status),
|
||||
validation_status=_job_validation_status(message.validation_status.value),
|
||||
queue_status=JobQueueStatus.DRAFT.value,
|
||||
send_status=JobSendStatus.NOT_QUEUED.value,
|
||||
send_status=(
|
||||
JobSendStatus.SKIPPED.value
|
||||
if message.send_status == SendStatus.SKIPPED
|
||||
else JobSendStatus.NOT_QUEUED.value
|
||||
),
|
||||
imap_status=message.imap_status.value if hasattr(message.imap_status, "value") else JobImapStatus.NOT_REQUESTED.value,
|
||||
resolved_recipients={
|
||||
"from": message.from_.model_dump(mode="json") if message.from_ else None,
|
||||
|
||||
@@ -114,10 +114,10 @@ def _load_delivery_info(
|
||||
"queueable_job_count": 0,
|
||||
"estimated_remaining_send_seconds": None,
|
||||
"estimated_remaining_send_human": None,
|
||||
"delivery_mode": version.delivery_mode if version else None,
|
||||
"delivery_mode": getattr(version, "delivery_mode", None) if version else None,
|
||||
"delivery_mode_selected_at": (
|
||||
version.delivery_mode_selected_at.isoformat()
|
||||
if version and version.delivery_mode_selected_at
|
||||
getattr(version, "delivery_mode_selected_at", None).isoformat()
|
||||
if version and getattr(version, "delivery_mode_selected_at", None)
|
||||
else None
|
||||
),
|
||||
}
|
||||
@@ -577,7 +577,7 @@ def _campaign_report_cards(version: CampaignVersion | None, jobs: list[CampaignJ
|
||||
1
|
||||
for job in jobs
|
||||
if job.send_status
|
||||
not in {"smtp_accepted", "sent", "outcome_unknown", "claimed", "sending", "cancelled"}
|
||||
not in {"skipped", "smtp_accepted", "sent", "outcome_unknown", "claimed", "sending", "cancelled"}
|
||||
)
|
||||
needs_attention = sum(
|
||||
1
|
||||
@@ -590,6 +590,7 @@ def _campaign_report_cards(version: CampaignVersion | None, jobs: list[CampaignJ
|
||||
failed = send_counts.get("failed_temporary", 0) + send_counts.get("failed_permanent", 0)
|
||||
outcome_unknown = send_counts.get("outcome_unknown", 0)
|
||||
not_attempted = send_counts.get("not_queued", 0)
|
||||
skipped = send_counts.get("skipped", 0)
|
||||
queued = send_counts.get("queued", 0) + send_counts.get("claimed", 0) + send_counts.get("sending", 0)
|
||||
cancelled = send_counts.get("cancelled", 0)
|
||||
inactive_entries = _inactive_entry_count(version)
|
||||
@@ -606,11 +607,13 @@ def _campaign_report_cards(version: CampaignVersion | None, jobs: list[CampaignJ
|
||||
"failed": failed,
|
||||
"outcome_unknown": outcome_unknown,
|
||||
"not_attempted": not_attempted,
|
||||
"skipped": skipped,
|
||||
"queued_or_active": queued,
|
||||
"cancelled": cancelled,
|
||||
"partially_completed": bool(sent and (failed or outcome_unknown or not_attempted or cancelled)),
|
||||
"imap_appended": imap_counts.get("appended", 0),
|
||||
"imap_failed": imap_counts.get("failed", 0),
|
||||
"imap_skipped": imap_counts.get("skipped", 0),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -88,8 +88,10 @@ def _text_summary(report: dict[str, Any]) -> str:
|
||||
f"- Needs attention: {cards['needs_attention']}",
|
||||
f"- Sent: {cards['sent']}",
|
||||
f"- Failed: {cards['failed']}",
|
||||
f"- SMTP skipped (excluded): {cards.get('skipped', status.get('send', {}).get('skipped', 0))}",
|
||||
f"- IMAP appended: {cards['imap_appended']}",
|
||||
f"- IMAP failed: {cards['imap_failed']}",
|
||||
f"- IMAP skipped: {cards.get('imap_skipped', status.get('imap', {}).get('skipped', 0))}",
|
||||
"",
|
||||
f"Build status: {status.get('build', {})}",
|
||||
f"Validation status: {status.get('validation', {})}",
|
||||
|
||||
@@ -212,6 +212,7 @@ DELIVERY_MODES = {
|
||||
AUTOMATICALLY_SENDABLE_STATUSES = {JobSendStatus.QUEUED.value}
|
||||
EXPLICIT_RETRY_STATUSES = {JobSendStatus.FAILED_TEMPORARY.value, JobSendStatus.FAILED_PERMANENT.value}
|
||||
INITIAL_QUEUE_SKIPPED_SEND_STATUSES = SMTP_ACCEPTED_STATUSES | {
|
||||
JobSendStatus.SKIPPED.value,
|
||||
JobSendStatus.CLAIMED.value,
|
||||
JobSendStatus.SENDING.value,
|
||||
JobSendStatus.OUTCOME_UNKNOWN.value,
|
||||
@@ -1001,9 +1002,13 @@ def cancel_campaign_jobs(session: Session, *, tenant_id: str, campaign_id: str)
|
||||
)
|
||||
cancelled_count = 0
|
||||
protected_count = 0
|
||||
skipped_count = 0
|
||||
version_ids: set[str] = set()
|
||||
for job in jobs:
|
||||
version_ids.add(job.campaign_version_id)
|
||||
if job.send_status == JobSendStatus.SKIPPED.value:
|
||||
skipped_count += 1
|
||||
continue
|
||||
if job.send_status in SMTP_ACCEPTED_STATUSES | {
|
||||
JobSendStatus.OUTCOME_UNKNOWN.value,
|
||||
JobSendStatus.CLAIMED.value,
|
||||
@@ -1024,6 +1029,7 @@ def cancel_campaign_jobs(session: Session, *, tenant_id: str, campaign_id: str)
|
||||
"campaign_id": campaign.id,
|
||||
"cancelled_count": cancelled_count,
|
||||
"protected_count": protected_count,
|
||||
"skipped_count": skipped_count,
|
||||
"campaign_status": campaign.status,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user