# API Reference -- Nexus Autoparts POS

Base URL: `http://[IP]:5001`

Todas las rutas empiezan con `/pos/api/`. Salvo indicacion contraria, todos los endpoints requieren autenticacion via header `Authorization: Bearer <token>`.

---

## Autenticacion

Todos los endpoints (excepto login y employees list) requieren un JWT token obtenido via `/pos/api/auth/login`. El token se envia en el header:

```
Authorization: Bearer eyJhbGciOi...
```

El token contiene: `tenant_id`, `employee_id`, `name`, `role`, `branch_id`, `permissions`, `device_id`.

Duracion por defecto: 8 horas.

---

## 1. Auth (3 endpoints)

Prefix: `/pos/api/auth`

### POST /login

Login con PIN. No requiere autenticacion.

**Request body:**
```json
{
  "tenant_id": 1,
  "pin": "1234",
  "device_id": "tablet-01",
  "branch_id": 1
}
```

**Response 200:**
```json
{
  "token": "eyJhbGciOi...",
  "employee": {
    "id": 1,
    "name": "Admin",
    "role": "owner",
    "branch_id": 1,
    "max_discount_pct": 100.0
  },
  "permissions": ["pos.sell", "pos.discount", "..."]
}
```

**Errores:** 400 (faltan campos), 401 (PIN incorrecto), 404 (tenant no encontrado), 429 (rate limit)

### GET /employees/:tenant_id

Lista empleados para la pantalla de login. No requiere autenticacion.

**Response 200:**
```json
{
  "data": [
    {"id": 1, "name": "Admin", "initials": "AD", "role": "owner", "role_label": "Dueno"}
  ]
}
```

### GET /me

Info del empleado actual desde el token.

**Response 200:**
```json
{
  "employee_id": 1,
  "name": "Admin",
  "role": "owner",
  "tenant_id": 1,
  "branch_id": 1,
  "permissions": ["pos.sell", "..."]
}
```

---

## 2. Config (5 endpoints)

Prefix: `/pos/api/config`

### GET /branches

Lista sucursales del tenant.

**Permiso:** cualquier empleado autenticado

**Response 200:**
```json
{
  "data": [
    {"id": 1, "name": "Sucursal Centro", "address": "Av. Juarez 100", "phone": "555-0001", "is_active": true}
  ]
}
```

### POST /branches

Crear sucursal.

**Permiso:** `config.edit`

**Request body:**
```json
{
  "name": "Sucursal Norte",
  "address": "Blvd. Independencia 200",
  "phone": "555-0002"
}
```

**Response 201:**
```json
{"id": 2, "message": "Branch created"}
```

### GET /employees

Lista empleados con detalle.

**Permiso:** `config.view`

**Response 200:**
```json
{
  "data": [
    {
      "id": 1, "name": "Admin", "email": null, "phone": null,
      "role": "owner", "branch_id": 1, "branch_name": "Sucursal Centro",
      "max_discount_pct": 100.0, "is_active": true
    }
  ]
}
```

### POST /employees

Crear empleado.

**Permiso:** `config.edit`

**Request body:**
```json
{
  "name": "Juan Lopez",
  "role": "cashier",
  "pin": "5678",
  "email": "juan@correo.com",
  "phone": "555-1234",
  "branch_id": 1,
  "max_discount_pct": 10
}
```

Roles validos: `admin`, `cashier`, `warehouse`, `accountant`

**Response 201:**
```json
{"id": 2, "message": "Employee created"}
```

### GET /theme

Obtener tema actual del tenant.

**Permiso:** cualquier empleado autenticado

**Response 200:**
```json
{
  "theme": "default",
  "variables": {
    "--color-primary": "#1a73e8",
    "--color-secondary": "#5f6368",
    "--color-accent": "#ff6b35",
    "--color-bg": "#ffffff",
    "--color-surface": "#f8f9fa",
    "--color-text": "#202124",
    "--color-border": "#dadce0",
    "--font-display": "'Sora', sans-serif",
    "--font-body": "'Plus Jakarta Sans', sans-serif",
    "--font-mono": "'JetBrains Mono', monospace",
    "--radius": "8px"
  }
}
```

---

## 3. Inventory (22 endpoints)

