# PurrCore — Guía completa para Claude Code

## ⚠️ LEER ANTES DE TOCAR CUALQUIER ARCHIVO

1. **Antes de editar un archivo, léelo completo primero**
2. **Valida sintaxis PHP después de cada cambio**: `php -l archivo.php`
3. **No sobreescribas lógica que ya funciona** — verifica que existe antes de añadir
4. **Las migraciones de BD van en `app/Core/Migrator.php`** — usar `ADD COLUMN IF NOT EXISTS`
5. **Deploy**: los archivos van a OVH hosting compartido por SFTP/WinSCP (sync automático)
6. **No hay SSH al servidor** — no puedes ejecutar comandos remotos
7. **La BD solo es accesible desde el servidor** — las migraciones corren al entrar al Dashboard admin

---

## Stack

- PHP 8.3, MVC propio sin framework
- MySQL/MariaDB (OVH hosting compartido)
- Stripe SDK para pagos y suscripciones
- PHPMailer para emails
- Sin Composer para dependencias propias (solo vendor/ para librerías externas)

---

## Estructura de directorios

```
purrcore/
├── CLAUDE.md                    ← este archivo
├── app/
│   ├── Controllers/
│   │   ├── Admin/               ← panel admin
│   │   │   ├── AuditController.php
│   │   │   ├── BackupAdminController.php
│   │   │   ├── CategoryAdminController.php
│   │   │   ├── CmsAdminController.php
│   │   │   ├── CouponAdminController.php
│   │   │   ├── CustomerAdminController.php
│   │   │   ├── DashboardController.php
│   │   │   ├── ExternalOrderAdminController.php
│   │   │   ├── InvoiceAdminController.php
│   │   │   ├── NewsletterAdminController.php
│   │   │   ├── OrderAdminController.php
│   │   │   ├── ProcurementController.php
│   │   │   ├── ProductAdminController.php
│   │   │   ├── ProductVariantController.php
│   │   │   ├── ReviewAdminController.php
│   │   │   ├── RoleAdminController.php
│   │   │   ├── SettingsController.php
│   │   │   ├── StaffAdminController.php
│   │   │   ├── StockAdminController.php
│   │   │   ├── SubscriptionAdminController.php
│   │   │   └── WarehouseController.php
│   │   ├── AccountController.php
│   │   ├── AuthController.php
│   │   ├── CartController.php
│   │   ├── CheckoutController.php
│   │   ├── CookieController.php
│   │   ├── ErrorController.php
│   │   ├── HomeController.php
│   │   ├── NewsletterController.php
│   │   ├── PageController.php
│   │   ├── ProductController.php
│   │   └── ShopController.php
│   ├── Core/
│   │   ├── Auth.php             ← autenticación clientes y staff
│   │   ├── Controller.php       ← base: render(), json(), redirect(), db(), flash()
│   │   ├── Csrf.php
│   │   ├── Database.php         ← PDO singleton
│   │   ├── Migrator.php         ← ⚠️ AQUÍ VAN TODAS LAS MIGRACIONES DE BD
│   │   ├── Router.php
│   │   ├── Session.php
│   │   └── View.php
│   ├── Helpers/
│   │   └── functions.php        ← s(), e(), route(), csrf_field(), setting()
│   ├── Models/
│   │   ├── Customer.php
│   │   ├── Order.php
│   │   └── Product.php
│   └── Services/
│       ├── AuditService.php
│       ├── BackupService.php
│       ├── CartService.php
│       ├── CmsService.php
│       ├── CouponService.php
│       ├── EmailService.php
│       ├── InvoiceService.php
│       ├── NewsletterService.php
│       ├── PointsService.php
│       ├── PosService.php
│       ├── SeoService.php
│       ├── StockService.php
│       └── StripeService.php
├── config/
│   └── routes.php               ← TODAS las rutas del proyecto
├── database/
│   └── schema.sql               ← schema completo de BD
├── public/
│   ├── index.php                ← front controller
│   ├── .htaccess                ← mod_rewrite + límites PHP 20MB
│   └── assets/
│       ├── css/main.css
│       ├── js/main.js
│       └── uploads/
│           ├── cms/             ← imágenes del CMS
│           └── products/        ← imágenes de productos
└── vendor/                      ← librerías externas (Stripe SDK, PHPMailer)
```

