<div align="center">

# @wepos/sdk

**SDK oficial de [WePOS](https://pos.wecodecr.com)** — facturación electrónica Hacienda Costa Rica v4.4 para integradores.

[![npm version](https://img.shields.io/npm/v/@wepos/sdk.svg?color=4f46e5)](https://www.npmjs.com/package/@wepos/sdk)
[![downloads](https://img.shields.io/npm/dm/@wepos/sdk.svg?color=4f46e5)](https://www.npmjs.com/package/@wepos/sdk)
[![node](https://img.shields.io/node/v/@wepos/sdk.svg)](https://nodejs.org)
[![types](https://img.shields.io/npm/types/@wepos/sdk.svg)](https://www.npmjs.com/package/@wepos/sdk)
[![license: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT)

TypeScript/Node · **cero dependencias en runtime** · ESM + CJS · tipos incluidos

</div>

---

## Tabla de contenido

- [¿Qué resuelve?](#qué-resuelve)
- [Características](#características-v10)
- [Requisitos](#requisitos)
- [Obtener una API key](#obtener-una-api-key)
- [Instalación](#instalación)
- [Inicio rápido](#inicio-rápido)
- [Ambientes](#ambientes-estilo-stripe)
- [Configuración](#configuración)
- [Cobertura de la API](#cobertura-de-la-api)
- [Recursos](#recursos)
- [Partner — backend fiscal embebido](#partner--wepos-como-backend-fiscal-embebido)
- [Actividad económica](#actividad-económica-emisor-y-receptor)
- [Idempotencia](#idempotencia)
- [Manejo de errores](#manejo-de-errores)
- [Reintentos y timeouts](#reintentos-y-timeouts)
- [TypeScript](#typescript)
- [Roadmap](#roadmap)
- [Versionado](#versionado)
- [Soporte](#soporte)
- [Licencia](#licencia)

## ¿Qué resuelve?

Conecta cualquier sistema de terceros (ecommerce, ERP, app a medida, backend) con WePOS para
**emitir comprobantes electrónicos ante Hacienda Costa Rica** y consultar catálogo, clientes e
inventario — sin lidiar con XML, firmas digitales, claves numéricas ni el ciclo asíncrono de
Hacienda. Tú llamas un método tipado; el SDK se encarga del resto.

```ts
const invoice = await wepos.invoices.issueAndWait({
  documentType: 'FE',
  customer: { idType: 'CEDULA_JURIDICA', idNumber: '3101123456', name: 'Cliente S.A.' },
  lines: [{ description: 'Consultoría', quantity: 1, unitPrice: 25000, cabysCode: '8314300000000' }],
})
invoice.haciendaStatus // 'aceptado'
```

## Características (v1.0)

- ✅ **Emisión completa**: Factura (FE), Tiquete (TE), Nota de Crédito (NC), Nota de Débito (ND) y Factura de Exportación (FEE).
- ✅ **`issueAndWait`** — emite y espera (poll) la respuesta de Hacienda, encapsulando el ciclo fiscal asíncrono.
- ✅ **Ambientes estilo Stripe** — el prefijo de la key (`wpk_test_` / `wpk_live_`) define Sandbox vs Producción; datos aislados.
- ✅ **Idempotencia** — `Idempotency-Key` para que un reintento nunca duplique un comprobante.
- ✅ **Reintentos automáticos** con backoff exponencial ante `429` / `5xx` / errores de red.
- ✅ **Errores tipados** con `code`, `status` y `requestId` (rastreables en soporte).
- ✅ **Catálogo, clientes, inventario y sucursales**.
- ✅ **Referencia Hacienda** — validar CABYS, contribuyentes y exoneraciones.
- ✅ **100% tipado**, ESM + CJS, **cero dependencias** en runtime.

## Requisitos

| Requisito      | Versión                                              |
| -------------- | ---------------------------------------------------- |
| Node.js        | ≥ 18 (usa `fetch` nativo)                            |
| Runtimes       | Node, Bun, Deno y entornos edge con `fetch`          |
| Una API key    | de WePOS — ver [abajo](#obtener-una-api-key)         |

## Obtener una API key

La API key se solicita y administra desde el panel de WePOS:

> **[pos.wecodecr.com](https://pos.wecodecr.com) → Configuración → API Keys → Crear API key**

Al crearla eliges sus **scopes** (permisos mínimos) y su **ambiente**:

- `wpk_test_…` → **Sandbox** (pruebas, sin validez fiscal).
- `wpk_live_…` → **Producción** (comprobantes con validez fiscal).

Guarda la key de forma segura (se muestra una sola vez) y **nunca la expongas en el cliente/navegador** — úsala solo desde tu backend.

## Instalación

```bash
npm install @wepos/sdk
# pnpm add @wepos/sdk · yarn add @wepos/sdk · bun add @wepos/sdk
```

## Inicio rápido

```ts
import { WeposClient } from '@wepos/sdk'

const wepos = new WeposClient({
  apiKey: process.env.WEPOS_API_KEY!, // wpk_test_… (Sandbox) o wpk_live_… (Producción)
})

const invoice = await wepos.invoices.issueAndWait({
  documentType: 'FE',
  customer: {
    idType: 'CEDULA_JURIDICA',
    idNumber: '3101123456',
    name: 'Cliente S.A.',
    email: 'cliente@correo.cr',
  },
  lines: [
    { description: 'Consultoría', quantity: 1, unitPrice: 25000, cabysCode: '8314300000000' },
  ],
})

console.log(invoice.haciendaStatus) // 'aceptado' | 'rechazado'
console.log(invoice.claveNumerica)
```

## Ambientes (estilo Stripe)

El ambiente lo determina el **prefijo de la API key** — no se configura aparte:

| Key            | Ambiente   | Validez fiscal |
| -------------- | ---------- | -------------- |
| `wpk_test_...` | Sandbox    | No (pruebas)   |
| `wpk_live_...` | Producción | Sí             |

Los datos están aislados por ambiente: una key de sandbox nunca ve comprobantes de producción.

## Configuración

```ts
new WeposClient({
  apiKey: 'wpk_live_...',
  baseUrl: 'https://pos.wecodecr.com', // default
  timeoutMs: 30_000,                   // default
  maxRetries: 2,                       // reintenta 429 / 5xx / red, con backoff
})
```

## Cobertura de la API

| Recurso                         | Estado | Scope requerido        |
| ------------------------------- | :----: | ---------------------- |
| Facturas (FE/TE/NC/ND/FEE)      |   ✅   | `invoices:read/write`  |
| Clientes                        |   ✅   | `customers:read/write` |
| Productos                       |   ✅   | `products:read`        |
| Inventario                      |   ✅   | `inventory:read`       |
| Sucursales                      |   ✅   | `branches:read`        |
| Referencia (CABYS, cédulas…)    |   ✅   | `reference:read`       |
| Webhooks (verificación)         |   🔜   | *(v1.1)*               |
| Ventas POS / sesiones de caja   |   🔜   | *(v1.4)*               |
| Cuentas por cobrar / pagar      |   🔜   | *(v1.4)*               |
| Reportes y exportación contable |   🔜   | *(v1.4)*               |

## Recursos

### Facturas — `wepos.invoices`

```ts
await wepos.invoices.create(input, { idempotencyKey })   // emite y retorna de inmediato
await wepos.invoices.issueAndWait(input, { timeoutMs, pollIntervalMs }) // emite y espera a Hacienda
await wepos.invoices.get(id)                              // estado + detalle
await wepos.invoices.list({ limit, documentType })
await wepos.invoices.getXml(id)                           // XML firmado + respuesta Hacienda
await wepos.invoices.createCreditNote(id, { reason, fullReversal })
await wepos.invoices.createDebitNote(id, { reason, referenceCode, lines })
await wepos.invoices.export(input)                        // Factura de Exportación (FEE)
```

### Clientes — `wepos.customers`

```ts
await wepos.customers.create({ idType, idNumber, name, email })
await wepos.customers.list({ search, limit })
```

### Catálogo e inventario

```ts
await wepos.products.list({ search, limit })
await wepos.inventory.list({ productId, warehouseId, search, limit })
await wepos.branches.list()  // para obtener el branchId a usar en emisiones
```

### Referencia Hacienda — `wepos.reference`

```ts
await wepos.reference.getCabys('8314300000000')   // valida código, retorna tarifa IVA
await wepos.reference.searchCabys('consultoría')  // busca por texto
await wepos.reference.getTaxpayer('3101123456')   // valida contribuyente
await wepos.reference.getExoneration('AUT-123')   // consulta exoneración DGT
```

## Partner — WePOS como backend fiscal embebido

Si tenés **tu propio SaaS multi-tenant** y querés que WePOS sea el motor de
facturación electrónica **detrás de escena** (tus clientes nunca entran a WePOS:
gestionan todo desde tu SaaS), usá la superficie **Partner**. Con **una sola
credencial** (la *partner key*) tu SaaS aprovisiona y administra **muchos**
tenants, cada uno con su propio certificado, credenciales de Hacienda y
comprobantes — todo guardado en WePOS.

### Dos tipos de credenciales

| Credencial | Prefijo | Para qué | De dónde sale |
|---|---|---|---|
| **Partner key** | `wppk_test_…` / `wppk_live_…` | Aprovisionar y configurar tenants (plano de control) | El super-admin de WePOS la genera en **`/partners`** |
| **Tenant key** | `wpk_test_…` / `wpk_live_…` | Emitir comprobantes de UN tenant (plano de datos) | La devuelve `partner.tenants.create()`, o se genera por tenant |

> La partner key **solo puede tocar sus propios tenants** (aislamiento estricto:
> un tenant de otro partner responde `404`). Guardala como secreto de servidor.

### Flujo de integración (end-to-end)

```ts
import { WeposClient } from '@wepos/sdk'

// El SaaS usa su PARTNER key (no una de tenant).
const wepos = new WeposClient({ apiKey: process.env.WEPOS_PARTNER_KEY! }) // wppk_…

// 1) Aprovisionar el tenant fiscal del cliente final del SaaS.
//    Crea el negocio completo (casa matriz, actividades, consecutivos…) y
//    devuelve una tenant key de datos lista para emitir.
const { tenantId, tenantApiKey } = await wepos.partner.tenants.create({
  name: 'Soda La Esquina',
  idType: 'CEDULA_JURIDICA',
  idNumber: '3101123456',
  adminEmail: 'dueno@soda.com',
  location: { province: '1', canton: '01', district: '01' }, // códigos Hacienda (o nombres: 'San José'/'Central'/'Carmen')
  activities: [{ codigo: '5610.0', tipo: 'P' }],   // opcional
})

// 2) Configurar Hacienda — el cliente sube su firma + credenciales desde tu UI.
//    Funciona por AMBIENTE: primero STAGING para probar, luego PRODUCTION.
await wepos.partner.tenants.setCredentials(tenantId, {
  environment: 'STAGING',
  user: 'cpj-3-101-123456@stag.comprobanteselectronicos.go.cr',
  password: '••••••',
})
await wepos.partner.tenants.uploadCertificate(tenantId, {
  environment: 'STAGING',
  p12Base64: fs.readFileSync('firma.p12').toString('base64'),
  pin: '1234',
})

// 3) Ver el estado de configuración (sin exponer secretos).
const cfg = await wepos.partner.tenants.get(tenantId)
// cfg.environments → [{ environment: 'STAGING', hasCredentials: true, hasCertificate: true, certExpiresAt }]

// 3.5) MIGRACIÓN — si el negocio viene de otro sistema, continuá su numeración.
//      value = último número YA emitido; el próximo comprobante sale value+1.
await wepos.partner.tenants.setConsecutives(tenantId, {
  environment: 'PRODUCTION',
  counters: [
    { branchCode: '001', terminalCode: '00001', documentType: 'FE', value: 400 }, // próxima FE = 401
    { branchCode: '001', terminalCode: '00001', documentType: 'TE', value: 1250 },
  ],
})
// Consultar los contadores actuales:
const { counters } = await wepos.partner.tenants.getConsecutives(tenantId, 'PRODUCTION')

// 4) Cuando el cliente pasa a producción: repetí el paso 2 con environment:'PRODUCTION'
//    y promové el ambiente por defecto del tenant.
await wepos.partner.tenants.update(tenantId, { defaultEnvironment: 'PRODUCTION' })

// 5) Emitir por ese tenant — con la tenant key de datos (plano de datos normal).
const tenant = new WeposClient({ apiKey: tenantApiKey }) // wpk_…
const invoice = await tenant.invoices.issueAndWait({
  documentType: 'FE',
  customer: { idType: 'CEDULA_JURIDICA', idNumber: '3101003937', name: 'Cliente S.A.' },
  lines: [{ description: 'Casado', quantity: 1, unitPrice: 3500, cabysCode: '2312000000300' }],
})
console.log(invoice.claveNumerica, invoice.haciendaStatus)

// 6) ¿El tenant se creó bajo la partner key equivocada (ej. live) y necesitás
//    emitir en sandbox? Pedí una data key del ambiente que querés — sin recrear
//    el tenant. El ambiente lo manda la data key (estilo Stripe), no el tenant.
const { tenantApiKey: sandboxKey } = await wepos.partner.tenants.issueKey(tenantId, { environment: 'STAGING' })
const sandbox = new WeposClient({ apiKey: sandboxKey }) // wpk_test_… → emite en STAGING
```

### Métodos de `wepos.partner.tenants`

| Método | Qué hace |
|---|---|
| `create(input)` | Aprovisiona un tenant fiscal completo; devuelve `tenantId` + `tenantApiKey` (salvo `issueApiKey: false`) |
| `list()` | Lista los tenants de este partner |
| `get(tenantId)` | Config + estado por ambiente (`hasCredentials`/`hasCertificate`/`certExpiresAt`) |
| `update(tenantId, { defaultEnvironment?, activities? })` | Promueve el ambiente por defecto y/o reemplaza actividades |
| `setCredentials(tenantId, { environment?, user, password })` | Credenciales ATV por ambiente |
| `uploadCertificate(tenantId, { environment?, p12Base64, pin })` | Certificado `.p12` (base64) por ambiente; valida el archivo + PIN y que la cédula del cert coincida con el emisor (evita `-60`) |
| `getConsecutives(tenantId, environment?)` | Lista los contadores de consecutivos por ambiente (con el próximo consecutivo de 20 dígitos) |
| `setConsecutives(tenantId, { environment?, counters })` | **Migración**: fija el consecutivo de arranque de forma atómica; `value` = último emitido, debe ser ≥ el actual |
| `issueKey(tenantId, { environment?, name?, scopes? })` | Emite una nueva data key (`wpk_…`) para el tenant, por ambiente. Mueve un tenant existente entre sandbox y producción sin recrearlo |
| `listKeys(tenantId)` | Lista las data keys del tenant (solo metadatos, nunca el secreto) |
| `revokeKey(tenantId, keyId)` | Revoca una data key (inmediato e irreversible) |

> **Certificado:** el `.p12` y su PIN se guardan **encriptados** en WePOS y nunca
> se devuelven. Manejalos como *pass-through* en tu SaaS (no los persistas ni
> loguees). El `environment` por defecto es el de la partner key.

## Actividad económica (emisor y receptor)

Un contribuyente puede tener **varias actividades** registradas en Hacienda. Puedes:

- Elegir con cuál actividad **emite** el emisor → `activityCode` (debe ser una actividad registrada del emisor; si se omite, usa la principal).
- Enviar la actividad del **comprador** → `receptorActivityCode` (opcional, v4.4).

Combínalo con `reference.getTaxpayer()` para que el comprador elija entre sus actividades:

```ts
// 1. Traer las actividades del comprador desde Hacienda
const tp = await wepos.reference.getTaxpayer('3101003937')
// tp.actividades → [{ codigo: '4730.0', descripcion: '…', tipo: 'P' }, …]

// 2. Emitir indicando la actividad del receptor (y opcionalmente la del emisor)
await wepos.invoices.create({
  documentType: 'FE',
  customer: { id: customerId },
  activityCode: '4610.0',            // actividad del EMISOR (si tiene varias)
  receptorActivityCode: tp.actividades[0].codigo, // actividad del RECEPTOR
  lines: [/* … */],
})
```

## Idempotencia

Pasa una `idempotencyKey` al emitir: un reintento con la misma key **no duplica** el comprobante.

```ts
await wepos.invoices.create(input, { idempotencyKey: 'orden-4821' })
```

## Manejo de errores

```ts
import { WeposApiError, WeposTimeoutError } from '@wepos/sdk'

try {
  await wepos.invoices.create(input)
} catch (err) {
  if (err instanceof WeposApiError) {
    console.error(err.code, err.status, err.message, err.requestId)
    if (err.isRateLimit) { /* 429 */ }
    if (err.isAuth) { /* 401 / 403 — revisar key/scopes */ }
  } else if (err instanceof WeposTimeoutError) {
    // Hacienda tardó; consulta el estado más tarde con invoices.get(err.invoiceId)
  }
}
```

Todos los errores exponen el `code` estable de WePOS, el `status` HTTP y el `requestId`.

## Reintentos y timeouts

- El SDK reintenta automáticamente ante `429`, `5xx` y errores de red, con **backoff exponencial** y respeto del header `Retry-After`. Configurable con `maxRetries`.
- Las **escrituras** solo deben reintentarse con una `idempotencyKey` (lo hace por ti `issueAndWait`).
- Cada request tiene un `timeoutMs` (default 30s). `issueAndWait` tiene su propio `timeoutMs` para la espera de Hacienda.

## TypeScript

El SDK incluye tipos completos. Los de la superficie pública están curados a mano para la mejor DX,
pero reflejan el OpenAPI publicado en `/api/v1/openapi`. Para cruzarlos contra el contrato real o
expandir a nuevos endpoints, regenera desde el spec vivo:

```bash
WEPOS_BASE_URL=https://pos.wecodecr.com npm run gen:types
```

## Roadmap

> El orden refleja prioridad por valor para integradores. Sugerencias en support@wecodecr.com.

### ✅ v1.0 — Núcleo de emisión (actual)
Emisión FE/TE/NC/ND/FEE, `issueAndWait`, clientes, catálogo, inventario, sucursales y referencia
Hacienda. DX: idempotencia, reintentos, errores tipados, ESM+CJS, tipos completos.

### 🔜 Webhooks y eventos
- `wepos.webhooks.verify(rawBody, signatureHeader, secret)` — verificación de firma HMAC estilo
  Stripe (`x-wepos-signature: t=…,v1=…`), con tolerancia anti-replay.
- Tipos de eventos fiscales (`document.accepted`, `document.rejected`, `document.error`, …).
- Handlers listos para **Next.js** y **Express**.

### ✅ v1.2 — Partner (backend fiscal embebido)
Superficie de plano de control para SaaS partner (partner key `wppk_…`):
`partner.tenants.create/list/get/update` + `setCredentials`/`uploadCertificate`

### ✅ v1.3 — Migración de consecutivos (Partner)
`partner.tenants.getConsecutives`/`setConsecutives` — fijar el consecutivo de
arranque cuando un negocio migra desde otro sistema, por ambiente
(STAGING/PRODUCTION), de forma atómica y monótona.
Ver [Partner](#partner--wepos-como-backend-fiscal-embebido).

### ✅ v1.4 — Data keys por ambiente (Partner)
`partner.tenants.issueKey`/`listKeys`/`revokeKey` — emitir/listar/revocar data keys
(`wpk_…`) de un tenant existente por ambiente. Mueve un tenant entre sandbox y
producción sin recrearlo.

### 🔜 Próximo — Listados y paginación
- Auto-paginación / async iterators (`for await (const inv of wepos.invoices.iterate())`).
- Filtros adicionales: rango de fechas, estado de Hacienda, sucursal, cliente.

### 🔜 Más recursos de negocio
- Ventas POS y sesiones de caja, devoluciones e impresión.
- Cuentas por cobrar / pagar (pagos, estados de cuenta, aging).
- Reportes (ventas, impuestos, reporte Z, libros, exportación contable).
- Catálogo avanzado (listas de precios, promociones, combos) y compras (procurement).

### 🔭 v2.0 — Apps móviles y desktop
- Auth de dispositivos (`login`/`refresh`) con refresco automático del token.
- Change-feed / sync con cursores para apps **offline-first**.
- Subida de imágenes (logo, productos) y modo contingencia.

### 🌎 Futuro
- SDKs de **PHP** y **Python** generados desde el OpenAPI.
- CLI `wepos` para emitir/consultar desde terminal y CI.
- Modo `mock` para tests de integradores sin tocar Hacienda.

## Versionado

Seguimos [SemVer](https://semver.org/lang/es/): los cambios incompatibles solo ocurren en versiones
*major*. Consulta el historial en la
[pestaña de versiones en npm](https://www.npmjs.com/package/@wepos/sdk?activeTab=versions).

## Soporte

- 📦 npm: https://www.npmjs.com/package/@wepos/sdk
- 🌐 Plataforma: https://pos.wecodecr.com
- ✉️ support@wecodecr.com

## Licencia

[MIT](https://opensource.org/licenses/MIT) © WeCode CR
