JK
Proyectos
JK
>CARGANDO // CARGANDO
INICIALIZANDO VISTA
Creado y diseñado porJK
JK
>CARGANDO // PROJECT DETAILS
PARSING ARCHITECTURE, DIAGRAMS & API ENDPOINTS
Volver a proyectos
Ver en GitHub
Sistema de Recompensas en Acciones
Completadogopostgresgingolang-migratevalidatorlogrusdocker

Sistema de Recompensas en Acciones

Libro mayor contable y backend de recompensas en acciones que implementa contabilidad por partida doble con valoración precisa en INR.

Problema y solución

La administración de recompensas patrimoniales exige precisión transaccional estricta, auditabilidad total y obtención de precios de mercado fiable para evitar discrepancias financieras.

Características

  • Libro contable basado en partida doble inmutable
  • Canal de valoración en INR en tiempo real con mecanismos de fallback
  • Procesamiento automatizado de devengo y ajustes de acciones
  • Pista de auditoría completa para cumplimiento normativo
  • Garantía de consistencia transaccional con PostgreSQL

Stack tecnológico

TecnologíaVersiónPropósitoPor qué se eligió
Go1.25.1Lenguaje de programaciónBiblioteca estándar sólida, primitivas de concurrencia, compilación rápida
Gin1.11.0Framework HTTP, enrutamiento, middlewareAlto rendimiento, soporte de middleware, comunidad activa
PostgreSQL17Base de datos relacionalFunciones SQL avanzadas (vistas, LATERAL, columnas generadas, ENUM)
golang-migratev4.19.0Migraciones de base de datosEjecutor de migraciones simple basado en archivos
logrus1.9.3Registro estructuradoCampos estructurados y registro por niveles
go-sqlmockv1.5.2Simulación de base de datos para pruebasSimula SQL para pruebas unitarias sin BD real
lib/pqv1.10.9Driver de PostgreSQLProbado en batalla, soporte nativo de PostgreSQL
Docker—ContenedorizaciónCompilaciones reproducibles y entornos aislados

Arquitectura

Docker Compose

Gin HTTP Server

:8080

Handlers

Reward, Adjustment, Today, Historical, Stats, Portfolio

PriceService Worker Pool

PriceCache

in-memory RWMutex

RandomPriceFetcher

PostgreSQL 17

:5432

7 Tables

3 Views

today_rewards, historical_rewards, user_portfolio

2 ENUMs

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

Componentes principales

  • Servidor HTTP Gin con middleware RequestID para el seguimiento de peticiones
  • Manejador de recompensas con patrón de idempotencia basado en SAVEPOINT
  • Manejador de ajustes con bloqueo a nivel de fila SELECT FOR UPDATE
  • Manejadores de lectura (today, historical, portfolio, stats) que consultan vistas de PostgreSQL
  • PriceService en segundo plano con pool de 5 goroutines y semáforo de 3 ranuras para la BD

Resiliencia del sistema

  • PriceCache en memoria con RWMutex para cargas de lectura intensivas
  • Control de concurrencia basado en CAS que evita ralentizaciones en cascada
  • Recuperación de pánicos en los workers de actualización de precios para evitar caídas del proceso
  • Apagado HTTP ordenado con tiempo de drenaje de 10 s
  • Envoltura transaccional con rollback diferido para todas las operaciones de escritura

Estructura del proyecto

cmd
stocky-api
main.goPunto de entrada: configuración de BD, migraciones, tarea en segundo plano, servidor HTTP y apagado ordenado
internal
config
database
migrations
handlers
stocky
jobs
price_updater.goPriceService en segundo plano con pool de workers, caché, reintentos y fetcher simulado
storage
models
utils

Endpoints de API

Stocky API (puerto 8080)

MétodoRutaPropósito
GET/healthComprobación de estado — devuelve {status: OK}
POST/api/v1/rewardCrear recompensa — idempotente, transaccional, cálculo de comisiones
POST/api/v1/adjustments/:idAplicar ajuste — bloqueo de fila, asientos de libro por tipo
GET/api/v1/today-stocks/:userIdRecompensas de hoy desde la vista today_rewards
GET/api/v1/historical-inr/:userIdValoración histórica en INR — paginada, multiplicada por eventos
GET/api/v1/stats/:userIdResumen de estadísticas — recompensas de hoy + valor total del portafolio
GET/api/v1/portfolio/:userIdDetalles del portafolio — paginados, filtrados por exclusión de cotización

