Módulo 4: Code Review de Output AI

Verificar Lógica de Negocio: La Parte Más Difícil del Review

Verificar Lógica de Negocio: La Parte Más Difícil del Review

Descripción de la cápsula

De todo lo que revisas en código generado por AI, la lógica de negocio es lo más difícil de verificar y lo más peligroso de ignorar. Un SQL injection lo detecta un scanner. Un import falso lo detecta el linter. Una función deprecated la detecta un type checker. Pero si el código aplica un descuento al precio unitario en vez del total, ninguna herramienta lo detecta. Solo tú, que entiendes el negocio, puedes verificar que el código hace lo que el negocio necesita.

Esta cápsula te enseña un proceso de 4 pasos para verificar lógica de negocio en código AI: entender los requisitos, trazar el flujo del código, verificar el comportamiento, y testear edge cases de negocio. Al final, tendrás un framework que puedes aplicar cada vez que Claude Code genere código con reglas de negocio.


Por Qué Es Tan Difícil

El linter no te ayuda

# Este código pasa TODOS los checks automáticos:
# ✅ Linter: sin errores
# ✅ Type checker: tipos correctos
# ✅ Formatter: bien formateado
# ✅ Import checker: imports válidos
# ✅ Tests (si AI los generó): pasan

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

# Pero... ¿el negocio dice 5%, 8%, 12%?
# ¿O dice 5%, 8%, 10%? 
# ¿El fallback a 5% es correcto o debería ser un error?
# ¿La comisión se calcula sobre sale_amount bruto o neto?
# Solo tú puedes verificar esto.

Los tests generados por AI tampoco ayudan

Si AI genera el código Y los tests, los tests verifican lo que AI cree que es correcto — no lo que el negocio necesita. Es un circuito cerrado sin validación externa.

# AI genera el código:
def apply_discount(total: Decimal, code: str) -> Decimal:
    if code == "SAVE20":
        return total * Decimal("0.80")  # 20% descuento
    return total

# AI genera el test:
def test_discount():
    result = apply_discount(Decimal("100"), "SAVE20")
    assert result == Decimal("80.00")  # ✅ Pasa — pero...
    
# ¿"SAVE20" es 20% off o $20 off?
# El test confirma lo que el código hace, no lo que debería hacer.
# Si SAVE20 es "$20 de descuento" (no 20%), el resultado correcto 
# sería Decimal("80.00") solo para total=100. 
# Para total=200 sería 180, no 160.

Eres la última línea de defensa

Capas de verificación:

Linter        → Detecta errores sintácticos
Type Checker  → Detecta errores de tipos
Tests         → Detecta errores de comportamiento (si los tests son correctos)
Code Review   → Detecta errores de lógica y seguridad
──────────────────────────────────────────────────
TÚ            → Verificas que el código cumple los requisitos del NEGOCIO

Si tú no verificas la lógica de negocio, nadie lo hace.

El Proceso de 4 Pasos

Paso 1: Entender los Requisitos (Antes de Leer Código)

Antes de mirar una sola línea de código, asegúrate de entender qué debería hacer. Este paso se salta frecuentemente y es la causa #1 de reviews de lógica de negocio incompletos.

ANTES de leer el código, responde:

1. ¿Qué debe HACER esta funcionalidad?
   → Descripción en 2-3 oraciones simples

2. ¿Qué REGLAS de negocio aplican?
   → Lista de reglas con condiciones y resultados esperados

3. ¿Qué DATOS de entrada recibe?
   → Tipos, rangos válidos, campos opcionales vs requeridos

4. ¿Qué RESULTADO produce?
   → Formato, valores esperados para inputs conocidos

5. ¿Qué NO debe hacer?
   → Comportamientos prohibidos, estados inválidos

Ejemplo aplicado:

Funcionalidad: "Sistema de precios con descuento por volumen."

1. QUÉ HACE:
   Calcula el precio total de una orden aplicando descuentos 
   progresivos basados en la cantidad de unidades compradas.

2. REGLAS:
   - 1-9 unidades: precio completo ($10/unidad)
   - 10-49 unidades: 10% descuento ($9/unidad)
   - 50-99 unidades: 20% descuento ($8/unidad)
   - 100+ unidades: 30% descuento ($7/unidad)
   - El descuento aplica a TODAS las unidades, no solo a las extras

