first version able to send

This commit is contained in:
2026-06-11 00:06:44 +02:00
parent ce43f2658f
commit 3b06f3670e
12 changed files with 740 additions and 67 deletions

View File

@@ -15,6 +15,7 @@ from app.db.models import (
CampaignJob,
CampaignStatus,
CampaignVersion,
CampaignVersionWorkflowState,
JobBuildStatus,
JobImapStatus,
JobQueueStatus,
@@ -61,6 +62,30 @@ class QueueCampaignResult:
}
@dataclass(frozen=True, slots=True)
class SendCampaignNowResult:
campaign_id: str
version_id: str
attempted_count: int
sent_count: int
failed_count: int
skipped_count: int
dry_run: bool = False
results: list[dict[str, Any]] | None = None
def as_dict(self) -> dict[str, Any]:
return {
"campaign_id": self.campaign_id,
"version_id": self.version_id,
"attempted_count": self.attempted_count,
"sent_count": self.sent_count,
"failed_count": self.failed_count,
"skipped_count": self.skipped_count,
"dry_run": self.dry_run,
"results": self.results or [],
}
@dataclass(frozen=True, slots=True)
class SendJobResult:
job_id: str
@@ -197,6 +222,10 @@ def queue_campaign_jobs(
if not dry_run:
if queued:
campaign.status = CampaignStatus.QUEUED.value
version.workflow_state = CampaignVersionWorkflowState.QUEUED.value
if version.locked_at is None:
version.locked_at = _utcnow()
session.add(version)
session.add(campaign)
session.commit()
@@ -217,6 +246,91 @@ def queue_campaign_jobs(
)
def send_campaign_now(
session: Session,
*,
tenant_id: str,
campaign_id: str,
version_id: str | None = None,
include_warnings: bool = True,
dry_run: bool = False,
use_rate_limit: bool = True,
enqueue_imap_task: bool = False,
) -> SendCampaignNowResult:
"""Queue and send a small campaign synchronously.
This is intended for WebUI test/small-campaign flows. Large campaigns can
still use queue_campaign_jobs with Celery workers.
"""
campaign = _get_campaign_for_tenant(session, campaign_id=campaign_id, tenant_id=tenant_id)
version = _get_current_version(session, campaign, version_id=version_id)
queue_result = queue_campaign_jobs(
session,
tenant_id=tenant_id,
campaign_id=campaign.id,
version_id=version.id,
include_warnings=include_warnings,
enqueue_celery=False,
dry_run=dry_run,
)
if dry_run:
return SendCampaignNowResult(
campaign_id=campaign.id,
version_id=version.id,
attempted_count=0,
sent_count=0,
failed_count=0,
skipped_count=queue_result.skipped_count + queue_result.blocked_count,
dry_run=True,
results=[queue_result.as_dict()],
)
jobs = (
session.query(CampaignJob)
.filter(
CampaignJob.tenant_id == tenant_id,
CampaignJob.campaign_version_id == version.id,
CampaignJob.queue_status == JobQueueStatus.QUEUED.value,
CampaignJob.send_status.in_([JobSendStatus.QUEUED.value, JobSendStatus.FAILED_TEMPORARY.value]),
)
.order_by(CampaignJob.entry_index.asc())
.all()
)
results: list[dict[str, Any]] = []
sent_count = 0
failed_count = 0
for job in jobs:
try:
result = send_campaign_job(
session,
job_id=job.id,
dry_run=False,
use_rate_limit=use_rate_limit,
enqueue_imap_task=enqueue_imap_task,
)
result_dict = result.as_dict()
results.append(result_dict)
if result.status in {"sent", "already_sent"}:
sent_count += 1
except Exception as exc: # keep sending other jobs and return per-job details
failed_count += 1
results.append({"job_id": job.id, "status": "failed", "message": str(exc)})
return SendCampaignNowResult(
campaign_id=campaign.id,
version_id=version.id,
attempted_count=len(jobs),
sent_count=sent_count,
failed_count=failed_count,
skipped_count=queue_result.skipped_count + queue_result.blocked_count,
dry_run=False,
results=results,
)
def enqueue_existing_queued_jobs(session: Session, *, tenant_id: str, campaign_id: str) -> int:
campaign = _get_campaign_for_tenant(session, campaign_id=campaign_id, tenant_id=tenant_id)
jobs = (
@@ -360,15 +474,18 @@ def _record_attempt_start(session: Session, job: CampaignJob) -> SendAttempt:
return attempt
def _update_campaign_after_job(session: Session, campaign_id: str) -> None:
def _update_campaign_after_job(session: Session, campaign_id: str, version_id: str | None = None) -> None:
session.flush()
campaign = session.get(Campaign, campaign_id)
if not campaign:
return
base_filters = [CampaignJob.campaign_id == campaign_id]
if version_id:
base_filters.append(CampaignJob.campaign_version_id == version_id)
remaining = (
session.query(CampaignJob)
.filter(
CampaignJob.campaign_id == campaign_id,
*base_filters,
CampaignJob.queue_status.in_([JobQueueStatus.QUEUED.value, JobQueueStatus.SENDING.value, JobQueueStatus.PAUSED.value]),
)
.count()
@@ -376,18 +493,25 @@ def _update_campaign_after_job(session: Session, campaign_id: str) -> None:
failed = (
session.query(CampaignJob)
.filter(
CampaignJob.campaign_id == campaign_id,
*base_filters,
CampaignJob.send_status.in_([JobSendStatus.FAILED_TEMPORARY.value, JobSendStatus.FAILED_PERMANENT.value]),
)
.count()
)
sent = session.query(CampaignJob).filter(CampaignJob.campaign_id == campaign_id, CampaignJob.send_status == JobSendStatus.SENT.value).count()
sent = session.query(CampaignJob).filter(*base_filters, CampaignJob.send_status == JobSendStatus.SENT.value).count()
if remaining:
campaign.status = CampaignStatus.QUEUED.value
elif failed:
campaign.status = CampaignStatus.FAILED.value if not sent else CampaignStatus.NEEDS_REVIEW.value
elif sent:
campaign.status = CampaignStatus.SENT.value
if version_id:
version = session.get(CampaignVersion, version_id)
if version:
version.workflow_state = CampaignVersionWorkflowState.COMPLETED.value
if version.locked_at is None:
version.locked_at = _utcnow()
session.add(version)
session.add(campaign)
@@ -452,7 +576,7 @@ def send_campaign_job(session: Session, *, job_id: str, dry_run: bool = False, u
job.last_error = None
session.add(attempt)
session.add(job)
_update_campaign_after_job(session, job.campaign_id)
_update_campaign_after_job(session, job.campaign_id, job.campaign_version_id)
session.commit()
if enqueue_imap_task and job.imap_status == JobImapStatus.PENDING.value:
_celery_enqueue_append_sent_job(job.id)
@@ -473,7 +597,7 @@ def send_campaign_job(session: Session, *, job_id: str, dry_run: bool = False, u
job.send_status = JobSendStatus.FAILED_PERMANENT.value
session.add(attempt)
session.add(job)
_update_campaign_after_job(session, job.campaign_id)
_update_campaign_after_job(session, job.campaign_id, job.campaign_version_id)
session.commit()
raise