Prefix: `/pos/api/inventory`

### GET /items

Lista productos con stock. Soporta busqueda y paginacion.

**Permiso:** `inventory.view`

**Query params:**
- `q` (string) -- buscar por nombre, part_number o barcode
- `page` (int, default 1)
- `per_page` (int, default 50, max 200)
- `branch_id` (int) -- filtrar por sucursal
- `category_id` (int) -- filtrar por categoria
- `low_stock` (bool) -- solo productos bajo minimo

**Response 200:**
```json
{
  "data": [
    {
      "id": 1, "part_number": "04465-02220", "barcode": "7501234567890",
      "name": "Pastillas de freno delanteras", "description": "...",
      "branch_id": 1, "branch_name": "Sucursal Centro",
      "category_id": 5, "brand": "Toyota", "unit": "PZA",
      "cost": 150.00, "price_1": 350.00, "price_2": 300.00, "price_3": 280.00,
      "tax_rate": 0.16, "min_stock": 5, "max_stock": 50, "location": "A-3-2",
      "image_url": null, "catalog_part_id": 12345, "stock": 23
    }
  ],
  "pagination": {"page": 1, "per_page": 50, "total": 1234, "total_pages": 25}
}
```

### GET /items/:item_id

Detalle de un producto con stock e historial de movimientos.

**Permiso:** `inventory.view`

### POST /items

Crear producto. Genera codigo de barras automaticamente si no se proporciona.

**Permiso:** `inventory.create`

**Request body:**
```json
{
  "part_number": "04465-02220",
  "name": "Pastillas de freno delanteras",
  "description": "Pastillas ceramicas originales Toyota",
  "branch_id": 1,
  "category_id": 5,
  "brand": "Toyota",
  "unit": "PZA",
  "cost": 150.00,
  "price_1": 350.00,
  "price_2": 300.00,
  "price_3": 280.00,
  "tax_rate": 0.16,
  "min_stock": 5,
  "max_stock": 50,
  "location": "A-3-2",
  "initial_stock": 10,
  "catalog_part_id": 12345
}
```

**Response 201:**
```json
{"id": 1, "barcode": "7501234567890", "message": "Item created"}
```

### PUT /items/:item_id

Actualizar datos de un producto.

**Permiso:** `inventory.edit`

**Request body:** Mismos campos que POST (parcial, solo campos a actualizar).

### POST /purchase

Registrar compra a proveedor. Incrementa stock.

**Permiso:** `inventory.create`

**Request body:**
```json
{
  "supplier": "Distribuidora Central",
  "invoice_number": "FAC-001",
  "items": [
    {"inventory_id": 1, "quantity": 20, "unit_cost": 145.00}
  ],
  "notes": "Compra semanal"
}
```

### POST /adjustment

Ajuste manual de inventario.

**Permiso:** `inventory.adjust`

**Request body:**
```json
{
  "inventory_id": 1,
  "quantity": -2,
  "reason": "Merma por dano en almacen"
}
```

### POST /transfer

Transferir stock entre sucursales.

**Permiso:** `inventory.transfer`

**Request body:**
```json
{
  "from_branch_id": 1,
  "to_branch_id": 2,
  "items": [
    {"inventory_id": 1, "quantity": 5}
  ],
  "notes": "Reabasto sucursal norte"
}
```

### POST /return

Registrar devolucion de producto. Incrementa stock.

**Permiso:** `inventory.create`

**Request body:**
```json
{
  "sale_id": 123,
  "items": [
    {"inventory_id": 1, "quantity": 1}
  ],
  "reason": "Parte equivocada"
}
```

### POST /physical-count/start

Iniciar toma fisica de inventario.

**Permiso:** `inventory.adjust`

**Request body:**
```json
{
  "branch_id": 1,
  "items": [
    {"inventory_id": 1, "counted_quantity": 22}
  ]
}
```

**Response 201:**
```json
{"count_id": 1, "items_counted": 1, "message": "Physical count started"}
```

### POST /physical-count/approve

Aprobar toma fisica y aplicar ajustes.

**Permiso:** `inventory.adjust`

**Request body:**
```json
{
  "count_id": 1
}
```

### GET /alerts

Obtener alertas de inventario (stock cero, bajo minimo, sobre maximo).

