Skip to content

Commit 976f476

Browse files
committed
add catalog listing delete endpoint
1 parent 86bef33 commit 976f476

File tree

14 files changed

+421
-32
lines changed

14 files changed

+421
-32
lines changed

src/api/main.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import time
22

33
from fastapi import FastAPI, Request
4+
from fastapi.responses import JSONResponse
45

56
import api.routers.catalog
67
from api.models import CurrentUser
78
from api.routers import catalog, iam
89
from config.api_config import ApiConfig
910
from config.container import Container
11+
from seedwork.domain.exceptions import DomainException, EntityNotFoundException
1012
from seedwork.infrastructure.logging import LoggerFactory, logger
1113
from seedwork.infrastructure.request_context import request_context
1214

@@ -26,6 +28,24 @@
2628
logger.info("using db engine %s" % str(container.engine()))
2729

2830

31+
@app.exception_handler(DomainException)
32+
async def unicorn_exception_handler(request: Request, exc: DomainException):
33+
return JSONResponse(
34+
status_code=500,
35+
content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
36+
)
37+
38+
39+
@app.exception_handler(EntityNotFoundException)
40+
async def unicorn_exception_handler(request: Request, exc: EntityNotFoundException):
41+
return JSONResponse(
42+
status_code=404,
43+
content={
44+
"message": f"Entity {exc.entity_id} not found in {exc.repository.__class__.__name__}"
45+
},
46+
)
47+
48+
2949
@app.middleware("http")
3050
async def add_request_context(request: Request, call_next):
3151
start_time = time.time()

