Skip to content

Add a Delete order lambda #940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions service/dal/db_handler.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from abc import ABC, ABCMeta, abstractmethod

from service.models.order import Order
from service.models.order import Order, OrderId


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

@abstractmethod
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add cdk code that generates all the required resources, make sure to add at the correct place

def delete_order(self, order_id: str) -> None: ... # pragma: no cover
def delete_order_in_db(self, order_id: OrderId) -> Order: ... # pragma: no cover
28 changes: 20 additions & 8 deletions service/dal/dynamo_dal_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from service.dal.db_handler import DalHandler
from service.dal.models.db import OrderEntry
from service.handlers.utils.observability import logger, tracer
from service.models.exceptions import InternalServerException
from service.models.order import Order
from service.models.exceptions import InternalServerException, OrderNotFoundException
from service.models.order import Order, OrderId


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

logger.info('finished create order successfully', order_item_count=order_item_count, customer_name=customer_name)
return Order(id=entry.id, name=entry.name, item_count=entry.item_count)

@tracer.capture_method(capture_response=False)
def delete_order(self, order_id: str) -> None:
def delete_order_in_db(self, order_id: OrderId) -> Order:
logger.append_keys(order_id=order_id)
logger.info('trying to delete order')

try:
table: Table = self._get_db_handler(self.table_name)
table.delete_item(Key={'PK': order_id})
except ClientError as exc: # pragma: no cover
response = table.get_item(Key={'id': order_id})

if 'Item' not in response:
error_msg = f'Order with id {order_id} not found'
logger.error(error_msg)
raise OrderNotFoundException(error_msg)

order_item = response['Item']
order = Order(id=order_item['id'], name=order_item['name'], item_count=order_item['item_count'])

table.delete_item(Key={'id': order_id})
except ClientError as exc:
error_msg = 'failed to delete order'
logger.exception(error_msg, order_id=order_id)
logger.exception(error_msg)
raise InternalServerException(error_msg) from exc

logger.info('successfully deleted order')
logger.info('finished delete order successfully')
return order
41 changes: 24 additions & 17 deletions service/handlers/handle_delete_order.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,51 @@
from http import HTTPStatus
from typing import Annotated, Any

from aws_lambda_env_modeler import get_environment_variables, init_environment_variables
from aws_lambda_powertools.event_handler.openapi.params import Path
from aws_lambda_powertools.event_handler import Response, content_types
from aws_lambda_powertools.event_handler.openapi.params import Body
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.metrics import MetricUnit
from aws_lambda_powertools.utilities.typing import LambdaContext

from service.handlers.models.dynamic_configuration import MyConfiguration
from service.handlers.models.env_vars import MyHandlerEnvVars
from service.handlers.utils.dynamic_configuration import parse_configuration
from service.handlers.utils.observability import logger, metrics, tracer
from service.handlers.utils.rest_api_resolver import ORDERS_PATH, app
from service.logic.delete_order import delete_order
from service.models.exceptions import OrderNotFoundException
from service.models.input import DeleteOrderRequest
from service.models.output import DeleteOrderOutput, InternalServerErrorOutput
from service.models.output import DeleteOrderOutput, InternalServerErrorOutput, OrderNotFoundOutput


