Revisión de Código F#

CultivaPay API — Módulo de Transacciones
PR #47 · feature/payment-transaction-module
BLOQUEADO
3 CRITICAL · 4 HIGH
requiere correcciones
3
Críticos
4
Altos
3
Medios
6
Superados

Archivos Revisados

src/CultivaPay.Domain/TransactionService.fs
src/CultivaPay.Api/PaymentHandlers.fs
Críticos — Seguridad
CRITICAL Inyección SQL por interpolación de cadenas TransactionService.fs:38

La consulta de búsqueda de transacciones construye SQL concatenando directamente el parámetro merchantId proporcionado por el usuario en el cuerpo de la petición. Un atacante puede inyectar SQL arbitrario para exfiltrar o destruir datos.

F# ❌ Código actual
let getTransactionsByMerchant (db: DbContext) (merchantId: string) =
    let sql = $"SELECT * FROM transactions WHERE merchant_id = '{merchantId}'"
    db.Database.ExecuteSqlRaw(sql)  // ⚠ SQL injection directo
F# ✅ Corrección
let getTransactionsByMerchant (db: DbContext) (merchantId: MerchantId) =
    db.Transactions
        .Where(fun t -> t.MerchantId = merchantId.Value)
        .AsNoTracking()
        .ToListAsync()  // Consulta parametrizada vía LINQ/EF Core

Usar LINQ o FromSqlInterpolated (no ExecuteSqlRaw) con parámetros tipados. Además envuelve merchantId en un tipo de dominio MerchantId (single-case DU) para evitar confusiones futuras.

CRITICAL Path traversal en descarga de recibos PaymentHandlers.fs:112

El handler de descarga de recibos acepta el nombre de fichero desde el query string y lo concatena directamente al directorio base sin validar que el path resultante esté dentro del directorio permitido. Permite GET /receipts?file=../../appsettings.json.

F# ❌ Código actual
let baseDir = "/var/cultivapay/receipts"
let filePath = Path.Combine(baseDir, fileName) // sin validar
let content = File.ReadAllBytes(filePath)
F# ✅ Corrección
let safeReadReceipt baseDir fileName =
    let fullPath = Path.GetFullPath(Path.Combine(baseDir, fileName))
    if not (fullPath.StartsWith(Path.GetFullPath(baseDir) + Path.DirectorySeparatorChar.ToString())) then
        Error "Ruta de fichero no permitida"
    else
        Ok (File.ReadAllBytes(fullPath))

Siempre aplicar Path.GetFullPath y verificar que el resultado tenga como prefijo el directorio base canonicalizado. Retornar Result<byte[], string> en lugar de lanzar excepción.

CRITICAL Clave de API hardcoded en código fuente TransactionService.fs:14

La API key del proveedor de pagos externo (Stripe) está hardcoded como literal de cadena en el módulo de servicio. Cualquier persona con acceso al repositorio puede obtenerla, incluyendo colaboradores externos o bots de GitHub.

F#❌ Código actual
let stripeApiKey = "sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
F#✅ Corrección
let getStripeKey (config: IConfiguration) =
    config.["Stripe:ApiKey"]
    |> Option.ofObj
    |> Result.ofOption "Stripe:ApiKey no configurada"

Usar IConfiguration inyectado + Azure Key Vault o variables de entorno. Rotar inmediatamente la clave comprometida. Añadir pre-commit hook con gitleaks para prevención automática.

Altos — Idiomática Funcional y Tipos
HIGH Bloqueo de async con .Result — riesgo de deadlock PaymentHandlers.fs:67

El handler Giraffe usa .Result para esperar la tarea asíncrona de guardado de transacción, bloqueando el hilo del ThreadPool y pudiendo causar deadlock bajo carga.

F#❌ Código actual
let result = saveTransactionAsync db tx .Result  // bloqueo síncrono
F#✅ Corrección
let processPaymentHandler: HttpHandler =
    fun next ctx ->
        task {
            let! body = ctx.BindJsonAsync<PaymentRequest>()
            let! result = saveTransactionAsync db body.ToTransaction()
            return! json result next ctx
        }

Usar task { let! … } de Giraffe para mantener el flujo completamente asíncrono.

HIGH Primitive obsession — IDs sin tipo de dominio TransactionService.fs:22–55

transactionId, merchantId y customerId son simples string, lo que permite pasarlos en el orden incorrecto sin que el compilador lo detecte. En una pasarela de pagos, confundir IDs tiene consecuencias graves.

