Skip to content

Commit c55d542

Browse files
feat: implement order deletion endpoint
Add delete order functionality with error handling, including integration and e2e tests. Returns order details upon successful deletion and appropriate error responses for not found cases.
1 parent 5460162 commit c55d542

File tree

10 files changed

+324
-41
lines changed

10 files changed

+324
-41
lines changed

service/dal/db_handler.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from abc import ABC, ABCMeta, abstractmethod
22

3-
from service.models.order import Order
3+
from service.models.order import Order, OrderId
44

55

66
class _SingletonMeta(ABCMeta):
@@ -18,4 +18,4 @@ class DalHandler(ABC, metaclass=_SingletonMeta):
1818
def create_order_in_db(self, customer_name: str, order_item_count: int) -> Order: ... # pragma: no cover
1919

2020
@abstractmethod
21-
def delete_order(self, order_id: str) -> None: ... # pragma: no cover
21+
def delete_order_in_db(self, order_id: OrderId) -> Order: ... # pragma: no cover

service/dal/dynamo_dal_handler.py

+20-8
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
from service.dal.db_handler import DalHandler
1212
from service.dal.models.db import OrderEntry
1313
from service.handlers.utils.observability import logger, tracer
14-
from service.models.exceptions import InternalServerException
15-
from service.models.order import Order
14+
from service.models.exceptions import InternalServerException, OrderNotFoundException
15+
from service.models.order import Order, OrderId
1616

1717

1818
class DynamoDalHandler(DalHandler):
@@ -50,17 +50,29 @@ def create_order_in_db(self, customer_name: str, order_item_count: int) -> Order
5050

5151
logger.info('finished create order successfully', order_item_count=order_item_count, customer_name=customer_name)
5252
return Order(id=entry.id, name=entry.name, item_count=entry.item_count)
53-
53+
5454
@tracer.capture_method(capture_response=False)
55-
def delete_order(self, order_id: str) -> None:
55+
def delete_order_in_db(self, order_id: OrderId) -> Order:
5656
logger.append_keys(order_id=order_id)
5757
logger.info('trying to delete order')
58+
5859
try:
5960
table: Table = self._get_db_handler(self.table_name)
60-
table.delete_item(Key={'PK': order_id})
61-
except ClientError as exc: # pragma: no cover
61+
response = table.get_item(Key={'id': order_id})
62+
63+
if 'Item' not in response:
64+
error_msg = f'Order with id {order_id} not found'
65+
logger.error(error_msg)
66+
raise OrderNotFoundException(error_msg)
67+
68+
order_item = response['Item']
69+
order = Order(id=order_item['id'], name=order_item['name'], item_count=order_item['item_count'])
70+
71+
table.delete_item(Key={'id': order_id})
72+
except ClientError as exc:
6273
error_msg = 'failed to delete order'
63-
logger.exception(error_msg, order_id=order_id)
74+
logger.exception(error_msg)
6475
raise InternalServerException(error_msg) from exc
6576

66-
logger.info('successfully deleted order')
77+
logger.info('finished delete order successfully')
78+
return order

service/handlers/handle_delete_order.py

+24-17
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,51 @@
1+
from http import HTTPStatus
12
from typing import Annotated, Any
23

34
from aws_lambda_env_modeler import get_environment_variables, init_environment_variables
4-
from aws_lambda_powertools.event_handler.openapi.params import Path
5+
from aws_lambda_powertools.event_handler import Response, content_types
6+
from aws_lambda_powertools.event_handler.openapi.params import Body
57
from aws_lambda_powertools.logging import correlation_paths
68
from aws_lambda_powertools.metrics import MetricUnit
79
from aws_lambda_powertools.utilities.typing import LambdaContext
810

9-
from service.handlers.models.dynamic_configuration import MyConfiguration
1011
from service.handlers.models.env_vars import MyHandlerEnvVars
11-
from service.handlers.utils.dynamic_configuration import parse_configuration
1212
from service.handlers.utils.observability import logger, metrics, tracer
1313
from service.handlers.utils.rest_api_resolver import ORDERS_PATH, app
1414
from service.logic.delete_order import delete_order
15+
from service.models.exceptions import OrderNotFoundException
1516
from service.models.input import DeleteOrderRequest
16-
from service.models.output import DeleteOrderOutput, InternalServerErrorOutput
17+
from service.models.output import DeleteOrderOutput, InternalServerErrorOutput, OrderNotFoundOutput
1718

