repo-x-ray/data/sample-service

sample-service

FastAPI order service — 4 modules, 2 endpoints, read in full before this page was built.

4
modules mapped
2
endpoints
9/10
tests passing
1
high-severity risk

Modules

4
main.py
App entrypoint. Mounts the orders router, wires startup/shutdown hooks for the DB session.
entrypoint
routes/orders.py
Owns both HTTP endpoints — order creation and order lookup. No auth or ownership check on lookup.
routing
services/pricing.py
Owns discount and total calculation. Contains the divide-by-item-count logic flagged below.
domain logic
db/models.py
SQLAlchemy models for Order and LineItem. Stores money as Float, not Decimal.
persistence

Endpoints

2
MethodPathIn / Out
POST /orders in {customer_id: int, items: [{sku, qty, price}]}

out {order_id, total, created_at}
GET /orders/{order_id} in order_id: int (path)

out {order_id, customer_id, items, total}

Test summary

9 pass · 1 fail
test_orders.py::test_create_order_okpassed
test_orders.py::test_get_order_okpassed
test_orders.py::test_create_order_bad_skupassed
test_pricing.py::test_discount_appliedpassed
test_pricing.py::test_no_discount_below_thresholdpassed
test_pricing.py::test_multiple_line_itemspassed
test_models.py::test_order_persistspassed
test_models.py::test_line_item_relationshippassed
test_main.py::test_app_bootspassed
test_pricing.py::test_empty_cart_totalZeroDivisionError

Top risks

ranked
#1high

Empty cart crashes total calculation

services/pricing.py:27

calculate_total() divides subtotal by len(items) to derive an average that's never actually used, so an empty items list raises ZeroDivisionError and turns into a 500 on POST /orders. This is the failing test above and the deliberate bug in this sample.

#2medium

Order lookup has no ownership check

routes/orders.py:41

GET /orders/{order_id} returns any order for any caller — there's no check that the requester's customer_id matches the order's. Any authenticated (or unauthenticated) client can enumerate order IDs and read other customers' orders.

#3low

Money stored as Float, not Decimal

db/models.py:15

total_amount is a SQLAlchemy Float column. Binary floating point drifts under repeated addition and discount math — small today, compounding as order volume grows.

Proposed fix for #1

services/pricing.py
services/pricing.py
def calculate_total(items: list[LineItem]) -> float:
- subtotal = sum(i.price * i.qty for i in items)
- avg_price = subtotal / len(items) # unused, but crashes on empty cart
- discount = loyalty_discount(items) * subtotal
- return subtotal - discount
+ if not items:
+ return 0.0
+
+ subtotal = sum(i.price * i.qty for i in items)
+ discount = loyalty_discount(items) * subtotal
+ return subtotal - discount

Drops the unused average-price line entirely and guards the empty-cart case with an early return, instead of computing something that was never used downstream.