631 lines
27 KiB
Python
631 lines
27 KiB
Python
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
from enum import StrEnum
|
|
from typing import Any
|
|
|
|
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, JSON, String, Text, UniqueConstraint
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from govoplan_core.core.concurrency import strong_resource_etag
|
|
from govoplan_core.db.base import Base, TimestampMixin
|
|
|
|
|
|
def new_uuid() -> str:
|
|
return str(uuid.uuid4())
|
|
|
|
|
|
class CampaignStatus(StrEnum):
|
|
DRAFT = "draft"
|
|
VALIDATED = "validated"
|
|
NEEDS_REVIEW = "needs_review"
|
|
READY_TO_QUEUE = "ready_to_queue"
|
|
QUEUED = "queued"
|
|
SENDING = "sending"
|
|
SENT = "sent"
|
|
PARTIALLY_COMPLETED = "partially_completed"
|
|
OUTCOME_UNKNOWN = "outcome_unknown"
|
|
FAILED = "failed"
|
|
CANCELLED = "cancelled"
|
|
ARCHIVED = "archived"
|
|
|
|
|
|
class CampaignVersionWorkflowState(StrEnum):
|
|
EDITING = "editing"
|
|
UNDER_REVIEW = "under_review"
|
|
APPROVED = "approved"
|
|
BUILT = "built"
|
|
QUEUED = "queued"
|
|
SENDING = "sending"
|
|
COMPLETED = "completed"
|
|
PARTIALLY_COMPLETED = "partially_completed"
|
|
OUTCOME_UNKNOWN = "outcome_unknown"
|
|
FAILED = "failed"
|
|
CANCELLED = "cancelled"
|
|
ARCHIVED = "archived"
|
|
|
|
|
|
class CampaignVersionFlow(StrEnum):
|
|
CREATE = "create"
|
|
REVIEW = "review"
|
|
SEND = "send"
|
|
MANUAL = "manual"
|
|
JSON = "json"
|
|
|
|
|
|
class JobBuildStatus(StrEnum):
|
|
PENDING = "pending"
|
|
BUILT = "built"
|
|
BUILD_FAILED = "build_failed"
|
|
|
|
|
|
class JobValidationStatus(StrEnum):
|
|
READY = "ready"
|
|
WARNING = "warning"
|
|
NEEDS_REVIEW = "needs_review"
|
|
BLOCKED = "blocked"
|
|
EXCLUDED = "excluded"
|
|
INACTIVE = "inactive"
|
|
|
|
|
|
class JobQueueStatus(StrEnum):
|
|
DRAFT = "draft"
|
|
QUEUED = "queued"
|
|
SENDING = "sending"
|
|
PAUSED = "paused"
|
|
CANCELLED = "cancelled"
|
|
|
|
|
|
class JobSendStatus(StrEnum):
|
|
NOT_QUEUED = "not_queued"
|
|
SKIPPED = "skipped"
|
|
QUEUED = "queued"
|
|
CLAIMED = "claimed"
|
|
SENDING = "sending"
|
|
SMTP_ACCEPTED = "smtp_accepted"
|
|
POSTBOX_ACCEPTED = "postbox_accepted"
|
|
DELIVERED = "delivered"
|
|
PARTIALLY_ACCEPTED = "partially_accepted"
|
|
SENT = "sent" # legacy value retained for existing databases/reports
|
|
OUTCOME_UNKNOWN = "outcome_unknown"
|
|
FAILED_TEMPORARY = "failed_temporary"
|
|
FAILED_PERMANENT = "failed_permanent"
|
|
CANCELLED = "cancelled"
|
|
|
|
|
|
class JobPostboxStatus(StrEnum):
|
|
NOT_REQUESTED = "not_requested"
|
|
PENDING = "pending"
|
|
DELIVERING = "delivering"
|
|
ACCEPTED = "accepted"
|
|
ACCEPTED_VACANT = "accepted_vacant"
|
|
PARTIALLY_ACCEPTED = "partially_accepted"
|
|
REJECTED_TEMPORARY = "rejected_temporary"
|
|
REJECTED_PERMANENT = "rejected_permanent"
|
|
OUTCOME_UNKNOWN = "outcome_unknown"
|
|
SKIPPED = "skipped"
|
|
|
|
|
|
class JobImapStatus(StrEnum):
|
|
NOT_REQUESTED = "not_requested"
|
|
PENDING = "pending"
|
|
APPENDING = "appending"
|
|
APPENDED = "appended"
|
|
OUTCOME_UNKNOWN = "outcome_unknown"
|
|
FAILED = "failed"
|
|
SKIPPED = "skipped"
|
|
|
|
|
|
class IssueSeverity(StrEnum):
|
|
INFO = "info"
|
|
WARNING = "warning"
|
|
ERROR = "error"
|
|
|
|
|
|
class Campaign(Base, TimestampMixin):
|
|
__tablename__ = "campaigns"
|
|
__table_args__ = (UniqueConstraint("tenant_id", "external_id", name="uq_campaigns_tenant_external_id"),)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
|
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
|
created_by_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
|
|
owner_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
|
|
owner_group_id: Mapped[str | None] = mapped_column(ForeignKey("access_groups.id", ondelete="SET NULL"), nullable=True, index=True)
|
|
external_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
description: Mapped[str | None] = mapped_column(Text)
|
|
status: Mapped[str] = mapped_column(String(50), default=CampaignStatus.DRAFT.value, nullable=False, index=True)
|
|
current_version_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
|
|
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
mail_profile_policy: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
|
|
versions: Mapped[list[CampaignVersion]] = relationship(back_populates="campaign", cascade="all, delete-orphan")
|
|
jobs: Mapped[list[CampaignJob]] = relationship(back_populates="campaign", cascade="all, delete-orphan")
|
|
|
|
|
|
class CampaignShare(Base, TimestampMixin):
|
|
__tablename__ = "campaign_shares"
|
|
__table_args__ = (UniqueConstraint("campaign_id", "target_type", "target_id", name="uq_campaign_share_target"),)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
|
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
|
campaign_id: Mapped[str] = mapped_column(ForeignKey("campaigns.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
target_type: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
|
target_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
|
permission: Mapped[str] = mapped_column(String(20), default="read", nullable=False)
|
|
created_by_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
|
|
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
|
|
|
|
|
|
class RecipientImportMappingProfile(Base, TimestampMixin):
|
|
__tablename__ = "campaign_recipient_import_mapping_profiles"
|
|
__table_args__ = (
|
|
Index("ix_recipient_import_profiles_owner", "tenant_id", "owner_user_id"),
|
|
Index("ix_recipient_import_profiles_ordered_fp", "tenant_id", "owner_user_id", "ordered_header_fingerprint"),
|
|
Index("ix_recipient_import_profiles_unordered_fp", "tenant_id", "owner_user_id", "unordered_header_fingerprint"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
|
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
|
owner_user_id: Mapped[str] = mapped_column(ForeignKey("access_users.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
column_count: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
headers: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
|
|
normalized_headers: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
|
|
ordered_header_fingerprint: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
|
unordered_header_fingerprint: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
|
delimiter: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
header_rows: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
|
quoted: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
value_separators: Mapped[str] = mapped_column(String(50), default=",;|", nullable=False)
|
|
mappings: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False)
|
|
|
|
|
|
class CampaignVersion(Base, TimestampMixin):
|
|
__tablename__ = "campaign_versions"
|
|
__table_args__ = (UniqueConstraint("campaign_id", "version_number", name="uq_campaign_versions_campaign_number"),)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
|
campaign_id: Mapped[str] = mapped_column(ForeignKey("campaigns.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
version_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
edit_revision: Mapped[int] = mapped_column(
|
|
Integer,
|
|
default=1,
|
|
nullable=False,
|
|
)
|
|
raw_json: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False)
|
|
schema_version: Mapped[str] = mapped_column(String(50), default="1.0", nullable=False)
|
|
source_filename: Mapped[str | None] = mapped_column(String(500))
|
|
source_base_path: Mapped[str | None] = mapped_column(String(1000))
|
|
|
|
# Editor/workflow metadata used by the WebUI and future desktop clients.
|
|
# A campaign version can be the autosaved working copy of a new or existing
|
|
# campaign, so no separate CampaignDraft entity is needed.
|
|
workflow_state: Mapped[str] = mapped_column(
|
|
String(50),
|
|
default=CampaignVersionWorkflowState.EDITING.value,
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
current_flow: Mapped[str] = mapped_column(
|
|
String(50),
|
|
default=CampaignVersionFlow.MANUAL.value,
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
current_step: Mapped[str | None] = mapped_column(String(100))
|
|
is_complete: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
editor_state: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
autosaved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
locked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
locked_by_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
|
|
|
|
# Explicit user-requested lock. This is deliberately separate from
|
|
# locked_at, which represents the reversible validation lock used by the
|
|
# build/send workflow. Temporary user locks may later receive a dedicated
|
|
# RBAC permission for unlocking; permanent locks never unlock in place.
|
|
user_lock_state: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True)
|
|
user_locked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
user_locked_by_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
|
|
|
|
validation_summary: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
|
build_summary: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
|
execution_snapshot: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
|
execution_snapshot_hash: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
|
execution_snapshot_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
delivery_mode: Mapped[str | None] = mapped_column(String(30), nullable=True, index=True)
|
|
delivery_mode_selected_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
|
campaign: Mapped[Campaign] = relationship(back_populates="versions")
|
|
|
|
__mapper_args__ = {
|
|
"version_id_col": edit_revision,
|
|
}
|
|
|
|
@property
|
|
def strong_etag(self) -> str:
|
|
return strong_resource_etag(
|
|
"campaign_version",
|
|
self.id,
|
|
self.edit_revision,
|
|
)
|
|
|
|
@property
|
|
def mail_profile_migration_required(self) -> bool:
|
|
from govoplan_campaign.backend.campaign.mail_profile_boundary import campaign_mail_profile_boundary_violations
|
|
|
|
return bool(campaign_mail_profile_boundary_violations(self.raw_json))
|
|
|
|
|
|
class CampaignJob(Base, TimestampMixin):
|
|
__tablename__ = "campaign_jobs"
|
|
__table_args__ = (UniqueConstraint("campaign_version_id", "entry_index", name="uq_campaign_jobs_version_entry"),)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
|
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
|
campaign_id: Mapped[str] = mapped_column(ForeignKey("campaigns.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
campaign_version_id: Mapped[str] = mapped_column(ForeignKey("campaign_versions.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
entry_index: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
entry_id: Mapped[str | None] = mapped_column(String(255), index=True)
|
|
|
|
recipient_email: Mapped[str | None] = mapped_column(String(320), index=True)
|
|
subject: Mapped[str | None] = mapped_column(String(998))
|
|
message_id_header: Mapped[str | None] = mapped_column(String(255))
|
|
eml_storage_key: Mapped[str | None] = mapped_column(String(1000))
|
|
eml_local_path: Mapped[str | None] = mapped_column(String(1000))
|
|
eml_size_bytes: Mapped[int | None] = mapped_column(Integer)
|
|
eml_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
|
execution_input_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
|
|
build_status: Mapped[str] = mapped_column(String(50), default=JobBuildStatus.PENDING.value, nullable=False, index=True)
|
|
validation_status: Mapped[str] = mapped_column(String(50), default=JobValidationStatus.NEEDS_REVIEW.value, nullable=False, index=True)
|
|
queue_status: Mapped[str] = mapped_column(String(50), default=JobQueueStatus.DRAFT.value, nullable=False, index=True)
|
|
send_status: Mapped[str] = mapped_column(String(50), default=JobSendStatus.NOT_QUEUED.value, nullable=False, index=True)
|
|
delivery_channel_policy: Mapped[str] = mapped_column(
|
|
String(30),
|
|
default="mail",
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
postbox_status: Mapped[str] = mapped_column(
|
|
String(50),
|
|
default=JobPostboxStatus.NOT_REQUESTED.value,
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
imap_status: Mapped[str] = mapped_column(String(50), default=JobImapStatus.NOT_REQUESTED.value, nullable=False, index=True)
|
|
|
|
attempt_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
postbox_attempt_count: Mapped[int] = mapped_column(
|
|
Integer,
|
|
default=0,
|
|
nullable=False,
|
|
)
|
|
last_error: Mapped[str | None] = mapped_column(Text)
|
|
queued_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
claimed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
claim_token: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
|
|
smtp_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
outcome_unknown_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
imap_claimed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
imap_claim_token: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
|
|
sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
|
resolved_recipients: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
|
resolved_postbox_targets: Mapped[list[dict[str, Any]]] = mapped_column(
|
|
JSON,
|
|
default=list,
|
|
nullable=False,
|
|
)
|
|
resolved_attachments: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
|
|
issues_snapshot: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
|
|
|
|
campaign: Mapped[Campaign] = relationship(back_populates="jobs")
|
|
|
|
|
|
class CampaignIssue(Base, TimestampMixin):
|
|
__tablename__ = "campaign_issues"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
|
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
|
campaign_id: Mapped[str] = mapped_column(ForeignKey("campaigns.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
campaign_version_id: Mapped[str | None] = mapped_column(ForeignKey("campaign_versions.id", ondelete="CASCADE"), nullable=True, index=True)
|
|
job_id: Mapped[str | None] = mapped_column(ForeignKey("campaign_jobs.id", ondelete="CASCADE"), nullable=True, index=True)
|
|
severity: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
|
code: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
|
message: Mapped[str] = mapped_column(Text, nullable=False)
|
|
source: Mapped[str | None] = mapped_column(String(255))
|
|
behavior: Mapped[str | None] = mapped_column(String(50))
|
|
|
|
|
|
class AttachmentBlob(Base, TimestampMixin):
|
|
__tablename__ = "attachment_blobs"
|
|
__table_args__ = (UniqueConstraint("tenant_id", "sha256", name="uq_attachment_blobs_tenant_sha256"),)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
|
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
|
sha256: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
|
size_bytes: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
mime_type: Mapped[str | None] = mapped_column(String(255))
|
|
storage_bucket: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
storage_key: Mapped[str] = mapped_column(String(1000), nullable=False)
|
|
|
|
|
|
class AttachmentInstance(Base, TimestampMixin):
|
|
__tablename__ = "attachment_instances"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
|
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
|
owner_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
|
|
campaign_id: Mapped[str | None] = mapped_column(ForeignKey("campaigns.id", ondelete="CASCADE"), nullable=True, index=True)
|
|
blob_id: Mapped[str] = mapped_column(ForeignKey("attachment_blobs.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
logical_name: Mapped[str | None] = mapped_column(String(500))
|
|
filename: Mapped[str] = mapped_column(String(500), nullable=False)
|
|
tags: Mapped[list[str]] = mapped_column(JSON, default=list)
|
|
metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True)
|
|
|
|
|
|
class SendAttempt(Base, TimestampMixin):
|
|
__tablename__ = "send_attempts"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
|
job_id: Mapped[str] = mapped_column(ForeignKey("campaign_jobs.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
attempt_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
status: Mapped[str] = mapped_column(String(50), default="started", nullable=False, index=True)
|
|
claim_token: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
|
|
smtp_status_code: Mapped[int | None] = mapped_column(Integer)
|
|
smtp_response: Mapped[str | None] = mapped_column(Text)
|
|
error_type: Mapped[str | None] = mapped_column(String(255))
|
|
error_message: Mapped[str | None] = mapped_column(Text)
|
|
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
|
|
|
class CampaignMessageAction(Base, TimestampMixin):
|
|
__tablename__ = "campaign_message_actions"
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id",
|
|
"idempotency_key",
|
|
name="uq_campaign_message_actions_idempotency",
|
|
),
|
|
Index(
|
|
"ix_campaign_message_actions_job_created",
|
|
"job_id",
|
|
"created_at",
|
|
),
|
|
Index(
|
|
"ix_campaign_message_actions_campaign_kind",
|
|
"campaign_id",
|
|
"kind",
|
|
"status",
|
|
),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
|
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
|
campaign_id: Mapped[str] = mapped_column(
|
|
ForeignKey("campaigns.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
campaign_version_id: Mapped[str] = mapped_column(
|
|
ForeignKey("campaign_versions.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
job_id: Mapped[str] = mapped_column(
|
|
ForeignKey("campaign_jobs.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
kind: Mapped[str] = mapped_column(String(30), nullable=False, index=True)
|
|
idempotency_key: Mapped[str] = mapped_column(String(200), nullable=False)
|
|
canonical_request_hash: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
context: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
actor_user_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("access_users.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
index=True,
|
|
)
|
|
actor_api_key_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
|
|
message_sha256: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
message_size_bytes: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
recipient_manifest_sha256: Mapped[str] = mapped_column(
|
|
String(64),
|
|
nullable=False,
|
|
)
|
|
recipient_count: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
prior_send_status: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
prior_attempt_count: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
final_send_status: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
status: Mapped[str] = mapped_column(
|
|
String(50),
|
|
default="initiated",
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
accepted_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
refused_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
refusal_summary: Mapped[dict[str, Any]] = mapped_column(
|
|
JSON,
|
|
default=dict,
|
|
nullable=False,
|
|
)
|
|
error_type: Mapped[str | None] = mapped_column(String(120), nullable=True)
|
|
error_message: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
linked_send_attempt_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("send_attempts.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
index=True,
|
|
)
|
|
effect_started_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True),
|
|
nullable=True,
|
|
)
|
|
completed_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True),
|
|
nullable=True,
|
|
)
|
|
|
|
|
|
class CampaignMessageActionAttempt(Base, TimestampMixin):
|
|
__tablename__ = "campaign_message_action_attempts"
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"action_id",
|
|
"attempt_number",
|
|
name="uq_campaign_message_action_attempt_number",
|
|
),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
|
action_id: Mapped[str] = mapped_column(
|
|
ForeignKey("campaign_message_actions.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
attempt_number: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
|
status: Mapped[str] = mapped_column(
|
|
String(50),
|
|
default="initiated",
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
started_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
nullable=False,
|
|
)
|
|
effect_started_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True),
|
|
nullable=True,
|
|
)
|
|
completed_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True),
|
|
nullable=True,
|
|
)
|
|
accepted_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
refused_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
outcome_code: Mapped[str | None] = mapped_column(String(80), nullable=True)
|
|
diagnostic_summary: Mapped[str | None] = mapped_column(
|
|
String(500),
|
|
nullable=True,
|
|
)
|
|
|
|
|
|
class ImapAppendAttempt(Base, TimestampMixin):
|
|
__tablename__ = "imap_append_attempts"
|
|
__table_args__ = (
|
|
UniqueConstraint("job_id", "attempt_number", name="uq_imap_append_attempts_job_attempt"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
|
job_id: Mapped[str] = mapped_column(ForeignKey("campaign_jobs.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
attempt_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
claim_token: Mapped[str | None] = mapped_column(String(36), nullable=True)
|
|
folder: Mapped[str | None] = mapped_column(String(500))
|
|
status: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
error_message: Mapped[str | None] = mapped_column(Text)
|
|
|
|
|
|
class PostboxDeliveryAttempt(Base, TimestampMixin):
|
|
__tablename__ = "campaign_postbox_delivery_attempts"
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"job_id",
|
|
"target_key",
|
|
"attempt_number",
|
|
name="uq_campaign_postbox_attempt_target_number",
|
|
),
|
|
Index(
|
|
"ix_campaign_postbox_attempt_idempotency",
|
|
"tenant_id",
|
|
"idempotency_key",
|
|
),
|
|
Index(
|
|
"ix_campaign_postbox_attempt_job_status",
|
|
"job_id",
|
|
"status",
|
|
),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(
|
|
String(36),
|
|
primary_key=True,
|
|
default=new_uuid,
|
|
)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
String(36),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
job_id: Mapped[str] = mapped_column(
|
|
ForeignKey("campaign_jobs.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
target_key: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
target_index: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
attempt_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
status: Mapped[str] = mapped_column(
|
|
String(50),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
target_snapshot: Mapped[dict[str, Any]] = mapped_column(
|
|
JSON,
|
|
default=dict,
|
|
nullable=False,
|
|
)
|
|
provider_delivery_id: Mapped[str | None] = mapped_column(String(36))
|
|
provider_message_id: Mapped[str | None] = mapped_column(String(36))
|
|
postbox_id: Mapped[str | None] = mapped_column(String(36), index=True)
|
|
address: Mapped[str | None] = mapped_column(String(500))
|
|
holder_count: Mapped[int | None] = mapped_column(Integer)
|
|
vacant: Mapped[bool | None] = mapped_column(Boolean)
|
|
duplicate: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
evidence: Mapped[dict[str, Any]] = mapped_column(
|
|
JSON,
|
|
default=dict,
|
|
nullable=False,
|
|
)
|
|
error_type: Mapped[str | None] = mapped_column(String(255))
|
|
error_code: Mapped[str | None] = mapped_column(String(100))
|
|
error_message: Mapped[str | None] = mapped_column(Text)
|
|
started_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True),
|
|
)
|
|
finished_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True),
|
|
)
|
|
|
|
|
|
|
|
|
|
__all__ = [
|
|
"AttachmentBlob",
|
|
"AttachmentInstance",
|
|
"Campaign",
|
|
"CampaignIssue",
|
|
"CampaignJob",
|
|
"CampaignShare",
|
|
"CampaignStatus",
|
|
"CampaignVersion",
|
|
"CampaignVersionFlow",
|
|
"CampaignVersionWorkflowState",
|
|
"ImapAppendAttempt",
|
|
"IssueSeverity",
|
|
"JobBuildStatus",
|
|
"JobImapStatus",
|
|
"JobPostboxStatus",
|
|
"JobQueueStatus",
|
|
"JobSendStatus",
|
|
"JobValidationStatus",
|
|
"SendAttempt",
|
|
"PostboxDeliveryAttempt",
|
|
]
|