3. DATOS DE ENTRADA:
   - quantity: int, >= 1
   - unit_price: Decimal, > 0 (default $10)
   
4. RESULTADO:
   - total: Decimal (quantity × unit_price × (1 - discount_rate))
   - Ejemplos: 5 unidades = $50, 10 unidades = $90, 50 unidades = $400
   
5. QUÉ NO DEBE HACER:
   - Aplicar descuento solo a unidades por encima del umbral
   - Aceptar quantity <= 0
   - Usar float para cálculos de dinero

Paso 2: Trazar el Flujo del Código (Lectura Activa)

Lee el código línea por línea con los requisitos en mente. No leas pasivamente — traza el flujo con valores concretos.

PROCESO DE TRAZADO:

1. Identificar la función/endpoint principal
2. Elegir 3 inputs representativos:
   - Un caso normal (happy path)
   - Un caso en el límite (boundary)
   - Un caso extremo (edge case)
3. Para cada input, seguir el flujo línea por línea
4. Comparar el resultado del trazado con el resultado esperado

Ejemplo aplicado:

Claude Code genera:

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,
    )

Trazado con 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
└── ✅ CORRECTO (5 unidades = $50, sin descuento)

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
└── ✅ CORRECTO (10 unidades = $90, 10% descuento)

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
└── ✅ CORRECTO (100 unidades = $700, 30% descuento)

Resultado del trazado: El código es correcto para los 3 inputs. Aplica el descuento a todas las unidades (no solo las extras), lo cual coincide con el requisito.

Paso 3: Verificar el Comportamiento (Preguntas Críticas)

Después de trazar el flujo, haz preguntas que van más allá del happy path:

PREGUNTAS CRÍTICAS DE LÓGICA DE NEGOCIO:

1. ¿Qué pasa en los BOUNDARIES exactos?
   → quantity = 9 (sin descuento) vs quantity = 10 (con descuento)
   → ¿Es correcto que 9→$90 y 10→$90? (mismo total, diferente descuento)

2. ¿El resultado tiene SENTIDO de negocio?
   → ¿Es correcto que 9 unidades cuestan $90 y 10 cuestan $90?
   → Un cliente compraría 10 en vez de 9 (misma plata, 1 extra)
   → ¿El negocio quiere esto? Tal vez sí (incentivo a comprar más)

3. ¿Qué CAMPOS devuelve que podrían confundir?
   → discount_rate devuelve 0.10 — ¿el frontend espera 10 (porcentaje)?
   → ¿Los montos incluyen IVA o no?

4. ¿Qué pasa con CAMBIOS futuros?
   → ¿Qué pasa si agregan un tier nuevo entre 50-99?
   → max_quantity=999999 — ¿y si alguien pide 1,000,000?

5. ¿Las REGLAS están HARDCODED o son configurables?
   → Los tiers están en código — ¿deberían estar en DB?
   → Si el negocio cambia los porcentajes, hay que hacer deploy

Paso 4: Testear Edge Cases de Negocio

Los edge cases de negocio son diferentes a los edge cases técnicos. No es "¿qué pasa con None?" sino "¿qué pasa cuando un cliente devuelve 5 de 10 unidades y pierde el descuento por volumen?"

EDGE CASES DE NEGOCIO:

1. Devoluciones parciales
   → Cliente compró 50 unidades (20% descuento = $400)
   → Devuelve 10 → ahora tiene 40 unidades (10% descuento)
   → ¿Se recalcula el precio? ¿Cuánto se devuelve?

2. Cambios de precio
   → Cliente cotizó ayer a $10/unidad
   → Hoy el precio subió a $12/unidad
   → ¿Se respeta el precio cotizado?

3. Combinación de productos
   → ¿El descuento por volumen aplica por producto o por orden total?
   → 5 de producto A + 5 de producto B = ¿10 unidades con descuento?

4. Moneda y redondeo
   → 15 unidades × $9.99 × 10% descuento = ?
   → ¿Cómo se redondea? ¿Al centavo más cercano?
   → $9.99 × 15 = $149.85 × 0.90 = $134.865 → ¿$134.86 o $134.87?

5. Límites del sistema
   → ¿Hay un máximo de unidades por orden?
   → ¿Hay un máximo de monto total?

Tres Escenarios Completos

Escenario 1: Permisos y Roles

