Metadata-Version: 2.5
Name: keysoft
Version: 0.1.0
Summary: SDK de Python para la API de KeySoft — facturación electrónica SUNAT.
Project-URL: Documentation, https://api.keysoft.pe/docs
License-Expression: MIT
License-File: LICENSE
Keywords: cpe,factura-electronica,facturacion,peru,sunat
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# keysoft

Facturación electrónica SUNAT desde Python. **Sin dependencias** — solo biblioteca
estándar, para que entre también donde el `pip` está restringido.

```bash
pip install keysoft
```

## De cero a una factura aceptada

```python
import os
from keysoft import KeySoft

ks = KeySoft(os.environ["KEYSOFT_API_KEY"])

# El ambiente sale de la clave: ks_test_ es pruebas, ks_live_ es producción.
print(ks.environment)  # 'DEMO'

emitido = ks.issue(company_id, {
    "documentType": "FACTURA",
    "series": "F001",
    "customer": {
        "identityType": "RUC",
        "identityValue": "20601234567",
        "legalName": "DISTRIBUIDORA DEL NORTE S.A.C.",
        "email": "compras@cliente.pe",   # le llega su factura al aceptarse
    },
    "lines": [
        {"description": "Servicio de consultoría", "quantity": 2, "unitValue": 150},
    ],
}, idempotency_key=f"pedido-{pedido.id}")

resultado = ks.send(company_id, emitido["document"]["id"])
print(resultado["cdr"]["description"])
# "La Factura numero F001-00000001, ha sido aceptada"
```

## Idempotencia

`issue()` **manda `Idempotency-Key` por defecto**. Si tu proceso reintenta por un
timeout, sin ella emitirías **dos facturas** — y una factura de más es un problema
fiscal, no un duplicado inocente.

Pásale la tuya: el id de tu pedido es la mejor opción, porque sobrevive a un
reinicio de tu proceso.

## Los errores dicen qué hacer

```python
from keysoft import KeySoftError

try:
    ks.issue(company_id, factura)
except KeySoftError as e:
    print(e.code)      # 'certificate_missing' — estable, programa contra él
    print(e.action)    # qué hacer para arreglarlo
    if e.retryable:
        reintentar()
```

## Descargar y verificar

`download()` **recalcula el SHA-256 y lanza si no cuadra**. No te fíes de nosotros:
la comprobación es el producto.

```python
xml = ks.download(company_id, document_id, "xml")
pdf = ks.download(company_id, document_id, "pdf")
```

## Boletas: el reloj de los 7 días

Una boleta no se envía sola — va en el resumen diario, y **caduca a los 7 días
calendario** sin que nadie te avise.

```python
estado = ks.summaries(company_id)
if estado["alert"]["level"] != "ok":
    print(estado["alert"]["message"])
    # "Quedan 2 día(s) para informar las boletas más antiguas sin resumen."

resumen = ks.create_summary(company_id)
ks.send_summary(company_id, resumen["summary"]["id"])

# El bucle de consulta lo hacemos nosotros: un ticket NO es una aceptación.
final = ks.wait_for_summary(company_id, resumen["summary"]["id"])
print(final["propagatedTo"], "boletas informadas")
```

## Webhooks

```python
from keysoft import verify_webhook

@app.post("/hooks/keysoft")
def hook():
    if not verify_webhook(
        request.get_data(),                      # el cuerpo CRUDO
        request.headers["keysoft-signature"],
        os.environ["KEYSOFT_WEBHOOK_SECRET"],
    ):
        return "", 400

    evento = request.get_json()
    return "", 200
```

**El cuerpo tiene que ser el crudo.** Si lo parseas y lo vuelves a serializar la
firma deja de cuadrar: es el fallo más común al integrarlo.

La firma **caduca a los 5 minutos**, que es lo que impide que quien capture una
petición válida te la reenvíe mañana.