---

## Convenciones del código

### Namespaces y autoload
```php
namespace Controllers\Admin;    // Controllers/Admin/MiController.php
namespace Models;               // Models/MiModelo.php
namespace Services;             // Services/MiServicio.php
namespace Core;                 // Core/MiClase.php
```
El autoload convierte `\` en `/` y busca en `APP_PATH/`.

### Funciones globales (app/Helpers/functions.php)
```php
s('store_name', 'Default')   // alias de setting() — leer config de BD
e($valor)                    // htmlspecialchars — SIEMPRE en vistas
csrf_field()                 // <input type="hidden" name="_csrf" value="...">
setting($key, $default)      // leer tabla settings de BD
```

### Controller base (Core/Controller.php)
```php
$this->db()                              // PDO singleton
$this->render('admin.products.edit', compact('product'), 'admin')  // vista
$this->json(['success' => true])         // respuesta JSON
$this->redirect('/admin/productos')      // redirección
$this->flash('success', 'Mensaje')       // mensaje flash
$this->abort(404)                        // error HTTP
$this->mustCan('products.edit')          // verificar permiso staff
$this->mustAdmin()                       // verificar que es admin
```

### Rutas (config/routes.php)
```php
$router->get('/ruta', 'Controllers\\Admin\\Controller::method', $admin_mw);
$router->post('/ruta/{id}', 'Controllers\\Admin\\Controller::method', $admin_mw);
// Sin middleware para rutas públicas:
$router->get('/tienda', 'Controllers\\ShopController::index');
```

### Vistas
- Ruta: `'admin.products.edit'` → `app/Views/admin/products/edit.php`
- Layout admin: tercer parámetro `'admin'` → usa `Views/layouts/admin.php`
- Layout público: por defecto → usa `Views/layouts/main.php`
- Variables disponibles en vista: las del `compact()` + `$content` (el HTML renderizado)

### Migraciones de BD
```php
// En app/Core/Migrator.php, array $migrations:
'2025_37_nueva_columna' => "
    ALTER TABLE productos ADD COLUMN IF NOT EXISTS nueva_col VARCHAR(255) NULL
",
```
**Usar siempre `IF NOT EXISTS` / `IF EXISTS` para idempotencia.**
Las migraciones corren al entrar al Dashboard admin (`/admin`).

---

## Base de datos — Tablas existentes

### Productos y catálogo
```sql
products          -- id, name, slug, sku, price, compare_price, cost_price,
                  -- description, category_id, stock_qty, active, featured,
                  -- has_variants, is_subscription, sort_order, created_at
product_images    -- id, product_id, filename, is_primary, sort_order
product_variants  -- id, product_id, sku, price, compare_price, stock_qty,
                  -- stock_status, weight, active, sort_order
product_attributes-- id, name  (ej: Sabor, Formato, Tamaño)
attribute_values  -- id, attribute_id, value, sort_order
variant_attributes-- variant_id, attribute_value_id  (tabla pivote)
categories        -- id, name, slug, parent_id, active, sort_order
```

### Pedidos
```sql
orders            -- id, order_number, external_order_id, external_platform,
                  -- customer_id, customer_email, status, payment_status,
                  -- subtotal, discount_amount, shipping_amount, total,
                  -- stripe_payment_intent, ship_first_name/last_name/address_1/
                  -- city/postcode/country/phone, ip_address, source, created_at
order_items       -- id, order_id, product_id, variant_id, product_name,
                  -- variant_label, sku, qty, unit_price, total_price
order_history     -- id, order_id, status_from, status_to, note, created_at
```

### Clientes
```sql
customers         -- id, email, first_name, last_name, phone, password_hash,
                  -- stripe_customer_id, points, active, blocked_from_buying,
                  -- blocked_subscriptions, last_purchase_ip, created_at
customer_addresses-- id, customer_id, type(billing/shipping), is_default,
                  -- first_name, last_name, address_1, address_2,
                  -- city, state, postcode, country, phone