Requisito: "Los usuarios con rol 'editor' pueden editar artículos de su propio equipo. Los 'admin' pueden editar cualquier artículo. Los 'viewer' solo pueden leer."

Código generado por AI:

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

Verificación paso a paso:

PASO 1 — Requisitos:
- viewer: solo leer → check_edit_permission devuelve False ✅
- editor: editar artículos de su equipo → verifica team_id ✅
- admin: editar cualquier artículo → return True sin checks ✅

PASO 2 — Trazado:
- Admin edita artículo de otro equipo → True ✅
- Editor edita artículo de su equipo → team_id match → True ✅
- Editor edita artículo de otro equipo → team_id no match → False ✅
- Viewer intenta editar → False ✅

PASO 3 — Preguntas críticas:
⚠️ ¿Un editor puede editar CUALQUIER artículo de su equipo, 
   incluyendo los de otros editores?
   → El requisito dice "de su propio equipo" — ¿incluye artículos 
     escritos por otros miembros del equipo?
   → El código lo permite (solo verifica team_id, no author_id)
   → Necesita clarificación con el negocio

⚠️ ¿Qué pasa con roles futuros? (moderator, super_admin)
   → check_edit_permission devuelve False para roles desconocidos
   → ¿Correcto? Sí — fail closed (deny by default)

⚠️ ¿El viewer puede ver todos los artículos o solo los de su equipo?
   → El endpoint de UPDATE está protegido, pero ¿el GET?
   → No está en este código — verificar en otro archivo

PASO 4 — Edge cases de negocio:
⚠️ ¿Qué pasa si un editor cambia de equipo?
   → Pierde acceso a artículos de su equipo anterior
   → ¿Correcto? Probablemente sí, pero verificar
   
⚠️ ¿Qué pasa si un artículo cambia de equipo?
   → Los editores del equipo anterior pierden acceso
   → Los editores del equipo nuevo ganan acceso
   → ¿Hay un periodo de transición?

Escenario 2: Workflow de Aprobación

Requisito: "Las solicitudes de compra requieren aprobación. Montos menores a $1,000 se auto-aprueban. Entre $1,000 y $10,000 requieren aprobación del manager. Más de $10,000 requieren aprobación del director."

Código generado por AI:

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

Verificación paso a paso:

PASO 1 — Requisitos vs Código:
- < $1,000 → auto-aprobación ✅ (status=APPROVED, approved_by="auto-approved")
- $1,000-$10,000 → requiere manager ⚠️ (status=PENDING, ¿quién es el approver?)
- > $10,000 → requiere director ⚠️ (status=PENDING, ¿quién es el approver?)

PASO 2 — Trazado:
- amount = $500 → APPROVED, "auto-approved" ✅
- amount = $1,000 → PENDING, None ❓ ¿El boundary es < o <=?
- amount = $5,000 → PENDING, None ✅ (pero ¿cómo se asigna al manager?)
- amount = $15,000 → PENDING, None ✅ (pero ¿cómo se asigna al director?)

PASO 3 — Preguntas críticas:
❌ El requisito dice "menores a $1,000" — ¿$1,000 exactos se 
   auto-aprueban o no?
   → Código usa < 1000, así que $1,000 va a PENDING
   → ¿Es correcto? "Menores a $1,000" → sí, < es correcto
   → PERO el segundo rango dice "entre $1,000 y $10,000"
   → amount <= 10000 incluye $10,000 exactos en rango de manager
   → ¿El requisito dice "$10,000 requiere director" — 
     entonces $10,000 exactos van a manager o director?

❌ determine_approval no distingue entre manager y director
   → Ambos rangos (1k-10k y >10k) devuelven PENDING, None
   → ¿Quién aprueba? ¿Cómo se enruta al approver correcto?
   → El código no tiene routing de aprobación

❌ ¿Dónde está el endpoint de aprobación?
   → El código crea la solicitud pero no tiene un endpoint 
     para que el manager/director la apruebe
   → ¿Es intencional o AI olvidó generarlo?

⚠️ ¿Qué pasa si el monto se modifica después de crear?
   → Si una solicitud de $500 (auto-aprobada) se modifica a $5,000
   → ¿Se re-evalúa la aprobación?

PASO 4 — Edge cases de negocio:
⚠️ ¿Quién es "el manager" y "el director"?
   → ¿Del creador de la solicitud? ¿Del departamento?
   → ¿Qué pasa si no hay manager asignado?

