# Despliegue en cPanel — sistemavex.kayitstech.com

Guía paso a paso para publicar el Sistema VEX en hosting compartido con
cPanel y PHP 8.3 o superior.

**Léala entera antes de empezar.** El paso 3 (dónde va el proyecto) condiciona
todo lo demás, y deshacerlo a medias deja el sitio caído.

> **Antes de nada:** el sistema anterior sigue atendiendo a la empresa. Nada
> de lo que hay aquí lo apaga. El cambio de uno a otro, y cómo volver atrás
> si algo sale mal, está en [MIGRACION.md](MIGRACION.md) — apartado
> «Puesta en marcha y plan de vuelta atrás».

---

## Lo que hace falta tener a mano

- Acceso a cPanel del hosting
- Acceso SSH (muy recomendable; casi todo se puede hacer sin él, pero es
  mucho más lento — al final hay una nota para ese caso)
- PHP 8.3+ con: `pdo_mysql`, `mbstring`, `openssl`, `tokenizer`, `xml`,
  `ctype`, `json`, `bcmath`, `fileinfo`, `curl`, `gd`, `zip`
- Composer (cPanel suele traerlo; si no, se sube `composer.phar`)
- Los datos de la base anterior, si se van a importar

---

## 1. Crear el subdominio

En cPanel → **Dominios** → *Crear un dominio*:

- **Dominio:** `sistemavex.kayitstech.com`
- **Raíz del documento:** `/home/qqbodint/sistemavex/public`

**Ojo con este segundo campo.** Al crear un subdominio, cPanel le asigna su
propia carpeta —hermana de `public_html`, no dentro— y por omisión rellena
algo como:

```
/home/qqbodint/sistemavex          ← lo que propone cPanel
/home/qqbodint/sistemavex/public   ← lo que hay que dejar escrito
```

Hay que **añadirle `/public` a mano**. Si se deja la ruta que cPanel propone,
la raíz web queda apuntando a la carpeta del proyecto entera y el `.env` —con
las contraseñas de la base y los datos de los clientes— se puede descargar
escribiendo su dirección en el navegador.

Si el panel no deja editar ese campo, se resuelve en el paso 3B.

Anote la ruta real del directorio personal (`/home/USUARIO/`). En adelante
se usa `/home/qqbodint/` como ejemplo.

---

## 2. Crear la base de datos

cPanel → **Bases de datos MySQL**:

1. **Crear base de datos:** `sistemavex`
   cPanel le antepone el prefijo de la cuenta → **`qqbodint_sistemavex`**
2. **Crear usuario:** `qqbodint_vex`, con una contraseña larga generada por
   cPanel (guárdela, no se vuelve a mostrar)
3. **Añadir el usuario a la base** con **TODOS LOS PRIVILEGIOS**

Compruebe la codificación en **phpMyAdmin** → base → *Operaciones*:
debe ser `utf8mb4_unicode_ci`. Si quedó en otra, cámbiela ahora; hacerlo
con datos dentro es mucho más incómodo.

> La base anterior (`qqbodint_sistemawiwi`) **no se toca**. El usuario nuevo
> necesita permiso de **solo lectura** sobre ella para poder importar.

---

## 3. Subir el proyecto

### 3A. Lo correcto: el proyecto en la carpeta del subdominio

El subdominio ya tiene su propia carpeta (la que cPanel creó en el paso 1).
Ahí va el proyecto entero, y la raíz web apunta una carpeta más adentro:

```
/home/qqbodint/
├── sistemavex/            ← carpeta del subdominio: el proyecto ENTERO
│   ├── app/
│   ├── bootstrap/
│   ├── config/
│   ├── database/
│   ├── public/            ← ÚNICA carpeta expuesta a internet
│   ├── resources/
│   ├── routes/
│   ├── storage/
│   ├── vendor/
│   └── .env               ← fuera del alcance del navegador
└── public_html/           ← el sitio principal, intacto y sin relación
```

El subdominio **no vive dentro de `public_html`**: es una carpeta aparte, al
mismo nivel. Eso ya está bien; lo único que hay que asegurar es que la raíz
del documento termine en `/public` y no en `/sistemavex`.

Solo `public/` queda accesible desde internet. Todo lo demás —incluido el
`.env` con las contraseñas de la base y los datos personales de los
clientes— queda por encima de la raíz web y **no se puede descargar
escribiendo su URL**.

Cómo subirlo, de mejor a peor:

```bash
# Con SSH y Git (lo ideal)
cd /home/qqbodint
git clone <url-del-repositorio> sistemavex

# Con SSH sin Git: subir un .zip por el Administrador de archivos y
cd /home/qqbodint && unzip sistemavex.zip -d sistemavex
```

Sin SSH: Administrador de archivos de cPanel → subir el `.zip` a
`/home/qqbodint/` → *Extraer*. **No** suba la carpeta
`vendor/` por FTP cuando pueda evitarlo: son miles de archivos y tarda horas.

### 3B. Si el panel NO deja cambiar la raíz del documento

Algunos planes dejan la raíz clavada en la carpeta que cPanel creó para el
subdominio (`/home/qqbodint/sistemavex/`) y no permiten añadirle `/public`.
En ese caso se invierte el montaje: esa carpeta pasa a ser la pública, y el
proyecto se pone **al lado**.

```
/home/qqbodint/
├── sistemavex/            ← raíz web (solo el contenido de public/)
├── sistemavex_app/        ← el proyecto, sin acceso desde internet
└── public_html/
```

1. Suba el proyecto a `/home/qqbodint/sistemavex_app/`.
2. Copie **solo el contenido** de `sistemavex_app/public/` a
   `/home/qqbodint/sistemavex/`.
3. Edite `sistemavex/index.php` y ajuste las dos rutas para que apunten al
   proyecto:

```php
// ANTES (el proyecto está un nivel arriba)
require __DIR__.'/../vendor/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';

// DESPUÉS (el proyecto está en otra carpeta)
require '/home/qqbodint/sistemavex_app/vendor/autoload.php';
$app = require_once '/home/qqbodint/sistemavex_app/bootstrap/app.php';
```

Ajuste también la línea del archivo de mantenimiento si su `index.php` la
trae:

```php
if (file_exists($mantenimiento = '/home/qqbodint/sistemavex_app/storage/framework/maintenance.php')) {
    require $mantenimiento;
}
```

4. Vuelva a crear el enlace de almacenamiento apuntando a la nueva
   ubicación (ver paso 9); `php artisan storage:link` lo crea dentro de
   `sistemavex_app/public/`, que en este montaje ya no es la carpeta servida:

```bash
ln -s /home/qqbodint/sistemavex_app/storage/app/public \
      /home/qqbodint/sistemavex/storage
```

> Con este montaje, **cada despliegue** que cambie algo de `public/` (el CSS,
> el JavaScript o una imagen) obliga a volver a copiar esos archivos. Anótelo
> en el procedimiento de despliegue o se olvidará.

### En los dos casos: proteger lo que queda expuesto

Cree `public/.htaccess` si no existe (Laravel ya trae uno) y verifique que
estas URL devuelvan **403 o 404**, nunca contenido:

- `https://sistemavex.kayitstech.com/.env`
- `https://sistemavex.kayitstech.com/storage/logs/laravel.log`
- `https://sistemavex.kayitstech.com/composer.json`

---

## 4. Configurar el `.env`

```bash
cd /home/qqbodint/sistemavex
cp .env.example .env
nano .env
```

Lo mínimo que hay que completar:

```dotenv
APP_ENV=production
APP_DEBUG=false                 # NUNCA true en producción: enseña rutas y credenciales
APP_URL=https://sistemavex.kayitstech.com
APP_TIMEZONE=America/El_Salvador

DB_DATABASE=qqbodint_sistemavex
DB_USERNAME=qqbodint_vex
DB_PASSWORD=«la contraseña del paso 2»

# Solo mientras dure la importación; después pueden quedar vacíos
LEGACY_DB_DATABASE=qqbodint_sistemawiwi
LEGACY_DB_USERNAME=qqbodint_vex
LEGACY_DB_PASSWORD=«la misma»

SESSION_DOMAIN=sistemavex.kayitstech.com
SESSION_SECURE_COOKIE=true      # requiere el SSL del paso 12

# Se usan una sola vez, al sembrar el primer administrador.
# BÓRRELOS del archivo en cuanto la cuenta exista.
ADMIN_EMAIL=
ADMIN_PASSWORD=
```

Permisos del archivo, para que ningún otro usuario del servidor compartido
pueda leerlo:

```bash
chmod 600 .env
```

---

## 5. Instalar las dependencias de PHP

```bash
cd /home/qqbodint/sistemavex
composer install --no-dev --optimize-autoloader
```

- `--no-dev` deja fuera PHPUnit, Faker y compañía: no pintan nada en
  producción y son superficie de ataque gratuita.