@app.delete(
ORDERS_PATH + '{order_id}',
@app.post(
f"{ORDERS_PATH}delete",
summary='Delete an order',
description='Delete an order identified by the order_id',
response_description='The deleted order',
description='Delete an order identified by the provided order ID',
response_description='The deleted order details',
responses={
200: {
'description': 'The deleted order',
'content': {'application/json': {'model': DeleteOrderOutput}},
},
404: {
'description': 'Order not found',
'content': {'application/json': {'model': OrderNotFoundOutput}},
},
501: {
'description': 'Internal server error',
'content': {'application/json': {'model': InternalServerErrorOutput}},
},
},
tags=['CRUD'],
)
def handle_delete_order(order_id: Annotated[str, Path(description="The ID of the order to delete")]) -> DeleteOrderOutput:
def handle_delete_order(delete_input: Annotated[DeleteOrderRequest, Body(embed=False, media_type='application/json')]) -> DeleteOrderOutput:
env_vars: MyHandlerEnvVars = get_environment_variables(model=MyHandlerEnvVars)
logger.debug('environment variables', env_vars=env_vars.model_dump())
logger.info('got delete order request', order_id=order_id)

my_configuration = parse_configuration(model=MyConfiguration)
logger.debug('fetched dynamic configuration', configuration=my_configuration.model_dump())
logger.info('got delete order request', order_id=delete_input.order_id)

metrics.add_metric(name='ValidDeleteOrderEvents', unit=MetricUnit.Count, value=1)

delete_request = DeleteOrderRequest(order_id=order_id)

response: DeleteOrderOutput = delete_order(
delete_request=delete_request,
delete_request=delete_input,
table_name=env_vars.TABLE_NAME,
context=app.lambda_context,
)
Expand All @@ -55,6 +54,14 @@ def handle_delete_order(order_id: Annotated[str, Path(description="The ID of the
return response


@app.exception_handler(OrderNotFoundException)
def handle_order_not_found_error(ex: OrderNotFoundException):
logger.exception('order not found')
return Response(
status_code=HTTPStatus.NOT_FOUND, content_type=content_types.APPLICATION_JSON, body=OrderNotFoundOutput().model_dump()
)


@init_environment_variables(model=MyHandlerEnvVars)
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
@metrics.log_metrics
Expand Down
18 changes: 10 additions & 8 deletions service/logic/delete_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from service.dal.db_handler import DalHandler
from service.handlers.utils.observability import logger, tracer
from service.logic.utils.idempotency import IDEMPOTENCY_CONFIG, IDEMPOTENCY_LAYER
from service.models.exceptions import OrderNotFoundException
from service.models.input import DeleteOrderRequest
from service.models.order import Order
from service.models.output import DeleteOrderOutput


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

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

dal_handler: DalHandler = get_dal_handler(table_name=table_name)

# Delete the order from the database
dal_handler.delete_order(order_id=delete_request.order_id)

logger.info('successfully deleted order', order_id=delete_request.order_id)

return DeleteOrderOutput(order_id=delete_request.order_id)
dal_handler: DalHandler = get_dal_handler(table_name)
try:
order: Order = dal_handler.delete_order_in_db(delete_request.order_id)
# convert from order object to output, they won't always be the same
return DeleteOrderOutput(name=order.name, item_count=order.item_count, id=order.id)
except OrderNotFoundException as exc:
logger.exception('order not found', order_id=delete_request.order_id)
raise exc
7 changes: 7 additions & 0 deletions service/models/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
class InternalServerException(Exception):
"""Raised when an unexpected error occurs in the server"""
pass


class OrderNotFoundException(Exception):
"""Raised when trying to access an order that doesn't exist"""
pass


class DynamicConfigurationException(Exception):
"""Raised when AppConfig fails to return configuration data"""
pass
4 changes: 3 additions & 1 deletion service/models/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from pydantic import BaseModel, Field, field_validator

from service.models.order import OrderId


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


class DeleteOrderRequest(BaseModel):
order_id: Annotated[str, Field(min_length=36, max_length=36, description='Order ID as UUID')]
order_id: OrderId
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add unit test for pydantic schema

13 changes: 8 additions & 5 deletions service/models/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ class CreateOrderOutput(Order):
pass


class DeleteOrderOutput(BaseModel):
order_id: Annotated[str, Field(description='ID of the deleted order')]
status: Annotated[str, Field(description='Status of the delete operation')] = 'deleted'


class InternalServerErrorOutput(BaseModel):
error: Annotated[str, Field(description='Error description')] = 'internal server error'


class DeleteOrderOutput(Order):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add unit test for pydantic schema

pass


class OrderNotFoundOutput(BaseModel):
error: Annotated[str, Field(description='Error description')] = 'order not found'
94 changes: 94 additions & 0 deletions service/tests/e2e/test_delete_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import json
import uuid
from typing import Dict

import pytest
import requests

from service.models.order import Order


@pytest.fixture(scope='module')
def api_gw_url():
import os
url = os.environ.get('ORDER_API_GW_URL')
if not url:
raise ValueError('Missing environment variable: ORDER_API_GW_URL')
return url


def test_delete_order_flow(api_gw_url):
# First create an order to delete
customer_name = 'E2E Test Customer'
order_item_count = 3

# Create order
create_url = f"{api_gw_url}/api/orders/"
create_response = requests.post(
create_url,
json={
'customer_name': customer_name,
'order_item_count': order_item_count
}
)

assert create_response.status_code == 200
created_order = create_response.json()
order_id = created_order['id']

# Delete the order
delete_url = f"{api_gw_url}/api/orders/delete"
delete_response = requests.post(
delete_url,
json={
'order_id': order_id
}
)

# Check the response
assert delete_response.status_code == 200
deleted_order = delete_response.json()
assert deleted_order['id'] == order_id
assert deleted_order['name'] == customer_name
assert deleted_order['item_count'] == order_item_count

# Try to delete the same order again, should get a 404
delete_again_response = requests.post(
delete_url,
json={
'order_id': order_id
}
)

assert delete_again_response.status_code == 404
assert delete_again_response.json()['error'] == 'order not found'


def test_delete_nonexistent_order(api_gw_url):
delete_url = f"{api_gw_url}/api/orders/delete"
nonexistent_order_id = str(uuid.uuid4())

response = requests.post(
delete_url,
json={
'order_id': nonexistent_order_id
}
)

assert response.status_code == 404
assert response.json()['error'] == 'order not found'


def test_delete_invalid_order_id(api_gw_url):
delete_url = f"{api_gw_url}/api/orders/delete"

# Test with an invalid UUID
response = requests.post(
delete_url,
json={
'order_id': 'not-a-uuid'
}
)

# Should get a validation error
assert response.status_code == 422
Loading
Loading