⚠️ ¿Hay timeout de aprobación?
   → Si el manager no aprueba en 48 horas, ¿se escala?

⚠️ ¿El creador puede aprobar su propia solicitud?
   → Si el creador ES el manager, ¿conflict of interest?

Escenario 3: Cálculo de Envío

Requisito: "Envío gratis en órdenes mayores a $50. Para órdenes menores, el envío es $5 estándar o $12 express. El peso máximo por paquete es 30kg."

Código generado por AI:

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,
    )

Verificación paso a paso:

PASO 2 — Trazado:
- 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 ✅

PASO 3 — Preguntas críticas:
❌ Envío gratis: ¿aplica también a express?
   → El código da envío gratis para CUALQUIER método si total > $50
   → El requisito dice "envío gratis" sin especificar método
   → ¿El negocio quiere express gratis también? Probablemente no.

❌ Paquetes en envío gratis: packages=1 (hardcoded)
   → Si un pedido de $60 pesa 70kg, ¿son 1 o 3 paquetes?
   → El envío es gratis pero ¿no hay límite de peso?
   → El código ignora el peso cuando hay envío gratis

❌ Cálculo de paquetes: int(weight/30) + 1
   → weight=30 → int(30/30)+1 = 1+1 = 2 paquetes
   → Pero 30kg cabe en 1 paquete (el máximo es 30kg)
   → Debería ser: math.ceil(weight/30)
   → O: int(weight/30) + (1 if weight % 30 > 0 else 0)

⚠️ Costo de envío escala con paquetes
   → 60kg, standard → 2 paquetes × $5 = $10
   → ¿Es correcto cobrar $10? ¿O es $5 fijo sin importar peso?
   → El requisito dice "$5 estándar" — no menciona peso
   → ¿El negocio cobra por paquete adicional?

⚠️ Boundary: total = $50 exactos
   → > 50 es False → cobra envío
   → "Mayores a $50" → > es correcto
   → Pero ¿el negocio espera que $50.00 sea gratis?

Patrones Comunes de Error en Lógica de Negocio AI

┌────────────────────────────────────┬────────────────────────────────────┐
│ Patrón de Error                    │ Ejemplo                            │
├────────────────────────────────────┼────────────────────────────────────┤
│ Boundary incorrecto (> vs >=)      │ "mayor a 100" → >= en vez de >     │
│ Descuento aplicado al campo wrong  │ Al unitario en vez del total       │
│ Estado permitido que no debería    │ cancelled → active                 │
│ Fallback silencioso                │ Rol desconocido → permiso default  │
│ Float para dinero                  │ 0.1 + 0.2 ≠ 0.3                   │
│ Resolver problema parecido        │ Descuento progresivo vs flat       │
│ Ignorar caso de igualdad          │ ¿50 exactos es envío gratis?       │
│ Calcular en orden incorrecto      │ Descuento antes de tax vs después  │
│ Omitir regla de negocio           │ Máximo de descuento no aplicado    │
│ Asumir datos completos            │ User sin dirección de envío        │
└────────────────────────────────────┴────────────────────────────────────┘

Conexión con Proyecto

Verificación de lógica en el proyecto integrador (Módulo 8)

El codebase del proyecto integrador tiene 3-4 problemas de lógica de negocio plantados intencionalmente. Estos son los problemas más difíciles de encontrar porque el código se ve correcto y puede pasar tests básicos.

Ejemplo del tipo de problema que encontrarás:

  • Un filtro que incluye cuando debería excluir (o viceversa)
  • Un cálculo estadístico que usa la fórmula casi-correcta
  • Una validación que acepta datos que deberían ser rechazados
  • Una transición de estado que no debería ser posible

Tu proceso de 4 pasos es tu herramienta principal para encontrar estos problemas.


Troubleshooting

Problema 1: "No tengo los requisitos escritos — ¿cómo verifico?"

Causa: En muchos proyectos los requisitos están en la cabeza de alguien, no en un documento. Solución: Escribe los requisitos tú antes del review. 5 minutos escribiendo "esto debería hacer X, Y, Z" te ahorran 30 minutos de review sin dirección. Si no sabes los requisitos, pregunta. No hagas code review de lógica de negocio sin entender el negocio.

Problema 2: "El trazado es tedioso para funciones largas"