1819

19-
@app.delete(
20-
ORDERS_PATH + '{order_id}',
20+
@app.post(
21+
f"{ORDERS_PATH}delete",
2122
summary='Delete an order',
22-
description='Delete an order identified by the order_id',
23-
response_description='The deleted order',
23+
description='Delete an order identified by the provided order ID',
24+
response_description='The deleted order details',
2425
responses={
2526
200: {
2627
'description': 'The deleted order',
2728
'content': {'application/json': {'model': DeleteOrderOutput}},
2829
},
30+
404: {
31+
'description': 'Order not found',
32+
'content': {'application/json': {'model': OrderNotFoundOutput}},
33+
},
2934
501: {
3035
'description': 'Internal server error',
3136
'content': {'application/json': {'model': InternalServerErrorOutput}},
3237
},
3338
},
3439
tags=['CRUD'],
3540
)
36-
def handle_delete_order(order_id: Annotated[str, Path(description="The ID of the order to delete")]) -> DeleteOrderOutput:
41+
def handle_delete_order(delete_input: Annotated[DeleteOrderRequest, Body(embed=False, media_type='application/json')]) -> DeleteOrderOutput:
3742
env_vars: MyHandlerEnvVars = get_environment_variables(model=MyHandlerEnvVars)
3843
logger.debug('environment variables', env_vars=env_vars.model_dump())
39-
logger.info('got delete order request', order_id=order_id)
40-
41-
my_configuration = parse_configuration(model=MyConfiguration)
42-
logger.debug('fetched dynamic configuration', configuration=my_configuration.model_dump())
44+
logger.info('got delete order request', order_id=delete_input.order_id)
4345

4446
metrics.add_metric(name='ValidDeleteOrderEvents', unit=MetricUnit.Count, value=1)
45-
46-
delete_request = DeleteOrderRequest(order_id=order_id)
47-
4847
response: DeleteOrderOutput = delete_order(
49-
delete_request=delete_request,
48+
delete_request=delete_input,
5049
table_name=env_vars.TABLE_NAME,
5150
context=app.lambda_context,
5251
)
@@ -55,6 +54,14 @@ def handle_delete_order(order_id: Annotated[str, Path(description="The ID of the
5554
return response
5655

5756

57+
@app.exception_handler(OrderNotFoundException)
58+
def handle_order_not_found_error(ex: OrderNotFoundException):
59+
logger.exception('order not found')
60+
return Response(
61+
status_code=HTTPStatus.NOT_FOUND, content_type=content_types.APPLICATION_JSON, body=OrderNotFoundOutput().model_dump()
62+
)
63+
64+
5865
@init_environment_variables(model=MyHandlerEnvVars)
5966
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
6067
@metrics.log_metrics

service/logic/delete_order.py

+10-8
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
from service.dal.db_handler import DalHandler
77
from service.handlers.utils.observability import logger, tracer
88
from service.logic.utils.idempotency import IDEMPOTENCY_CONFIG, IDEMPOTENCY_LAYER
9+
from service.models.exceptions import OrderNotFoundException
910
from service.models.input import DeleteOrderRequest
11+
from service.models.order import Order
1012
from service.models.output import DeleteOrderOutput
1113

1214

@@ -22,11 +24,11 @@ def delete_order(delete_request: DeleteOrderRequest, table_name: str, context: L
2224

2325
logger.info('starting to handle delete request', order_id=delete_request.order_id)
2426

25-
dal_handler: DalHandler = get_dal_handler(table_name=table_name)
26-
27-
# Delete the order from the database
28-
dal_handler.delete_order(order_id=delete_request.order_id)
29-
30-
logger.info('successfully deleted order', order_id=delete_request.order_id)
31-
32-
return DeleteOrderOutput(order_id=delete_request.order_id)
27+
dal_handler: DalHandler = get_dal_handler(table_name)
28+
try:
29+
order: Order = dal_handler.delete_order_in_db(delete_request.order_id)
30+
# convert from order object to output, they won't always be the same
31+
return DeleteOrderOutput(name=order.name, item_count=order.item_count, id=order.id)
32+
except OrderNotFoundException as exc:
33+
logger.exception('order not found', order_id=delete_request.order_id)
34+
raise exc

service/models/exceptions.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
class InternalServerException(Exception):
2+
"""Raised when an unexpected error occurs in the server"""
3+
pass
4+
5+
6+
class OrderNotFoundException(Exception):
7+
"""Raised when trying to access an order that doesn't exist"""
28
pass
39

410

511
class DynamicConfigurationException(Exception):
12+
"""Raised when AppConfig fails to return configuration data"""
613
pass

service/models/input.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from pydantic import BaseModel, Field, field_validator
44

5+
from service.models.order import OrderId
6+
57

68
class CreateOrderRequest(BaseModel):
79
customer_name: Annotated[str, Field(min_length=1, max_length=20, description='Customer name')]
@@ -18,4 +20,4 @@ def check_order_item_count(cls, v):
1820

1921

2022
class DeleteOrderRequest(BaseModel):
21-
order_id: Annotated[str, Field(min_length=36, max_length=36, description='Order ID as UUID')]
23+
order_id: OrderId

service/models/output.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ class CreateOrderOutput(Order):
1212
pass
1313

1414

15-
class DeleteOrderOutput(BaseModel):
16-
order_id: Annotated[str, Field(description='ID of the deleted order')]
17-
status: Annotated[str, Field(description='Status of the delete operation')] = 'deleted'
18-
19-
2015
class InternalServerErrorOutput(BaseModel):
2116
error: Annotated[str, Field(description='Error description')] = 'internal server error'
17+
18+
19+
class DeleteOrderOutput(Order):
20+
pass
21+
22+
23+
class OrderNotFoundOutput(BaseModel):
24+
error: Annotated[str, Field(description='Error description')] = 'order not found'
+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import json
2+
import uuid
3+
from typing import Dict
4+
5+
import pytest
6+
import requests
7+
8+
from service.models.order import Order
9+
10+
11+
@pytest.fixture(scope='module')
12+
def api_gw_url():
13+
import os
14+
url = os.environ.get('ORDER_API_GW_URL')
15+
if not url:
16+
raise ValueError('Missing environment variable: ORDER_API_GW_URL')
17+
return url
18+
19+
20+
def test_delete_order_flow(api_gw_url):
21+
# First create an order to delete
22+
customer_name = 'E2E Test Customer'
23+
order_item_count = 3
24+
25+
# Create order
26+
create_url = f"{api_gw_url}/api/orders/"
27+
create_response = requests.post(
28+
create_url,
29+
json={
30+
'customer_name': customer_name,
31+
'order_item_count': order_item_count
32+
}
33+
)
34+
35+
assert create_response.status_code == 200
36+
created_order = create_response.json()
37+
order_id = created_order['id']
38+
39+
# Delete the order
40+
delete_url = f"{api_gw_url}/api/orders/delete"
41+
delete_response = requests.post(
42+
delete_url,
43+
json={
44+
'order_id': order_id
45+
}
46+
)
47+
48+
# Check the response
49+
assert delete_response.status_code == 200
50+
deleted_order = delete_response.json()
51+
assert deleted_order['id'] == order_id
52+
assert deleted_order['name'] == customer_name
53+
assert deleted_order['item_count'] == order_item_count
54+
55+
# Try to delete the same order again, should get a 404
56+
delete_again_response = requests.post(
57+
delete_url,
58+
json={
59+
'order_id': order_id
60+
}
61+
)
62+
63+
assert delete_again_response.status_code == 404
64+
assert delete_again_response.json()['error'] == 'order not found'
65+
66+
67+
def test_delete_nonexistent_order(api_gw_url):
68+
delete_url = f"{api_gw_url}/api/orders/delete"
69+
nonexistent_order_id = str(uuid.uuid4())
70+
71+
response = requests.post(
72+
delete_url,
73+
json={
74+
'order_id': nonexistent_order_id
75+
}
76+
)
77+
78+
assert response.status_code == 404
79+
assert response.json()['error'] == 'order not found'
80+
81+
82+
def test_delete_invalid_order_id(api_gw_url):
83+
delete_url = f"{api_gw_url}/api/orders/delete"
84+
85+
# Test with an invalid UUID
86+
response = requests.post(
87+
delete_url,
88+
json={
89+
'order_id': 'not-a-uuid'
90+
}
91+
)
92+
93+
# Should get a validation error
94+
assert response.status_code == 422

0 commit comments

Comments
 (0)