Skip to content

Commit 79f9558

Browse files
committed
collect domain event from aggregate roots when command handler is executed
1 parent 204ee71 commit 79f9558

25 files changed

+178
-150
lines changed

poetry.lock

+15-40
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ click = "8.0.4"
2828
httpx = "^0.23.1"
2929
requests = "^2.28.1"
3030
bcrypt = "^4.0.1"
31+
mypy = "^1.4.1"
3132

3233
[tool.poetry.dev-dependencies]
3334
poethepoet = "^0.10.0"
3435
pytest-cov = "^2.12.1"
35-
mypy = "^0.910"
3636
vulture = "^2.7"
3737

3838
[build-system]

src/modules/bidding/application/command/place_bid.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ def place_bid(
2121
command: PlaceBidCommand, listing_repository: ListingRepository
2222
) -> CommandResult:
2323
bidder = Bidder(id=command.bidder_id)
24-
bid = Bid(bidder=bidder, max_price=Money(command.amount))
24+
bid = Bid(bidder=bidder, max_price=Money(amount=command.amount))
2525

2626
listing = listing_repository.get_by_id(command.listing_id)
2727
listing.place_bid(bid)
28-
29-
return CommandResult.success()

src/modules/bidding/application/command/retract_bid.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,4 @@ def retract_bid(
1919
bidder = Bidder(id=command.bidder_id)
2020

2121
listing: Listing = listing_repository.get_by_id(id=command.listing_id)
22-
event = listing.retract_bid_of(bidder)
23-
24-
return CommandResult.success(event=event)
22+
listing.retract_bid_of(bidder)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .notify_outbid_winner import notify_outbid_winner
2+
from .when_listing_is_published_start_auction import (
3+
when_listing_is_published_start_auction,
4+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from modules.bidding.application import bidding_module
2+
from modules.bidding.domain.events import BidWasPlaced
3+
from seedwork.infrastructure.logging import logger
4+
5+
6+
@bidding_module.domain_event_handler
7+
def notify_outbid_winner(event: BidWasPlaced):
8+
logger.info(f"Message from a handler: Listing {event.listing_id} was published")

src/modules/bidding/application/event.py renamed to src/modules/bidding/application/event/when_listing_is_published_start_auction.py

-1
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,3 @@ def when_listing_is_published_start_auction(
2020
ends_at=datetime.now() + timedelta(days=7),
2121
)
2222
listing_repository.add(listing)
23-
return EventResult.success()

src/modules/bidding/domain/entities.py

+39-23
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22
from datetime import datetime, timedelta
33
from typing import Optional
44

5+
from modules.bidding.domain.events import (
6+
BidWasPlaced,
7+
BidWasRetracted,
8+
HighestBidderWasOutbid,
9+
ListingWasCancelled,
10+
)
511
from modules.bidding.domain.rules import (
612
BidCanBeRetracted,
713
ListingCanBeCancelled,
8-
PlacedBidMustBeGreaterOrEqualThanNextMinimumBid,
14+
PriceOfPlacedBidMustBeGreaterOrEqualThanNextMinimumPrice,
915
)
1016
from modules.bidding.domain.value_objects import Bid, Bidder, Seller
1117
from seedwork.domain.entities import AggregateRoot
@@ -26,18 +32,6 @@ class ListingCannotBeCancelled(DomainException):
2632
...
2733

2834

29-
class BidPlacedEvent(DomainEvent):
30-
...
31-
32-
33-
class BidRetractedEvent(DomainEvent):
34-
...
35-
36-
37-
class ListingCancelledEvent(DomainEvent):
38-
...
39-
40-
4135
@dataclass(kw_only=True)
4236
class Listing(AggregateRoot[GenericUUID]):
4337
seller: Seller
@@ -61,34 +55,57 @@ def next_minimum_price(self) -> Money:
6155
return self.current_price + Money(amount=1, currency=self.ask_price.currency)
6256

6357
# public commands
64-
def place_bid(self, bid: Bid) -> DomainEvent:
58+
def place_bid(self, bid: Bid):
6559
"""Public method"""
6660
self.check_rule(
67-
PlacedBidMustBeGreaterOrEqualThanNextMinimumBid(
61+
PriceOfPlacedBidMustBeGreaterOrEqualThanNextMinimumPrice(
6862
current_price=bid.max_price, next_minimum_price=self.next_minimum_price
6963
)
7064
)
7165

66+
previous_winner_id = self.highest_bid.bidder.id if self.highest_bid else None
67+
current_winner_id = bid.bidder.id
68+
7269
if self.has_bid_placed_by(bidder=bid.bidder):
7370
self._update_bid(bid)
7471
else:
7572
self._add_bid(bid)
7673

77-
return BidPlacedEvent(
78-
listing_id=self.id, bidder=bid.bidder, max_price=bid.max_price
74+
self.register_event(
75+
BidWasPlaced(
76+
listing_id=self.id,
77+
bidder_id=bid.bidder.id,
78+
)
7979
)
8080

81-
def retract_bid_of(self, bidder: Bidder) -> DomainEvent:
81+
# if there was previous winner...
82+
if previous_winner_id is not None and previous_winner_id != current_winner_id:
83+
self.register_event(
84+
HighestBidderWasOutbid(
85+
listing_id=self.id,
86+
outbid_bidder_id=previous_winner_id,
87+
)
88+
)
89+
90+
def retract_bid_of(self, bidder: Bidder):
8291
"""Public method"""
8392
bid = self.get_bid_of(bidder)
8493
self.check_rule(
8594
BidCanBeRetracted(listing_ends_at=self.ends_at, bid_placed_at=bid.placed_at)
8695
)
8796

8897
self._remove_bid_of(bidder=bidder)
89-
return BidRetractedEvent(listing_id=self.id, bidder_id=bidder.id)
98+
self.register_event(
99+
BidWasRetracted(
100+
listing_id=self.id,
101+
retracting_bidder_id=bidder.id,
102+
winning_bidder_id=self.highest_bid.bidder.id
103+
if self.highest_bid
104+
else None,
105+
)
106+
)
90107

91-
def cancel(self) -> DomainEvent:
108+
def cancel(self):
92109
"""
93110
Seller can cancel a listing (end a listing early). Listing must be eligible to cancelled,
94111
depending on time left and if bids have been placed.
@@ -100,14 +117,13 @@ def cancel(self) -> DomainEvent:
100117
)
101118
)
102119
self.ends_at = datetime.utcnow()
103-
return ListingCancelledEvent(listing_id=self.id)
120+
self.register_event(ListingWasCancelled(listing_id=self.id))
104121

105122
def end(self) -> DomainEvent:
106123
"""
107124
Ends listing.
108125
"""
109126
raise NotImplementedError()
110-
return []
111127

112128
# public queries
113129
def get_bid_of(self, bidder: Bidder) -> Bid:
@@ -126,7 +142,7 @@ def has_bid_placed_by(self, bidder: Bidder) -> bool:
126142
return True
127143

128144
@property
129-
def winning_bid(self) -> Optional[Bid]:
145+
def highest_bid(self) -> Optional[Bid]:
130146
try:
131147
highest_bid = max(self.bids, key=lambda bid: bid.max_price)
132148
except ValueError:

src/modules/bidding/domain/events.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,20 @@
1-
class PlacedBidIsGreaterThanCurrentWinningBid:
1+
from seedwork.domain.events import DomainEvent
2+
from seedwork.domain.value_objects import GenericUUID
3+
4+
5+
class BidWasPlaced(DomainEvent):
6+
listing_id: GenericUUID
7+
bidder_id: GenericUUID
8+
9+
10+
class HighestBidderWasOutbid(DomainEvent):
11+
listing_id: GenericUUID
12+
outbid_bidder_id: GenericUUID
13+
14+
15+
class BidWasRetracted(DomainEvent):
16+
...
17+
18+
19+
class ListingWasCancelled(DomainEvent):
220
...

src/modules/bidding/domain/rules.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from seedwork.domain.value_objects import Money
77

88

9-
class PlacedBidMustBeGreaterOrEqualThanNextMinimumBid(BusinessRule):
9+
class PriceOfPlacedBidMustBeGreaterOrEqualThanNextMinimumPrice(BusinessRule):
1010
__message = "Placed bid must be greater or equal than {next_minimum_price}"
1111

1212
current_price: Money

src/modules/bidding/tests/domain/test_bidding.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def test_listing_initial_price():
1818
starts_at=datetime.utcnow(),
1919
ends_at=datetime.utcnow(),
2020
)
21-
assert listing.winning_bid is None
21+
assert listing.highest_bid is None
2222

2323

2424
@pytest.mark.unit
@@ -35,7 +35,7 @@ def test_place_one_bid():
3535
ends_at=datetime.utcnow(),
3636
)
3737
listing.place_bid(bid)
38-
assert listing.winning_bid == Bid(Money(20), bidder=bidder, placed_at=now)
38+
assert listing.highest_bid == Bid(Money(20), bidder=bidder, placed_at=now)
3939
assert listing.current_price == Money(10)
4040

4141

@@ -64,7 +64,7 @@ def test_place_two_bids_second_buyer_outbids():
6464
listing.place_bid(Bid(bidder=bidder2, max_price=Money(30), placed_at=now))
6565
assert listing.current_price == Money(15)
6666
assert listing.next_minimum_price == Money(16)
67-
assert listing.winning_bid == Bid(Money(30), bidder=bidder2, placed_at=now)
67+
assert listing.highest_bid == Bid(Money(30), bidder=bidder2, placed_at=now)
6868

6969

7070
@pytest.mark.unit
@@ -90,7 +90,7 @@ def test_place_two_bids_second_buyer_fails_to_outbid():
9090
listing.place_bid(Bid(bidder=bidder2, max_price=Money(20), placed_at=now))
9191

9292
# ...but he fails. bidder1 is still a winner, but current price changes
93-
assert listing.winning_bid == Bid(Money(30), bidder=bidder1, placed_at=now)
93+
assert listing.highest_bid == Bid(Money(30), bidder=bidder1, placed_at=now)
9494
assert listing.current_price == Money(20)
9595

9696

@@ -109,7 +109,7 @@ def test_place_two_bids_second_buyer_fails_to_outbid_with_same_amount():
109109
)
110110
listing.place_bid(Bid(bidder=bidder1, max_price=Money(30), placed_at=now))
111111
listing.place_bid(Bid(bidder=bidder2, max_price=Money(30), placed_at=now))
112-
assert listing.winning_bid == Bid(Money(30), bidder=bidder1, placed_at=now)
112+
assert listing.highest_bid == Bid(Money(30), bidder=bidder1, placed_at=now)
113113
assert listing.current_price == Money(30)
114114

115115

@@ -129,7 +129,7 @@ def test_place_two_bids_by_same_bidder():
129129
listing.place_bid(Bid(max_price=Money(30), bidder=bidder, placed_at=now))
130130

131131
assert len(listing.bids) == 1
132-
assert listing.winning_bid == Bid(max_price=Money(30), bidder=bidder, placed_at=now)
132+
assert listing.highest_bid == Bid(max_price=Money(30), bidder=bidder, placed_at=now)
133133
assert listing.current_price == Money(10)
134134

135135

@@ -151,7 +151,7 @@ def test_cannot_place_bid_if_listing_ended():
151151
)
152152
with pytest.raises(
153153
BusinessRuleValidationException,
154-
match="PlacedBidMustBeGreaterOrEqualThanNextMinimumBid",
154+
match="PriceOfPlacedBidMustBeGreaterOrEqualThanNextMinimumPrice",
155155
):
156156
listing.place_bid(bid)
157157

src/modules/catalog/application/command/create_listing_draft.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,4 @@ def create_listing_draft(
3232
seller_id=command.seller_id,
3333
)
3434
repository.add(listing)
35-
return CommandResult.success(
36-
entity_id=listing.id, events=[ListingDraftCreatedEvent(listing_id=listing.id)]
37-
)
35+
return CommandResult.success(event=ListingDraftCreatedEvent(listing_id=listing.id))

src/modules/catalog/application/command/delete_listing_draft.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,4 @@ def delete_listing_draft(
3434
)
3535
check_rule(PublishedListingMustNotBeDeleted(status=listing.status))
3636
repository.remove(listing)
37-
return CommandResult.success(
38-
entity_id=listing.id, events=[ListingDraftDeletedEvent(listing_id=listing.id)]
39-
)
37+
return CommandResult.success(event=ListingDraftDeletedEvent(listing_id=listing.id))

src/modules/catalog/application/command/publish_listing_draft.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from modules.catalog.domain.repositories import ListingRepository
66
from modules.catalog.domain.rules import OnlyListingOwnerCanPublishListing
77
from modules.catalog.domain.value_objects import ListingId, SellerId
8-
from seedwork.application.command_handlers import CommandResult
98
from seedwork.application.commands import Command
109
from seedwork.domain.mixins import check_rule
1110

@@ -30,9 +29,4 @@ def publish_listing_draft(
3029
listing_seller_id=listing.seller_id, current_seller_id=command.seller_id
3130
)
3231
)
33-
34-
events = listing.publish()
35-
36-
listing_repository.persist_all()
37-
38-
return CommandResult.success(entity_id=listing.id, events=events)
32+
listing.publish()

0 commit comments

Comments
 (0)