Causa: Funciones con más de 20-30 líneas son difíciles de trazar mentalmente. Solución: No traces toda la función. Traza los decision points: cada if, cada cálculo, cada comparación. Son los puntos donde los errores de lógica se esconden. Una función de 50 líneas probablemente tiene 5-8 decision points — traza esos.

Problema 3: "No sé si un edge case de negocio es relevante o no"

Causa: Los edge cases de negocio dependen del contexto del negocio, que tú podrías no conocer completamente. Solución: Si un edge case te parece posible, documéntalo como pregunta en el review. "¿Qué pasa si el cliente devuelve unidades y pierde el descuento por volumen?" No necesitas la respuesta — necesitas que alguien del negocio la dé. Tu trabajo es encontrar la pregunta, no responderla.

Problema 4: "AI generó lógica que parece correcta pero 'se siente' rara"

Causa: Tu intuición detecta algo que tu análisis consciente no identifica aún. Solución: Confía en la intuición y profundiza. "Se siente raro" casi siempre significa que hay algo raro. Traza con más valores, busca edge cases, compara con requisitos. Tu experiencia como developer te da intuición que el análisis formal complementa.

Problema 5: "Los tests pasan — ¿realmente necesito verificar la lógica manualmente?"

Causa: Los tests generan falsa confianza, especialmente si AI también los generó. Solución: Sí, necesitas verificar manualmente. Los tests te dicen que el código hace lo que los tests esperan. La verificación manual te dice que el código hace lo que el negocio espera. Si AI generó tanto el código como los tests, son dos opiniones de la misma fuente — no verificación independiente.


Ejercicios

Ejercicio 1: Verificar requisito simple (Fácil)

Requisito: "Los usuarios mayores de 18 años pueden crear cuenta. Los menores no."

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

Aplica los 4 pasos y encuentra los problemas de lógica.

Ver solución

Paso 1 — Requisitos: Mayores de 18 → pueden registrarse. Menores de 18 → no.

Paso 2 — Trazado:

  • 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 ✅

Paso 3 — Preguntas críticas:

❌ Cálculo de edad incorrecto. Solo resta años, no considera mes y día.

  • birth_date = 2008-06-15, today = 2026-03-14
  • age = 2026 - 2008 = 18 → True
  • Pero la persona cumple 18 el 15 de junio de 2026 — hoy tiene 17 años
  • El código dice que puede registrarse cuando aún es menor

❌ >= 18 vs > 18. "Mayores de 18" → ¿18 cuenta como mayor o no? En la mayoría de jurisdicciones, "mayor de edad" incluye 18 (cumplidos). Pero "mayor de 18" técnicamente sería > 18 = 19+. ¿El requisito dice "mayores de 18" o "de 18 años o más"? Necesita clarificación.

Paso 4 — Cálculo correcto:

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

Ejercicio 2: Verificar workflow complejo (Medio)

Requisito: "Un cupón de descuento puede usarse máximo 3 veces por usuario y tiene fecha de expiración. El descuento no puede superar el 50% del 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

Aplica los 4 pasos. Hay al menos 2 problemas de lógica.

Ver solución

Paso 2 — Trazado:

  • 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 ✅ (cap al 50%)

Paso 3 — Problemas encontrados:

❌ No incrementa el contador de uso. La función VERIFICA que usage_count < max_uses_per_user, pero no INCREMENTA el contador. Si el caller no lo incrementa, el usuario puede usar el cupón infinitas veces. ¿Quién es responsable de incrementar? ¿Es atómico con la aplicación del descuento?

❌ No valida discount_percent. Si discount_percent es negativo (-20), el "descuento" incrementa el precio: $100 × (-20) / 100 = -$20, max_discount = $50, -$20 < $50, return $100 - (-$20) = $120. El usuario paga más por usar un "cupón." Si discount_percent es 0, devuelve subtotal sin descuento (correcto pero inútil).

⚠️ Expiración exacta. Si expires_at = 2026-03-14 00:00:00 y ahora es 2026-03-14 00:00:01, el cupón expiró. ¿El negocio espera que expire al final del día o al inicio? Generalmente, "expira el 14 de marzo" significa que funciona durante todo el 14.

⚠️ Return value. La función devuelve el total final, no el monto del descuento. ¿El caller necesita saber cuánto fue el descuento? Para mostrar al usuario "Ahorraste $20" necesitaría hacer la resta.

