JK
>CARGANDO // CARGANDO
INICIALIZANDO VISTA
Libro mayor contable y backend de recompensas en acciones que implementa contabilidad por partida doble con valoración precisa en INR.
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.
| Tecnología | Versión | Propósito | Por qué se eligió |
|---|---|---|---|
| Go | 1.25.1 | Lenguaje de programación | Biblioteca estándar sólida, primitivas de concurrencia, compilación rápida |
| Gin | 1.11.0 | Framework HTTP, enrutamiento, middleware | Alto rendimiento, soporte de middleware, comunidad activa |
| PostgreSQL | 17 | Base de datos relacional | Funciones SQL avanzadas (vistas, LATERAL, columnas generadas, ENUM) |
| golang-migrate | v4.19.0 | Migraciones de base de datos | Ejecutor de migraciones simple basado en archivos |
| logrus | 1.9.3 | Registro estructurado | Campos estructurados y registro por niveles |
| go-sqlmock | v1.5.2 | Simulación de base de datos para pruebas | Simula SQL para pruebas unitarias sin BD real |
| lib/pq | v1.10.9 | Driver de PostgreSQL | Probado en batalla, soporte nativo de PostgreSQL |
| Docker | — | Contenedorización | Compilaciones reproducibles y entornos aislados |
| Método | Ruta | Propósito |
|---|---|---|
| GET | /health | Comprobación de estado — devuelve {status: OK} |
| POST | /api/v1/reward | Crear recompensa — idempotente, transaccional, cálculo de comisiones |
| POST | /api/v1/adjustments/:id | Aplicar ajuste — bloqueo de fila, asientos de libro por tipo |
| GET | /api/v1/today-stocks/:userId | Recompensas de hoy desde la vista today_rewards |
| GET | /api/v1/historical-inr/:userId | Valoración histórica en INR — paginada, multiplicada por eventos |
| GET | /api/v1/stats/:userId | Resumen de estadísticas — recompensas de hoy + valor total del portafolio |
| GET | /api/v1/portfolio/:userId | Detalles del portafolio — paginados, filtrados por exclusión de cotización |
| Columna | Tipo | Notas |
|---|---|---|
| idPK | SERIAL | Generado automáticamente |
| username | VARCHAR(50) | NOT NULL, UNIQUE |
| VARCHAR(100) | NOT NULL, UNIQUE | |
| created_at | TIMESTAMP | Predeterminado: NOW() |
Cuentas de usuario. Sembradas con un usuario de prueba.
| Columna | Tipo | Notas |
|---|---|---|
| idPK | SERIAL | Generado automáticamente |
| user_id | INT | FK → users(id) ON DELETE CASCADE |
| stock_symbol | VARCHAR(20) | NOT NULL |
| quantity | NUMERIC(18,6) | NOT NULL, negativo = reversión |
| idempotency_key | VARCHAR(64) | UNIQUE, permite NULL |
| reward_date | DATE | GENERATED ALWAYS AS (created_at::DATE) STORED |
Eventos centrales de recompensa. Único en (user_id, stock_symbol, reward_date) con variante insensible a mayúsculas.
| Columna | Tipo | Notas |
|---|---|---|
| idPK | SERIAL | Generado automáticamente |
| reward_id | INT | FK → rewards(id) ON DELETE CASCADE |
| entry_type | ledger_entry_type | ENUM: stock_units, inr_outflow, fees, brokerage_fee, stt_fee, gst_fee |
| stock_symbol | VARCHAR(20) | Permite NULL |
| quantity | NUMERIC(18,6) | Permite NULL |
| amount | NUMERIC(18,4) | Permite NULL |
Libro contable de doble entrada. Cada recompensa genera de 2 a 5 asientos.
| Columna | Tipo | Notas |
|---|---|---|
| idPK | SERIAL | Generado automáticamente |
| stock_symbol | VARCHAR(20) | UNIQUE |
| price | NUMERIC(18,4) | Precio actual |
| updated_at | TIMESTAMP | Actualizado por PriceService |
Último precio conocido por acción. Actualizado por el PriceService en segundo plano.
| Columna | Tipo | Notas |
|---|---|---|
| idPK | SERIAL | Generado automáticamente |
| stock_symbol | VARCHAR(20) | NOT NULL |
| price | NUMERIC(18,4) | NOT NULL |
| date | DATE | NOT NULL, UNIQUE con stock_symbol |
Instantáneas diarias de precios para valoración histórica. Insertadas o actualizadas por PriceService.
| Columna | Tipo | Notas |
|---|---|---|
| idPK | SERIAL | Generado automáticamente |
| stock_symbol | VARCHAR(20) | NOT NULL |
| event_type | stock_event_type | ENUM: split, bonus, merge, delist |
| ratio_num | NUMERIC(18,6) | Numerador del multiplicador |
| ratio_den | NUMERIC(18,6) | Denominador del multiplicador |
| new_symbol | VARCHAR(20) | Para fusiones |
| effective_date | TIMESTAMP | NOT NULL |
Acciones corporativas. ratio_num/ratio_den define el multiplicador (p. ej., split 2:1).
| Columna | Tipo | Notas |
|---|---|---|
| idPK | SERIAL | Generado automáticamente |
| reward_id | INT | FK → rewards(id) ON DELETE CASCADE |
| adjustment_type | VARCHAR(64) | reward_reversal, fee_refund, manual_correction |
| delta_quantity | NUMERIC(18,6) | NOT NULL |
| delta_amount | NUMERIC(18,4) | NOT NULL |
| reason | VARCHAR(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.
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
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
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
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
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
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