Skip to main content

Creación de Base de Datos

MySQL Setup

1

Conectar a MySQL

mysql -u root -p
2

Crear base de datos

CREATE DATABASE factura_santoD 
  CHARACTER SET utf8mb4 
  COLLATE utf8mb4_unicode_ci;
Usa utf8mb4 para soportar caracteres especiales y emojis correctamente.
3

Crear usuario dedicado

CREATE USER 'factura_user'@'localhost' 
  IDENTIFIED BY 'password_seguro_aqui';

GRANT ALL PRIVILEGES ON factura_santoD.* 
  TO 'factura_user'@'localhost';

FLUSH PRIVILEGES;
Seguridad: En producción, usa contraseñas fuertes y limita privilegios según necesidad.
4

Verificar creación

SHOW DATABASES;
USE factura_santoD;
SELECT DATABASE();

Configurar .env

Edita el archivo .env con los datos de conexión:
.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=factura_santoD
DB_USERNAME=factura_user
DB_PASSWORD=password_seguro_aqui

Migraciones

Ejecutar Migraciones

Si ejecutaste composer setup, las migraciones ya están aplicadas. Si no:
php artisan migrate
Salida esperada:
Migration table created successfully.
Migrating: 2026_03_04_000001_create_bancos_table
Migrated:  2026_03_04_000001_create_bancos_table (45.23ms)
Migrating: 2026_03_04_000002_create_metodos_pago_table
Migrated:  2026_03_04_000002_create_metodos_pago_table (52.17ms)
...

Comandos de Migración

php artisan migrate

Migraciones del Sistema

El sistema incluye 29+ migraciones que crean:
  • bancos - Catálogo de bancos peruanos
  • metodos_pago - Métodos de pago (efectivo, tarjeta, etc.)
  • denominaciones_billetes - Billetes peruanos (S/200, S/100…)
  • cajas - Cajas registradoras
  • movimientos_caja - Ingresos/egresos de caja
  • apertura_caja_billetes - Detalle billetes en apertura
  • cierre_caja_billetes - Detalle billetes en cierre
  • auditoria_cajas - Auditoría de operaciones
  • permisos_caja - Permisos por usuario
  • cuentas_bancarias - Cuentas bancarias de empresa
  • titulares_cuenta_bancaria - Titulares de cuentas
  • movimientos_bancarios - Transacciones bancarias
  • conciliaciones_bancarias - Conciliaciones bancarias
  • billeteras_digitales - Yape, Plin, BIM, etc.
  • configuracion_metodos_pago - Config de métodos
  • reportes_financieros - Reportes generados
  • caja_metodos_pago - Relación caja-métodos

Estructura de Tablas Principales

Tabla: empresas

CREATE TABLE empresas (
    id_empresa INT PRIMARY KEY AUTO_INCREMENT,
    ruc VARCHAR(11) NOT NULL UNIQUE,
    razon_social VARCHAR(255) NOT NULL,
    nombre_comercial VARCHAR(255),
    direccion TEXT,
    ubigeo VARCHAR(6),
    distrito VARCHAR(100),
    provincia VARCHAR(100),
    departamento VARCHAR(100),
    
    -- Credenciales SUNAT
    user_sol VARCHAR(255),
    clave_sol VARCHAR(255),  -- Encriptada
    
    -- Modo de operación
    modo ENUM('beta', 'production') DEFAULT 'beta',
    
    -- Contacto
    telefono VARCHAR(20),
    email VARCHAR(255),
    web VARCHAR(255),
    
    created_at TIMESTAMP,
    updated_at TIMESTAMP
);
Importante: La columna id_empresa es INT (signed), no UNSIGNED. Todas las FKs deben usar integer() en migraciones.

Tabla: users

CREATE TABLE users (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    id_empresa INT NOT NULL,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    rol_id INT,
    
    -- Permisos de caja (por usuario)
    puede_abrir_caja BOOLEAN DEFAULT false,
    puede_cerrar_caja BOOLEAN DEFAULT false,
    puede_autorizar_cierre BOOLEAN DEFAULT false,
    puede_registrar_movimientos BOOLEAN DEFAULT false,
    puede_ver_reportes BOOLEAN DEFAULT false,
    
    remember_token VARCHAR(100),
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    
    FOREIGN KEY (id_empresa) REFERENCES empresas(id_empresa),
    FOREIGN KEY (rol_id) REFERENCES roles(id)
);

