@@ -239,81 +239,160 @@ Important
239239 Multiple files
240240 Rule: Type hints on all function parameters and return types
241241
242- services/book_service.py:15 get_all_books() -- missing return type
243- services/book_service.py:25 get_book(book_id) -- missing param + return
244- services/order_service.py:9 create_order(...) -- missing all params + return
245- services/order_service.py:80 update_status(...) -- missing all params + return
246- utils/helpers.py:4 format_price(...) -- missing all params + return
247- utils/helpers.py:15 get_bestsellers(limit) -- missing param + return
248- ... and 9 more functions
242+ File Function Missing
243+ ------------------------------ ------------------------------- ---------------------
244+ services/book_service.py:15 get_all_books() return type
245+ services/book_service.py:25 get_book(book_id) param + return
246+ services/book_service.py:34 search_books(query) param + return
247+ services/order_service.py:9 create_order(self, ...) all params + return
248+ services/order_service.py:66 get_order(self, order_id) param + return
249+ services/order_service.py:80 update_status(self, ...) all params + return
250+ services/order_service.py:105 cancel_order(self, order_id) param + return
251+ services/order_service.py:133 get_order_history(self, ...) param + return
252+ services/order_service.py:139 calculate_shipping(self, ...) param + return
253+ services/order_service.py:154 _send_email(self, ...) all params + return
254+ utils/helpers.py:4 format_price(amount, currency) all params + return
255+ utils/helpers.py:15 get_bestsellers(limit) param + return
256+ utils/helpers.py:22 validate_isbn(isbn) param + return
257+ utils/helpers.py:30 generate_report() return
249258
2502597. Error responses return 200 with error body instead of HTTPException
251260 routers/books.py:21-22, routers/orders.py:13-14,24-25
252261
262+ Returning {"error": "Book not found"} sends HTTP 200 with an error
263+ body. Clients cannot distinguish success from failure by status code.
264+
253265 # Before (routers/books.py:20-23) -- returns HTTP 200 with error!
266+ book = get_book(book_id)
254267 if not book:
255268 return {"error": "Book not found"}
269+ return book
256270
257271 # After -- proper HTTP semantics
272+ from fastapi import HTTPException
273+
274+ book = get_book(book_id)
258275 if not book:
259276 raise HTTPException(status_code=404, detail="Book not found")
277+ return book
278+
279+ 8. Missing HTTP status codes on POST endpoints
280+ routers/books.py:8, routers/orders.py:10
281+
282+ POST endpoints default to 200 instead of 201 Created.
283+
284+ # Before
285+ @router.post("/")
286+ def add_book(data: BookCreate):
287+
288+ # After
289+ @router.post("/", status_code=201, response_model=Book)
290+ def add_book(data: BookCreate):
260291
261- 8 . Returning raw dicts instead of Pydantic models
262- services/book_service.py:20-22, services/order_service.py:58-64
292+ 9 . Returning raw dicts instead of Pydantic models
293+ services/book_service.py:20-22, services/order_service.py:58-64,71-77
263294 Principle: P6 Start with the Data
264295
265296 Functions return hand-built {"id": ..., "title": ...} dicts instead
266297 of using the Pydantic models already defined in models/. Loses type
267- safety, validation, and IDE support.
298+ safety, validation, serialization control, and IDE support.
268299
269- # Before
270- result.append({"id": b.id, "title": b.title, "author": b.author, ...})
300+ # Before (services/book_service.py:19-22)
301+ result = []
302+ for b in books:
303+ result.append({"id": b.id, "title": b.title, "author": b.author,
304+ "isbn": b.isbn, "price": b.price})
305+ return result
271306
272307 # After -- use the model you already defined
273308 return [Book(id=b.id, title=b.title, author=b.author,
274309 isbn=b.isbn, price=b.price) for b in books]
275310
311+ 10. Deprecated Pydantic v1 API
312+ services/book_service.py:7 -- data.dict()
313+
314+ Pydantic v2 renamed .dict() to .model_dump(). The old method
315+ still works but is deprecated and will be removed.
316+
317+ # Before (Pydantic v1)
318+ book = BookModel(**data.dict())
319+
320+ # After (Pydantic v2)
321+ book = BookModel(**data.model_dump())
322+
276323Suggestions
277324
278- 9 . if/elif chain for discount logic -- Strategy pattern
279- services/order_service.py:19-31
280- Pattern: Strategy -- Callable type alias + dict mapping
325+ 11 . if/elif chain for discount logic -- Strategy pattern
326+ services/order_service.py:19-31
327+ Pattern: Strategy -- Callable type alias + dict mapping
281328
282- # Before
283- if discount_type == "bulk":
284- total = total * 0.8
285- elif discount_type == "medium":
286- total = total * 0.9
329+ # Before (services/order_service.py:19-31)
330+ if quantity >= 10:
331+ discount_type = "bulk"
332+ elif quantity >= 5:
333+ discount_type = "medium"
334+ else:
335+ discount_type = "none"
287336
288- # After
289- DiscountFn = Callable[[float, int], float]
337+ if discount_type == "bulk":
338+ total = total * 0.8
339+ elif discount_type == "medium":
340+ total = total * 0.9
341+ elif discount_type == "none":
342+ pass
343+
344+ # After -- discount strategies as functions
345+ from typing import Callable
346+
347+ DiscountFn = Callable[[float, int], float]
290348
291- def bulk_discount(total: float, qty : int) -> float:
292- return total * 0.8
349+ def bulk_discount(total: float, quantity : int) -> float:
350+ return total * 0.8
293351
294- def get_discount(quantity: int) -> DiscountFn:
295- if quantity >= 10: return bulk_discount
296- if quantity >= 5: return medium_discount
297- return no_discount
352+ def medium_discount(total: float, quantity: int) -> float:
353+ return total * 0.9
298354
299- total = get_discount(quantity)(total, quantity)
355+ def no_discount(total: float, quantity: int) -> float:
356+ return total
300357
301- 10. if/elif chain for currency formatting -- dict mapping
358+ def get_discount(quantity: int) -> DiscountFn:
359+ if quantity >= 10:
360+ return bulk_discount
361+ elif quantity >= 5:
362+ return medium_discount
363+ return no_discount
364+
365+ # Usage
366+ discount = get_discount(quantity)
367+ total = discount(total, quantity)
368+
369+ 12. if/elif chain for currency formatting -- dict mapping
302370 utils/helpers.py:4-12
303371 Pattern: Registry -- dict mapping replaces if/elif
304372
305373 # Before
306- if currency == "USD": return f"${amount:.2f}"
307- elif currency == "EUR": return f"E{amount:.2f}"
374+ def format_price(amount, currency):
375+ if currency == "USD":
376+ return f"${amount:.2f}"
377+ elif currency == "EUR":
378+ return f"E{amount:.2f}"
379+ elif currency == "GBP":
380+ return f"L{amount:.2f}"
381+ else:
382+ return f"{amount:.2f} {currency}"
308383
309384 # After
310- SYMBOLS: dict[str, str] = {"USD": "$", "EUR": "E", "GBP": "L"}
385+ CURRENCY_SYMBOLS: dict[str, str] = {
386+ "USD": "$", "EUR": "E", "GBP": "L",
387+ }
311388
312389 def format_price(amount: float, currency: str) -> str:
313- symbol = SYMBOLS.get(currency)
314- return f"{symbol}{amount:.2f}" if symbol else f"{amount:.2f} {currency}"
390+ symbol = CURRENCY_SYMBOLS.get(currency)
391+ if symbol:
392+ return f"{symbol}{amount:.2f}"
393+ return f"{amount:.2f} {currency}"
315394
316- 11 . Bare string constants for order status -- use StrEnum
395+ 13 . Bare string constants for order status -- use StrEnum
317396 services/order_service.py:38,86,89,95,111,114
318397
319398 # Before -- typo-prone strings scattered across the codebase
@@ -328,7 +407,7 @@ Suggestions
328407 DELIVERED = "delivered"
329408 CANCELLED = "cancelled"
330409
331- 12 . Mixed concerns in utils/helpers.py -- split by domain
410+ 14 . Mixed concerns in utils/helpers.py -- split by domain
332411 utils/helpers.py:1-38
333412 Principle: P1 High Cohesion | Rule: #16 Avoid Generic Package Names
334413
@@ -339,23 +418,38 @@ Suggestions
339418 Fix: Move get_bestsellers/generate_report into services/.
340419 Move format_price into formatting.py. Move validate_isbn into models/.
341420
342- 13 . Notification side effects interleaved with logic -- Pub/Sub pattern
421+ 15 . Notification side effects interleaved with business logic -- Pub/Sub
343422 services/order_service.py:44-49,89-100,121-125
344423 Pattern: Pub/Sub
345424
346- _send_email calls scattered through create_order, update_status,
347- and cancel_order. Each new side effect (Slack, analytics, audit)
348- requires modifying every method.
425+ _send_email calls are scattered throughout create_order, update_status,
426+ and cancel_order. Each new side effect (Slack notification, analytics
427+ event, audit log) requires modifying every method.
428+
429+ # After -- event-based notification
430+ EventHandler = Callable[[dict], None]
431+ subscribers: dict[str, list[EventHandler]] = {}
432+
433+ def subscribe(event: str, handler: EventHandler) -> None:
434+ subscribers.setdefault(event, []).append(handler)
435+
436+ def post_event(event: str, data: dict) -> None:
437+ for handler in subscribers.get(event, []):
438+ handler(data)
349439
350- # After -- event-based
351- post_event("order.created", {"order_id": order.id, "email": email })
440+ # In create_order:
441+ post_event("order.created", {"order_id": order.id, "email": customer_email })
352442
353- # Wire in composition root:
443+ # Wire handlers in composition root:
354444 subscribe("order.created", send_confirmation_email)
355445 subscribe("order.created", log_order)
356446
357- Summary: 3 critical, 5 important, 5 suggestions. Biggest wins:
358- introduce a DataInterface Protocol and split OrderService.
447+ ---
448+ Summary: 3 critical (layer skip, no DI, no resource management),
449+ 7 important (god class, broad catch, missing types, wrong HTTP responses,
450+ missing status codes, raw dicts, deprecated API), 5 suggestions (Strategy,
451+ Registry, StrEnum, split utils, Pub/Sub). The biggest wins would be
452+ introducing a DataInterface Protocol and splitting OrderService.
359453```
360454
361455### All Commands
0 commit comments