Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 57 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,25 @@ webhooks y calculá totales con las mismas reglas que la API.
- Tipos generados desde el OpenAPI de la API.
- Reintentos seguros: cada operación tributaria viaja con `Idempotency-Key`.

> Usalo sólo del lado del servidor. La API key no puede viajar al navegador ni dentro de una app distribuida.
## Instalación

```bash
npm install @sifende/sdk
```

Necesitás una API key de Sifende. El prefijo de la clave fija el ambiente: `sk_live_`, `sk_test_` o `sk_sandbox_`. La
referencia completa de la API está en [sifende.com.py/docs](https://sifende.com.py/docs).

> La API key emite documentos a nombre del contribuyente: usá el SDK solo en el servidor, nunca en el navegador ni
> dentro de una app distribuida.

## Uso

```ts
import { Sifende, calcularTotales } from '@sifende/sdk';

const sifende = new Sifende({ apiKey: process.env.SIFENDE_API_KEY! });
const pedidoId = '1001'; // el id de la venta en tu sistema

const items = [
{ codigo: 'REM-01', descripcion: 'Remera', cantidad: 2, unidadMedida: 'UNI', precioUnitario: 150_000, afectacionTributaria: 'GRAVADO', tasaIVA: 10 },
Expand All @@ -41,32 +52,53 @@ const documento = await sifende.documentos.emitir(
},
items: [...items],
},
{ idempotencyKey: `pedido-${pedido.id}` },
{ idempotencyKey: `pedido-${pedidoId}` },
);

console.log(documento.cdc, documento.estado); // PENDIENTE hasta que SIFEN responda
console.log(documento.cdc, documento.estado); // 'PENDIENTE'
```

`calcularTotales` reproduce el redondeo oficial de la SET (múltiplo de 50 hacia abajo en guaraníes): en una operación
al contado, `montoPago` tiene que coincidir con ese total.
El documento queda en `PENDIENTE` hasta que SIFEN lo procesa. El resultado llega por [webhook](#webhooks) o con
`documentos.consultarEstado(cdc)`.

En una operación al contado, `montoPago` tiene que coincidir con el total redondeado. `calcularTotales` aplica el
redondeo oficial de la SET: en guaraníes, al múltiplo de 50 inferior.

## Configuración

| Opción | Por defecto | |
|---|---|---|
| `apiKey` | | Obligatoria. |
| `timeoutMs` | `30000` | Tiempo máximo por intento. |
| `maxReintentos` | `2` | Reintentos ante fallas transitorias. `0` los desactiva. |
| `baseUrl` | `https://api.sifende.com.py` | |
| `fetch` | `globalThis.fetch` | Para usar otro cliente HTTP o simular la API en tests. |

## Operaciones

| Método | Endpoint |
|---|---|
| `documentos.emitir(documento, { idempotencyKey })` | `POST /api/v1/documento-electronico` |
| `documentos.consultarEstado(cdc)` | `GET /api/v1/documento-electronico/status/{cdc}` |
| `documentos.descargarKude(cdc)` | `GET /api/v1/documento-electronico/{cdc}/kude`; espera mientras el PDF se prepara |
| `documentos.descargarKude(cdc)` | `GET /api/v1/documento-electronico/{cdc}/kude` |
| `documentos.cancelar(cdc, { motivo })` | `POST /api/v1/documento-electronico/{cdc}/cancelar` |
| `documentos.nominar(cdc, { motivo, receptor })` | `POST /api/v1/documento-electronico/{cdc}/nominar` |
| `documentos.inutilizar(rango)` | `POST /api/v1/documento-electronico/inutilizar` |
| `documentos.listarEventos(filtro)` / `consultarEvento(id)` | `GET /api/v1/documento-electronico/eventos` |
| `contribuyente.consultar()` / `consultarConsumoPlan()` | `GET /api/v1/contribuyente`, `/plan-consumo` |
| `documentos.listarEventos(filtro)` | `GET /api/v1/documento-electronico/eventos` |
| `documentos.consultarEvento(id)` | `GET /api/v1/documento-electronico/eventos/{id}` |
| `contribuyente.consultar()` | `GET /api/v1/contribuyente` |
| `contribuyente.consultarConsumoPlan()` | `GET /api/v1/contribuyente/plan-consumo` |
| `padron.consultar(rucOCedula)` | `GET /api/v1/padron/{documento}` |
| `catalogos.enumeraciones()`, `departamentos()`, `distritos(id)`, `ciudades(id)` | Catálogos públicos |
| `catalogos.enumeraciones()` | `GET /api/v1/public/enums` |
| `catalogos.departamentos()`, `distritos(id)`, `ciudades(id)` | `GET /api/v1/geografia/…` |

Los catálogos son públicos y no envían la API key.

`descargarKude` espera hasta 3 minutos a que el PDF esté listo; si no lo está, lanza `KudePendienteError`. Cambiá la
espera con `{ esperarMs }`.

Si no pasás `idempotencyKey`, el SDK genera un UUID y lo reusa en sus reintentos. Para poder recuperar una operación
después de un corte, generá vos la clave y guardala antes de enviar.
Para poder retomar una operación después de un corte, generá vos la `idempotencyKey` y guardala antes de enviar. Si no
la pasás, el SDK genera un UUID que solo reusa en sus propios reintentos.

## Errores

Expand All @@ -86,14 +118,19 @@ try {
}
```

El SDK reintenta solo los casos que la API marca como seguros: cortes de conexión con clave de idempotencia,
`409 idempotency-in-progress`, `503 idempotency-upstream-unknown`, `503 evento-nominacion-error`, `429`, y 500, 502,
503 o 504 en consultas. Respeta `Retry-After`. Un corte o un timeout mientras llega la respuesta cuenta como corte de
conexión.
`SifendeTimeoutError` extiende `SifendeConexionError`: un timeout, incluso a mitad de la respuesta, se trata como un
corte de conexión.

El SDK reintenta solo los casos que la API marca como seguros, y respeta `Retry-After`:

- Cortes de conexión en consultas y en operaciones con clave de idempotencia.
- `409 idempotency-in-progress`, `503 idempotency-upstream-unknown` y `503 evento-nominacion-error`.
- `429`.
- `500`, `502`, `503` y `504` en consultas.

## Webhooks

Pasá el cuerpo crudo, antes de cualquier parser JSON:
La firma se calcula sobre el cuerpo tal como llegó. Pasá el texto crudo, antes de cualquier parser JSON:

```ts
import { FirmaWebhookInvalidaError, verificarWebhook } from '@sifende/sdk';
Expand All @@ -116,8 +153,10 @@ export async function action({ request }: { request: Request }) {
}
```

Durante una rotación de secreto podés pasar `secreto: [nuevo, anterior]`. Una entrega puede repetirse: deduplicá por
`evento.id`.
Los tipos de evento son `documento.aprobado`, `documento.rechazado`, `documento.cancelado` y `lote.procesado`.

Una entrega puede repetirse: deduplicá por `evento.id`. `verificarWebhook` rechaza una entrega si su
`X-Sifende-Timestamp` difiere más de 5 minutos de tu reloj; ajustalo con `toleranciaSegundos`. Durante una rotación de secreto, pasá `secreto: [nuevo, anterior]`.

## Desarrollo

Expand Down
10 changes: 10 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Seguridad

Para que una vulnerabilidad no quede expuesta antes de corregirla, reportala en privado, no en un issue público.

Usá **Report a vulnerability** en la pestaña [Security](https://github.com/ithdev/sifende-node/security) del repo.
Incluí la versión del SDK, los pasos para reproducirla y el impacto que ves.

Solo la última versión publicada de `@sifende/sdk` recibe correcciones de seguridad.

Si la vulnerabilidad está en la API de Sifende y no en el SDK, reportala por el mismo canal.
Loading