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

@@ -351,6 +351,7 @@ def validate_version(
tenant_id=principal.tenant_id,
version_id=version_id,
check_files=payload.check_files if payload else False,
user_id=principal.user.id,
)
audit_from_principal(
session,
@@ -536,7 +537,14 @@ def email_campaign_report(
# Queue / delivery control -------------------------------------------------
from app.api.v1.schemas import AppendSentRequest, CampaignActionResponse, QueueCampaignRequest, QueueCampaignResponse
from app.api.v1.schemas import (
AppendSentRequest,
CampaignActionResponse,
QueueCampaignRequest,
QueueCampaignResponse,
SendCampaignNowRequest,
SendCampaignNowResponse,
)
from app.mailer.sending.jobs import (
QueueingError,
cancel_campaign_jobs,
@@ -544,6 +552,7 @@ from app.mailer.sending.jobs import (
pause_campaign_jobs,
queue_campaign_jobs,
resume_campaign_jobs,
send_campaign_now,
)
@@ -579,6 +588,78 @@ def queue_campaign(
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
@router.post("/{campaign_id}/send-now", response_model=SendCampaignNowResponse)
def send_campaign_now_endpoint(
campaign_id: str,
payload: SendCampaignNowRequest | None = None,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("campaign:send")),
):
"""Validate/build/queue and synchronously send a small campaign version.
This endpoint is intentionally conservative and suitable for a first small
test campaign. Larger campaigns should use the queue/Celery flow.
"""
payload = payload or SendCampaignNowRequest()
try:
campaign = _get_campaign_for_tenant(session, campaign_id, principal.tenant_id)
version_id = payload.version_id or campaign.current_version_id
if not version_id:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Campaign has no current version")
validation_result: dict[str, object] | None = None
build_result: dict[str, object] | None = None
if payload.validate_before_send:
validation_result = validate_campaign_version(
session,
tenant_id=principal.tenant_id,
version_id=version_id,
check_files=payload.check_files,
user_id=principal.user.id,
)
if not validation_result.get("ok"):
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail={"message": "Campaign validation failed", "validation": validation_result})
if payload.build_before_send:
build_result = build_campaign_version(
session,
tenant_id=principal.tenant_id,
version_id=version_id,
write_eml=True,
)
result = send_campaign_now(
session,
tenant_id=principal.tenant_id,
campaign_id=campaign_id,
version_id=version_id,
include_warnings=payload.include_warnings,
dry_run=payload.dry_run,
use_rate_limit=payload.use_rate_limit,
enqueue_imap_task=payload.enqueue_imap_task,
).as_dict()
result["validation"] = validation_result
result["build"] = build_result
audit_from_principal(
session,
principal,
action="campaign.sent_now" if not payload.dry_run else "campaign.send_now_dry_run",
object_type="campaign",
object_id=campaign_id,
details=result,
commit=True,
)
return SendCampaignNowResponse(result=result)
except HTTPException:
raise
except (CampaignPersistenceError, QueueingError) as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
@router.post("/{campaign_id}/pause", response_model=CampaignActionResponse)
def pause_campaign(
campaign_id: str,

View File

@@ -199,6 +199,23 @@ class QueueCampaignResponse(BaseModel):
dry_run: bool = False
class SendCampaignNowRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
version_id: str | None = None
include_warnings: bool = True
check_files: bool = False
validate_before_send: bool = True
build_before_send: bool = True
dry_run: bool = False
use_rate_limit: bool = True
enqueue_imap_task: bool = False
class SendCampaignNowResponse(BaseModel):
result: dict[str, Any]
class AppendSentRequest(BaseModel):
model_config = ConfigDict(extra="forbid")