Tabla: ventas

CREATE TABLE ventas (
    id_venta BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    id_empresa INT NOT NULL,
    id_cliente BIGINT UNSIGNED NOT NULL,
    
    -- Datos del comprobante
    tipo_comprobante ENUM('01', '03', '07', '08'),  -- 01=Factura, 03=Boleta, 07=NC, 08=ND
    serie VARCHAR(4) NOT NULL,  -- F001, B001, FC01, etc.
    numero INT NOT NULL,
    
    fecha_emision DATE NOT NULL,
    fecha_vencimiento DATE,
    
    -- Montos
    subtotal DECIMAL(12,2) NOT NULL,
    igv DECIMAL(12,2) NOT NULL,
    total DECIMAL(12,2) NOT NULL,
    
    -- SUNAT
    estado_sunat ENUM('pendiente', 'aceptado', 'rechazado', 'anulado'),
    mensaje_sunat TEXT,
    xml_path VARCHAR(255),
    cdr_path VARCHAR(255),
    hash_cpe VARCHAR(255),
    
    -- Pago
    forma_pago ENUM('contado', 'credito') DEFAULT 'contado',
    estado_pago ENUM('P', 'C', 'V'),  -- P=Pendiente, C=Cancelado, V=Vencido
    
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    
    FOREIGN KEY (id_empresa) REFERENCES empresas(id_empresa),
    FOREIGN KEY (id_cliente) REFERENCES clientes(id_cliente),
    
    UNIQUE KEY (id_empresa, serie, numero)
);

Tabla: detalle_ventas

CREATE TABLE detalle_ventas (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    id_venta BIGINT UNSIGNED NOT NULL,
    id_producto BIGINT UNSIGNED,
    
    codigo VARCHAR(50),
    descripcion TEXT NOT NULL,
    cantidad DECIMAL(12,4) NOT NULL,
    unidad VARCHAR(10) DEFAULT 'NIU',  -- Código SUNAT
    
    precio_unitario DECIMAL(12,2) NOT NULL,
    descuento DECIMAL(12,2) DEFAULT 0,
    subtotal DECIMAL(12,2) NOT NULL,
    igv DECIMAL(12,2) NOT NULL,
    total DECIMAL(12,2) NOT NULL,
    
    FOREIGN KEY (id_venta) REFERENCES ventas(id_venta) ON DELETE CASCADE,
    FOREIGN KEY (id_producto) REFERENCES productos(id_producto)
);

Tabla: dias_ventas (Cuotas CxC)

CREATE TABLE dias_ventas (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    id_venta BIGINT UNSIGNED NOT NULL,
    id_empresa INT NOT NULL,
    
    numero_cuota INT NOT NULL,
    monto DECIMAL(12,2) NOT NULL,
    fecha_vencimiento DATE NOT NULL,
    fecha_pago DATE,
    
    estado ENUM('P', 'C', 'V') DEFAULT 'P',  -- Pendiente, Cancelado, Vencido
    observaciones TEXT,
    
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    
    FOREIGN KEY (id_venta) REFERENCES ventas(id_venta) ON DELETE CASCADE,
    FOREIGN KEY (id_empresa) REFERENCES empresas(id_empresa)
);

Tabla: guia_remisions

