Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 19 additions & 0 deletions apps/api/backend/api/routes/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from fastapi import APIRouter, status

from backend.schemas.event_log import (
EventLogCreateRequest,
EventLogCreateResponse,
)
from backend.services.event_log_service import create_event_log

router = APIRouter(prefix="/events", tags=["events"])


@router.post(
"",
response_model=EventLogCreateResponse,
status_code=status.HTTP_201_CREATED,
)
def create_event(payload: EventLogCreateRequest) -> EventLogCreateResponse:
result = create_event_log(payload)
return EventLogCreateResponse(**result)
3 changes: 3 additions & 0 deletions apps/api/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from backend.api.routes.products import router as products_router
from backend.api.routes.reviews import router as reviews_router
from backend.api.routes.sessions import router as sessions_router
from backend.api.routes.events import router as events_router


app = FastAPI(title="D2C Commerce Prototype API")

Expand Down Expand Up @@ -45,6 +47,7 @@
app.include_router(order_history_router)
app.include_router(payments_router)
app.include_router(reviews_router)
app.include_router(events_router)

@app.get("/health")
def health_check() -> dict[str, str]:
Expand Down
31 changes: 31 additions & 0 deletions apps/api/backend/schemas/event_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from datetime import datetime
from typing import Any
from uuid import UUID

from pydantic import BaseModel, Field


class EventLogCreateRequest(BaseModel):
event_name: str = Field(..., min_length=1, max_length=100)
event_type: str = Field(..., min_length=1, max_length=50)
user_id: UUID | None = None
session_id: UUID | None = None
entity_type: str | None = Field(default=None, max_length=50)
entity_id: UUID | None = None
source: str = Field(..., min_length=1, max_length=50)
properties: dict[str, Any] = Field(default_factory=dict)


class EventLogCreateResponse(BaseModel):
event_id: UUID
event_name: str
event_type: str
user_id: UUID | None = None
session_id: UUID | None = None
entity_type: str | None = None
entity_id: UUID | None = None
occurred_at: datetime
source: str
properties: dict[str, Any]
created_at: datetime
message: str
79 changes: 77 additions & 2 deletions apps/api/backend/services/cart_item_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@
CartItemCreateRequest,
CartItemQuantityUpdateRequest,
)
from backend.services.event_log_service import record_domain_event_safely


MAX_CART_ITEM_QUANTITY = 99