**Permiso:** `inventory.view`

**Response 200:**
```json
{
  "zero_stock": [...],
  "low_stock": [...],
  "over_stock": [...]
}
```

### GET /items/:item_id/history

Historial de movimientos de un producto.

**Permiso:** `inventory.view`

**Query params:**
- `limit` (int, default 50)

### GET /reports/valuation

Reporte de valorizacion del inventario.

**Permiso:** `inventory.view`

**Query params:**
- `branch_id` (int, opcional)

**Response 200:**
```json
{
  "total_cost_value": 125000.00,
  "total_sale_value": 350000.00,
  "item_count": 450,
  "items": [...]
}
```

### GET /reports/abc

Clasificacion ABC de productos por volumen de venta.

**Permiso:** `inventory.view`

**Query params:**
- `branch_id` (int, opcional)
- `days` (int, default 90)

### GET /reports/no-movement

Productos sin movimiento en un periodo.

**Permiso:** `inventory.view`

**Query params:**
- `branch_id` (int, opcional)
- `days` (int, default 60)

### GET /reports/low-stock

Productos por debajo del stock minimo.

**Permiso:** `inventory.view`

**Query params:**
- `branch_id` (int, opcional)

### GET /reports/branch-comparison

Comparativo de stock entre sucursales.

**Permiso:** `inventory.view`

**Query params:**
- `inventory_id` (int, opcional) -- para un producto especifico

### GET /categories

Lista categorias de inventario.

**Permiso:** `inventory.view`

### GET /brands

Lista marcas de productos en inventario.

**Permiso:** `inventory.view`

### POST /generate-barcode

Generar codigo de barras para un producto.

**Permiso:** `inventory.create`

**Response 200:**
```json
{"barcode": "7501234567891"}
```

---

## 4. Catalog (9 endpoints)

Prefix: `/pos/api/catalog`

Estos endpoints consultan la base maestra TecDoc y enriquecen con stock local del tenant.

### GET /brands

Lista marcas de vehiculos con partes.

**Permiso:** `catalog.view`

**Response 200:**
```json
{
  "data": [
    {"id": 1, "name": "TOYOTA", "part_count": 245000}
  ]
}
```

### GET /models

Modelos por marca.

**Permiso:** `catalog.view`

**Query params:**
- `brand_id` (int, requerido)

### GET /years

Anos disponibles por modelo.

**Permiso:** `catalog.view`

**Query params:**
- `model_id` (int, requerido)

### GET /engines

Motorizaciones por modelo + ano.

**Permiso:** `catalog.view`

**Query params:**
- `model_id` (int, requerido)
- `year_id` (int, requerido)

### GET /categories

Categorias de partes para un vehiculo.

**Permiso:** `catalog.view`

**Query params:**
- `mye_id` (int, requerido) -- ID de model_year_engine

### GET /groups

Subcategorias (grupos) de partes.

**Permiso:** `catalog.view`

**Query params:**
- `mye_id` (int, requerido)
- `category_id` (int, requerido)

### GET /parts

Partes con enriquecimiento de stock local.

**Permiso:** `catalog.view`

**Query params:**
- `mye_id` (int, requerido)
- `group_id` (int, requerido)
- `page` (int, default 1)
- `per_page` (int, default 30)
- `branch_id` (int, opcional)

**Response 200:**
```json
{
  "data": [
    {
      "id": 12345, "part_number": "04465-02220", "name": "Pastillas de freno",
      "manufacturer": "TOYOTA", "local_stock": 5, "local_price": 350.00,
      "alternatives_count": 3
    }
  ],
  "pagination": {"page": 1, "per_page": 30, "total": 15, "total_pages": 1}
}
```

### GET /part/:part_id

Detalle completo: stock local, stock en bodegas, alternativas aftermarket, cross-references.

**Permiso:** `catalog.view`

**Query params:**
- `branch_id` (int, opcional)

### GET /search

Busqueda inteligente por numero de parte o texto.

**Permiso:** `catalog.view`

**Query params:**
- `q` (string, requerido, minimo 2 caracteres)
- `limit` (int, default 50)
- `branch_id` (int, opcional)

---

## 5. POS / Sales (16 endpoints)