- `--optimize-autoloader` genera el mapa de clases completo. Sin él, cada
  petición busca los archivos por el sistema de archivos, que en hosting
  compartido es lo más lento que hay.

Si el hosting mata el proceso por memoria:

```bash
php -d memory_limit=-1 /usr/local/bin/composer install --no-dev --optimize-autoloader
```

Si no hay Composer en el servidor, ejecútelo **en local** con las mismas
banderas y suba la carpeta `vendor/` resultante comprimida.

---

## 6. La interfaz no se compila

**No hay paso de compilación. No hace falta Node ni npm, ni en el servidor
ni en su computadora.**

El CSS es CSS plano —solo usa variables, que entienden todos los navegadores
desde 2017— y el JavaScript son módulos nativos (`<script type="module">`),
que funcionan sin empaquetar desde 2018. Ambos viven ya listos dentro de
`public/`:

```
public/
├── css/vex.css     ← el sistema de diseño completo
└── js/
    ├── app.js      ← comportamiento común (modales, filtros, pestañas)
    ├── tema.js     ← tema claro y oscuro
    └── fotos.js    ← compresión de imágenes antes de subirlas
```

Las vistas los cargan con `asset()`, como cualquier otro archivo estático.
Si edita uno, el cambio está en línea al recargar; no hay nada que rehacer.

> **Lo único que hay que recordar:** el navegador guarda estos archivos en
> caché. Tras editar `vex.css` o `app.js`, pida a los empleados un recargado
> forzado (Ctrl+Shift+R) o renombre el archivo. Con dos archivos y cambios
> poco frecuentes, sale más barato que montar un compilador.

> Si usó el montaje 3B, `public/css/` y `public/js/` se copian igual que el
> resto del contenido de `public/`.

---

## 7. Generar la clave de la aplicación

```bash
php artisan key:generate --force
```

Escribe `APP_KEY` en el `.env`. Con ella se cifran las sesiones y las
cookies.

> **No vuelva a generarla nunca después.** Cambiar `APP_KEY` invalida todas
> las sesiones activas y hace ilegible cualquier dato cifrado que ya esté
> guardado.

---

## 8. Crear el esquema y sembrar

```bash
# --force es obligatorio en producción: sin él, Artisan se niega a correr
php artisan migrate --force
```

Revise que terminen las ocho migraciones sin errores. Y después:

```bash
php artisan db:seed --force
```

El sembrado hace dos cosas:

1. Crea las categorías de precio de arranque: **General** ($7.50/lb, mínimo
   5 lb, solo si es la única categoría) y **Medicina** ($30.00/lb, mínimo
   1 lb, siempre).
2. Crea el **primer administrador**, tomando `ADMIN_EMAIL` y
   `ADMIN_PASSWORD` del `.env`. No hay ninguna contraseña escrita en el
   código: si esas variables están vacías y hay una consola interactiva, las
   pide por teclado.

**En cuanto la cuenta exista, borre `ADMIN_PASSWORD` del `.env`.**

### Importar los datos del sistema anterior

```bash
# SIEMPRE en seco primero
php artisan vex:importar-legacy --dry-run
```

Lea el informe: filas leídas e insertadas por tabla, y el recuento de
huérfanos (movimientos de caja que citaban una orden inexistente, bultos sin
orden, clientes de Estados Unidos sin oficina). Si los números cuadran:

```bash
php artisan vex:importar-legacy
```

Queda copia en `storage/logs/importacion-AAAA-MM-DD.txt`. El comando
conserva los ids originales y es idempotente: si algo falla a mitad, se
corrige y se vuelve a ejecutar sin miedo a duplicar nada.

---

## 9. Permisos y enlace de almacenamiento

```bash
cd /home/qqbodint/sistemavex

# Laravel escribe en estas dos carpetas y en ninguna más
chmod -R 775 storage bootstrap/cache

# Lo demás, solo lectura para el servidor web
find . -type f -not -path "./storage/*" -not -path "./vendor/*" -exec chmod 644 {} \;
find . -type d -not -path "./storage/*" -not -path "./vendor/*" -exec chmod 755 {} \;

chmod 600 .env
```

En cPanel, PHP corre como el propio usuario de la cuenta, así que `775`
suele bastar. Si el servidor usa un usuario distinto para Apache y le da
error de escritura, pruebe `chmod -R 777 storage bootstrap/cache` **como
último recurso** y avise al soporte del hosting: 777 en un servidor
compartido es de todo menos deseable.

