first version able to send
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user