feat(campaigns): campaign start date — scheduled activation, Jira start_date
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

DB: migration b047 adds start_date (DateTime nullable) + index to campaigns.

Backend:
- Campaign model: start_date field
- CampaignCreate/Update schemas: accept start_date (ISO string)
- CRUD service: persist + serialize start_date in both serializers
- Activation endpoint: blocks manual activation if start_date is in the future
  (campaign will auto-activate via scheduler)
- Scheduler: new hourly job _run_scheduled_campaign_activation — finds draft
  campaigns with start_date <= now, activates them, creates Jira tickets,
  notifies red_tech team
- Jira: campaign + test tickets now include JIRA_START_DATE_FIELD (configurable,
  default customfield_10015). Campaign uses start_date if set, else created_at.
  Tests inherit campaign start_date.
- config.py: JIRA_START_DATE_FIELD setting

Frontend:
- Campaign type: start_date field on Campaign + CampaignSummary
- CampaignCreatePayload: start_date optional field
- Create form: date picker with min=today, warning message explaining behavior
- Campaign detail header: start_date badge showing days remaining or started date

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-06-03 16:57:06 +02:00
parent 3db9809be5
commit c62dafbc1f
10 changed files with 218 additions and 2 deletions

View File

@@ -54,6 +54,7 @@ class CampaignCreate(BaseModel):
target_platform: Optional[str] = None
tags: Optional[list[str]] = Field(default_factory=list)
scheduled_at: Optional[str] = None
start_date: Optional[str] = None # ISO date — campaign won't activate before this
class CampaignUpdate(BaseModel):
name: Optional[str] = None
@@ -62,6 +63,7 @@ class CampaignUpdate(BaseModel):
target_platform: Optional[str] = None
tags: Optional[list[str]] = None
scheduled_at: Optional[str] = None
start_date: Optional[str] = None # ISO date — can be updated while still in draft
class AddTestPayload(BaseModel):
test_id: str
@@ -125,6 +127,7 @@ def create_campaign(
target_platform=payload.target_platform,
tags=payload.tags,
scheduled_at=payload.scheduled_at,
start_date=payload.start_date,
)
campaign_id = result["id"]
log_action(
@@ -273,7 +276,27 @@ def activate_campaign(
db: Session = Depends(get_db),
current_user: User = Depends(require_any_role("red_lead", "blue_lead")),
):
"""Activate a campaign, moving it from draft to active."""
"""Activate a campaign, moving it from draft to active.
If the campaign has a start_date in the future, manual activation is blocked —
the campaign will be auto-activated by the scheduler when the date arrives.
"""
# Guard: start_date must have been reached before manual activation
campaign_obj = db.query(Campaign).filter(Campaign.id == campaign_id).first()
if campaign_obj and campaign_obj.start_date:
now = datetime.utcnow()
if campaign_obj.start_date > now:
from fastapi import HTTPException
raise HTTPException(
status_code=422,
detail=(
f"This campaign is scheduled to start on "
f"{campaign_obj.start_date.strftime('%Y-%m-%d')}. "
f"It will be activated automatically on that date. "
f"To activate it now, remove the start date first."
),
)
with UnitOfWork(db) as uow:
campaign = crud_activate(db, campaign_id)
notify_role(
@@ -314,6 +337,7 @@ def activate_campaign(
auto_create_test_issue(
db, ct.test, current_user,
parent_ticket_override=campaign_jira_key,
campaign_start_date=campaign.start_date,
)
db.commit()
except Exception: