687 lines
23 KiB
Python
687 lines
23 KiB
Python
from __future__ import annotations
|
|
|
|
|
|
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Response, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from govoplan_campaign.backend.schemas import (
|
|
BuildCampaignRequest,
|
|
CampaignCreateResponse,
|
|
CampaignResponse,
|
|
CampaignVersionDetailResponse,
|
|
CampaignVersionResponse,
|
|
CampaignVersionSetStepRequest,
|
|
CampaignReviewStateRequest,
|
|
CampaignVersionUpdateRequest,
|
|
CampaignPartialValidationRequest,
|
|
CampaignPartialValidationResponse,
|
|
ValidateCampaignRequest,
|
|
)
|
|
from govoplan_core.auth import ApiPrincipal, has_scope, require_scope
|
|
from govoplan_core.audit.logging import audit_from_principal
|
|
from govoplan_campaign.backend.db.models import (
|
|
CampaignVersion,
|
|
)
|
|
from govoplan_core.db.session import get_session
|
|
from govoplan_campaign.backend.response_security import (
|
|
public_campaign_payload,
|
|
)
|
|
from govoplan_campaign.backend.persistence.campaigns import (
|
|
CampaignPersistenceError,
|
|
build_campaign_version,
|
|
validate_campaign_version,
|
|
)
|
|
from govoplan_campaign.backend.path_security import CampaignPathSecurityError
|
|
from govoplan_campaign.backend.persistence.versions import (
|
|
LockedCampaignVersionError,
|
|
fork_campaign_version_for_edit,
|
|
is_version_final_locked,
|
|
is_user_locked_version,
|
|
get_campaign_version_for_tenant,
|
|
lock_campaign_version_temporarily,
|
|
permanently_lock_campaign_version,
|
|
publish_campaign_version,
|
|
unlock_user_locked_campaign_version,
|
|
unlock_validated_campaign_version,
|
|
update_campaign_version,
|
|
update_campaign_review_state,
|
|
validate_campaign_partial,
|
|
)
|
|
|
|
|
|
from govoplan_campaign.backend.route_support import (
|
|
_campaign_response_context,
|
|
_campaign_version_detail_response,
|
|
_get_campaign_for_principal,
|
|
_get_campaign_for_tenant,
|
|
_get_version_for_principal,
|
|
_get_version_for_tenant,
|
|
_require_mail_profile_use_if_needed,
|
|
_require_permission,
|
|
_update_campaign_version_detail_response,
|
|
_write_current_version_snapshot_if_available,
|
|
bounded_query_rows as _bounded_query_rows,
|
|
)
|
|
from govoplan_campaign.backend.routes.attachments import (
|
|
CampaignAttachmentLinkMatchesResponse,
|
|
_link_campaign_attachment_matches,
|
|
)
|
|
|
|
router = APIRouter(prefix="/campaigns", tags=["campaigns"])
|
|
|
|
|
|
@router.get("/{campaign_id}/versions", response_model=list[CampaignVersionResponse])
|
|
def list_versions(
|
|
campaign_id: str,
|
|
limit: int = Query(default=500, ge=1, le=1000),
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:read")),
|
|
):
|
|
_get_campaign_for_principal(session, campaign_id, principal)
|
|
campaign = _get_campaign_for_tenant(session, campaign_id, principal.tenant_id)
|
|
versions = _bounded_query_rows(
|
|
session.query(CampaignVersion)
|
|
.filter(CampaignVersion.campaign_id == campaign.id)
|
|
.order_by(CampaignVersion.version_number.desc()),
|
|
limit=limit,
|
|
label="Campaign version history",
|
|
)
|
|
return [
|
|
CampaignVersionResponse.model_validate(
|
|
item,
|
|
context=_campaign_response_context(principal),
|
|
)
|
|
for item in versions
|
|
]
|
|
|
|
|
|
@router.get(
|
|
"/{campaign_id}/versions/{version_id}", response_model=CampaignVersionDetailResponse
|
|
)
|
|
def get_version_detail(
|
|
campaign_id: str,
|
|
version_id: str,
|
|
response: Response,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:read")),
|
|
):
|
|
_get_campaign_for_principal(session, campaign_id, principal)
|
|
_require_permission(principal, "campaigns:recipient:read")
|
|
try:
|
|
version = get_campaign_version_for_tenant(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
campaign_id=campaign_id,
|
|
version_id=version_id,
|
|
)
|
|
result = CampaignVersionDetailResponse.model_validate(
|
|
version,
|
|
context=_campaign_response_context(principal),
|
|
)
|
|
response.headers["ETag"] = version.strong_etag
|
|
return result
|
|
except CampaignPersistenceError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
|
) from exc
|
|
|
|
|
|
@router.post(
|
|
"/{campaign_id}/versions/{version_id}/fork", response_model=CampaignCreateResponse
|
|
)
|
|
def fork_version_for_edit(
|
|
campaign_id: str,
|
|
version_id: str,
|
|
payload: CampaignVersionUpdateRequest | None = None,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:copy")),
|
|
):
|
|
"""Create the campaign's next and only editable working version.
|
|
|
|
A new working copy may be created only after the current version is
|
|
permanently user-locked or delivery-final. Validation and temporary user
|
|
locks must be removed in place instead of creating parallel drafts.
|
|
"""
|
|
_get_campaign_for_principal(session, campaign_id, principal, write=True)
|
|
_require_permission(principal, "campaigns:recipient:read")
|
|
|
|
payload = payload or CampaignVersionUpdateRequest()
|
|
source_version = _get_version_for_tenant(session, version_id, principal.tenant_id)
|
|
if source_version.campaign_id != campaign_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Campaign version not found"
|
|
)
|
|
effective_json = (
|
|
payload.campaign_json
|
|
if isinstance(payload.campaign_json, dict)
|
|
else source_version.raw_json
|
|
)
|
|
_require_mail_profile_use_if_needed(
|
|
principal,
|
|
effective_json if isinstance(effective_json, dict) else {},
|
|
)
|
|
try:
|
|
version = fork_campaign_version_for_edit(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
campaign_id=campaign_id,
|
|
version_id=version_id,
|
|
raw_json=payload.campaign_json,
|
|
current_flow=payload.current_flow or "manual",
|
|
current_step=payload.current_step,
|
|
editor_state=payload.editor_state,
|
|
source_filename=payload.source_filename,
|
|
source_base_path=payload.source_base_path,
|
|
autosave=True,
|
|
migrate_legacy_mail_settings=payload.migrate_legacy_mail_settings,
|
|
commit=False,
|
|
)
|
|
campaign = _get_campaign_for_tenant(session, campaign_id, principal.tenant_id)
|
|
audit_from_principal(
|
|
session,
|
|
principal,
|
|
action="campaign.version_forked_for_edit",
|
|
object_type="campaign_version",
|
|
object_id=version.id,
|
|
details={
|
|
"campaign_id": campaign_id,
|
|
"source_version_id": version_id,
|
|
"version_number": version.version_number,
|
|
"legacy_mail_settings_migrated": payload.migrate_legacy_mail_settings,
|
|
},
|
|
commit=True,
|
|
)
|
|
_write_current_version_snapshot_if_available(version)
|
|
return CampaignCreateResponse(
|
|
campaign=CampaignResponse.model_validate(campaign),
|
|
version=CampaignVersionResponse.model_validate(
|
|
version,
|
|
context=_campaign_response_context(principal),
|
|
),
|
|
)
|
|
except LockedCampaignVersionError as exc:
|
|
session.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT, detail=str(exc)
|
|
) from exc
|
|
except CampaignPathSecurityError as exc:
|
|
session.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)
|
|
) from exc
|
|
except CampaignPersistenceError as exc:
|
|
session.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
|
) from exc
|
|
except Exception:
|
|
session.rollback()
|
|
raise
|
|
|
|
|
|
@router.post(
|
|
"/{campaign_id}/versions/{version_id}/unlock-validation",
|
|
response_model=CampaignVersionDetailResponse,
|
|
)
|
|
def unlock_version_validation(
|
|
campaign_id: str,
|
|
version_id: str,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:update")),
|
|
):
|
|
"""Unlock a successfully validated version before delivery starts.
|
|
|
|
Unlocking invalidates validation/build state and removes generated jobs for
|
|
that version. Sent/final versions cannot be unlocked and must be copied.
|
|
"""
|
|
_get_campaign_for_principal(session, campaign_id, principal, write=True)
|
|
|
|
return _campaign_version_detail_response(
|
|
session,
|
|
principal,
|
|
campaign_id,
|
|
lambda: unlock_validated_campaign_version(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
campaign_id=campaign_id,
|
|
version_id=version_id,
|
|
commit=False,
|
|
),
|
|
audit_action="campaign.version_validation_unlocked",
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/{campaign_id}/versions/{version_id}/lock-temporarily",
|
|
response_model=CampaignVersionDetailResponse,
|
|
)
|
|
def lock_version_temporarily(
|
|
campaign_id: str,
|
|
version_id: str,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:update")),
|
|
):
|
|
_get_campaign_for_principal(session, campaign_id, principal, write=True)
|
|
return _campaign_version_detail_response(
|
|
session,
|
|
principal,
|
|
campaign_id,
|
|
lambda: lock_campaign_version_temporarily(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
campaign_id=campaign_id,
|
|
version_id=version_id,
|
|
user_id=principal.user.id,
|
|
commit=False,
|
|
),
|
|
audit_action="campaign.version_user_locked_temporarily",
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/{campaign_id}/versions/{version_id}/unlock-user-lock",
|
|
response_model=CampaignVersionDetailResponse,
|
|
)
|
|
def unlock_version_user_lock(
|
|
campaign_id: str,
|
|
version_id: str,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:update")),
|
|
):
|
|
_get_campaign_for_principal(session, campaign_id, principal, write=True)
|
|
return _campaign_version_detail_response(
|
|
session,
|
|
principal,
|
|
campaign_id,
|
|
lambda: unlock_user_locked_campaign_version(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
campaign_id=campaign_id,
|
|
version_id=version_id,
|
|
commit=False,
|
|
),
|
|
audit_action="campaign.version_user_lock_removed",
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/{campaign_id}/versions/{version_id}/lock-permanently",
|
|
response_model=CampaignVersionDetailResponse,
|
|
)
|
|
def lock_version_permanently(
|
|
campaign_id: str,
|
|
version_id: str,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:update")),
|
|
):
|
|
_get_campaign_for_principal(session, campaign_id, principal, write=True)
|
|
return _campaign_version_detail_response(
|
|
session,
|
|
principal,
|
|
campaign_id,
|
|
lambda: permanently_lock_campaign_version(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
campaign_id=campaign_id,
|
|
version_id=version_id,
|
|
user_id=principal.user.id,
|
|
commit=False,
|
|
),
|
|
audit_action="campaign.version_user_locked_permanently",
|
|
)
|
|
|
|
|
|
@router.put(
|
|
"/{campaign_id}/versions/{version_id}", response_model=CampaignVersionDetailResponse
|
|
)
|
|
def update_version_detail(
|
|
campaign_id: str,
|
|
version_id: str,
|
|
payload: CampaignVersionUpdateRequest,
|
|
response: Response,
|
|
if_match: str | None = Header(default=None, alias="If-Match"),
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:update")),
|
|
):
|
|
result = _update_campaign_version_detail_response(
|
|
session,
|
|
principal,
|
|
campaign_id,
|
|
version_id,
|
|
payload,
|
|
if_match=if_match,
|
|
autosave=False,
|
|
audit_action="campaign.version_updated",
|
|
)
|
|
response.headers["ETag"] = result.strong_etag
|
|
return result
|
|
|
|
|
|
@router.post(
|
|
"/{campaign_id}/versions/{version_id}/autosave",
|
|
response_model=CampaignVersionDetailResponse,
|
|
)
|
|
def autosave_version(
|
|
campaign_id: str,
|
|
version_id: str,
|
|
payload: CampaignVersionUpdateRequest,
|
|
response: Response,
|
|
if_match: str | None = Header(default=None, alias="If-Match"),
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:update")),
|
|
):
|
|
result = _update_campaign_version_detail_response(
|
|
session,
|
|
principal,
|
|
campaign_id,
|
|
version_id,
|
|
payload,
|
|
if_match=if_match,
|
|
autosave=True,
|
|
audit_action="campaign.version_autosaved",
|
|
)
|
|
response.headers["ETag"] = result.strong_etag
|
|
return result
|
|
|
|
|
|
@router.post(
|
|
"/{campaign_id}/versions/{version_id}/set-step",
|
|
response_model=CampaignVersionDetailResponse,
|
|
)
|
|
def set_version_step(
|
|
campaign_id: str,
|
|
version_id: str,
|
|
payload: CampaignVersionSetStepRequest,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:update")),
|
|
):
|
|
_get_campaign_for_principal(session, campaign_id, principal, write=True)
|
|
return _campaign_version_detail_response(
|
|
session,
|
|
principal,
|
|
campaign_id,
|
|
lambda: update_campaign_version(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
campaign_id=campaign_id,
|
|
version_id=version_id,
|
|
current_flow=payload.current_flow,
|
|
current_step=payload.current_step,
|
|
autosave=True,
|
|
commit=False,
|
|
),
|
|
audit_action="campaign.version_step_updated",
|
|
details={
|
|
"campaign_id": campaign_id,
|
|
"current_flow": payload.current_flow,
|
|
"current_step": payload.current_step,
|
|
},
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/{campaign_id}/versions/{version_id}/review-state",
|
|
response_model=CampaignVersionDetailResponse,
|
|
)
|
|
def set_version_review_state(
|
|
campaign_id: str,
|
|
version_id: str,
|
|
payload: CampaignReviewStateRequest,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:review")),
|
|
):
|
|
_get_campaign_for_principal(session, campaign_id, principal, write=True)
|
|
try:
|
|
version = update_campaign_review_state(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
campaign_id=campaign_id,
|
|
version_id=version_id,
|
|
inspection_complete=payload.inspection_complete,
|
|
reviewed_message_keys=payload.reviewed_message_keys,
|
|
issue_decisions=[
|
|
item.model_dump()
|
|
for item in payload.issue_decisions
|
|
],
|
|
user_id=principal.user.id,
|
|
commit=False,
|
|
)
|
|
audit_from_principal(
|
|
session,
|
|
principal,
|
|
action="campaign.message_review_updated",
|
|
object_type="campaign_version",
|
|
object_id=version.id,
|
|
details={
|
|
"campaign_id": campaign_id,
|
|
"inspection_complete": payload.inspection_complete,
|
|
"reviewed_message_count": len(payload.reviewed_message_keys),
|
|
"issue_decision_count": len(payload.issue_decisions),
|
|
},
|
|
commit=True,
|
|
)
|
|
return CampaignVersionDetailResponse.model_validate(
|
|
version,
|
|
context=_campaign_response_context(principal),
|
|
)
|
|
except LockedCampaignVersionError as exc:
|
|
session.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT, detail=str(exc)
|
|
) from exc
|
|
except CampaignPersistenceError as exc:
|
|
session.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)
|
|
) from exc
|
|
except Exception:
|
|
session.rollback()
|
|
raise
|
|
|
|
|
|
@router.post(
|
|
"/{campaign_id}/versions/{version_id}/validate-partial",
|
|
response_model=CampaignPartialValidationResponse,
|
|
)
|
|
def validate_version_partial(
|
|
campaign_id: str,
|
|
version_id: str,
|
|
payload: CampaignPartialValidationRequest | None = None,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:validate")),
|
|
):
|
|
_get_campaign_for_principal(session, campaign_id, principal, write=True)
|
|
try:
|
|
version = get_campaign_version_for_tenant(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
campaign_id=campaign_id,
|
|
version_id=version_id,
|
|
)
|
|
campaign_json = (
|
|
payload.campaign_json
|
|
if payload and payload.campaign_json is not None
|
|
else version.raw_json
|
|
)
|
|
result = validate_campaign_partial(
|
|
campaign_json, section=payload.section if payload else None
|
|
)
|
|
audit_from_principal(
|
|
session,
|
|
principal,
|
|
action="campaign.version_partially_validated",
|
|
object_type="campaign_version",
|
|
object_id=version.id,
|
|
details={
|
|
"campaign_id": campaign_id,
|
|
"section": result.get("section"),
|
|
"ok": result.get("ok"),
|
|
},
|
|
commit=True,
|
|
)
|
|
return CampaignPartialValidationResponse(**result)
|
|
except CampaignPersistenceError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
|
) from exc
|
|
|
|
|
|
@router.post(
|
|
"/{campaign_id}/versions/{version_id}/publish",
|
|
response_model=CampaignVersionDetailResponse,
|
|
)
|
|
def publish_version(
|
|
campaign_id: str,
|
|
version_id: str,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:update")),
|
|
):
|
|
_get_campaign_for_principal(session, campaign_id, principal, write=True)
|
|
return _campaign_version_detail_response(
|
|
session,
|
|
principal,
|
|
campaign_id,
|
|
lambda: publish_campaign_version(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
campaign_id=campaign_id,
|
|
version_id=version_id,
|
|
user_id=principal.user.id,
|
|
commit=False,
|
|
),
|
|
audit_action="campaign.version_user_locked_permanently",
|
|
)
|
|
|
|
|
|
@router.post("/versions/{version_id}/validate")
|
|
def validate_version(
|
|
version_id: str,
|
|
payload: ValidateCampaignRequest | None = None,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:validate")),
|
|
):
|
|
_get_version_for_principal(session, version_id, principal, write=True)
|
|
_require_permission(principal, "campaigns:recipient:read")
|
|
payload = payload or ValidateCampaignRequest()
|
|
try:
|
|
version = _get_version_for_tenant(session, version_id, principal.tenant_id)
|
|
_require_mail_profile_use_if_needed(
|
|
principal, version.raw_json if isinstance(version.raw_json, dict) else {}
|
|
)
|
|
if is_user_locked_version(version) or is_version_final_locked(version):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail="This version has a user lock or final delivery lock and cannot be validated. Remove a temporary lock or create an editable copy.",
|
|
)
|
|
link_result: CampaignAttachmentLinkMatchesResponse | None = None
|
|
if payload.check_files and payload.link_unshared_matches:
|
|
_require_permission(principal, "files:file:share")
|
|
campaign = _get_campaign_for_tenant(
|
|
session, version.campaign_id, principal.tenant_id
|
|
)
|
|
link_result = _link_campaign_attachment_matches(
|
|
session,
|
|
principal,
|
|
campaign=campaign,
|
|
version=version,
|
|
raw=version.raw_json if isinstance(version.raw_json, dict) else {},
|
|
dry_run=False,
|
|
)
|
|
audit_from_principal(
|
|
session,
|
|
principal,
|
|
action="campaign.attachment_matches_linked",
|
|
object_type="campaign_version",
|
|
object_id=version_id,
|
|
details={
|
|
"matched_file_count": link_result.matched_file_count,
|
|
"already_linked_file_count": link_result.already_linked_file_count,
|
|
"linked_file_count": link_result.linked_file_count,
|
|
"during_validation": True,
|
|
},
|
|
commit=True,
|
|
)
|
|
result = validate_campaign_version(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
version_id=version_id,
|
|
check_files=payload.check_files,
|
|
user_id=principal.user.id,
|
|
)
|
|
audit_from_principal(
|
|
session,
|
|
principal,
|
|
action="campaign.validated",
|
|
object_type="campaign_version",
|
|
object_id=version_id,
|
|
details={
|
|
"check_files": payload.check_files,
|
|
"link_unshared_matches": payload.link_unshared_matches,
|
|
"linked_file_count": link_result.linked_file_count
|
|
if link_result
|
|
else 0,
|
|
"ok": result.get("ok"),
|
|
},
|
|
commit=True,
|
|
)
|
|
return public_campaign_payload(
|
|
result,
|
|
include_diagnostics=has_scope(principal, "campaigns:diagnostic:read"),
|
|
)
|
|
except HTTPException:
|
|
raise
|
|
except CampaignPersistenceError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
|
) from exc
|
|
except Exception as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)
|
|
) from exc
|
|
|
|
|
|
@router.post("/versions/{version_id}/build")
|
|
def build_version(
|
|
version_id: str,
|
|
payload: BuildCampaignRequest | None = None,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:build")),
|
|
):
|
|
version = _get_version_for_principal(session, version_id, principal, write=True)
|
|
_require_permission(principal, "campaigns:recipient:read")
|
|
_require_mail_profile_use_if_needed(
|
|
principal, version.raw_json if isinstance(version.raw_json, dict) else {}
|
|
)
|
|
try:
|
|
result = build_campaign_version(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
version_id=version_id,
|
|
write_eml=payload.write_eml if payload else True,
|
|
user_id=principal.user.id,
|
|
)
|
|
audit_from_principal(
|
|
session,
|
|
principal,
|
|
action="campaign.messages_built",
|
|
object_type="campaign_version",
|
|
object_id=version_id,
|
|
details={
|
|
"write_eml": payload.write_eml if payload else True,
|
|
"built_count": result.get("built_count"),
|
|
},
|
|
commit=True,
|
|
)
|
|
return public_campaign_payload(
|
|
result,
|
|
include_diagnostics=has_scope(principal, "campaigns:diagnostic:read"),
|
|
)
|
|
except CampaignPersistenceError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
|
) from exc
|
|
except Exception as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)
|
|
) from exc
|