Prefix: `/pos/api`

### POST /sales

Crear venta.

**Permiso:** `pos.sell`

**Request body:**
```json
{
  "items": [
    {"inventory_id": 1, "quantity": 2, "unit_price": 350.00, "discount_pct": 0, "tax_rate": 0.16}
  ],
  "customer_id": null,
  "payment_method": "efectivo",
  "sale_type": "cash",
  "register_id": 1,
  "amount_paid": 812.00,
  "payment_details": [],
  "notes": ""
}
```

`sale_type`: `cash` (contado), `credit` (credito), `mixed`

`payment_method`: `efectivo`, `transferencia`, `tarjeta`, `mixto`

Para pagos mixtos, `payment_details` es un array:
```json
[
  {"method": "efectivo", "amount": 500.00},
  {"method": "tarjeta", "amount": 312.00, "reference": "AUTH-12345"}
]
```

**Response 201:**
```json
{
  "sale_id": 1,
  "folio": "V-0001",
  "total": 812.00,
  "change": 0.00,
  "items_count": 1
}
```

### GET /sales

Lista ventas con filtros.

**Permiso:** `pos.view`

**Query params:**
- `date_from` (YYYY-MM-DD)
- `date_to` (YYYY-MM-DD)
- `employee_id` (int)
- `customer_id` (int)
- `status` (string: `completed`, `cancelled`, `returned`)
- `register_id` (int)
- `page` (int, default 1)
- `per_page` (int, default 50, max 200)

### GET /sales/:sale_id

Detalle de una venta con items, pagos y datos del cliente.

**Permiso:** `pos.view`

### PUT /sales/:sale_id/cancel

Cancelar una venta. Restaura inventario.

**Permiso:** `pos.cancel`

**Request body:**
```json
{
  "reason": "Cliente cambio de opinion"
}
```

### GET /sales/last

Obtener la ultima venta (para reimprimir ticket).

**Permiso:** `pos.sell`

### POST /quotations

Crear cotizacion (venta sin cobrar).

**Permiso:** `pos.sell`

**Request body:**
```json
{
  "items": [
    {"inventory_id": 1, "quantity": 2, "unit_price": 350.00, "discount_pct": 0, "tax_rate": 0.16}
  ],
  "customer_id": 5,
  "notes": "Cotizacion para taller Perez",
  "valid_days": 15
}
```

### GET /quotations

Lista cotizaciones.

**Permiso:** `pos.view`

**Query params:**
- `status` (string: `active`, `converted`, `expired`, `cancelled`)
- `customer_id` (int)
- `page` (int, default 1)
- `per_page` (int, default 50)

### GET /quotations/:quot_id

Detalle de una cotizacion.

**Permiso:** `pos.view`

### POST /quotations/:quot_id/convert

Convertir cotizacion a venta. Aplica los mismos campos que POST /sales.

**Permiso:** `pos.sell`

**Request body:**
```json
{
  "payment_method": "efectivo",
  "register_id": 1,
  "amount_paid": 812.00
}
```

### PUT /quotations/:quot_id/cancel

Cancelar cotizacion.

**Permiso:** `pos.cancel`

### POST /layaways

Crear apartado con pago parcial.

**Permiso:** `pos.sell`

**Request body:**
```json
{
  "items": [
    {"inventory_id": 1, "quantity": 1, "unit_price": 5000.00, "discount_pct": 0, "tax_rate": 0.16}
  ],
  "customer_id": 5,
  "down_payment": 2000.00,
  "payment_method": "efectivo",
  "register_id": 1,
  "notes": "Apartado motor de arranque",
  "due_date": "2026-04-30"
}
```

### GET /layaways

Lista apartados.

**Permiso:** `pos.view`

**Query params:**
- `status` (string: `active`, `completed`, `cancelled`)
- `customer_id` (int)
- `page`, `per_page`

### GET /layaways/:layaway_id

Detalle de un apartado con items y pagos.

**Permiso:** `pos.view`

### POST /layaways/:layaway_id/payment

Registrar abono a un apartado.

**Permiso:** `pos.sell`

**Request body:**
```json
{
  "amount": 1500.00,
  "payment_method": "efectivo",
  "register_id": 1
}
```