Diseño de base de datos

users

4 cols
idPK
serial
username
varchar
email
varchar
created_at
timestamp

rewards

6 cols
idPK
serial
user_id
int
stock_symbol
varchar
quantity
numeric(18,6)
idempotency_key
varchar
reward_date
date

ledger

6 cols
idPK
serial
reward_id
int
entry_type
ledger_entry_type
stock_symbol
varchar
quantity
numeric(18,6)
amount
numeric(18,4)

adjustments

6 cols
idPK
serial
reward_id
int
adjustment_type
varchar
delta_quantity
numeric(18,6)
delta_amount
numeric(18,4)
reason
varchar

stock_prices

4 cols
idPK
serial
stock_symbol
varchar
price
numeric(18,4)
updated_at
timestamp

stock_price_history

4 cols
idPK
serial
stock_symbol
varchar
price
numeric(18,4)
date
date

stock_events

6 cols
idPK
serial
stock_symbol
varchar
event_type
stock_event_type
ratio_num
numeric(18,6)
ratio_den
numeric(18,6)
new_symbol
varchar
Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

users

ColumnaTipoNotas
idPKSERIALGenerado automáticamente
usernameVARCHAR(50)NOT NULL, UNIQUE
emailVARCHAR(100)NOT NULL, UNIQUE
created_atTIMESTAMPPredeterminado: NOW()

Cuentas de usuario. Sembradas con un usuario de prueba.

rewards

ColumnaTipoNotas
idPKSERIALGenerado automáticamente
user_idINTFK → users(id) ON DELETE CASCADE
stock_symbolVARCHAR(20)NOT NULL
quantityNUMERIC(18,6)NOT NULL, negativo = reversión
idempotency_keyVARCHAR(64)UNIQUE, permite NULL
reward_dateDATEGENERATED ALWAYS AS (created_at::DATE) STORED

Eventos centrales de recompensa. Único en (user_id, stock_symbol, reward_date) con variante insensible a mayúsculas.

ledger

ColumnaTipoNotas
idPKSERIALGenerado automáticamente
reward_idINTFK → rewards(id) ON DELETE CASCADE
entry_typeledger_entry_typeENUM: stock_units, inr_outflow, fees, brokerage_fee, stt_fee, gst_fee
stock_symbolVARCHAR(20)Permite NULL
quantityNUMERIC(18,6)Permite NULL
amountNUMERIC(18,4)Permite NULL

Libro contable de doble entrada. Cada recompensa genera de 2 a 5 asientos.

stock_prices

ColumnaTipoNotas
idPKSERIALGenerado automáticamente
stock_symbolVARCHAR(20)UNIQUE
priceNUMERIC(18,4)Precio actual
updated_atTIMESTAMPActualizado por PriceService

Último precio conocido por acción. Actualizado por el PriceService en segundo plano.

stock_price_history

ColumnaTipoNotas
idPKSERIALGenerado automáticamente
stock_symbolVARCHAR(20)NOT NULL
priceNUMERIC(18,4)NOT NULL
dateDATENOT NULL, UNIQUE con stock_symbol

Instantáneas diarias de precios para valoración histórica. Insertadas o actualizadas por PriceService.

stock_events

ColumnaTipoNotas
idPKSERIALGenerado automáticamente
stock_symbolVARCHAR(20)NOT NULL
event_typestock_event_typeENUM: split, bonus, merge, delist
ratio_numNUMERIC(18,6)Numerador del multiplicador
ratio_denNUMERIC(18,6)Denominador del multiplicador
new_symbolVARCHAR(20)Para fusiones
effective_dateTIMESTAMPNOT NULL

Acciones corporativas. ratio_num/ratio_den define el multiplicador (p. ej., split 2:1).

adjustments

ColumnaTipoNotas
idPKSERIALGenerado automáticamente
reward_idINTFK → rewards(id) ON DELETE CASCADE
adjustment_typeVARCHAR(64)reward_reversal, fee_refund, manual_correction
delta_quantityNUMERIC(18,6)NOT NULL
delta_amountNUMERIC(18,4)NOT NULL
reasonVARCHAR(255)Permite NULL

Modificaciones de recompensas existentes con bloqueo a nivel de fila.