Ejercicio 3: Verificar permisos (Medio)

Requisito: "Los usuarios pueden ver sus propios datos. Los managers pueden ver datos de usuarios de su departamento. Los admins pueden ver todo."

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

Encuentra al menos 2 problemas de lógica de negocio.

Ver solución

❌ Double fetch del target user. can_view_user llama get_user(target_user_id) para verificar departamento, y luego get_user_profile vuelve a llamar get_user(user_id). Si el usuario no existe, can_view_user hace el get (podría crashear con None), y luego se hace otra vez en el endpoint. Debería verificar existencia primero.

❌ Devuelve TODOS los datos del usuario. El endpoint devuelve el user completo. ¿Un manager debería ver salary, personal_email, SSN de sus reportes? Probablemente hay campos que el manager puede ver y campos que no. El requisito dice "datos" pero ¿cuáles datos?

⚠️ Manager puede verse a sí mismo por dos caminos. Si viewer.id == target_user_id, devuelve True (self-view). Si viewer es manager del mismo departamento, también devuelve True (manager-view). Funciona pero son dos code paths para el mismo resultado — ¿el response debería ser diferente? (ej: ver tu propio perfil muestra todo, ver un reporte muestra menos).

⚠️ ¿Qué pasa si el target no tiene departamento? Si target["department"] es None y viewer["department"] es None, la comparación None == None es True. Un manager sin departamento podría ver usuarios sin departamento.

⚠️ El admin no necesita existir el target. Si viewer es admin, can_view_user devuelve True sin verificar que target_user_id existe. Luego get_user(user_id) podría devolver None, y el endpoint devolvería None como response.

Ejercicio 4: Tu propio escenario de negocio (Difícil)

Elige una regla de negocio de tu trabajo y pide a Claude Code que la implemente. Luego aplica el proceso de 4 pasos:

  1. Escribe los requisitos ANTES de generar el código
  2. Genera el código con Claude Code
  3. Traza el flujo con 3 inputs
  4. Haz las preguntas críticas
  5. Documenta los edge cases de negocio
Ver guía de evaluación

Tu verificación debería incluir:

  • ✅ Requisitos escritos antes de leer código (Paso 1)
  • ✅ Trazado con al menos 3 inputs diferentes (Paso 2)
  • ✅ Al menos 3 preguntas críticas de negocio (Paso 3)
  • ✅ Al menos 2 edge cases de negocio identificados (Paso 4)
  • ✅ Conclusión: ¿el código es correcto, parcialmente correcto, o incorrecto?

Señal de que lo hiciste bien: Si encontraste al menos una discrepancia entre tus requisitos y el código generado. Si no encontraste ninguna, tu requisito era demasiado simple o tu análisis no fue suficientemente profundo.


Resumen

En esta cápsula aprendiste:

  • Verificar lógica de negocio es la parte más difícil y más importante del code review de AI
  • Ninguna herramienta automática detecta errores de lógica de negocio — eres la última línea de defensa
  • Si AI genera el código Y los tests, los tests son verificación circular, no independiente
  • El proceso de 4 pasos: entender requisitos → trazar flujo → verificar comportamiento → testear edge cases de negocio
  • Escribe los requisitos ANTES de leer el código — es la causa #1 de reviews incompletos
  • Los errores de boundaries (> vs >=) son el error de lógica más frecuente en código AI
  • Los edge cases de negocio son diferentes a los edge cases técnicos — son sobre el negocio, no sobre el código
  • Tu intuición como developer es una herramienta válida — si algo "se siente raro", profundiza

Próxima cápsula: Ejercicio — Code Review de un PR completo generado por Claude Code.


Recursos Adicionales

  1. Domain-Driven Design — Eric Evans - El libro que enseña a modelar lógica de negocio en código
  2. Writing Effective Requirements - Cómo escribir requisitos que puedas verificar
  3. Boundary Value Analysis - Técnica para encontrar errores en boundaries
  4. Property-Based Testing with Hypothesis - Testing que genera inputs automáticamente para encontrar edge cases
  5. OWASP — Business Logic Vulnerabilities - Vulnerabilidades de lógica de negocio

Debugging & Code Review with Claude Code — Módulo 4, Cápsula 05 Claude Code Agentic Development Path — Guía #6 de 11