### POST /layaways/:layaway_id/complete

Completar apartado (cuando el saldo llega a cero). Descuenta inventario.

**Permiso:** `pos.sell`

### PUT /layaways/:layaway_id/cancel

Cancelar apartado.

**Permiso:** `pos.cancel`

**Request body:**
```json
{
  "reason": "Cliente no completo pagos"
}
```

---

## 6. Customers (7 endpoints)

Prefix: `/pos/api/customers`

### GET /

Lista clientes con busqueda.

**Permiso:** `customers.view`

**Query params:**
- `q` (string) -- busca por nombre, RFC, telefono, razon social
- `page` (int, default 1)
- `per_page` (int, default 50, max 200)
- `branch_id` (int)

### GET /:customer_id

Detalle de un cliente.

**Permiso:** `customers.view`

### POST /

Crear cliente.

**Permiso:** `customers.create`

**Request body:**
```json
{
  "name": "Taller Perez",
  "rfc": "XAXX010101000",
  "razon_social": "Taller Mecanico Perez SA de CV",
  "regimen_fiscal": "601",
  "codigo_postal": "06600",
  "uso_cfdi": "G03",
  "email": "taller@correo.com",
  "phone": "555-9876",
  "address": "Calle Reforma 50",
  "price_tier": "taller",
  "credit_limit": 50000.00,
  "branch_id": 1
}
```

`price_tier`: `mostrador` (default), `taller`, `mayoreo`

### PUT /:customer_id

Actualizar cliente.

**Permiso:** `customers.edit`

### GET /:customer_id/statement

Estado de cuenta del cliente.

**Permiso:** `customers.view`

**Query params:**
- `date_from` (YYYY-MM-DD)
- `date_to` (YYYY-MM-DD)

**Response 200:**
```json
{
  "customer": {"id": 5, "name": "Taller Perez", "credit_limit": 50000.00, "balance": 12500.00},
  "transactions": [
    {"date": "2026-03-28", "type": "sale", "reference": "V-0045", "amount": 5000.00, "balance": 12500.00},
    {"date": "2026-03-25", "type": "payment", "reference": "PAG-012", "amount": -3000.00, "balance": 7500.00}
  ]
}
```

### GET /:customer_id/vehicles

Vehiculos asociados al cliente.

**Permiso:** `customers.view`

### POST /:customer_id/payment

Registrar pago/abono del cliente.

**Permiso:** `customers.edit`

**Request body:**
```json
{
  "amount": 5000.00,
  "payment_method": "transferencia",
  "reference": "SPEI-20260401-001",
  "notes": "Abono parcial"
}
```

---

## 7. Cash Register (7 endpoints)

Prefix: `/pos/api/register`

### POST /open

Abrir caja registradora.

**Permiso:** `pos.sell`

**Request body:**
```json
{
  "register_number": 1,
  "opening_amount": 2000.00
}
```

**Response 201:**
```json
{
  "register_id": 1,
  "register_number": 1,
  "opened_at": "2026-04-01T08:00:00",
  "opening_amount": 2000.00
}
```

**Errores:** 400 (ya tiene caja abierta)

### GET /current

Obtener la caja abierta del empleado actual.

**Permiso:** `pos.sell`

**Response 200:**
```json
{
  "register_id": 1,
  "register_number": 1,
  "opened_at": "2026-04-01T08:00:00",
  "opening_amount": 2000.00,
  "current_cash": 5430.00,
  "sales_count": 12,
  "sales_total": 8750.00
}
```

### POST /movement

Registrar entrada o salida de efectivo.

**Permiso:** `pos.sell`

**Request body:**
```json
{
  "register_id": 1,
  "type": "out",
  "amount": 500.00,
  "reason": "Compra de papeleria"
}
```

`type`: `in` (entrada) o `out` (salida)

### GET /cut-x

Corte X (consulta sin cerrar caja).

**Permiso:** `pos.sell`

**Response 200:**
```json
{
  "register_id": 1,
  "opening_amount": 2000.00,
  "sales_cash": 3500.00,
  "sales_card": 2100.00,
  "sales_transfer": 1150.00,
  "movements_in": 0.00,
  "movements_out": 500.00,
  "expected_cash": 5000.00,
  "sales_count": 12,
  "total_sales": 6750.00
}
```

