From de0707bb4feb63e1e090a9d139dccf051532a7fb Mon Sep 17 00:00:00 2001 From: Paul de Raaij Date: Wed, 29 Jul 2026 02:11:17 -0400 Subject: [PATCH] feat: add SetTitle to ClassifiedAd with domain events, extract EnsureValidState --- domain/marketplace/classified_ad.py | 31 ++++++++- tests/test_classified_ad.py | 104 ++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/domain/marketplace/classified_ad.py b/domain/marketplace/classified_ad.py index f21a966..cc6ff5b 100644 --- a/domain/marketplace/classified_ad.py +++ b/domain/marketplace/classified_ad.py @@ -1,10 +1,17 @@ +from __future__ import annotations + from dataclasses import dataclass, field from decimal import Decimal +from domain.framework.entity import Entity from domain.marketplace.classified_ad_id import ClassifiedAdId from domain.marketplace.classified_ad_state import ClassifiedAdState from domain.marketplace.classified_ad_text import ClassifiedAdText from domain.marketplace.classified_ad_title import ClassifiedAdTitle +from domain.marketplace.domain_event import ( + ClassifiedAdTitleChanged, + DomainEvent, +) from domain.marketplace.invalid_entity_state_exception import ( InvalidEntityStateException, ) @@ -13,15 +20,16 @@ from domain.marketplace.user_id import UserId @dataclass(frozen=True) -class ClassifiedAd: +class ClassifiedAd(Entity): id: ClassifiedAdId _ownerId: UserId title: ClassifiedAdTitle text: ClassifiedAdText price: Price = field(default_factory=lambda: Price(amount=Decimal("0.00"))) state: ClassifiedAdState | None = None + _events: tuple[DomainEvent, ...] = () - def request_to_publish(self): + def _ensure_valid_state(self): errors = [] if not self.title.title: errors.append("title") @@ -35,6 +43,9 @@ class ClassifiedAd: f"Cannot publish classified ad with missing or invalid: {', '.join(errors)}" ) + def request_to_publish(self): + self._ensure_valid_state() + return ClassifiedAd( id=self.id, _ownerId=self._ownerId, @@ -42,4 +53,20 @@ class ClassifiedAd: text=self.text, price=self.price, state=ClassifiedAdState.PendingReview, + _events=self._events, + ) + + def set_title(self, title: ClassifiedAdTitle) -> ClassifiedAd: + updated = ClassifiedAd( + id=self.id, + _ownerId=self._ownerId, + title=title, + text=self.text, + price=self.price, + state=self.state, + _events=self._events, + ) + updated._ensure_valid_state() + return updated.raise_event( + ClassifiedAdTitleChanged(id=self.id.value, title=title.title) ) diff --git a/tests/test_classified_ad.py b/tests/test_classified_ad.py index c96644f..9220121 100644 --- a/tests/test_classified_ad.py +++ b/tests/test_classified_ad.py @@ -3,11 +3,16 @@ from uuid import uuid4 import pytest +from domain.framework.entity import Entity from domain.marketplace.classified_ad import ClassifiedAd from domain.marketplace.classified_ad_id import ClassifiedAdId from domain.marketplace.classified_ad_state import ClassifiedAdState from domain.marketplace.classified_ad_text import ClassifiedAdText from domain.marketplace.classified_ad_title import ClassifiedAdTitle +from domain.marketplace.domain_event import ( + ClassifiedAdSentForReview, + ClassifiedAdTitleChanged, +) from domain.marketplace.invalid_entity_state_exception import ( InvalidEntityStateException, ) @@ -219,3 +224,102 @@ def test_request_to_publish_does_not_mutate_original(): ad.request_to_publish() assert ad.state is None + + +def test_classified_ad_is_instance_of_entity(): + ad = ClassifiedAd( + id=ClassifiedAdId(value=uuid4()), + _ownerId=UserId(value=uuid4()), + title=ClassifiedAdTitle(title="Title"), + text=ClassifiedAdText(text="Text"), + price=Price(amount=Decimal("10.00")), + ) + + assert isinstance(ad, Entity) + + +def test_classified_ad_can_use_entity_raise_event(): + ad = ClassifiedAd( + id=ClassifiedAdId(value=uuid4()), + _ownerId=UserId(value=uuid4()), + title=ClassifiedAdTitle(title="Title"), + text=ClassifiedAdText(text="Text"), + price=Price(amount=Decimal("10.00")), + ) + event = ClassifiedAdSentForReview(id=ad.id.value) + + updated_ad = ad.raise_event(event) + + assert updated_ad.get_changes() == (event,) + assert ad.get_changes() == () + + +def test_set_title_updates_title(): + ad = ClassifiedAd( + id=ClassifiedAdId(value=uuid4()), + _ownerId=UserId(value=uuid4()), + title=ClassifiedAdTitle(title="Old Title"), + text=ClassifiedAdText(text="Text"), + price=Price(amount=Decimal("10.00")), + ) + new_title = ClassifiedAdTitle(title="New Title") + + updated = ad.set_title(new_title) + + assert updated.title == new_title + assert ad.title == ClassifiedAdTitle(title="Old Title") + + +def test_set_title_preserves_other_fields(): + ad_id = ClassifiedAdId(value=uuid4()) + owner_id = UserId(value=uuid4()) + text = ClassifiedAdText(text="Text") + price = Price(amount=Decimal("10.00")) + ad = ClassifiedAd( + id=ad_id, + _ownerId=owner_id, + title=ClassifiedAdTitle(title="Old Title"), + text=text, + price=price, + ) + + updated = ad.set_title(ClassifiedAdTitle(title="New Title")) + + assert updated.id == ad_id + assert updated._ownerId == owner_id + assert updated.text == text + assert updated.price == price + + +def test_set_title_fails_when_new_title_empty(): + ad = ClassifiedAd( + id=ClassifiedAdId(value=uuid4()), + _ownerId=UserId(value=uuid4()), + title=ClassifiedAdTitle(title="Title"), + text=ClassifiedAdText(text="Text"), + price=Price(amount=Decimal("10.00")), + ) + + with pytest.raises(InvalidEntityStateException) as exc_info: + ad.set_title(ClassifiedAdTitle(title="")) + + assert "title" in str(exc_info.value) + + +def test_set_title_raises_classified_ad_title_changed_event(): + ad = ClassifiedAd( + id=ClassifiedAdId(value=uuid4()), + _ownerId=UserId(value=uuid4()), + title=ClassifiedAdTitle(title="Old Title"), + text=ClassifiedAdText(text="Text"), + price=Price(amount=Decimal("10.00")), + ) + + updated = ad.set_title(ClassifiedAdTitle(title="New Title")) + + changes = updated.get_changes() + assert len(changes) == 1 + event = changes[0] + assert isinstance(event, ClassifiedAdTitleChanged) + assert event.id == ad.id.value + assert event.title == "New Title"