Uno a muchos: users→rewards (CASCADE), rewards→ledger (CASCADE), rewards→adjustments (CASCADE). Referencias cruzadas implícitas: rewards.stock_symbol ↔ stock_prices.stock_symbol (mediante UPPER()), rewards.stock_symbol ↔ stock_events.stock_symbol (multiplicadores de eventos). No hay FK formal entre rewards y stock_prices/stock_events; se aplica a nivel de vista.

Decisiones de diseño clave

Vistas SQL para la agregación

Decisión: Los cálculos complejos de portafolio e históricos se implementan como vistas de PostgreSQL en lugar de código de aplicación

Motivo: Reduce la complejidad de la aplicación; aprovecha el optimizador de consultas de PostgreSQL; fuente única de verdad para los cálculos

Beneficios: Cálculo coherente en todos los endpoints; la base de datos procesa JOIN y agregaciones de forma eficiente

Compensaciones: Las vistas pueden ser lentas con grandes volúmenes; más difíciles de depurar que el código de aplicación; dependencia de PostgreSQL

Producto logarítmico para multiplicadores

Decisión: Usar EXP(SUM(LN(ratio))) para calcular multiplicadores acumulados de eventos corporativos

Motivo: Evita desbordamiento o subdesbordamiento de coma flotante cuando se componen muchos eventos

Beneficios: Numéricamente estable; admite cualquier número de eventos

Compensaciones: Menos legible que la multiplicación directa; requiere ratios positivos

Arquitectura solo de manejadores (sin capa de servicio)

Decisión: La lógica de negocio vive directamente en los manejadores, sin una capa de servicio separada

Motivo: Más simple para un proyecto pequeño; menos archivos y abstracciones que mantener

Beneficios: Poco código repetitivo; fácil de rastrear el flujo de la petición

Compensaciones: Difícil reutilizar la lógica; acoplamiento fuerte entre HTTP y acceso a datos; difícil probar la lógica de negocio de forma independiente

Fetcher de precios simulado

Decisión: Usar RandomPriceFetcher en lugar de una API de datos de mercado real

Motivo: Entorno de desarrollo/pruebas; no se necesita gestión de claves API

Beneficios: Cero dependencias externas; comportamiento casi determinista

Compensaciones: No apto para producción; los valores del portafolio no tienen sentido; debe reemplazarse para un despliegue real

Idempotencia basada en SAVEPOINT

Decisión: Usar SAVEPOINT de PostgreSQL para gestionar la idempotencia sin abortar la transacción externa

Motivo: Permite seguridad ante reintentos sin un servicio externo de idempotencia

Beneficios: Idempotencia sofisticada dentro de la transacción que respeta las restricciones de PostgreSQL

Compensaciones: Manejo de errores complejo; sobrecarga de SAVEPOINT

Sin autenticación/autorización

Decisión: Todos los endpoints son de acceso público sin autenticación

Motivo: Proyecto de desarrollo/demostración; se espera añadir autenticación más adelante (p. ej., mediante una pasarela API)

Beneficios: Implementación más simple; pruebas más sencillas

Compensaciones: No apto para producción; cualquier cliente puede crear recompensas o aplicar ajustes

Pruebas

Pruebas estándar de Go + go-sqlmock v1.5.2•19 pruebas unitarias

Qué se prueba

  • Manejador de recompensas: idempotencia NULL, JSON inválido, cantidad cero/negativa, normalización, reintento idempotente (8 pruebas)
  • Manejador de ajustes: ID de recompensa inválido, JSON inválido, tipo inválido, comportamiento del libro en reversión (4 pruebas)
  • Manejador histórico: userId inválido, límite inválido (2 pruebas)
  • Manejador de portafolio: userId inválido, offset inválido, filtro de exclusión insensible a mayúsculas (3 pruebas)
  • Manejador de hoy: userId inválido (1 prueba)
  • Manejador de estadísticas: userId inválido (1 prueba)

Cobertura faltante

  • Pruebas de integración contra PostgreSQL real
  • Pruebas de manejadores de lectura con verificación de datos reales
  • Pruebas del actualizador de precios en segundo plano (rutas de error, pánicos, reintentos)
  • Pruebas de ajustes concurrentes
  • Pruebas de escenarios de rollback de transacciones
  • Pruebas de casos límite de paginación

Desafíos clave

  • Evitar condiciones de carrera en transacciones financieras concurrentes
  • Diseñar mecanismos de fallback cuando fallan las APIs externas de precios

Aprendizajes clave

  • Principios de contabilidad por partida doble en bases de datos relacionales
  • Patrones de bloqueo pesimista y optimista para operaciones financieras