### POST /cut-z

Corte Z (cierre de caja).

**Permiso:** `pos.sell`

**Request body:**
```json
{
  "register_id": 1,
  "counted_amount": 4980.00
}
```

**Response 200:**
```json
{
  "register_id": 1,
  "expected_cash": 5000.00,
  "counted_amount": 4980.00,
  "difference": -20.00,
  "closed_at": "2026-04-01T22:00:00",
  "summary": {"..."}
}
```

### GET /history

Historial de cortes.

**Permiso:** `pos.sell`

**Query params:**
- `date_from` (YYYY-MM-DD)
- `date_to` (YYYY-MM-DD)
- `page`, `per_page`

### GET /daily-summary

Resumen consolidado del dia (todas las cajas).

**Permiso:** `pos.sell`

**Query params:**
- `date` (YYYY-MM-DD, default hoy)

---

## 8. Invoicing (6 endpoints)

Prefix: `/pos/api/invoicing`

### POST /invoice

Generar factura CFDI 4.0 desde una venta.

**Permiso:** `invoicing.create`

**Request body:**
```json
{
  "sale_id": 45,
  "tipo_comprobante": "I",
  "uso_cfdi": "G03",
  "forma_pago": "01",
  "metodo_pago": "PUE"
}
```

`tipo_comprobante`: `I` (Ingreso), `E` (Egreso), `P` (Pago)

`metodo_pago`: `PUE` (Pago en Una sola Exhibicion) o `PPD` (Pago en Parcialidades o Diferido)

**Response 201:**
```json
{
  "cfdi_id": 1,
  "uuid": null,
  "status": "pending",
  "xml_preview": "<?xml ..."
}
```

### GET /queue

Lista cola de timbrado.

**Permiso:** `invoicing.view`

**Query params:**
- `status` (string: `pending`, `stamped`, `error`, `cancelled`)
- `page`, `per_page`

### GET /queue/:cfdi_id

Detalle de un CFDI en cola.

**Permiso:** `invoicing.view`

### POST /queue/process

Procesar cola de timbrado (enviar pendientes al PAC).

**Permiso:** `invoicing.create`

### POST /cancel/:cfdi_id

Cancelar factura ante el SAT.

**Permiso:** `invoicing.cancel`

**Request body:**
```json
{
  "motivo": "02",
  "folio_sustitucion": null
}
```

Motivos SAT:
- `01`: Comprobante emitido con errores con relacion (requiere `folio_sustitucion`)
- `02`: Comprobante emitido con errores sin relacion
- `03`: No se llevo a cabo la operacion
- `04`: Operacion nominativa relacionada en una factura global

### GET /:sale_id/pdf

Descargar PDF de representacion impresa de la factura.

**Permiso:** `invoicing.view`

**Response:** PDF file (application/pdf)

---

## 9. Accounting (11 endpoints)

Prefix: `/pos/api/accounting`

### GET /accounts

Catalogo de cuentas contables (lista plana, el frontend construye el arbol).

**Permiso:** `accounting.view`

**Response 200:**
```json
{
  "data": [
    {
      "id": 1, "code": "100", "sat_code": "100",
      "name": "Activo", "type": "debit",
      "parent_id": null, "level": 1, "is_active": true
    }
  ]
}
```

### POST /accounts

Crear cuenta contable.

**Permiso:** `accounting.create`

**Request body:**
```json
{
  "code": "100.01",
  "sat_code": "100.01",
  "name": "Caja",
  "type": "debit",
  "parent_id": 1,
  "description": "Efectivo en caja"
}
```

### GET /entries

Lista polizas contables.

**Permiso:** `accounting.view`

**Query params:**
- `date_from` (YYYY-MM-DD)
- `date_to` (YYYY-MM-DD)
- `type` (string: `sale`, `purchase`, `adjustment`, `manual`)
- `page`, `per_page`

### GET /entries/:entry_id

Detalle de una poliza con lineas de cargo y abono.

**Permiso:** `accounting.view`

