Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8c27afe
fix(get_bookings): add requester role handling and user owner details…
veloy25 Jul 24, 2026
14bec38
fix(get_bookings_viewmodel): enhance initialization to handle owner l…
veloy25 Jul 24, 2026
9339aa9
refactor(get_bookings_presenter): encapsulate controller initializati…
veloy25 Jul 24, 2026
9df4ef6
fix(get_bookings_presenter): enhance user role handling and JSON pars…
veloy25 Jul 24, 2026
b371bad
fix(get_bookings_usecase): improve user client handling based on envi…
veloy25 Jul 24, 2026
7c95dc7
fix(get_bookings): enhance user information handling in lambda handle…
veloy25 Jul 24, 2026
1dff22b
fix(get_bookings_usecase): enhance user client initialization and imp…
veloy25 Jul 24, 2026
04eb148
fix(get_bookings_viewmodel): improve owner data retrieval by ensuring…
veloy25 Jul 24, 2026
2691b61
fix(get_bookings_usecase): correct owner assignment logic in booking …
veloy25 Jul 24, 2026
49c3eec
fix(get_bookings_usecase): add debug print for requester_role in book…
veloy25 Jul 24, 2026
2affcd9
fix(lambda_construct): add authorizer to API Gateway integration for …
veloy25 Jul 24, 2026
5da5ba7
test(authorizer): adiciona testes de caracterizacao do user_mss_autho…
VictorGasperi Aug 5, 2026
29cdc46
refactor(authorizer): extrai _fetch_user_data e _get_authorization_he…
VictorGasperi Aug 5, 2026
b462b1b
feat(authorizer): adiciona optional_lambda_handler que permite chamad…
VictorGasperi Aug 5, 2026
38b915a
fix(iac): usa request authorizer opcional na rota get_bookings
VictorGasperi Aug 5, 2026
f9e8c17
test(get_bookings): garante resposta anonima identica a de um STUDENT
VictorGasperi Aug 5, 2026
c0dae04
fix(get_bookings): reaproveita UserAPIClient e remove print de debug
VictorGasperi Aug 5, 2026
6316f3f
chore: adiciona CLAUDE.md
VictorGasperi Aug 5, 2026
9c6195d
Merge pull request #67 from Maua-Dev/fix/get-bookings
VictorGasperi Aug 5, 2026
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
77 changes: 77 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
# Activate virtual environment (required before running anything)
source venv/bin/activate

# Install dependencies
pip install -r requirements-app.txt

# Run all tests
pytest

# Run a single test file
pytest tests/modules/create_booking/app/test_create_booking_usecase.py

# Run a single test by name
pytest tests/modules/create_booking/app/test_create_booking_usecase.py::TestCreateBookingUsecase::test_create_booking_valid