def add_item_to_cart(cart_id: UUID, payload: CartItemCreateRequest) -> dict[str, Any]:
cart_query = text("""
SELECT
cart_id,
user_id,
cart_status
FROM carts
WHERE cart_id = :cart_id
Expand Down Expand Up @@ -157,6 +160,24 @@ def add_item_to_cart(cart_id: UUID, payload: CartItemCreateRequest) -> dict[str,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update cart item",
)

record_domain_event_safely(
event_name="cart_item_quantity_changed",
user_id=cart["user_id"],
entity_type="cart",
entity_id=updated_item["cart_id"],
properties={
"cart_id": updated_item["cart_id"],
"cart_item_id": updated_item["cart_item_id"],
"product_id": updated_item["product_id"],
"previous_quantity": existing_item["quantity"],
"next_quantity": updated_item["quantity"],
"quantity_delta": payload.quantity,
"unit_price": updated_item["unit_price"],
"currency": updated_item["currency"],
"change_source": "add_item_to_cart",
},
)

return {
"cart_item_id": updated_item["cart_item_id"],
Expand Down Expand Up @@ -185,6 +206,21 @@ def add_item_to_cart(cart_id: UUID, payload: CartItemCreateRequest) -> dict[str,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to add item to cart",
)

record_domain_event_safely(
event_name="cart_item_added",
user_id=cart["user_id"],
entity_type="cart",
entity_id=created_item["cart_id"],
properties={
"cart_id": created_item["cart_id"],
"cart_item_id": created_item["cart_item_id"],
"product_id": created_item["product_id"],
"quantity": created_item["quantity"],
"unit_price": created_item["unit_price"],
"currency": created_item["currency"],
},
)

return {
"cart_item_id": created_item["cart_item_id"],
Expand All @@ -201,7 +237,8 @@ def add_item_to_cart(cart_id: UUID, payload: CartItemCreateRequest) -> dict[str,
def remove_item_from_cart(cart_id: UUID, cart_item_id: UUID) -> dict[str, Any]:
cart_query = text("""
SELECT
cart_id
cart_id,
user_id
FROM carts
WHERE cart_id = :cart_id
AND cart_status = 'active'
Expand All @@ -214,7 +251,11 @@ def remove_item_from_cart(cart_id: UUID, cart_item_id: UUID) -> dict[str, Any]:
AND cart_id = :cart_id
RETURNING
cart_item_id,
cart_id
cart_id,
product_id,
quantity,
unit_price,
currency
""")

with engine.begin() as connection:
Expand Down Expand Up @@ -242,6 +283,21 @@ def remove_item_from_cart(cart_id: UUID, cart_item_id: UUID) -> dict[str, Any]:
status_code=status.HTTP_404_NOT_FOUND,
detail="Cart item not found",
)

record_domain_event_safely(
event_name="cart_item_removed",
user_id=cart["user_id"],
entity_type="cart",
entity_id=deleted_item["cart_id"],
properties={
"cart_id": deleted_item["cart_id"],
"cart_item_id": deleted_item["cart_item_id"],
"product_id": deleted_item["product_id"],
"quantity": deleted_item["quantity"],
"unit_price": deleted_item["unit_price"],
"currency": deleted_item["currency"],
},
)

return {
"cart_item_id": deleted_item["cart_item_id"],
Expand All @@ -257,6 +313,7 @@ def update_cart_item_quantity(
cart_query = text("""
SELECT
cart_id,
user_id,
cart_status
FROM carts
WHERE cart_id = :cart_id
Expand All @@ -269,6 +326,7 @@ def update_cart_item_quantity(
cart_item_id,
cart_id,
product_id,
quantity,
unit_price,
currency
FROM cart_items
Expand Down Expand Up @@ -335,6 +393,23 @@ def update_cart_item_quantity(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update cart item quantity",
)

record_domain_event_safely(
event_name="cart_item_quantity_changed",
user_id=cart["user_id"],
entity_type="cart",
entity_id=updated_item["cart_id"],
properties={
"cart_id": updated_item["cart_id"],
"cart_item_id": updated_item["cart_item_id"],
"product_id": updated_item["product_id"],
"previous_quantity": item["quantity"],
"next_quantity": updated_item["quantity"],
"unit_price": updated_item["unit_price"],
"currency": updated_item["currency"],
"change_source": "update_cart_item_quantity",
},
)

return {
"cart_item_id": updated_item["cart_item_id"],
Expand Down
67 changes: 39 additions & 28 deletions apps/api/backend/services/cart_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from backend.db.connection import engine
from backend.schemas.cart import CartCreateRequest
from backend.services.event_log_service import record_domain_event_safely


def create_or_get_active_cart(payload: CartCreateRequest) -> dict[str, Any]:
Expand Down Expand Up @@ -49,42 +50,52 @@ def create_or_get_active_cart(payload: CartCreateRequest) -> dict[str, Any]:
checked_out_at
""")

is_created = False

with engine.begin() as connection:
existing = connection.execute(
select_query,
{"user_id": payload.user_id},
).mappings().first()

if existing is not None:
return {
"cart_id": existing["cart_id"],
"user_id": existing["user_id"],
"cart_status": existing["cart_status"],
"created_at": existing["created_at"],
"updated_at": existing["updated_at"],
"checked_out_at": existing["checked_out_at"],
}

created = connection.execute(
insert_query,
{"user_id": payload.user_id},
).mappings().first()

if created is None:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create cart",
)

return {
"cart_id": created["cart_id"],
"user_id": created["user_id"],
"cart_status": created["cart_status"],
"created_at": created["created_at"],
"updated_at": created["updated_at"],
"checked_out_at": created["checked_out_at"],
}
cart = existing
else:
created = connection.execute(
insert_query,
{"user_id": payload.user_id},
).mappings().first()

if created is None:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create cart",
)

cart = created
is_created = True

if is_created:
record_domain_event_safely(
event_name="cart_created",
user_id=cart["user_id"],
entity_type="cart",
entity_id=cart["cart_id"],
properties={
"cart_id": cart["cart_id"],
"cart_status": cart["cart_status"],
"created_at": cart["created_at"],
},
)

return {
"cart_id": cart["cart_id"],
"user_id": cart["user_id"],
"cart_status": cart["cart_status"],
"created_at": cart["created_at"],
"updated_at": cart["updated_at"],
"checked_out_at": cart["checked_out_at"],
}

def get_cart_detail(cart_id: UUID) -> dict[str, Any]:
cart_query = text("""
Expand Down
46 changes: 46 additions & 0 deletions apps/api/backend/services/coupon_service.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from decimal import Decimal, ROUND_HALF_UP
from typing import Any
from uuid import UUID
Expand All @@ -7,8 +8,34 @@

from backend.db.connection import engine
from backend.schemas.coupon_apply import CouponApplyRequest
from backend.services.event_log_service import record_event


logger = logging.getLogger(__name__)


def record_domain_event_safely(
*,
event_name: str,
user_id: UUID | None,
entity_type: str | None,
entity_id: UUID | None,
properties: dict[str, Any],
) -> None:
try:
record_event(
event_name=event_name,
event_type="domain_event",
source="backend",
user_id=user_id,
session_id=None,
entity_type=entity_type,
entity_id=entity_id,
properties=properties,
)
except Exception: # pylint: disable=broad-exception-caught
logger.exception("Failed to record domain event: %s", event_name)

def apply_coupon_to_cart(cart_id: UUID, payload: CouponApplyRequest) -> dict[str, Any]:
cart_query = text("""
SELECT
Expand Down Expand Up @@ -136,6 +163,25 @@ def apply_coupon_to_cart(cart_id: UUID, payload: CouponApplyRequest) -> dict[str

final_amount = total_amount - discount_amount

record_domain_event_safely(
event_name="coupon_applied",
user_id=cart["user_id"],
entity_type="coupon",
entity_id=coupon["coupon_id"],
properties={
"cart_id": cart_id,
"coupon_id": coupon["coupon_id"],
"coupon_name": coupon["coupon_name"],
"coupon_type": coupon["coupon_type"],
"discount_value": discount_value,
"minimum_order_amount": minimum_order_amount,
"total_amount": total_amount,
"discount_amount": discount_amount,
"final_amount": final_amount,
"currency": currency,
},
)

return {
"cart_id": cart_id,
"coupon": {
Expand Down
Loading
Loading