F#❌ Código actual
let processPayment (txId: string) (merchantId: string) (customerId: string) = ...
F#✅ Corrección
type TransactionId = TransactionId of string
type MerchantId    = MerchantId    of string
type CustomerId    = CustomerId    of string

let processPayment (txId: TransactionId) (merchantId: MerchantId) (customerId: CustomerId) = ...

Single-case DUs (Discriminated Unions) hacen que confundir IDs sea un error de compilación, sin coste en runtime.

HIGH Excepción silenciada — fallo de pago invisible TransactionService.fs:88

El bloque de captura de error del procesamiento de pago captura con with _ -> None, haciendo que cualquier fallo (timeout de red, error de base de datos) sea silencioso e indistinguible de un pago correctamente completado.

F#❌ Código actual
let chargeCard amount cardToken =
    try stripeClient.Charge(amount, cardToken) |> Some
    with _ -> None  // ⚠ excepción completamente silenciada
F#✅ Corrección
let chargeCard amount cardToken =
    try
        stripeClient.Charge(amount, cardToken) |> Ok
    with
    | :? StripeException as ex -> Error (StripeError ex.Message)
    | :? TimeoutException    -> Error NetworkTimeout
    | ex                     -> Error (Unexpected ex.Message)

Retornar Result<ChargeResponse, PaymentError> con variantes de error explícitas. El caller debe manejar cada caso.

HIGH Bucle imperativo donde List.map es la expresión idiomática TransactionService.fs:134

La transformación de DTO a entidad de dominio usa un for con acumulador mutable en lugar de las funciones de colección estándar de F#, añadiendo complejidad innecesaria.

F#❌ Código actual
let mutable results = []
for dto in dtos do
    results <- results @ [mapToTransaction dto]  // O(n²) y mutable
F#✅ Corrección
let results = dtos |> List.map mapToTransaction

Una sola línea, inmutable, y O(n) en lugar de O(n²) por la concatenación con @.

Medios — Calidad y Rendimiento
MEDIUM Seq perezoso en hot path de validación de pagos TransactionService.fs:162

La secuencia de validaciones se define como Seq.map perezoso y se itera dos veces (primero para detectar errores, luego para contar). Cada iteración re-evalúa las validaciones. Usar |> List.ofSeq para materializar una sola vez.

Materializar con Seq.toList antes del primer uso cuando la secuencia se consume más de una vez.

MEDIUM Nombres en camelCase para tipos (violación de convención) TransactionService.fs:18–20

Los tipos de dominio transactionStatus, paymentMethod están declarados en camelCase. En F#, tipos, módulos y casos de DU usan PascalCase.

Renombrar a TransactionStatus, PaymentMethod. Añadir [<RequireQualifiedAccess>] al módulo para evitar colisiones de nombres.

MEDIUM Función de 58 líneas — demasiado larga PaymentHandlers.fs:45–103

El handler processPaymentHandler tiene 58 líneas mezclando validación, llamada a servicio, mapeo de respuesta y logging. Difícil de testear unitariamente.

Extraer validateRequest, buildResponse y logPaymentEvent como funciones separadas. El handler queda en 10–15 líneas de pipeline con |>.

✅ Aspectos Correctos (6)

Autenticación JWT en todos los endpoints — middleware configurado correctamente en Program.fs.
DTOs de entrada decorados con [<CLIMutable>] y anotaciones de validación de ASP.NET.
Tests unitarios presentes para el módulo de cálculo de comisiones (FeeCalculator.fs).
Migrations de EF Core en carpeta separada, sin cambios destructivos en este PR.
Logging estructurado con Serilog en todas las rutas de éxito.
Ausencia de código comentado o TODOs pendientes de resolución urgente.

Resumen de Hallazgos

# Prioridad Categoría Título Archivo
1 CRITICAL Seguridad Inyección SQL por interpolación TransactionService.fs:38
2 CRITICAL Seguridad Path traversal en descarga de recibos PaymentHandlers.fs:112
3 CRITICAL Seguridad Clave de API hardcoded TransactionService.fs:14
4 HIGH Async Bloqueo con .Result — riesgo deadlock PaymentHandlers.fs:67
5 HIGH Tipos Primitive obsession — IDs sin tipo TransactionService.fs:22
6 HIGH Error handling Excepción silenciada con with _ -> None TransactionService.fs:88
7 HIGH Idiomática Bucle imperativo mutable O(n²) TransactionService.fs:134
8 MEDIUM Rendimiento Seq perezoso iterado dos veces TransactionService.fs:162
9 MEDIUM Estilo Tipos en camelCase TransactionService.fs:18
10 MEDIUM Calidad Función de 58 líneas PaymentHandlers.fs:45