login_attempts    -- id, email, ip_address, attempted_at
ip_blocks         -- id, ip_address, type, reason, expires_at, created_at
```

### Suscripciones
```sql
subscription_plans-- id, product_id, variant_id, name, interval_unit,
                  -- interval_count, price, discount_percent, stripe_price_id, active
subscriptions     -- id, customer_id, plan_id, stripe_subscription_id,
                  -- status, next_payment_at, cancelled_at, created_at
```

### CMS
```sql
cms_pages         -- key, title
cms_blocks        -- id, page_key, block_key, label, sort_order
cms_fields        -- id, page_key, block_key, field_key, field_type,
                  -- label, value, sort_order
                  -- field_type: text|textarea|richtext|image|url|color|toggle
cms_menu_items    -- id, menu_key, label, url, target, sort_order, active
```

### Staff y permisos
```sql
staff             -- id, email, password_hash, first_name, last_name, role_id, active
roles             -- id, name, description
permissions       -- id, name, description
role_permissions  -- role_id, permission_id
staff_permissions -- staff_id, permission_id, granted(0/1)
```

### Otros módulos
```sql
reviews           -- id, product_id, customer_id, rating, title, body,
                  -- source, author_platform, image_url, video_url,
                  -- approved, featured, auto_added, created_at
coupons           -- id, code, type, value, min_order, max_uses, used_count,
                  -- expires_at, active
newsletter_subscribers -- id, email, name, token, source, active, confirmed, created_at
newsletter_campaigns   -- id, subject, body_html, status, sent_at
settings          -- key, value, group  (configuración global de la app)
audit_log         -- id, action, module, entity_type, entity_id, staff_id,
                  -- old_data, new_data, created_at
migrations_run    -- migration_key, run_at
suppliers         -- id, name, contact_name, email, phone, active, payment_terms
purchase_orders   -- id, reference, supplier_id, status, subtotal, total, created_at
purchase_order_items -- id, purchase_order_id, product_id, qty_ordered,
                     -- qty_received, unit_cost, real_unit_cost
