Skip to content

Commit 3f4fe5c

Browse files
committed
publishing listing creates bidding
1 parent f98ad77 commit 3f4fe5c

36 files changed

+327
-118
lines changed

.idea/.gitignore

-8
This file was deleted.

.idea/inspectionProfiles/Project_Default.xml

-22
This file was deleted.

.idea/inspectionProfiles/profiles_settings.xml

-6
This file was deleted.

.idea/modules.xml

-8
This file was deleted.

.idea/vcs.xml

-6
This file was deleted.

src/api/main.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,25 @@
44
from fastapi.responses import JSONResponse
55

66
import api.routers.catalog
7-
from api.models import CurrentUser
8-
from api.routers import catalog, iam
7+
from api.models.catalog import CurrentUser
8+
from api.routers import bidding, catalog, iam
99
from config.api_config import ApiConfig
1010
from config.container import Container
1111
from seedwork.domain.exceptions import DomainException, EntityNotFoundException
1212
from seedwork.infrastructure.logging import LoggerFactory, logger
1313
from seedwork.infrastructure.request_context import request_context
1414

1515
# configure logger prior to first usage
16-
LoggerFactory.configure(logger_name="cli")
16+
LoggerFactory.configure(logger_name="api")
1717

1818
# dependency injection container
1919
container = Container()
2020
container.config.from_pydantic(ApiConfig())
21-
container.wire(modules=[api.routers.catalog, api.routers.iam])
21+
container.wire(modules=[api.routers.catalog, api.routers.bidding, api.routers.iam])
2222

2323
app = FastAPI(debug=container.config.DEBUG)
2424
app.include_router(catalog.router)
25+
app.include_router(bidding.router)
2526
app.include_router(iam.router)
2627
app.container = container
2728

src/api/models/__init__.py

Whitespace-only changes.

src/api/models/bidding.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from datetime import datetime
2+
from uuid import UUID
3+
4+
from pydantic import BaseModel
5+
6+
7+
class BidReadModel(BaseModel):
8+
amount: float
9+
currency: str
10+
bidder_id: UUID
11+
bidder_username: str
12+
13+
14+
class BiddingReadModel(BaseModel):
15+
listing_id: UUID
16+
auction_status: str = "active" # active, ended
17+
auction_end_date: datetime
18+
bids: list[BidReadModel]

src/api/models.py renamed to src/api/models/catalog.py

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ class ListingWriteModel(BaseModel):
1919
ask_price_currency: str = "USD"
2020

2121

22+
class ListingPublishModel(BaseModel):
23+
id: UUID
24+
25+
2226
class ListingReadModel(BaseModel):
2327
id: UUID
2428
title: str = ""

src/api/models/common.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from uuid import UUID, uuid4
2+
3+
from pydantic import BaseModel
4+
5+
6+
class CurrentUser(BaseModel):
7+
id: UUID
8+
username: str
9+
10+
@classmethod
11+
def fake_user(cls):
12+
return CurrentUser(id=uuid4(), username="fake_user")

src/api/routers/bidding.py

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from fastapi import APIRouter
2+
3+
from api.models.bidding import BiddingReadModel
4+
from api.shared import dependency
5+
from config.container import Container, inject
6+
from modules.bidding.application.query.get_bidding_details import GetBiddingDetails
7+
from seedwork.application import Application
8+
9+
router = APIRouter()
10+
11+
"""
12+
Inspired by https://developer.ebay.com/api-docs/buy/offer/types/api:Bidding
13+
"""
14+
15+
16+
@router.get("/bidding/{listing_id}", tags=["bidding"], response_model=BiddingReadModel)
17+
@inject
18+
async def get_bidding_details_of_listing(
19+
listing_id,
20+
app: Application = dependency(Container.application),
21+
):
22+
"""
23+
Shows listing details
24+
"""
25+
query = GetBiddingDetails(listing_id=listing_id)
26+
query_result = app.execute_query(query)
27+
payload = query_result.payload
28+
return BiddingReadModel(
29+
listing_id=str(payload.id),
30+
auction_end_date=payload.ends_at,
31+
bids=payload.bids,
32+
)

src/api/routers/catalog.py

+31-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
from fastapi import APIRouter
22

3-
from api.models import ListingIndexModel, ListingReadModel, ListingWriteModel
3+
from api.models.catalog import ListingIndexModel, ListingReadModel, ListingWriteModel
44
from api.shared import dependency
55
from config.container import Container, inject
66
from modules.catalog.application.command import (
77
CreateListingDraftCommand,
88
DeleteListingDraftCommand,
9+
PublishListingDraftCommand,
910
)
1011
from modules.catalog.application.query.get_all_listings import GetAllListings
1112
from modules.catalog.application.query.get_listing_details import GetListingDetails
1213
from seedwork.application import Application
1314
from seedwork.domain.value_objects import Money
1415
from seedwork.infrastructure.request_context import request_context
1516

17+
"""
18+
Inspired by https://developer.ebay.com/api-docs/sell/inventory/resources/offer/methods/createOffer
19+
"""
20+
1621
router = APIRouter()
1722

1823