**Response 200:**
```json
{
  "id": 1,
  "date": "2026-04-01",
  "type": "sale",
  "reference": "V-0045",
  "description": "Venta al contado",
  "lines": [
    {"account_id": 5, "account_code": "100.01", "account_name": "Caja", "debit": 812.00, "credit": 0.00},
    {"account_id": 10, "account_code": "400.01", "account_name": "Ventas", "debit": 0.00, "credit": 700.00},
    {"account_id": 15, "account_code": "210.01", "account_name": "IVA Trasladado", "debit": 0.00, "credit": 112.00}
  ]
}
```

### POST /entries

Crear poliza manual.

**Permiso:** `accounting.create`

**Request body:**
```json
{
  "date": "2026-04-01",
  "description": "Pago de renta del local",
  "lines": [
    {"account_id": 20, "debit": 15000.00, "credit": 0.00},
    {"account_id": 5, "debit": 0.00, "credit": 15000.00}
  ]
}
```

La suma de cargos debe ser igual a la suma de abonos.

### GET /trial-balance

Balanza de comprobacion.

**Permiso:** `accounting.view`

**Query params:**
- `date_from` (YYYY-MM-DD)
- `date_to` (YYYY-MM-DD)

**Response 200:**
```json
{
  "period": {"from": "2026-04-01", "to": "2026-04-30"},
  "accounts": [
    {
      "code": "100.01", "name": "Caja",
      "opening_debit": 50000.00, "opening_credit": 0.00,
      "period_debit": 25000.00, "period_credit": 18000.00,
      "closing_debit": 57000.00, "closing_credit": 0.00
    }
  ],
  "totals": {
    "opening_debit": 100000.00, "opening_credit": 100000.00,
    "period_debit": 45000.00, "period_credit": 45000.00,
    "closing_debit": 145000.00, "closing_credit": 145000.00
  }
}
```

### GET /income-statement

Estado de resultados.

**Permiso:** `accounting.view`

**Query params:**
- `date_from` (YYYY-MM-DD)
- `date_to` (YYYY-MM-DD)

**Response 200:**
```json
{
  "period": {"from": "2026-04-01", "to": "2026-04-30"},
  "income": [{"code": "400.01", "name": "Ventas", "amount": 150000.00}],
  "cost_of_sales": [{"code": "500.01", "name": "Costo de Ventas", "amount": 90000.00}],
  "expenses": [{"code": "600.01", "name": "Gastos de Operacion", "amount": 25000.00}],
  "gross_profit": 60000.00,
  "operating_profit": 35000.00,
  "net_income": 35000.00
}
```

### GET /balance-sheet

Balance general.

**Permiso:** `accounting.view`

**Query params:**
- `date` (YYYY-MM-DD, default hoy)

**Response 200:**
```json
{
  "date": "2026-04-30",
  "assets": [{"code": "100", "name": "Activo Circulante", "amount": 250000.00, "children": [...]}],
  "liabilities": [{"code": "200", "name": "Pasivo", "amount": 80000.00, "children": [...]}],
  "equity": [{"code": "300", "name": "Capital", "amount": 170000.00, "children": [...]}],
  "total_assets": 250000.00,
  "total_liabilities_equity": 250000.00
}
```

### GET /aging

Antiguedad de saldos (cuentas por cobrar).

**Permiso:** `accounting.view`

**Query params:**
- `date` (YYYY-MM-DD, default hoy)

**Response 200:**
```json
{
  "date": "2026-04-30",
  "customers": [
    {
      "customer_id": 5, "name": "Taller Perez",
      "current": 5000.00,
      "days_1_30": 3000.00,
      "days_31_60": 2000.00,
      "days_61_90": 0.00,
      "days_over_90": 0.00,
      "total": 10000.00
    }
  ],
  "totals": {
    "current": 15000.00, "days_1_30": 8000.00,
    "days_31_60": 3000.00, "days_61_90": 1000.00,
    "days_over_90": 500.00, "total": 27500.00
  }
}
```

### GET /periods

Lista periodos fiscales.

**Permiso:** `accounting.view`

**Response 200:**
```json
{
  "data": [
    {"year": 2026, "month": 4, "status": "open", "closed_at": null, "closed_by": null},
    {"year": 2026, "month": 3, "status": "closed", "closed_at": "2026-04-05T10:00:00", "closed_by": "Admin"}
  ]
}
```