Enlace del disco público:

```bash
php artisan storage:link
```

> **Las fotos de los bultos NO van por ahí.** Viven en un disco privado y se
> sirven por una ruta con sesión (`documentos.foto`): son fotos de etiquetas
> con el nombre, el teléfono y la dirección de personas reales, y no deben
> quedar descargables por URL directa.

Compruebe que existe la carpeta de respaldos:

```bash
mkdir -p storage/app/private/respaldos
chmod 750 storage/app/private/respaldos
```

---

## 10. Cachés de producción

```bash
php artisan config:cache     # compila config/ en un solo archivo
php artisan route:cache      # compila el mapa de rutas
php artisan view:cache       # precompila todas las vistas Blade
php artisan event:cache
```

O todo junto:

```bash
php artisan optimize
```

Esto es lo que más se nota en hosting compartido: sin `config:cache`, cada
petición vuelve a leer y parsear los quince archivos de `config/`.

> **Con `config:cache` activo, `env()` devuelve `null` fuera de los archivos
> de `config/`.** Es el comportamiento normal de Laravel, y la razón de que
> en este proyecto todos los valores se lean con `config('vex.…')`.
>
> **Cada vez que edite `.env`, `config/` o `routes/`:**
> ```bash
> php artisan optimize:clear && php artisan optimize
> ```

---

## 11. La entrada de cron

cPanel → **Trabajos cron** → *Añadir nuevo trabajo* → **Cada minuto**
(`* * * * *`):

```cron
* * * * * cd /home/qqbodint/sistemavex && /usr/local/bin/php artisan schedule:run >> /dev/null 2>&1
```

Una sola línea. Laravel decide dentro qué toca ejecutarse en cada minuto:

| Tarea | Hora |
|---|---|
| Limpiar tokens de «recuérdame» vencidos | 03:15 |
| Limpiar intentos de rastreo con más de 30 días | 03:25 |
| Limpiar sesiones caducadas | 03:35 |
| **Respaldo de la base de datos** | **04:00** |

Verifíquelo:

```bash
php artisan schedule:list      # qué hay programado y cuándo corre
php artisan vex:respaldar      # probar el respaldo a mano, ahora mismo
ls -lh storage/app/private/respaldos/
```

Si el respaldo avisa de que **no encuentra `mysqldump`**, indíquele la ruta
en el `.env` y vuelva a cachear la configuración:

```dotenv
DB_MYSQLDUMP=/usr/bin/mysqldump
```

Si el hosting no lo tiene en absoluto, programe el respaldo desde
**cPanel → Copias de seguridad** y avise al equipo: el comando devuelve
error a propósito en lugar de fallar en silencio, porque un respaldo que
nadie sabe que no existe es peor que no tener ninguno.

Averigüe la ruta exacta del PHP de la línea de comandos —no siempre es
`/usr/bin/php`— con:

```bash
which php
php -v      # debe decir 8.3 o superior
```

---

## 12. Certificado SSL