src/api/routers/catalog.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
from api.shared import dependency
55
from config.container import Container, inject
66
from modules.catalog import CatalogModule
7-
from modules.catalog.application.command.create_listing_draft import (
7+
from modules.catalog.application.command import (
88
CreateListingDraftCommand,
9+
DeleteListingDraftCommand,
910
)
1011
from modules.catalog.application.query.get_all_listings import GetAllListings
1112
from modules.catalog.application.query.get_listing_details import GetListingDetails
@@ -69,7 +70,19 @@ async def create_listing(
6970
return query_result.payload
7071

7172

72-
#
73-
# # TODO: for now we return just the id, but in the future we should return
74-
# # a representation of a newly created listing resource
75-
# return {"id": result.id}
73+
@router.delete(
74+
"/catalog/{listing_id}", tags=["catalog"], status_code=204, response_model=None
75+
)
76+
@inject
77+
async def delete_listing(
78+
listing_id,
79+
module: CatalogModule = dependency(Container.catalog_module),
80+
):
81+
"""
82+
Delete listing
83+
"""
84+
command = DeleteListingDraftCommand(
85+
listing_id=listing_id,
86+
)
87+
with module.unit_of_work():
88+
module.execute_command(command)

src/api/tests/test_catalog.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22

33
from modules.catalog.application.command import CreateListingDraftCommand
4-
from seedwork.domain.value_objects import Money
4+
from seedwork.domain.value_objects import UUID, Money
55

66

77
@pytest.mark.integration
@@ -18,7 +18,10 @@ def test_catalog_list_with_one_item(api, api_client):
1818
with catalog_module.unit_of_work():
1919
command_result = catalog_module.execute_command(
2020
CreateListingDraftCommand(
21-
title="Foo", description="Bar", ask_price=Money(10), seller_id="abcd"
21+
title="Foo",
22+
description="Bar",
23+
ask_price=Money(10),
24+
seller_id=UUID("00000000000000000000000000000002"),
2225
)
2326
)
2427

@@ -49,12 +52,18 @@ def test_catalog_list_with_two_items(api, api_client):
4952
with catalog_module.unit_of_work():
5053
catalog_module.execute_command(
5154
CreateListingDraftCommand(
52-
title="Foo #1", description="Bar", ask_price=Money(10), seller_id="abcd"
55+
title="Foo #1",
56+
description="Bar",
57+
ask_price=Money(10),
58+
seller_id=UUID("00000000000000000000000000000002"),
5359
)
5460
)
5561
catalog_module.execute_command(
5662
CreateListingDraftCommand(
57-
title="Foo #2", description="Bar", ask_price=Money(10), seller_id="abcd"
63+
title="Foo #2",
64+
description="Bar",
65+
ask_price=Money(10),
66+
seller_id=UUID("00000000000000000000000000000002"),
5867
)
5968
)
6069

@@ -65,3 +74,28 @@ def test_catalog_list_with_two_items(api, api_client):
6574
assert response.status_code == 200
6675
response_data = response.json()["data"]
6776
assert len(response_data) == 2
77+
78+
79+
@pytest.mark.integration
80+
def test_catalog_delete_draft(api, api_client):
81+
catalog_module = api.container.catalog_module()
82+
with catalog_module.unit_of_work():
83+
result = catalog_module.execute_command(
84+
CreateListingDraftCommand(
85+
title="Foo to be deleted",
86+
description="Bar",
87+
ask_price=Money(10),
88+
seller_id=UUID("00000000000000000000000000000002"),
89+
)
90+
)
91+
92+
response = api_client.delete(f"/catalog/{result.entity_id}")
93+
94+
assert response.status_code == 204
95+
96+
97+
@pytest.mark.integration
98+
def test_catalog_delete_non_existing_draft_returns_404(api, api_client):
99+
listing_id = UUID("00000000000000000000000000000001")
100+
response = api_client.delete(f"/catalog/{listing_id}")
101+
assert response.status_code == 404

src/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def engine():
1616
with engine.begin() as connection:
1717
Base.metadata.drop_all(connection)
1818
Base.metadata.create_all(connection)
19+
print("---- engine ready ----")
1920
return engine
2021

2122

src/modules/catalog/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from modules.catalog.application.command import (
22
CreateListingDraftCommand,
3+
DeleteListingDraftCommand,
34
PublishListingCommand,
45
UpdateListingDraftCommand,
56
)
@@ -19,6 +20,7 @@ class CatalogModule(BusinessModule):
1920
supported_commands = (
2021
CreateListingDraftCommand,
2122
UpdateListingDraftCommand,
23+
DeleteListingDraftCommand,
2224
PublishListingCommand,
2325
)
2426
supported_queries = (GetAllListings, GetListingDetails, GetListingsOfSeller)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .create_listing_draft import CreateListingDraftCommand
2+
from .delete_listing_draft import DeleteListingDraftCommand
23
from .publish_listing import PublishListingCommand
34
from .update_listing_draft import UpdateListingDraftCommand
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from dataclasses import dataclass
2+
3+
from modules.catalog.domain.entities import Listing
4+
from modules.catalog.domain.events import ListingDraftDeletedEvent
5+
from modules.catalog.domain.repositories import ListingRepository
6+
from seedwork.application.command_handlers import CommandResult
7+
from seedwork.application.commands import Command
8+
from seedwork.application.decorators import command_handler
9+
from seedwork.domain.value_objects import UUID
10+
11+
12+
@dataclass
13+
class DeleteListingDraftCommand(Command):
14+
"""A command for updating a listing"""
15+
16+
listing_id: UUID
17+
18+
19+
@command_handler
20+
def delete_listing_draft(
21+
command: DeleteListingDraftCommand, repository: ListingRepository
22+
) -> CommandResult:
23+
listing: Listing = repository.get_by_id(command.listing_id)
24+
repository.remove(listing)
25+
return CommandResult.success(
26+
entity_id=listing.id, events=[ListingDraftDeletedEvent(listing_id=listing.id)]
27+
)

src/modules/catalog/domain/events.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,9 @@ class ListingDraftUpdatedEvent(DomainEvent):
1010
listing_id: UUID
1111

1212

13+
class ListingDraftDeletedEvent(DomainEvent):
14+
listing_id: UUID
15+
16+
1317
class ListingPublishedEvent(DomainEvent):
1418
listing_id: UUID
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import pytest
2+
3+
from modules.catalog.application.command.delete_listing_draft import (
4+
DeleteListingDraftCommand,
5+
delete_listing_draft,
6+
)
7+
from modules.catalog.domain.entities import Listing
8+
from modules.catalog.domain.events import ListingDraftDeletedEvent
9+
from modules.catalog.infrastructure.listing_repository import (
10+
PostgresJsonListingRepository,
11+
)
12+
from seedwork.domain.value_objects import UUID, Money
13+
from seedwork.infrastructure.repository import InMemoryRepository
14+
15+
16+
@pytest.mark.unit
17+
def test_delete_listing_draft():
18+
# arrange
19+
repository = InMemoryRepository()
20+
listing = Listing(
21+
id=Listing.next_id(),
22+
title="Tiny dragon",
23+
description="Tiny dragon for sale",
24+
ask_price=Money(1),
25+
seller_id=UUID.v4(),
26+
)
27+
repository.add(listing)
28+
29+
command = DeleteListingDraftCommand(
30+
listing_id=listing.id,
31+
)
32+
33+
# act
34+
result = delete_listing_draft(command, repository)
35+
36+
# assert
37+
assert result.is_success()
38+
assert result.entity_id == listing.id
39+
assert result.events == [ListingDraftDeletedEvent(listing_id=listing.id)]
40+
41+
42+
@pytest.mark.integration
43+
def test_delete_listing_draft_removes_from_database(db_session):
44+
repository = PostgresJsonListingRepository(db_session=db_session)
45+
listing = Listing(
46+
id=Listing.next_id(),
47+
title="Tiny dragon",
48+
description="Tiny dragon for sale",
49+
ask_price=Money(1),
50+
seller_id=UUID.v4(),
51+
)
52+
repository.add(listing)
53+
54+
command = DeleteListingDraftCommand(
55+
listing_id=listing.id,
56+
)
57+
58+
# act
59+
delete_listing_draft(command, repository)
60+
61+
# assert
62+
assert repository.count() == 0
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,2 @@
11
class ApplicationException(Exception):
22
pass
3-
4-
5-
class EntityNotFoundException(Exception):
6-
pass

0 commit comments

Comments
 (0)