feat: add campaign copying scheduling and residual handling
This commit is contained in:
@@ -52,6 +52,126 @@ class CampaignCopyRequest(CampaignLifecycleMutationRequest):
|
||||
source_version_id: str = Field(min_length=1, max_length=36)
|
||||
external_id: str | None = Field(default=None, min_length=1, max_length=255)
|
||||
name: str | None = Field(default=None, min_length=1, max_length=255)
|
||||
include_recipients: bool = True
|
||||
include_files: bool = True
|
||||
include_shares: bool = False
|
||||
include_policies: bool = True
|
||||
include_mail_profile: bool = True
|
||||
|
||||
|
||||
class CampaignScheduleCreateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
source_version_id: str = Field(min_length=1, max_length=36)
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
recurrence_kind: Literal["once", "daily", "weekly", "monthly"] = "once"
|
||||
interval_count: int = Field(default=1, ge=1, le=365)
|
||||
timezone: str = Field(default="UTC", min_length=1, max_length=100)
|
||||
starts_at: datetime
|
||||
ends_at: datetime | None = None
|
||||
max_occurrences: int = Field(default=1, ge=1, le=1000)
|
||||
include_recipients: bool = True
|
||||
include_files: bool = True
|
||||
include_shares: bool = False
|
||||
include_policies: bool = True
|
||||
include_mail_profile: bool = True
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_schedule(self) -> "CampaignScheduleCreateRequest":
|
||||
if self.starts_at.tzinfo is None:
|
||||
raise ValueError("Campaign schedule start must include a timezone.")
|
||||
if self.ends_at is not None:
|
||||
if self.ends_at.tzinfo is None:
|
||||
raise ValueError("Campaign schedule end must include a timezone.")
|
||||
if self.ends_at <= self.starts_at:
|
||||
raise ValueError("Campaign schedule end must be after its start.")
|
||||
if self.recurrence_kind == "once":
|
||||
self.max_occurrences = 1
|
||||
self.interval_count = 1
|
||||
elif self.max_occurrences < 2:
|
||||
raise ValueError("A recurring campaign schedule needs at least two occurrences.")
|
||||
return self
|
||||
|
||||
|
||||
class CampaignScheduleStateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
active: bool
|
||||
base_revision: int = Field(ge=1)
|
||||
|
||||
|
||||
class CampaignScheduleOccurrenceResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
schedule_id: str
|
||||
scheduled_for: datetime
|
||||
status: str
|
||||
generated_campaign_id: str | None = None
|
||||
generated_version_id: str | None = None
|
||||
error: str | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class CampaignScheduleResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
campaign_id: str
|
||||
source_version_id: str
|
||||
name: str
|
||||
recurrence_kind: str
|
||||
interval_count: int
|
||||
timezone: str
|
||||
starts_at: datetime
|
||||
next_fire_at: datetime | None = None
|
||||
ends_at: datetime | None = None
|
||||
max_occurrences: int
|
||||
occurrence_count: int
|
||||
active: bool
|
||||
resource_revision: int
|
||||
last_fired_at: datetime | None = None
|
||||
last_campaign_id: str | None = None
|
||||
last_error: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
occurrences: list[CampaignScheduleOccurrenceResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class CampaignScheduleListResponse(BaseModel):
|
||||
items: list[CampaignScheduleResponse]
|
||||
|
||||
|
||||
class CampaignContentLibrarySaveRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str = Field(min_length=1, max_length=300)
|
||||
description: str | None = Field(default=None, max_length=4000)
|
||||
kind: Literal["fragment", "campaign_part"]
|
||||
target: Literal["subject", "text", "html"] | None = None
|
||||
subject: str | None = Field(default=None, max_length=1000)
|
||||
text: str | None = Field(default=None, max_length=1_000_000)
|
||||
html: str | None = Field(default=None, max_length=2_000_000)
|
||||
body_mode: Literal["text", "html", "both"] = "both"
|
||||
locale: str = Field(default="de", min_length=2, max_length=35)
|
||||
visibility: Literal["personal", "tenant"] = "personal"
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_content(self) -> "CampaignContentLibrarySaveRequest":
|
||||
if self.kind == "fragment" and self.target is None:
|
||||
raise ValueError("A content fragment requires a target field.")
|
||||
values = {
|
||||
"subject": self.subject,
|
||||
"text": self.text,
|
||||
"html": self.html,
|
||||
}
|
||||
if self.kind == "fragment":
|
||||
selected = values[self.target or "text"]
|
||||
if not selected or not selected.strip():
|
||||
raise ValueError("The selected fragment field is empty.")
|
||||
elif not any(value and value.strip() for value in (self.text, self.html)):
|
||||
raise ValueError("A campaign part requires text or HTML body content.")
|
||||
return self
|
||||
|
||||
|
||||
class CampaignCreateMinimalRequest(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user