cPanel → **SSL/TLS Status** → marcar `sistemavex.kayitstech.com` →
*Run AutoSSL*. Con AutoSSL (Let's Encrypt) el certificado se renueva solo.

Después, forzar HTTPS. El proyecto ya llama a `URL::forceScheme('https')`
en producción, pero conviene además redirigir en el servidor. En
`public/.htaccess`, justo después de `RewriteEngine On`:

```apache
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
```

`SESSION_SECURE_COOKIE=true` **exige** HTTPS: sin certificado, el navegador
descarta la cookie de sesión y nadie consigue iniciar sesión. Si está
probando sin SSL todavía, póngalo en `false` temporalmente y acuérdese de
volver a activarlo.

---

## 13. Lista de verificación final

Recórrala entera antes de decirle a nadie que el sistema está arriba.

### Configuración

- [ ] `https://sistemavex.kayitstech.com` abre con el candado cerrado
- [ ] `http://` redirige a `https://`
- [ ] `APP_DEBUG=false` (una pantalla de error no debe enseñar rutas ni credenciales)
- [ ] `APP_ENV=production`
- [ ] `.env` con permisos `600` y **sin** `ADMIN_PASSWORD`
- [ ] `https://sistemavex.kayitstech.com/.env` devuelve 403 o 404
- [ ] `https://sistemavex.kayitstech.com/storage/logs/laravel.log` devuelve 403 o 404

### Funcionamiento

- [ ] Se puede iniciar sesión con la cuenta de administrador
- [ ] El panel carga **con estilos** (si se ve texto sin formato, revise que
      `public/css/vex.css` exista y se sirva: ábralo directo en el navegador)
- [ ] `/estado-salud` responde correctamente
- [ ] Se registra una orden de prueba de principio a fin
- [ ] El ticket de esa orden se genera en PDF
- [ ] El código QR del ticket abre el rastreo público
- [ ] `/rastreo` encuentra la orden pidiendo los últimos 4 dígitos del teléfono
- [ ] Se sube una foto en Correo en Línea y se ve después
- [ ] Un movimiento de caja se registra y aparece en el corte
- [ ] **Borre la orden de prueba** antes de entregar el sistema

### Datos

- [ ] `php artisan migrate:status` muestra las ocho migraciones ejecutadas
- [ ] Existen las categorías **General** y **Medicina** en Tarifas
- [ ] Si se importó: los totales por tabla del informe cuadran con la base anterior
- [ ] Si se importó: una orden conocida (por ejemplo `#ORD-0045`) abre **el mismo envío** que en el sistema viejo
- [ ] Si se importó: el saldo de caja de un cajero coincide con el del sistema viejo

### Mantenimiento

- [ ] `php artisan schedule:list` muestra las cuatro tareas
- [ ] `php artisan vex:respaldar` deja un `.sql.gz` en `storage/app/private/respaldos/`
- [ ] El cron de un minuto está creado y activo
- [ ] Alguien del equipo sabe dónde están los respaldos y cómo restaurarlos

---

## Despliegues siguientes

```bash
cd /home/qqbodint/sistemavex

php artisan down --render="errors::503"   # cartel de mantenimiento

git pull
composer install --no-dev --optimize-autoloader
php artisan migrate --force

php artisan optimize:clear
php artisan optimize

php artisan up
```

---

## Si algo va mal

| Síntoma | Causa casi segura |
|---|---|
| **500 en todas las pantallas** | Permisos de `storage/` o `bootstrap/cache/`, o falta `APP_KEY`. Mire `storage/logs/`. |
| **Todo sin estilos** | No se está sirviendo `public/css/vex.css`. Ábralo directo en el navegador: si da 404, la raíz del documento apunta mal (paso 1). |
| **404 en todas las rutas menos la portada** | `mod_rewrite` apagado o `.htaccess` ignorado. Falta `AllowOverride All`. |
| **«No application encryption key»** | `php artisan key:generate --force` y volver a cachear. |
| **Cambié el `.env` y no pasa nada** | Está cacheado: `php artisan optimize:clear && php artisan optimize`. |
| **No puedo iniciar sesión, la página se recarga** | `SESSION_SECURE_COOKIE=true` sin HTTPS, o `SESSION_DOMAIN` mal escrito. |
| **«SQLSTATE[HY000] [1045]»** | Usuario o contraseña de la base; o falta añadir el usuario a la base en cPanel. |
| **«could not find driver»** | Falta `pdo_mysql` en la versión de PHP seleccionada en *Select PHP Version*. |
| **El cron no corre nada** | Ruta de PHP equivocada. Compruébela con `which php` y póngala completa. |
| **`vex:respaldar` no encuentra `mysqldump`** | Póngale la ruta con `DB_MYSQLDUMP` en el `.env`. |

Los registros del día están en `storage/logs/`. Para verlos en vivo:

```bash
tail -f storage/logs/laravel-$(date +%Y-%m-%d).log
```

---

## Nota para quien no tenga SSH

Casi todo se puede hacer desde cPanel:

- **Subir y extraer:** Administrador de archivos
- **Permisos:** Administrador de archivos → clic derecho → *Cambiar permisos*
- **Base de datos:** Bases de datos MySQL y phpMyAdmin
- **Comandos de Artisan:** cPanel → **Terminal** (si el plan lo incluye). Si
  no, se pueden ejecutar como **trabajos cron de una sola vez**: cree el
  trabajo con la hora dentro de dos minutos, espere a que corra y bórrelo.

```cron
# Ejemplo: ejecutar las migraciones una sola vez a las 14:32
32 14 * * * cd /home/qqbodint/sistemavex && /usr/local/bin/php artisan migrate --force >> /home/qqbodint/salida-migracion.txt 2>&1
```

Lo único que **no** se puede hacer sin SSH es `composer install`.
Ambos se resuelven ejecutándolos en su computadora y subiendo las carpetas
`vendor/` ya lista.
