Module 4: Code Review of AI Output
Verifying Business Logic: The Hardest Part of the Review
Verifying Business Logic: The Hardest Part of the Review
Capsule overview
Of everything you review in AI-generated code, business logic is the hardest to verify and the most dangerous to ignore. A SQL injection is detected by a scanner. A fake import is detected by the linter. A deprecated function is detected by a type checker. But if the code applies a discount to the unit price instead of the total, no tool detects it. Only you, who understand the business, can verify that the code does what the business needs.
This capsule teaches you a 4-step process for verifying business logic in AI code: understanding the requirements, tracing the code's flow, verifying the behavior, and testing business edge cases. By the end, you'll have a framework you can apply every time Claude Code generates code with business rules.
Why It's So Hard
The linter doesn't help you
# This code passes ALL the automatic checks:
# ✅ Linter: no errors
# ✅ Type checker: correct types
# ✅ Formatter: well formatted
# ✅ Import checker: valid imports
# ✅ Tests (if AI generated them): pass
def calculate_commission(sale_amount: Decimal, agent_level: str) -> Decimal:
"""Calculate agent commission based on level."""
rates = {
"junior": Decimal("0.05"),
"senior": Decimal("0.08"),
"director": Decimal("0.12"),
}
rate = rates.get(agent_level, Decimal("0.05"))
return sale_amount * rate
# But... does the business say 5%, 8%, 12%?
# Or does it say 5%, 8%, 10%?
# Is the fallback to 5% correct or should it be an error?
# Is the commission calculated on gross or net sale_amount?
# Only you can verify this.
AI-generated tests don't help either
If AI generates the code AND the tests, the tests verify what AI thinks is correct — not what the business needs. It's a closed loop with no external validation.
# AI generates the code:
def apply_discount(total: Decimal, code: str) -> Decimal:
if code == "SAVE20":
return total * Decimal("0.80") # 20% discount
return total
# AI generates the test:
def test_discount():
result = apply_discount(Decimal("100"), "SAVE20")
assert result == Decimal("80.00") # ✅ Passes — but...
# Is "SAVE20" 20% off or $20 off?
# The test confirms what the code does, not what it should do.
# If SAVE20 is "$20 off" (not 20%), the correct result
# would be Decimal("80.00") only for total=100.
# For total=200 it would be 180, not 160.
You're the last line of defense
Verification layers:
Linter → Detects syntax errors
Type Checker → Detects type errors
Tests → Detects behavior errors (if the tests are correct)
Code Review → Detects logic and security errors
──────────────────────────────────────────────────
YOU → Verify that the code meets the BUSINESS requirements
If you don't verify the business logic, nobody does.
The 4-Step Process
Step 1: Understand the Requirements (Before Reading Code)
Before looking at a single line of code, make sure you understand what it should do. This step is frequently skipped and is the #1 cause of incomplete business logic reviews.
BEFORE reading the code, answer:
1. What should this functionality DO?
→ A description in 2-3 simple sentences
2. What business RULES apply?
→ A list of rules with conditions and expected results
3. What input DATA does it receive?
→ Types, valid ranges, optional vs required fields
4. What RESULT does it produce?
→ Format, expected values for known inputs
5. What should it NOT do?
→ Prohibited behaviors, invalid states
Applied example:
Functionality: "A pricing system with volume discount."
1. WHAT IT DOES:
Calculates the total price of an order applying progressive
discounts based on the quantity of units purchased.
2. RULES:
- 1-9 units: full price ($10/unit)
- 10-49 units: 10% discount ($9/unit)
- 50-99 units: 20% discount ($8/unit)
- 100+ units: 30% discount ($7/unit)
- The discount applies to ALL units, not just the extra ones
3. INPUT DATA:
- quantity: int, >= 1
- unit_price: Decimal, > 0 (default $10)
4. RESULT:
- total: Decimal (quantity × unit_price × (1 - discount_rate))
- Examples: 5 units = $50, 10 units = $90, 50 units = $400
5. WHAT IT SHOULD NOT DO:
- Apply the discount only to units above the threshold
- Accept quantity <= 0
- Use float for money calculations
Step 2: Trace the Code's Flow (Active Reading)
Read the code line by line with the requirements in mind. Don't read passively — trace the flow with concrete values.
TRACING PROCESS:
1. Identify the main function/endpoint
2. Choose 3 representative inputs:
- A normal case (happy path)
- A boundary case (boundary)
- An extreme case (edge case)
3. For each input, follow the flow line by line
4. Compare the trace result with the expected result
Applied example:
Claude Code generates:
from decimal import Decimal
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, Field
app = FastAPI()
class PricingTier(BaseModel):
min_quantity: int
max_quantity: int
discount_rate: Decimal
PRICING_TIERS = [
PricingTier(min_quantity=1, max_quantity=9, discount_rate=Decimal("0")),
PricingTier(min_quantity=10, max_quantity=49, discount_rate=Decimal("0.10")),
PricingTier(min_quantity=50, max_quantity=99, discount_rate=Decimal("0.20")),
PricingTier(min_quantity=100, max_quantity=999999, discount_rate=Decimal("0.30")),
]
class OrderRequest(BaseModel):
quantity: int = Field(..., ge=1)
unit_price: Decimal = Field(default=Decimal("10.00"), gt=0)
class OrderResponse(BaseModel):
quantity: int
unit_price: Decimal
discount_rate: Decimal
subtotal: Decimal
discount_amount: Decimal
total: Decimal
def get_discount_rate(quantity: int) -> Decimal:
for tier in PRICING_TIERS:
if tier.min_quantity <= quantity <= tier.max_quantity:
return tier.discount_rate
return Decimal("0")
@app.post("/orders/calculate", response_model=OrderResponse)
async def calculate_order(order: OrderRequest):
discount_rate = get_discount_rate(order.quantity)
subtotal = order.unit_price * order.quantity
discount_amount = subtotal * discount_rate
total = subtotal - discount_amount
return OrderResponse(
quantity=order.quantity,
unit_price=order.unit_price,
discount_rate=discount_rate,
subtotal=subtotal,
discount_amount=discount_amount,
total=total,
)
Trace with 3 inputs:
INPUT 1: quantity=5, unit_price=$10 (happy path)
├── get_discount_rate(5): 1 <= 5 <= 9 → 0%
├── subtotal: $10 × 5 = $50
├── discount: $50 × 0 = $0
├── total: $50 - $0 = $50
└── ✅ CORRECT (5 units = $50, no discount)
INPUT 2: quantity=10, unit_price=$10 (boundary)
├── get_discount_rate(10): 10 <= 10 <= 49 → 10%
├── subtotal: $10 × 10 = $100
├── discount: $100 × 0.10 = $10
├── total: $100 - $10 = $90
└── ✅ CORRECT (10 units = $90, 10% discount)
INPUT 3: quantity=100, unit_price=$10 (edge)
├── get_discount_rate(100): 100 <= 100 <= 999999 → 30%
├── subtotal: $10 × 100 = $1000
├── discount: $1000 × 0.30 = $300
├── total: $1000 - $300 = $700
└── ✅ CORRECT (100 units = $700, 30% discount)
Result of the trace: The code is correct for all 3 inputs. It applies the discount to all units (not just the extra ones), which matches the requirement.
Step 3: Verify the Behavior (Critical Questions)
After tracing the flow, ask questions that go beyond the happy path:
CRITICAL BUSINESS LOGIC QUESTIONS:
1. What happens at the exact BOUNDARIES?
→ quantity = 9 (no discount) vs quantity = 10 (with discount)
→ Is it correct that 9→$90 and 10→$90? (same total, different discount)
2. Does the result make BUSINESS sense?
→ Is it correct that 9 units cost $90 and 10 cost $90?
→ A customer would buy 10 instead of 9 (same money, 1 extra)
→ Does the business want this? Maybe yes (incentive to buy more)
3. What FIELDS does it return that could be confusing?
→ discount_rate returns 0.10 — does the frontend expect 10 (percent)?
→ Do the amounts include tax or not?
4. What happens with future CHANGES?
→ What happens if they add a new tier between 50-99?
→ max_quantity=999999 — what if someone orders 1,000,000?
5. Are the RULES HARDCODED or configurable?
→ The tiers are in the code — should they be in the DB?
→ If the business changes the percentages, you have to deploy
Step 4: Test Business Edge Cases
Business edge cases are different from technical edge cases. It's not "what happens with None?" but "what happens when a customer returns 5 of 10 units and loses the volume discount?"
BUSINESS EDGE CASES:
1. Partial returns
→ Customer bought 50 units (20% discount = $400)
→ Returns 10 → now has 40 units (10% discount)
→ Is the price recalculated? How much is refunded?
2. Price changes
→ Customer quoted yesterday at $10/unit
→ Today the price went up to $12/unit
→ Is the quoted price honored?
3. Combination of products
→ Does the volume discount apply per product or per total order?
→ 5 of product A + 5 of product B = 10 units with discount?
4. Currency and rounding
→ 15 units × $9.99 × 10% discount = ?
→ How is it rounded? To the nearest cent?
→ $9.99 × 15 = $149.85 × 0.90 = $134.865 → $134.86 or $134.87?
5. System limits
→ Is there a maximum of units per order?
→ Is there a maximum total amount?
Three Complete Scenarios
Scenario 1: Permissions and Roles
Requirement: "Users with the 'editor' role can edit articles from their own team. 'admin' users can edit any article. 'viewer' users can only read."
AI-generated code:
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
from typing import Optional
from enum import Enum
app = FastAPI()
class UserRole(str, Enum):
VIEWER = "viewer"
EDITOR = "editor"
ADMIN = "admin"
class User(BaseModel):
id: str
role: UserRole
team_id: str
class Article(BaseModel):
id: str
title: str
content: str
team_id: str
author_id: str
class ArticleUpdate(BaseModel):
title: Optional[str] = None
content: Optional[str] = None
def check_edit_permission(user: User, article: Article) -> bool:
if user.role == UserRole.ADMIN:
return True
if user.role == UserRole.EDITOR:
return user.team_id == article.team_id
return False
@app.patch("/articles/{article_id}")
async def update_article(
article_id: str,
update: ArticleUpdate,
current_user: User = Depends(get_current_user),
):
article = get_article(article_id)
if not article:
raise HTTPException(status_code=404, detail="Article not found")
if not check_edit_permission(current_user, article):
raise HTTPException(status_code=403, detail="Not authorized")
if update.title is not None:
article.title = update.title
if update.content is not None:
article.content = update.content
save_article(article)
return article
Step-by-step verification:
STEP 1 — Requirements:
- viewer: only read → check_edit_permission returns False ✅
- editor: edit their team's articles → checks team_id ✅
- admin: edit any article → return True with no checks ✅
STEP 2 — Trace:
- Admin edits another team's article → True ✅
- Editor edits their team's article → team_id match → True ✅
- Editor edits another team's article → team_id no match → False ✅
- Viewer tries to edit → False ✅
STEP 3 — Critical questions:
⚠️ Can an editor edit ANY article from their team,
including those from other editors?
→ The requirement says "from their own team" — does it include articles
written by other team members?
→ The code allows it (only checks team_id, not author_id)
→ Needs clarification with the business
⚠️ What happens with future roles? (moderator, super_admin)
→ check_edit_permission returns False for unknown roles
→ Correct? Yes — fail closed (deny by default)
⚠️ Can the viewer see all articles or only their team's?
→ The UPDATE endpoint is protected, but the GET?
→ It's not in this code — verify in another file
STEP 4 — Business edge cases:
⚠️ What happens if an editor changes teams?
→ They lose access to their previous team's articles
→ Correct? Probably yes, but verify
⚠️ What happens if an article changes teams?
→ The previous team's editors lose access
→ The new team's editors gain access
→ Is there a transition period?
Scenario 2: Approval Workflow
Requirement: "Purchase requests require approval. Amounts under $1,000 are auto-approved. Between $1,000 and $10,000 require manager approval. Over $10,000 require director approval."
AI-generated code:
from decimal import Decimal
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime, timezone
from enum import Enum
import uuid
app = FastAPI()
class ApprovalStatus(str, Enum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
class PurchaseRequest(BaseModel):
description: str = Field(..., max_length=500)
amount: Decimal = Field(..., gt=0)
vendor: str = Field(..., min_length=1)
class PurchaseResponse(BaseModel):
id: str
description: str
amount: Decimal
vendor: str
status: ApprovalStatus
approved_by: Optional[str]
created_at: datetime
def determine_approval(amount: Decimal) -> tuple[ApprovalStatus, Optional[str]]:
if amount < Decimal("1000"):
return ApprovalStatus.APPROVED, "auto-approved"
elif amount <= Decimal("10000"):
return ApprovalStatus.PENDING, None
else:
return ApprovalStatus.PENDING, None
@app.post("/purchase-requests", response_model=PurchaseResponse)
async def create_purchase_request(
request: PurchaseRequest,
current_user: dict = Depends(get_current_user),
):
status, approved_by = determine_approval(request.amount)
purchase = PurchaseResponse(
id=str(uuid.uuid4()),
description=request.description,
amount=request.amount,
vendor=request.vendor,
status=status,
approved_by=approved_by,
created_at=datetime.now(timezone.utc),
)
save_purchase(purchase)
return purchase
Step-by-step verification:
STEP 1 — Requirements vs Code:
- < $1,000 → auto-approval ✅ (status=APPROVED, approved_by="auto-approved")
- $1,000-$10,000 → requires manager ⚠️ (status=PENDING, who is the approver?)
- > $10,000 → requires director ⚠️ (status=PENDING, who is the approver?)
STEP 2 — Trace:
- amount = $500 → APPROVED, "auto-approved" ✅
- amount = $1,000 → PENDING, None ❓ Is the boundary < or <=?
- amount = $5,000 → PENDING, None ✅ (but how is it assigned to the manager?)
- amount = $15,000 → PENDING, None ✅ (but how is it assigned to the director?)
STEP 3 — Critical questions:
❌ The requirement says "under $1,000" — is exactly $1,000
auto-approved or not?
→ Code uses < 1000, so $1,000 goes to PENDING
→ Is it correct? "Under $1,000" → yes, < is correct
→ BUT the second range says "between $1,000 and $10,000"
→ amount <= 10000 includes exactly $10,000 in the manager range
→ The requirement says "$10,000 requires director" —
so does exactly $10,000 go to manager or director?
❌ determine_approval doesn't distinguish between manager and director
→ Both ranges (1k-10k and >10k) return PENDING, None
→ Who approves? How is it routed to the correct approver?
→ The code has no approval routing
❌ Where's the approval endpoint?
→ The code creates the request but doesn't have an endpoint
for the manager/director to approve it
→ Is it intentional or did AI forget to generate it?
⚠️ What happens if the amount is modified after creation?
→ If a $500 request (auto-approved) is modified to $5,000
→ Is the approval re-evaluated?
STEP 4 — Business edge cases:
⚠️ Who is "the manager" and "the director"?
→ Of the request's creator? Of the department?
→ What happens if there's no assigned manager?
⚠️ Is there an approval timeout?
→ If the manager doesn't approve within 48 hours, does it escalate?
⚠️ Can the creator approve their own request?
→ If the creator IS the manager, conflict of interest?
Scenario 3: Shipping Calculation
Requirement: "Free shipping on orders over $50. For smaller orders, shipping is $5 standard or $12 express. The maximum weight per package is 30kg."
AI-generated code:
from decimal import Decimal
from pydantic import BaseModel, Field
from enum import Enum
class ShippingMethod(str, Enum):
STANDARD = "standard"
EXPRESS = "express"
class ShippingRequest(BaseModel):
order_total: Decimal = Field(..., gt=0)
weight_kg: Decimal = Field(..., gt=0)
shipping_method: ShippingMethod = ShippingMethod.STANDARD
class ShippingResponse(BaseModel):
shipping_cost: Decimal
is_free: bool
method: ShippingMethod
packages: int
def calculate_shipping(request: ShippingRequest) -> ShippingResponse:
if request.order_total > Decimal("50"):
return ShippingResponse(
shipping_cost=Decimal("0"),
is_free=True,
method=request.shipping_method,
packages=1,
)
if request.shipping_method == ShippingMethod.STANDARD:
base_cost = Decimal("5")
else:
base_cost = Decimal("12")
packages = int(request.weight_kg / Decimal("30")) + 1
return ShippingResponse(
shipping_cost=base_cost * packages,
is_free=False,
method=request.shipping_method,
packages=packages,
)
Step-by-step verification:
STEP 2 — Trace:
- total=$60, weight=5kg, standard
→ total > 50 → free, packages=1 ✅
- total=$30, weight=5kg, standard
→ base_cost=$5, packages = int(5/30)+1 = 0+1 = 1
→ shipping = $5 × 1 = $5 ✅
- total=$30, weight=5kg, express
→ base_cost=$12, packages = int(5/30)+1 = 1
→ shipping = $12 × 1 = $12 ✅
STEP 3 — Critical questions:
❌ Free shipping: does it also apply to express?
→ The code gives free shipping for ANY method if total > $50
→ The requirement says "free shipping" without specifying the method
→ Does the business want express free too? Probably not.
❌ Packages in free shipping: packages=1 (hardcoded)
→ If a $60 order weighs 70kg, is it 1 or 3 packages?
→ Shipping is free but is there no weight limit?
→ The code ignores the weight when there's free shipping
❌ Package calculation: int(weight/30) + 1
→ weight=30 → int(30/30)+1 = 1+1 = 2 packages
→ But 30kg fits in 1 package (the maximum is 30kg)
→ It should be: math.ceil(weight/30)
→ Or: int(weight/30) + (1 if weight % 30 > 0 else 0)
⚠️ Shipping cost scales with packages
→ 60kg, standard → 2 packages × $5 = $10
→ Is it correct to charge $10? Or is it a flat $5 regardless of weight?
→ The requirement says "$5 standard" — it doesn't mention weight
→ Does the business charge per additional package?
⚠️ Boundary: total = exactly $50
→ > 50 is False → charges shipping
→ "Over $50" → > is correct
→ But does the business expect $50.00 to be free?
Common Error Patterns in AI Business Logic
┌────────────────────────────────────┬────────────────────────────────────┐
│ Error Pattern │ Example │
├────────────────────────────────────┼────────────────────────────────────┤
│ Incorrect boundary (> vs >=) │ "over 100" → >= instead of > │
│ Discount applied to wrong field │ To unit price instead of the total │
│ Allowed state that shouldn't be │ cancelled → active │
│ Silent fallback │ Unknown role → default permission │
│ Float for money │ 0.1 + 0.2 ≠ 0.3 │
│ Solving a similar problem │ Progressive discount vs flat │
│ Ignoring the equality case │ Is exactly 50 free shipping? │
│ Calculating in the wrong order │ Discount before tax vs after │
│ Omitting a business rule │ Maximum discount not applied │
│ Assuming complete data │ User with no shipping address │
└────────────────────────────────────┴────────────────────────────────────┘
Connection to the Project
Logic verification in the capstone project (Module 8)
The codebase of the capstone project has 3-4 business logic problems planted intentionally. These are the hardest problems to find because the code looks correct and can pass basic tests.
Example of the type of problem you'll find:
- A filter that includes when it should exclude (or vice versa)
- A statistical calculation that uses the almost-correct formula
- A validation that accepts data that should be rejected
- A state transition that shouldn't be possible
Your 4-step process is your main tool for finding these problems.
Troubleshooting
Problem 1: "I don't have the requirements written down — how do I verify?"
Cause: In many projects the requirements are in someone's head, not in a document. Solution: Write the requirements yourself before the review. 5 minutes writing "this should do X, Y, Z" saves you 30 minutes of aimless review. If you don't know the requirements, ask. Don't do a business logic code review without understanding the business.
Problem 2: "Tracing is tedious for long functions"
Cause: Functions over 20-30 lines are hard to trace mentally.
Solution: Don't trace the whole function. Trace the decision points: each if, each calculation, each comparison. Those are the points where logic errors hide. A 50-line function probably has 5-8 decision points — trace those.
Problem 3: "I don't know if a business edge case is relevant or not"
Cause: Business edge cases depend on the business context, which you might not fully know. Solution: If an edge case seems possible to you, document it as a question in the review. "What happens if the customer returns units and loses the volume discount?" You don't need the answer — you need someone from the business to give it. Your job is to find the question, not answer it.
Problem 4: "AI generated logic that looks correct but 'feels' off"
Cause: Your intuition detects something your conscious analysis hasn't identified yet. Solution: Trust the intuition and go deeper. "It feels off" almost always means there's something off. Trace with more values, look for edge cases, compare with requirements. Your experience as a developer gives you intuition that formal analysis complements.
Problem 5: "The tests pass — do I really need to verify the logic manually?"
Cause: Tests generate false confidence, especially if AI also generated them. Solution: Yes, you need to verify manually. The tests tell you the code does what the tests expect. Manual verification tells you the code does what the business expects. If AI generated both the code and the tests, they're two opinions from the same source — not independent verification.
Exercises
Exercise 1: Verify a simple requirement (Easy)
Requirement: "Users over 18 years old can create an account. Minors can't."
from datetime import date
from pydantic import BaseModel
class UserRegistration(BaseModel):
name: str
birth_date: date
def can_register(registration: UserRegistration) -> bool:
today = date.today()
age = today.year - registration.birth_date.year
return age >= 18
Apply the 4 steps and find the logic problems.
See solution
Step 1 — Requirements: Over 18 → can register. Under 18 → can't.
Step 2 — Trace:
- birth_date = 2000-06-15, today = 2026-03-14 → age = 2026 - 2000 = 26 → True ✅
- birth_date = 2010-01-01, today = 2026-03-14 → age = 2026 - 2010 = 16 → False ✅
Step 3 — Critical questions:
❌ Incorrect age calculation. It only subtracts years, doesn't consider month and day.
- birth_date = 2008-06-15, today = 2026-03-14
- age = 2026 - 2008 = 18 → True
- But the person turns 18 on June 15, 2026 — today they're 17
- The code says they can register while they're still a minor
❌ >= 18 vs > 18. "Over 18" → does 18 count as over or not? In most jurisdictions, "of legal age" includes 18 (turned). But "over 18" would technically be > 18 = 19+. Does the requirement say "over 18" or "18 years old or more"? Needs clarification.
Step 4 — Correct calculation:
def can_register(registration: UserRegistration) -> bool:
today = date.today()
age = (
today.year - registration.birth_date.year
- (
(today.month, today.day)
< (registration.birth_date.month, registration.birth_date.day)
)
)
return age >= 18
Exercise 2: Verify a complex workflow (Medium)
Requirement: "A discount coupon can be used a maximum of 3 times per user and has an expiration date. The discount can't exceed 50% of the subtotal."
from decimal import Decimal
from datetime import datetime, timezone
from pydantic import BaseModel
from typing import Optional
class Coupon(BaseModel):
code: str
discount_percent: Decimal
expires_at: datetime
max_uses_per_user: int = 3
def apply_coupon(
subtotal: Decimal,
coupon: Coupon,
user_id: str,
usage_count: int,
) -> Decimal:
if datetime.now(timezone.utc) > coupon.expires_at:
raise ValueError("Coupon expired")
if usage_count >= coupon.max_uses_per_user:
raise ValueError("Coupon usage limit reached")
discount = subtotal * coupon.discount_percent / Decimal("100")
max_discount = subtotal * Decimal("0.50")
if discount > max_discount:
discount = max_discount
return subtotal - discount
Apply the 4 steps. There are at least 2 logic problems.
See solution
Step 2 — Trace:
-
subtotal=$100, discount_percent=20, usage_count=0 → discount = $100 × 20 / 100 = $20 → max = $100 × 0.50 = $50 → $20 < $50 → discount = $20 → return $100 - $20 = $80 ✅
-
subtotal=$100, discount_percent=60, usage_count=0 → discount = $100 × 60 / 100 = $60 → max = $50 → $60 > $50 → discount = $50 → return $100 - $50 = $50 ✅ (capped at 50%)
Step 3 — Problems found:
❌ Doesn't increment the usage counter. The function VERIFIES that usage_count < max_uses_per_user, but doesn't INCREMENT the counter. If the caller doesn't increment it, the user can use the coupon infinite times. Who's responsible for incrementing? Is it atomic with applying the discount?
❌ Doesn't validate discount_percent. If discount_percent is negative (-20), the "discount" increases the price: $100 × (-20) / 100 = -$20, max_discount = $50, -$20 < $50, return $100 - (-$20) = $120. The user pays more for using a "coupon." If discount_percent is 0, it returns the subtotal with no discount (correct but useless).
⚠️ Exact expiration. If expires_at = 2026-03-14 00:00:00 and now is 2026-03-14 00:00:01, the coupon expired. Does the business expect it to expire at the end of the day or the beginning? Generally, "expires on March 14" means it works throughout the 14th.
⚠️ Return value. The function returns the final total, not the discount amount. Does the caller need to know how much the discount was? To show the user "You saved $20" they'd need to do the subtraction.
Exercise 3: Verify permissions (Medium)
Requirement: "Users can view their own data. Managers can view data of users in their department. Admins can view everything."
from fastapi import FastAPI, HTTPException, Depends
from enum import Enum
app = FastAPI()
class Role(str, Enum):
USER = "user"
MANAGER = "manager"
ADMIN = "admin"
def can_view_user(viewer: dict, target_user_id: str) -> bool:
if viewer["role"] == Role.ADMIN:
return True
if viewer["id"] == target_user_id:
return True
if viewer["role"] == Role.MANAGER:
target = get_user(target_user_id)
return target["department"] == viewer["department"]
return False
@app.get("/users/{user_id}")
async def get_user_profile(
user_id: str,
current_user: dict = Depends(get_current_user),
):
if not can_view_user(current_user, user_id):
raise HTTPException(status_code=403, detail="Not authorized")
user = get_user(user_id)
return user
Find at least 2 business logic problems.
See solution
❌ Double fetch of the target user. can_view_user calls get_user(target_user_id) to check the department, and then get_user_profile calls get_user(user_id) again. If the user doesn't exist, can_view_user does the get (could crash with None), and then it's done again in the endpoint. It should check existence first.
❌ Returns ALL the user's data. The endpoint returns the complete user. Should a manager see the salary, personal_email, SSN of their reports? There are probably fields the manager can see and fields they can't. The requirement says "data" but which data?
⚠️ A manager can view themselves through two paths. If viewer.id == target_user_id, it returns True (self-view). If the viewer is a manager of the same department, it also returns True (manager-view). It works but they're two code paths for the same result — should the response be different? (e.g., viewing your own profile shows everything, viewing a report shows less).
⚠️ What happens if the target has no department? If target["department"] is None and viewer["department"] is None, the comparison None == None is True. A manager with no department could view users with no department.
⚠️ Admin doesn't need the target to exist. If viewer is admin, can_view_user returns True without verifying that target_user_id exists. Then get_user(user_id) could return None, and the endpoint would return None as the response.
Exercise 4: Your own business scenario (Hard)
Choose a business rule from your work and ask Claude Code to implement it. Then apply the 4-step process:
- Write the requirements BEFORE generating the code
- Generate the code with Claude Code
- Trace the flow with 3 inputs
- Ask the critical questions
- Document the business edge cases
See evaluation guide
Your verification should include:
- ✅ Requirements written before reading the code (Step 1)
- ✅ Trace with at least 3 different inputs (Step 2)
- ✅ At least 3 critical business questions (Step 3)
- ✅ At least 2 business edge cases identified (Step 4)
- ✅ Conclusion: is the code correct, partially correct, or incorrect?
A sign you did it well: If you found at least one discrepancy between your requirements and the generated code. If you didn't find any, your requirement was too simple or your analysis wasn't deep enough.
Summary
In this capsule you learned:
- Verifying business logic is the hardest and most important part of AI code review
- No automatic tool detects business logic errors — you're the last line of defense
- If AI generates the code AND the tests, the tests are circular verification, not independent
- The 4-step process: understand requirements → trace flow → verify behavior → test business edge cases
- Write the requirements BEFORE reading the code — it's the #1 cause of incomplete reviews
- Boundary errors (> vs >=) are the most frequent logic error in AI code
- Business edge cases are different from technical edge cases — they're about the business, not the code
- Your intuition as a developer is a valid tool — if something "feels off", go deeper
Next capsule: Exercise — Code Review of a complete PR generated by Claude Code.
Additional resources
- Domain-Driven Design — Eric Evans - The book that teaches how to model business logic in code
- Writing Effective Requirements - How to write requirements you can verify
- Boundary Value Analysis - A technique for finding errors at boundaries
- Property-Based Testing with Hypothesis - Testing that generates inputs automatically to find edge cases
- OWASP — Business Logic Vulnerabilities - Business logic vulnerabilities
Debugging & Code Review with Claude Code — Module 4, Capsule 05 Claude Code Agentic Development Path — Guide #6 of 11