CREATE TABLE guia_remisions (
    id_guia BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    id_empresa INT NOT NULL,
    id_cliente BIGINT UNSIGNED,
    id_transportista BIGINT UNSIGNED,
    
    -- Comprobante
    serie VARCHAR(4) NOT NULL,  -- T001
    numero INT NOT NULL,
    fecha_emision DATE NOT NULL,
    
    -- Envío
    fecha_inicio_traslado DATE NOT NULL,
    motivo_traslado VARCHAR(2) NOT NULL,  -- Código SUNAT
    modalidad_traslado VARCHAR(2) NOT NULL,  -- 01=Público, 02=Privado
    peso_bruto DECIMAL(12,3),
    numero_bultos INT,
    
    -- Dirección origen
    direccion_origen TEXT,
    ubigeo_origen VARCHAR(6),
    
    -- Dirección destino
    direccion_destino TEXT,
    ubigeo_destino VARCHAR(6),
    
    -- Remitente (nuevo)
    remitente_tipo_doc VARCHAR(1),
    remitente_num_doc VARCHAR(15),
    remitente_razon_social VARCHAR(255),
    remitente_direccion TEXT,
    remitente_ubigeo VARCHAR(6),
    
    -- Transporte
    conductor_nombre VARCHAR(255),
    conductor_documento VARCHAR(20),
    conductor_licencia VARCHAR(20),
    placa_vehiculo VARCHAR(10),
    placa_secundaria VARCHAR(10),  -- Remolque/carreta
    
    -- SUNAT (GRE API)
    ticket VARCHAR(255),  -- Ticket async
    estado_sunat ENUM('pendiente', 'proceso', 'aceptado', 'rechazado'),
    mensaje_sunat TEXT,
    xml_path VARCHAR(255),
    cdr_path VARCHAR(255),
    
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    
    FOREIGN KEY (id_empresa) REFERENCES empresas(id_empresa),
    FOREIGN KEY (id_cliente) REFERENCES clientes(id_cliente),
    FOREIGN KEY (id_transportista) REFERENCES transportistas(id_transportista),
    
    UNIQUE KEY (id_empresa, serie, numero)
);

Tabla: cajas

CREATE TABLE cajas (
    id_caja BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    id_empresa INT NOT NULL,
    nombre VARCHAR(255),  -- "Caja Principal", "Caja 2", etc.
    responsable VARCHAR(255),
    
    id_usuario_apertura BIGINT UNSIGNED,
    id_usuario_cierre BIGINT UNSIGNED,
    id_usuario_autoriza_cierre BIGINT UNSIGNED,
    
    fecha_apertura DATETIME NOT NULL,
    fecha_cierre DATETIME,
    fecha_autorizacion_cierre DATETIME,
    
    saldo_inicial DECIMAL(12,2) DEFAULT 0,
    saldo_final_teorico DECIMAL(12,2),
    saldo_final_real DECIMAL(12,2),
    diferencia DECIMAL(12,2),
    
    estado ENUM('Abierta', 'Cerrada', 'Pendiente Autorización', 'Inactiva') DEFAULT 'Abierta',
    
    observaciones TEXT,
    observaciones_cierre TEXT,
    
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    
    FOREIGN KEY (id_empresa) REFERENCES empresas(id_empresa),
    FOREIGN KEY (id_usuario_apertura) REFERENCES users(id),
    FOREIGN KEY (id_usuario_cierre) REFERENCES users(id),
    FOREIGN KEY (id_usuario_autoriza_cierre) REFERENCES users(id)
);

Seeders

Ejecutar Seeders

php artisan db:seed
Seeders incluidos:
Crea las denominaciones de billetes y monedas peruanos:
// Billetes
S/ 200.00
S/ 100.00
S/ 50.00
S/ 20.00
S/ 10.00

// Monedas
S/ 5.00
S/ 2.00
S/ 1.00
S/ 0.50
S/ 0.20
S/ 0.10
Usado para:
  • Apertura de caja (desglose de billetes)
  • Cierre de caja (arqueo)
  • Reportes de efectivo

Ejecutar Seeder Específico

php artisan db:seed --class=DenominacionesBilletesSeeder

Crear Seeder Personalizado

php artisan make:seeder UsuarioAdminSeeder
database/seeders/UsuarioAdminSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\User;
use Illuminate\Support\Facades\Hash;

class UsuarioAdminSeeder extends Seeder
{
    public function run(): void
    {
        User::create([
            'name' => 'Administrador',
            'email' => '[email protected]',
            'password' => Hash::make('password'),
            'rol_id' => 1,  // Admin
            'id_empresa' => 1,
            'puede_abrir_caja' => true,
            'puede_cerrar_caja' => true,
            'puede_autorizar_cierre' => true,
            'puede_registrar_movimientos' => true,
            'puede_ver_reportes' => true,
        ]);
    }
}