@@ -64,7 +69,7 @@ async def create_listing(
6469

6570
query = GetListingDetails(listing_id=command_result.payload)
6671
query_result = app.execute_query(query)
67-
return dict(data=query_result.payload)
72+
return dict(query_result.payload)
6873

6974

7075
@router.delete(
@@ -82,3 +87,27 @@ async def delete_listing(
8287
listing_id=listing_id,
8388
)
8489
app.execute_command(command)
90+
91+
92+
@router.post(
93+
"/catalog/{listing_id}/publish",
94+
tags=["catalog"],
95+
status_code=200,
96+
response_model=ListingReadModel,
97+
)
98+
@inject
99+
async def publish_listing(
100+
listing_id,
101+
app: Application = dependency(Container.application),
102+
):
103+
"""
104+
Creates a new listing.
105+
"""
106+
command = PublishListingDraftCommand(
107+
listing_id=listing_id,
108+
)
109+
command_result = app.execute_command(command)
110+
111+
query = GetListingDetails(listing_id=command_result.entity_id)
112+
query_result = app.execute_query(query)
113+
return dict(query_result.payload)

src/api/tests/test_catalog.py

+46-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import pytest
22

3-
from modules.catalog.application.command import CreateListingDraftCommand
3+
from modules.catalog.application.command import (
4+
CreateListingDraftCommand,
5+
PublishListingDraftCommand,
6+
)
47
from seedwork.domain.value_objects import UUID, Money
58

69

@@ -107,3 +110,45 @@ def test_catalog_delete_non_existing_draft_returns_404(api, api_client):
107110
listing_id = UUID("00000000000000000000000000000001")
108111
response = api_client.delete(f"/catalog/{listing_id}")
109112
assert response.status_code == 404
113+
114+
115+
@pytest.mark.integration
116+
def test_catalog_publish_listing_draft(api, api_client):
117+
# arrange
118+
app = api.container.application()
119+
command_result = app.execute_command(
120+
CreateListingDraftCommand(
121+
title="Foo to be deleted",
122+
description="Bar",
123+
ask_price=Money(10),
124+
seller_id=UUID("00000000000000000000000000000002"),
125+
)
126+
)
127+
128+
# act
129+
response = api_client.post(f"/catalog/{command_result.entity_id}/publish")
130+
131+
# assert that the listing was published
132+
assert response.status_code == 200
133+
134+
135+
def test_published_listing_appears_in_biddings(api, api_client):
136+
# arrange
137+
app = api.container.application()
138+
command_result = app.execute_command(
139+
CreateListingDraftCommand(
140+
title="Foo to be deleted",
141+
description="Bar",
142+
ask_price=Money(10),
143+
seller_id=UUID("00000000000000000000000000000002"),
144+
)
145+
)
146+
command_result = app.execute_command(
147+
PublishListingDraftCommand(
148+
listing_id=command_result.entity_id,
149+
)
150+
)
151+
152+
url = f"/bidding/{command_result.entity_id}"
153+
response = api_client.get(url)
154+
assert response.status_code == 200

src/config/container.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from dependency_injector.wiring import inject # noqa
33
from sqlalchemy import create_engine
44

5+
from modules.bidding import BiddingModule
56
from modules.catalog import CatalogModule
67
from modules.iam import IamModule
78
from seedwork.application import Application
@@ -48,15 +49,17 @@ def create_engine_once(config):
4849
return engine
4950

5051

51-
def create_app(name, version, config, engine, catalog_module, outbox) -> Application:
52+
def create_app(
53+
name, version, config, engine, catalog_module, bidding_module, outbox
54+
) -> Application:
5255
app = Application(
5356
name=name,
5457
version=version,
5558
config=config,
5659
engine=engine,
5760
outbox=outbox,
5861
)
59-
app.add_module("catalog", catalog_module)
62+
app.add_modules(catalog=catalog_module, bidding=bidding_module)
6063
return app
6164

6265

@@ -76,6 +79,10 @@ class Container(containers.DeclarativeContainer):
7679
CatalogModule,
7780
)
7881

82+
bidding_module = providers.Factory(
83+
BiddingModule,
84+
)
85+
7986
iam_module = providers.Factory(
8087
IamModule,
8188
)
@@ -87,5 +94,6 @@ class Container(containers.DeclarativeContainer):
8794
config=config,
8895
engine=engine,
8996
catalog_module=catalog_module,
97+
bidding_module=bidding_module,
9098
outbox=outbox,
9199
)

src/modules/bidding/__init__.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from modules.bidding.application.command import PlaceBidCommand, RetractBidCommand
2-
from modules.bidding.application.query import GetPastdueListingsQuery
2+
from modules.bidding.application.event import when_listing_is_published_start_auction
3+
from modules.bidding.application.query import GetBiddingDetails, GetPastdueListings
34
from modules.bidding.infrastructure.listing_repository import (
45
PostgresJsonListingRepository,
56
)
@@ -8,8 +9,8 @@
89

910
class BiddingModule(BusinessModule):
1011
supported_commands = (PlaceBidCommand, RetractBidCommand)
11-
supported_queries = (GetPastdueListingsQuery,)
12-
supported_events = ()
12+
supported_queries = (GetPastdueListings, GetBiddingDetails)
13+
event_handlers = (when_listing_is_published_start_auction,)
1314

1415
def configure_unit_of_work(self, uow):
1516
"""Here we have a chance to add extra UOW attributes to be injected into command/query handlers"""
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from datetime import datetime, timedelta
2+
3+
from modules.bidding.domain.entities import Listing
4+
from modules.bidding.domain.value_objects import Seller
5+
from modules.catalog.domain.events import ListingPublishedEvent
6+
from seedwork.application.decorators import domain_event_handler
7+
8+
9+
@domain_event_handler
10+
def when_listing_is_published_start_auction(
11+
event: ListingPublishedEvent, listing_repository
12+
):
13+
listing = Listing(
14+
id=event.listing_id,
15+
seller=Seller(id=event.seller_id),
16+
initial_price=event.ask_price,
17+
ends_at=datetime.now() + timedelta(days=7),
18+
)
19+
listing_repository.add(listing)
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
from .get_pastdue_listings import GetPastdueListingsQuery
1+
from .get_bidding_details import GetBiddingDetails
2+
from .get_pastdue_listings import GetPastdueListings

0 commit comments

Comments
 (0)