### POST /periods/close

Cerrar periodo fiscal. Irreversible.

**Permiso:** `accounting.create`

**Request body:**
```json
{
  "year": 2026,
  "month": 3
}
```

---

## 10. Health Check (1 endpoint)

### GET /pos/health

No requiere autenticacion.

**Response 200:**
```json
{"status": "ok"}
```

---

## Resumen de endpoints

| Modulo | Prefix | Endpoints |
|--------|--------|-----------|
| Auth | `/pos/api/auth` | 3 |
| Config | `/pos/api/config` | 5 |
| Inventory | `/pos/api/inventory` | 22 |
| Catalog | `/pos/api/catalog` | 9 |
| POS/Sales | `/pos/api` | 16 |
| Customers | `/pos/api/customers` | 7 |
| Cash Register | `/pos/api/register` | 7 |
| Invoicing | `/pos/api/invoicing` | 6 |
| Accounting | `/pos/api/accounting` | 11 |
| Health | `/pos` | 1 |
| **Total** | | **87** |

---

## Codigos de error comunes

| Codigo | Significado |
|--------|------------|
| 400 | Request invalido (faltan campos requeridos, datos incorrectos) |
| 401 | No autenticado (token faltante, expirado o invalido) |
| 403 | Sin permiso (el rol no tiene el permiso requerido) |
| 404 | Recurso no encontrado |
| 429 | Rate limit excedido (demasiados intentos de PIN) |
| 500 | Error interno del servidor |

Todos los errores retornan:
```json
{"error": "Descripcion del error"}
```

---

## 11. BNPL (Buy Now Pay Later) — Stub

Prefix: `/pos/api/bnpl`

Stubs ready for APLAZO, Kueski, Clip integration.

### GET /providers

List configured BNPL providers.

**Response 200:**
```json
{
  "providers": [
    {"id": "ap lazo", "name": "APLAZO", "enabled": false, "config_needed": ["api_key", "merchant_id"]},
    {"id": "kueski", "name": "Kueski Pay", "enabled": false, "config_needed": ["api_key", "secret"]},
    {"id": "clip", "name": "Clip Pagos", "enabled": false, "config_needed": ["api_key"]}
  ]
}
```

### POST /applications

Create a BNPL application for a sale.

**Request body:**
```json
{
  "sale_id": 123,
  "amount": 1500.00,
  "provider": "ap lazo",
  "customer": {"name": "Juan Perez", "phone": "5512345678"}
}
```

### GET /applications/:id

Get application status.

### POST /webhook/:provider

Receive provider webhooks.

---

## 12. ERP Sync — Stub

Prefix: `/pos/api/erp`

Stubs ready for Aspel SAE, CONTPAQi, SAP B1, Odoo.

### GET /providers

List supported ERP systems.

### POST /sync

Start a sync job.

**Request body:**
```json
{
  "provider": "aspel_sae",
  "sync_type": "sales"
}
```

### GET /sync/:job_id

Get sync job status.

### POST /sync/:job_id/run

Mock execute sync job.

---

## 13. WhatsApp Business API (Meta Cloud) — Stub

Prefix: `/pos/api/whatsapp-cloud`

### GET/POST /webhook

Meta Cloud API webhook verification and message reception.

### GET /status

Check Meta Cloud API connection status.

### GET /templates

List approved message templates.

### POST /messages

Send a message.

**Request body:**
```json
{
  "to": "5215512345678",
  "body": "Su orden esta lista",
  "template": "order_ready"
}
```

---

## 14. Supplier Portal

Prefix: `/pos/api/supplier-portal`

### GET /demand

Aggregated demand by zone, part group, and time range.

**Query params:** `days` (default 30), `group_id`, `branch_id`

### GET /top-parts

Top 50 moving parts with current stock.

**Query params:** `days` (default 30)

---

## 15. Dashboard Stats

Prefix: `/pos/api/dashboard`

### GET /stats

Summary stats for today and this month.

**Response 200:**
```json
{
  "today": {"sales_count": 42, "sales_total": 12500.00},
  "month": {"sales_count": 1200, "sales_total": 450000.00},
  "top_products": [...],
  "hourly_sales": [...]
}
```

### GET /stats/employees

Sales per employee today.