Scoping Multi-empresa

Aunque el sistema tiene esquema multi-empresa, Santo Domingo está configurado como monempresa (id_empresa = 1).

Global Scope en Modelos

Todos los modelos principales tienen scoping automático:
app/Models/Venta.php
protected static function booted()
{
    static::addGlobalScope('empresa', function (Builder $query) {
        if (auth()->check()) {
            $query->where('id_empresa', auth()->user()->id_empresa);
        }
    });
}
Esto significa que automáticamente todas las consultas filtran por empresa del usuario autenticado.

Desactivar Scope Temporalmente

// Sin scope
$todasLasVentas = Venta::withoutGlobalScope('empresa')->get();

// Con scope (default)
$ventasDeEmpresa = Venta::all();  // Solo id_empresa = 1

Mantenimiento de Base de Datos

Backup

mysqldump -u factura_user -p factura_santoD > backup_$(date +%Y%m%d).sql

Restaurar Backup

# Desde SQL
mysql -u factura_user -p factura_santoD < backup_20240306.sql

# Desde GZ
gunzip < backup_20240306.sql.gz | mysql -u factura_user -p factura_santoD

Optimizar Tablas

-- Optimizar todas las tablas
OPTIMIZE TABLE ventas, detalle_ventas, clientes, productos;

-- Analizar tablas
ANALYZE TABLE ventas, detalle_ventas;

-- Reparar tabla (si hay corrupción)
REPAIR TABLE ventas;

Limpiar Datos Antiguos

-- Eliminar ventas de prueba (beta)
DELETE FROM ventas WHERE estado_sunat = 'pendiente' AND created_at < DATE_SUB(NOW(), INTERVAL 90 DAY);

-- Archivar logs antiguos
DELETE FROM auditoria_cajas WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);

-- Limpiar sesiones expiradas
DELETE FROM sessions WHERE last_activity < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY));

Performance

Índices Importantes

Las migraciones ya incluyen índices optimizados:
-- Ventas
INDEX idx_empresa (id_empresa)
INDEX idx_cliente (id_cliente)
INDEX idx_fecha (fecha_emision)
INDEX idx_estado (estado_sunat)
UNIQUE idx_serie_numero (id_empresa, serie, numero)

-- Productos
INDEX idx_codigo (codigo)
INDEX idx_empresa (id_empresa)

-- Clientes
INDEX idx_documento (tipo_documento, numero_documento)
INDEX idx_empresa (id_empresa)

Query Optimization

// Mal - N+1 queries
$ventas = Venta::all();
foreach ($ventas as $venta) {
    echo $venta->cliente->nombre;  // Query por cada venta
}

// Bien - Eager loading
$ventas = Venta::with('cliente', 'detalles.producto')->get();
foreach ($ventas as $venta) {
    echo $venta->cliente->nombre;  // Sin queries adicionales
}

Testing Database

Los tests usan SQLite in-memory (configurado en phpunit.xml):
phpunit.xml
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
Ejecutar tests:
composer test
# o
php artisan test

Troubleshooting

SQLSTATE[HY000]: General error: 3780 Referencing column 
'id_empresa' and referenced column 'id_empresa' in foreign 
key constraint are incompatible.
Solución: Usa $table->integer('id_empresa') (NO unsignedInteger) porque empresas.id_empresa es INT signed.
Si ves caracteres extraños (ñ, tildes):
ALTER DATABASE factura_santoD CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- Por cada tabla
ALTER TABLE ventas CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Si las migraciones fallan por dependencias:
php artisan migrate:fresh  # Drop all + re-run
migrate:fresh elimina todos los datos. Solo en desarrollo.
Verifica que MySQL esté corriendo:
sudo systemctl status mysql
sudo systemctl start mysql

Próximos Pasos

Certificados SUNAT

Configurar certificados digitales y credenciales

Configuración

Configuración detallada del archivo .env

Arquitectura

Comprender la arquitectura del sistema

Inicio Rápido

Guía de instalación rápida

Build docs developers (and LLMs) love