# Run tests with coverage
pytest --cov=src
```

Set `STAGE=TEST` in your `.env` file (or environment) for local development — this switches to mock repositories and local DynamoDB config automatically.

## Architecture

This is a **Clean Architecture** Python microservice deployed as AWS Lambda functions behind API Gateway, with DynamoDB as the database. Each Lambda function is a module under `src/modules/`.

### Layer flow (outer → inner)

```
Lambda event → Presenter → Controller → Usecase → Repository Interface
↑
Mock (TEST) or DynamoDB (DEV/PROD)
```

- **Presenter** (`*_presenter.py`): Lambda entry point. Instantiates repo/usecase/controller from `Environments`, wraps the raw Lambda event into `LambdaHttpRequest`, injects `user_from_authorizer` from the API Gateway authorizer context, and returns `LambdaHttpResponse.toDict()`.
- **Controller** (`*_controller.py`): Validates and extracts parameters from the request, calls the usecase, wraps the result in a Viewmodel, and returns an HTTP code object (`Created`, `BadRequest`, etc.).
- **Usecase** (`*_usecase.py`): Business logic. Receives primitive types, raises domain/usecase errors.
- **Viewmodel** (`*_viewmodel.py`): Serializes domain entities to response dicts.
- **Repository interface** (`src/shared/domain/repositories/`): Abstract base classes (`IBookingRepository`, `IReservationRepository`) that define the data contract.
- **Repository implementations** (`src/shared/infra/repositories/`): `*_mock.py` for tests, `*_dynamo.py` for production.

### Environment / repo selection

`Environments.get_envs()` (in `src/shared/environments.py`) reads the `STAGE` env var and returns the correct repository class. When `STAGE=TEST`, mocks are used; otherwise DynamoDB implementations are used. All presenters call this at module load time.

### Authentication

A Lambda Authorizer (`src/shared/authorizer/user_mss_authorizer.py`) validates Bearer tokens against an external User MSS API and injects user data into the API Gateway request context. Controllers receive it via `request.data['user_from_authorizer']` (a dict with `user_id`, role, etc.).

### Key shared paths

| Path | Purpose |
|------|---------|
| `src/shared/domain/entities/` | `Booking` and `Court` domain entities with validation |
| `src/shared/domain/enums/` | `SPORT`, `BOOKING_TYPE`, `STATUS_ENUM` enums |
| `src/shared/helpers/errors/` | `domain_errors`, `usecase_errors`, `controller_errors` — raised by different layers |
| `src/shared/helpers/external_interfaces/` | `LambdaHttpRequest/Response`, HTTP status code wrappers |
| `src/shared/infra/dto/` | DynamoDB ↔ domain entity conversion (`*_dynamo_dto.py`) |
| `src/shared/clients/` | External HTTP clients (e.g., `user_api_client.py`) |
| `iac/` | AWS CDK infrastructure (API Gateway, Lambda, DynamoDB, S3, SSM constructs) |

### Naming conventions

- Files and directories: `snake_case`
- Classes: `PascalCase` with type suffix — `CreateBookingController`, `BookingRepositoryMock`, `IBookingRepository`
- Enums: `UPPER_SNAKE_CASE` with `_ENUM` suffix where applicable
- Tests mirror the `src/` directory structure under `tests/`

### Infrastructure

Defined in `iac/` using AWS CDK (Python). The stack provisions API Gateway, Lambda functions, DynamoDB table, S3 bucket, and SSM parameters. The `STAGE` variable controls deployment target (`DEV`, `HOMOLOG`, `PROD`). Local development uses Docker Compose with DynamoDB Local and MinIO (see `iac/local/`).
27 changes: 27 additions & 0 deletions iac/components/lambda_construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,32 @@ def __init__(
results_cache_ttl=Duration.seconds(0)
)

# Segundo authorizer, para rotas em que o token é opcional.
# Reaproveita o mesmo asset do authorizer obrigatório, mudando só o handler.
optional_authorizer_lambda = lambda_.Function(
self,
id=f"LambdaOptionalUserMssAuthorizer-{self.stack_name}-{self.stage}",
function_name=f"lambda_optional_user_mss_authorizer-{self.stack_name}-{self.stage}"[:63],
code=lambda_.Code.from_asset("../src/shared/authorizer"),
handler="user_mss_authorizer.optional_lambda_handler",
runtime=lambda_.Runtime("python3.13"),
layers=[self.lambda_layer],
environment=environment_variables,
timeout=Duration.seconds(15)
)

# identity_sources=[] só é aceito com results_cache_ttl=0, e é justamente essa
# combinação que faz o API Gateway invocar o authorizer mesmo sem header Authorization.
# Com um TokenAuthorizer, a requisição sem header morre em 401 antes de chegar aqui.
optional_request_authorizer = apigw.RequestAuthorizer(
self,
id=f"RequestOptionalUserMssAuthorizer-{self.stack_name}-{self.stage}",
authorizer_name=f"optional_user_mss_authorizer-{self.stack_name}-{self.stage}",
handler=optional_authorizer_lambda,
identity_sources=[],
results_cache_ttl=Duration.seconds(0)
)

self.create_booking = self.create_lambda_api_gateway_integration(
module_name="create_booking",
method="POST",
Expand Down Expand Up @@ -156,6 +182,7 @@ def __init__(
method="GET",
api_resource=api_gateway_resource,
environment_variables=environment_variables,
authorizer=optional_request_authorizer
)

self.delete_booking = self.create_lambda_api_gateway_integration(
Expand Down
6 changes: 5 additions & 1 deletion src/modules/get_bookings/app/get_bookings_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,18 @@ def __call__(self, request: IRequest):
fieldTypeExpected='int',
fieldTypeReceived=start_date)

user = request.data.get('user_from_authorizer')
requester_role = user.get('role') if isinstance(user, dict) else None

booking = self.usecase(
booking_id=booking_id,
user_id=user_id,
sport=sport,
court_number=court_number,
end_date=end_date,
start_date=start_date,
booking_type=booking_type
booking_type=booking_type,
requester_role=requester_role
)
booking_viewmodel = GetBookingsViewmodel(booking)
return OK(booking_viewmodel.to_dict())
Expand Down
27 changes: 23 additions & 4 deletions src/modules/get_bookings/app/get_bookings_presenter.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
import json

from .get_bookings_controller import GetBookingsController
from .get_bookings_usecase import GetBookingsUseCase
from src.shared.environments import Environments
from src.shared.helpers.external_interfaces.http_lambda_requests import LambdaHttpRequest, LambdaHttpResponse
from src.shared.infra.repositories.booking_repository_mock import BookingRepositoryMock

repo = Environments.get_booking_repo()()
usecase = GetBookingsUseCase(repo=repo)
controller = GetBookingsController(usecase=usecase)

def _build_controller():
repo = Environments.get_booking_repo()()
usecase = GetBookingsUseCase(repo=repo)
return GetBookingsController(usecase=usecase)


def lambda_handler(event, context):
httpRequest = LambdaHttpRequest(data=event)

user_info_value = event.get('requestContext', {}).get('authorizer', {}).get('user')
if isinstance(user_info_value, str):
try:
parsed_user = json.loads(user_info_value)
user_info = parsed_user.get('user') if isinstance(parsed_user, dict) else None
except json.JSONDecodeError:
user_info = None
elif isinstance(user_info_value, dict):
user_info = user_info_value
else:
user_info = None

httpRequest.data['user_from_authorizer'] = user_info

controller = _build_controller()
response = controller(request=httpRequest)
httpResponse = LambdaHttpResponse(status_code=response.status_code, body=response.body, headers=response.headers)

Expand Down
35 changes: 32 additions & 3 deletions src/modules/get_bookings/app/get_bookings_usecase.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
from src.shared.domain.enums.sport import SPORT
from src.shared.domain.enums.type import BOOKING_TYPE
from src.shared.domain.repositories.booking_repository_interface import IBookingRepository
from src.shared.clients.user_api_client import UserAPIClient
from src.shared.helpers.errors.domain_errors import EntityError
from src.shared.helpers.errors.usecase_errors import NoItemsFound, DependantFilter

class GetBookingsUseCase:
repo: IBookingRepository

def __init__(self, repo: IBookingRepository):
def __init__(self, repo: IBookingRepository, user_client=None):
self.repo = repo
self.user_client = user_client


def __call__(self,
booking_id: Optional[str] = None,
Expand All @@ -20,7 +23,8 @@ def __call__(self,
court_number: Optional[int] = None,
end_date: Optional[int] = None,
start_date: Optional[int] = None,
booking_type: Optional[str] = None):
booking_type: Optional[str] = None,
requester_role: str = None):

if booking_id:
if not Booking.validate_booking_id(booking_id):
Expand Down Expand Up @@ -56,4 +60,29 @@ def __call__(self,
if bookings is None or bookings == []:
raise NoItemsFound('booking filters passed')

return bookings
owner_list = []

if requester_role == 'ADMIN':
client = self.user_client

for booking in bookings:
try:
# Construído sob demanda e reaproveitado: cada UserAPIClient() baixa
# a lista inteira de usuários do user mss.
if client is None:
client = UserAPIClient()

owner = {
'name': client.get_user_name(booking.user_id),
'network_id': client.get_user_network_id(booking.user_id),
}
except Exception as e:
print(f"ERRO NA API DE USER: {e}")
owner = {
'name': 'Erro de integração',
'network_id': 'Erro de integração',
}

owner_list.append(owner)

return {'bookings': bookings, 'owner': owner_list}
28 changes: 22 additions & 6 deletions src/modules/get_bookings/app/get_bookings_viewmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,30 @@
class GetBookingsViewmodel:
bookings: List[Booking]

def __init__(self, bookings: list):
self.bookings = bookings
def __init__(self, bookings: list, owner_list: list = None):
if isinstance(bookings, dict):
data = bookings
self.bookings = data.get('bookings', [])
self.owner_list = data.get('owner', [])
else:
self.bookings = bookings or []
self.owner_list = owner_list or []

def to_dict(self):
bookings_response = []

for index, booking in enumerate(self.bookings):
booking_dict = {
k: v for k, v in booking.to_dict().items() if k != 'user_id'
}

if index < len(self.owner_list) and self.owner_list[index] is not None:
owner = self.owner_list[index]
booking_dict['owner_name'] = owner.get('name')
booking_dict['owner_network_id'] = owner.get('network_id')
bookings_response.append(booking_dict)

return {
'bookings': [
{k: v for k, v in booking.to_dict().items() if k != 'user_id'}
for booking in self.bookings
],
'bookings': bookings_response,
'message': 'the bookings were retrieved'
}
Loading
Loading