From a113f3687ccc601d1856719c48b6e98b91d1b7a5 Mon Sep 17 00:00:00 2001 From: kitos Date: Thu, 2 Jul 2026 16:00:18 +0200 Subject: [PATCH] feat(campaigns): add pending_approval state to campaign domain entity --- backend/app/domain/entities/campaign.py | 48 ++++++++++++++++++++++- backend/tests/test_campaign_entity.py | 52 +++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/backend/app/domain/entities/campaign.py b/backend/app/domain/entities/campaign.py index 53e25b6..f53aea7 100644 --- a/backend/app/domain/entities/campaign.py +++ b/backend/app/domain/entities/campaign.py @@ -33,6 +33,8 @@ class CampaignStatus(str, enum.Enum): # Assign draft = "draft" draft = "draft" + # Assign pending_approval = "pending_approval" + pending_approval = "pending_approval" # Assign active = "active" active = "active" # Assign completed = "completed" @@ -57,7 +59,8 @@ class CampaignType(str, enum.Enum): # Assign VALID_TRANSITIONS = { VALID_TRANSITIONS: dict[CampaignStatus, list[CampaignStatus]] = { - CampaignStatus.draft: [CampaignStatus.active], + CampaignStatus.draft: [CampaignStatus.pending_approval, CampaignStatus.active], + CampaignStatus.pending_approval: [CampaignStatus.active, CampaignStatus.draft], CampaignStatus.active: [CampaignStatus.completed], CampaignStatus.completed: [CampaignStatus.archived], CampaignStatus.archived: [], @@ -132,6 +135,49 @@ class CampaignEntity: # Assign self.status = CampaignStatus.active self.status = CampaignStatus.active + def submit_for_approval(self) -> None: + """Transition the campaign from ``draft`` to ``pending_approval``. + + Returns: + None + """ + if not self.can_transition_to(CampaignStatus.pending_approval): + raise InvalidStateTransition( + self.status.value, CampaignStatus.pending_approval.value, + [s.value for s in VALID_TRANSITIONS[self.status]], + ) + if self.test_count == 0: + raise BusinessRuleViolation( + "Campaign must have at least one test to submit for approval" + ) + self.status = CampaignStatus.pending_approval + + def approve(self) -> None: + """Transition the campaign from ``pending_approval`` to ``active``. + + Returns: + None + """ + if self.status != CampaignStatus.pending_approval: + raise InvalidStateTransition( + self.status.value, CampaignStatus.active.value, + [s.value for s in VALID_TRANSITIONS[self.status]], + ) + self.status = CampaignStatus.active + + def reject(self) -> None: + """Transition the campaign from ``pending_approval`` back to ``draft``. + + Returns: + None + """ + if not self.can_transition_to(CampaignStatus.draft): + raise InvalidStateTransition( + self.status.value, CampaignStatus.draft.value, + [s.value for s in VALID_TRANSITIONS[self.status]], + ) + self.status = CampaignStatus.draft + # Define function complete def complete(self) -> None: """Transition the campaign from ``active`` to ``completed``. diff --git a/backend/tests/test_campaign_entity.py b/backend/tests/test_campaign_entity.py index 3b5b9eb..5e23bd4 100644 --- a/backend/tests/test_campaign_entity.py +++ b/backend/tests/test_campaign_entity.py @@ -173,3 +173,55 @@ def test_from_orm_handles_none_tags(): orm.tags = None e = CampaignEntity.from_orm(orm) assert e.tags == [] + + +# ── 9. Submit for approval ──────────────────────────────────────────── + + +def test_submit_for_approval_from_draft_with_tests_success(): + e = _entity("draft", test_count=1) + e.submit_for_approval() + assert e.status == CampaignStatus.pending_approval + + +def test_submit_for_approval_from_draft_with_zero_tests_raises(): + e = _entity("draft", test_count=0) + with pytest.raises(BusinessRuleViolation, match="at least one test"): + e.submit_for_approval() + assert e.status == CampaignStatus.draft + + +def test_submit_for_approval_from_active_raises(): + e = _entity("active", test_count=1) + with pytest.raises(InvalidStateTransition): + e.submit_for_approval() + + +# ── 10. Approve ──────────────────────────────────────────────────────── + + +def test_approve_from_pending_approval_success(): + e = _entity("pending_approval", test_count=1) + e.approve() + assert e.status == CampaignStatus.active + + +def test_approve_from_draft_raises(): + e = _entity("draft", test_count=1) + with pytest.raises(InvalidStateTransition): + e.approve() + + +# ── 11. Reject ───────────────────────────────────────────────────────── + + +def test_reject_from_pending_approval_returns_to_draft(): + e = _entity("pending_approval", test_count=1) + e.reject() + assert e.status == CampaignStatus.draft + + +def test_reject_from_draft_raises(): + e = _entity("draft", test_count=1) + with pytest.raises(InvalidStateTransition): + e.reject()