stock_levels      -- product_id, location_id, quantity
pos_sessions      -- id, staff_id, location_id, status, opened_at, closed_at
invoices          -- id, order_id, invoice_number, status, issued_at
points_transactions -- id, customer_id, order_id, type, points, created_at
```

---

## Servicios clave — firmas importantes

### AuditService::log ⚠️
```php
// IMPORTANTE: $entityType DEBE ser string, nunca null
AuditService::log(
    string $action,      // ej: 'product.created'
    string $module,      // ej: 'products'
    string $entityType,  // ej: 'product'  ← NUNCA null
    ?int $entityId,      // id del registro
    ?array $oldData,     // datos anteriores
    ?array $newData      // datos nuevos
);
```

### StripeService — métodos principales
```php
$stripe->createPaymentIntent(float $amount, string $currency, array $metadata, ?string $customerId, bool $saveMethod)
$stripe->createRecurringPrice(string $name, float $amount, string $interval, int $count, string $currency): string // price_xxx
$stripe->createSubscriptionWithPaymentMethod(string $cusId, string $priceId, string $pmId, array $metadata): array
$stripe->createOrGetCustomer(string $email, string $name): string // cus_xxx
$stripe->cancelSubscription(string $subId, bool $atPeriodEnd): Subscription
$stripe->refund(string $paymentIntentId, ?float $amount): array
```

### EmailService — métodos
```php
$email->sendRaw(string $to, string $toName, string $subject, string $html): bool
$email->sendNewsletterConfirm(string $email, string $name, string $token): bool
$email->sendOrderConfirmation(array $order): bool
$email->sendShippingNotification(array $order): bool
$email->sendPasswordReset(string $email, string $token): bool
$email->sendContactForm(string $from, string $name, string $subject, string $msg): bool
```

### CmsService
```php
$cms->getPage(string $pageKey): array        // ['block_key' => ['field_key' => 'value']]
$cms->getBlocks(string $pageKey): array      // bloques con sus campos
$cms->getMenu(string $menuKey): array        // items del menú
$cms->uploadImage(array $file): ?string      // devuelve URL o null
$cms->saveFields(string $page, string $block, array $values): void
```

---

## Patrones de upload de imágenes

### Producto (via AJAX al endpoint /admin/productos/{id}/imagen)
```javascript
// En products/edit.php
async function uploadProdImg(input, productId) {
    const fd = new FormData();
    fd.append('image', input.files[0]);
    fd.append('_csrf', CSRF_TOKEN);
    const res = await fetch('/admin/productos/' + productId + '/imagen', {method:'POST', body:fd});
    const data = await res.json();
    // data.success, data.filename
}
```

### CMS (via AJAX al endpoint /admin/cms/imagen)
```javascript
// En cms/edit.php
async function uploadCmsImg(input, blockKey, fieldKey) {
    const fd = new FormData();
    fd.append('image', input.files[0]);
    fd.append('_csrf', CSRF_TOKEN);
    const res = await fetch('/admin/cms/imagen', {method:'POST', body:fd});
    const data = await res.json();
    // data.success, data.url
}
```

---

## Configuración (tabla settings)

Valores clave que existen en BD:
```
store_name, store_email, store_phone, store_city
stripe_public_key, stripe_secret_key, stripe_webhook_secret
mail_host, mail_user, mail_pass, mail_port, mail_encryption
free_shipping_min, shipping_cost, shipping_urgent_cost
invoice_prefix, invoice_number_format, invoice_number_padding, invoice_start_number
order_prefix, order_default_variant
site_favicon, admin_logo
```

Leer en PHP: `setting('store_name', 'Default')` o `s('store_name', 'Default')`
Leer en BD: `SELECT value FROM settings WHERE key='store_name'`

---

## Estado de funcionalidades

### ✅ Funcionando en producción
- Tienda pública, carrito, checkout con Stripe
- Panel admin completo con sidebar
- Pedidos: importación CSV TikTok, gestión, estados, facturación
- Clientes: perfiles, direcciones, bloqueos por cuenta e IP
- Sincronización pedidos↔clientes (botón en /admin/clientes)
- CMS frontpage: hero, historia (con imagen fondo+overlay), CTA, footer
- Footer dinámico con RRSS (WhatsApp, Instagram, TikTok, Facebook)
- Color de texto en editor richtext del CMS
- Imágenes: upload AJAX en productos y CMS (20MB, sin form multipart)
- Variantes: atributos globales + variantes por producto con precio/SKU/stock
- Vista de variantes en editar producto
- Suscripciones Stripe con creación automática de precios (sin Dashboard)
- Reseñas con filtros (estrellas 1-5, plataforma, fechas)
- Newsletter con email de bienvenida
- Compras internas (proveedores, órdenes de compra, recepción)
- Almacén con scanner EAN y estados de envío
- Atributos: edición correcta con vista attribute_edit.php
- IP del cliente capturada en pedidos (getClientIp con soporte Cloudflare)
- Sincronización direcciones de envío desde pedidos hacia perfiles de clientes

### ⚠️ Pendiente / por revisar
- Email SMTP: configurar en /admin/configuracion?tab=email y probar con botón "Enviar prueba"
- Selector de variantes en tienda pública (ficha de producto): el JS puede necesitar ajuste
- Webhook de Stripe para cobros recurrentes de suscripciones: verificar que llega

---

## Flujo de deploy

1. Claude Code edita archivo en local
2. WinSCP (sync automático activo) detecta el cambio y lo sube a OVH por SFTP
3. Si hay nueva migración en Migrator.php → entrar al Dashboard admin activa la migración
4. Verificar en producción

**No hay caché de PHP** en OVH hosting compartido — los cambios son inmediatos.

---

## Lo que NO hacer

- ❌ No crear archivos `.env` — la config va en la tabla `settings`
- ❌ No usar `$db->exec()` con UPDATE...JOIN — usar SELECT+UPDATE separados con rowCount()
- ❌ No pasar `null` como tercer argumento de `AuditService::log()` — usar string `'entity_type'`
- ❌ No añadir `function s()` en los layouts — ya existe en functions.php
- ❌ No redefinir funciones sin `if (!function_exists(...))`
- ❌ No usar `GROUP_CONCAT` con separador `|` si el valor puede contener `|`
- ❌ No hacer INSERT de migración que ya corrió sin IF NOT EXISTS
