Initial commit: Horux Strategy Platform

- Laravel 11 backend with API REST
- React 18 + TypeScript + Vite frontend
- Multi-parser architecture for accounting systems (CONTPAQi, Aspel, SAP)
- 27+ financial metrics calculation
- PDF report generation with Browsershot
- Complete documentation (10 documents)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 22:24:00 -06:00
commit 4c3dc94ff2
107 changed files with 10701 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Balanza extends Model
{
use HasFactory;
protected $fillable = [
'cliente_id',
'periodo_inicio',
'periodo_fin',
'sistema_origen',
'archivo_original',
'status',
'error_mensaje',
];
protected function casts(): array
{
return [
'periodo_inicio' => 'date',
'periodo_fin' => 'date',
];
}
public function cliente(): BelongsTo
{
return $this->belongsTo(Cliente::class);
}
public function cuentas(): HasMany
{
return $this->hasMany(Cuenta::class);
}
public function reportes(): BelongsToMany
{
return $this->belongsToMany(Reporte::class, 'reporte_balanza');
}
public function cuentasActivas(): HasMany
{
return $this->hasMany(Cuenta::class)->where('excluida', false);
}
public function cuentasPadre(): HasMany
{
return $this->hasMany(Cuenta::class)->where('es_cuenta_padre', true);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class CategoriaContable extends Model
{
use HasFactory;
protected $table = 'categorias_contables';
protected $fillable = [
'reporte_contable_id',
'nombre',
'orden',
];
public function reporteContable(): BelongsTo
{
return $this->belongsTo(ReporteContable::class);
}
public function cuentas(): HasMany
{
return $this->hasMany(Cuenta::class);
}
public function reglasMapeoo(): HasMany
{
return $this->hasMany(ReglaMapeoo::class);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Cliente extends Model
{
use HasFactory;
protected $fillable = [
'nombre_empresa',
'logo',
'giro_id',
'moneda',
'configuracion',
];
protected function casts(): array
{
return [
'configuracion' => 'array',
];
}
public function giro(): BelongsTo
{
return $this->belongsTo(Giro::class);
}
public function usuarios(): HasMany
{
return $this->hasMany(User::class);
}
public function balanzas(): HasMany
{
return $this->hasMany(Balanza::class);
}
public function reportes(): HasMany
{
return $this->hasMany(Reporte::class);
}
public function mapeoCuentas(): HasMany
{
return $this->hasMany(MapeoCuenta::class);
}
public function permisosEmpleado(): HasMany
{
return $this->hasMany(PermisoEmpleado::class);
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Cuenta extends Model
{
use HasFactory;
protected $fillable = [
'balanza_id',
'codigo',
'nombre',
'nivel',
'reporte_contable_id',
'categoria_contable_id',
'cuenta_padre_id',
'saldo_inicial_deudor',
'saldo_inicial_acreedor',
'cargos',
'abonos',
'saldo_final_deudor',
'saldo_final_acreedor',
'excluida',
'es_cuenta_padre',
'requiere_revision',
'nota_revision',
];
protected function casts(): array
{
return [
'saldo_inicial_deudor' => 'decimal:2',
'saldo_inicial_acreedor' => 'decimal:2',
'cargos' => 'decimal:2',
'abonos' => 'decimal:2',
'saldo_final_deudor' => 'decimal:2',
'saldo_final_acreedor' => 'decimal:2',
'excluida' => 'boolean',
'es_cuenta_padre' => 'boolean',
'requiere_revision' => 'boolean',
];
}
public function balanza(): BelongsTo
{
return $this->belongsTo(Balanza::class);
}
public function reporteContable(): BelongsTo
{
return $this->belongsTo(ReporteContable::class);
}
public function categoriaContable(): BelongsTo
{
return $this->belongsTo(CategoriaContable::class);
}
public function cuentaPadre(): BelongsTo
{
return $this->belongsTo(Cuenta::class, 'cuenta_padre_id');
}
public function cuentasHijo(): HasMany
{
return $this->hasMany(Cuenta::class, 'cuenta_padre_id');
}
public function getSaldoInicialNetoAttribute(): float
{
return $this->saldo_inicial_deudor - $this->saldo_inicial_acreedor;
}
public function getSaldoFinalNetoAttribute(): float
{
return $this->saldo_final_deudor - $this->saldo_final_acreedor;
}
public function getMovimientoNetoAttribute(): float
{
return $this->cargos - $this->abonos;
}
/**
* Recalcula el saldo considerando solo cuentas hijo activas
*/
public function recalcularSaldoDesdeHijos(): void
{
if (!$this->es_cuenta_padre) {
return;
}
$hijosActivos = $this->cuentasHijo()->where('excluida', false)->get();
$this->saldo_inicial_deudor = $hijosActivos->sum('saldo_inicial_deudor');
$this->saldo_inicial_acreedor = $hijosActivos->sum('saldo_inicial_acreedor');
$this->cargos = $hijosActivos->sum('cargos');
$this->abonos = $hijosActivos->sum('abonos');
$this->saldo_final_deudor = $hijosActivos->sum('saldo_final_deudor');
$this->saldo_final_acreedor = $hijosActivos->sum('saldo_final_acreedor');
$this->save();
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Giro extends Model
{
use HasFactory;
protected $fillable = [
'nombre',
'activo',
];
protected function casts(): array
{
return [
'activo' => 'boolean',
];
}
public function clientes(): HasMany
{
return $this->hasMany(Cliente::class);
}
public function umbrales(): HasMany
{
return $this->hasMany(Umbral::class);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class MapeoCuenta extends Model
{
use HasFactory;
protected $table = 'mapeo_cuentas';
protected $fillable = [
'cliente_id',
'codigo_patron',
'categoria_contable_id',
'notas',
];
public function cliente(): BelongsTo
{
return $this->belongsTo(Cliente::class);
}
public function categoriaContable(): BelongsTo
{
return $this->belongsTo(CategoriaContable::class);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PermisoEmpleado extends Model
{
use HasFactory;
protected $table = 'permisos_empleado';
protected $fillable = [
'user_id',
'cliente_id',
'permisos',
];
protected function casts(): array
{
return [
'permisos' => 'array',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function cliente(): BelongsTo
{
return $this->belongsTo(Cliente::class);
}
public function tienePermiso(string $permiso): bool
{
return in_array($permiso, $this->permisos ?? []);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ReglaMapeo extends Model
{
use HasFactory;
protected $table = 'reglas_mapeo';
protected $fillable = [
'sistema_origen',
'cuenta_padre_codigo',
'rango_inicio',
'rango_fin',
'patron_regex',
'reporte_contable_id',
'categoria_contable_id',
'prioridad',
'activo',
];
protected function casts(): array
{
return [
'activo' => 'boolean',
];
}
public function reporteContable(): BelongsTo
{
return $this->belongsTo(ReporteContable::class);
}
public function categoriaContable(): BelongsTo
{
return $this->belongsTo(CategoriaContable::class);
}
/**
* Verifica si un código de cuenta coincide con esta regla
*/
public function coincideCon(string $codigo): bool
{
// Si tiene patrón regex, usarlo
if ($this->patron_regex) {
return (bool) preg_match($this->patron_regex, $codigo);
}
// Si tiene rango, verificar
if ($this->rango_inicio && $this->rango_fin) {
$codigoNumerico = $this->codigoANumero($codigo);
$inicioNumerico = $this->codigoANumero($this->rango_inicio);
$finNumerico = $this->codigoANumero($this->rango_fin);
return $codigoNumerico >= $inicioNumerico && $codigoNumerico <= $finNumerico;
}
return false;
}
/**
* Convierte código tipo "001-100-000" a número para comparación
*/
private function codigoANumero(string $codigo): int
{
return (int) str_replace(['-', '.', ' '], '', $codigo);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Reporte extends Model
{
use HasFactory;
protected $fillable = [
'cliente_id',
'nombre',
'periodo_tipo',
'periodo_inicio',
'periodo_fin',
'fecha_generacion',
'data_calculada',
'pdf_path',
'status',
];
protected function casts(): array
{
return [
'periodo_inicio' => 'date',
'periodo_fin' => 'date',
'fecha_generacion' => 'datetime',
'data_calculada' => 'array',
];
}
public function cliente(): BelongsTo
{
return $this->belongsTo(Cliente::class);
}
public function balanzas(): BelongsToMany
{
return $this->belongsToMany(Balanza::class, 'reporte_balanza');
}
public function getMetrica(string $nombre): ?array
{
return $this->data_calculada['metricas'][$nombre] ?? null;
}
public function getEstadoFinanciero(string $tipo): ?array
{
return $this->data_calculada['estados_financieros'][$tipo] ?? null;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ReporteContable extends Model
{
use HasFactory;
protected $table = 'reportes_contables';
protected $fillable = [
'nombre',
];
public function categorias(): HasMany
{
return $this->hasMany(CategoriaContable::class)->orderBy('orden');
}
public function cuentas(): HasMany
{
return $this->hasMany(Cuenta::class);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Umbral extends Model
{
use HasFactory;
protected $table = 'umbrales';
protected $fillable = [
'metrica',
'muy_positivo',
'positivo',
'neutral',
'negativo',
'muy_negativo',
'giro_id',
];
protected function casts(): array
{
return [
'muy_positivo' => 'decimal:4',
'positivo' => 'decimal:4',
'neutral' => 'decimal:4',
'negativo' => 'decimal:4',
'muy_negativo' => 'decimal:4',
];
}
public function giro(): BelongsTo
{
return $this->belongsTo(Giro::class);
}
public function evaluarValor(float $valor): string
{
if ($this->muy_positivo !== null && $valor >= $this->muy_positivo) {
return 'muy_positivo';
}
if ($this->positivo !== null && $valor >= $this->positivo) {
return 'positivo';
}
if ($this->neutral !== null && $valor >= $this->neutral) {
return 'neutral';
}
if ($this->negativo !== null && $valor >= $this->negativo) {
return 'negativo';
}
return 'muy_negativo';
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
protected $fillable = [
'nombre',
'email',
'password',
'role',
'cliente_id',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
public function cliente(): BelongsTo
{
return $this->belongsTo(Cliente::class);
}
public function permisosEmpleado(): HasMany
{
return $this->hasMany(PermisoEmpleado::class);
}
public function isAdmin(): bool
{
return $this->role === 'admin';
}
public function isAnalista(): bool
{
return $this->role === 'analista';
}
public function isCliente(): bool
{
return $this->role === 'cliente';
}
public function isEmpleado(): bool
{
return $this->role === 'empleado';
}
public function canAccessCliente(int $clienteId): bool
{
if ($this->isAdmin() || $this->isAnalista()) {
return true;
}
return $this->cliente_id === $clienteId;
}
}