fiscalapi 4.0.433__tar.gz → 4.0.450__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fiscalapi-4.0.433/fiscalapi.egg-info → fiscalapi-4.0.450}/PKG-INFO +86 -4
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/README.md +85 -3
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/__init__.py +48 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/models/__init__.py +38 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/models/common_models.py +1 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/models/fiscalapi_models.py +10 -6
- fiscalapi-4.0.450/fiscalapi/models/ticket_models.py +183 -0
- fiscalapi-4.0.450/fiscalapi/models/webhook_models.py +101 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/__init__.py +2 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/base_service.py +2 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/fiscalapi_client.py +2 -0
- fiscalapi-4.0.450/fiscalapi/services/ticket_service.py +102 -0
- fiscalapi-4.0.450/fiscalapi/webhooks.py +263 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450/fiscalapi.egg-info}/PKG-INFO +86 -4
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi.egg-info/SOURCES.txt +9 -1
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/setup.py +1 -1
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/tests/test_cycle_contract.py +12 -11
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/tests/test_error_trace_identifier.py +1 -1
- fiscalapi-4.0.450/tests/test_invoice_item_ticket_id.py +167 -0
- fiscalapi-4.0.450/tests/test_response_tolerance.py +193 -0
- fiscalapi-4.0.450/tests/test_ticket_service.py +368 -0
- fiscalapi-4.0.450/tests/test_webhook_back_interop.py +159 -0
- fiscalapi-4.0.450/tests/test_webhook_verify.py +432 -0
- fiscalapi-4.0.433/tests/test_response_tolerance.py +0 -143
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/LICENSE.txt +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/MANIFEST.in +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/models/carta_porte_models.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/models/comercio_exterior_models.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/models/manifest_models.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/models/sat_validation_models.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/py.typed +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/api_key_service.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/catalog_service.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/download_catalog_service.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/download_request_service.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/download_rule_service.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/employee_service.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/employer_service.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/invoice_service.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/manifest_service.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/people_service.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/product_service.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/sat_validation_service.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/stamp_service.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi/services/tax_file_service.py +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi.egg-info/dependency_links.txt +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi.egg-info/requires.txt +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/fiscalapi.egg-info/top_level.txt +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/pyproject.toml +0 -0
- {fiscalapi-4.0.433 → fiscalapi-4.0.450}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fiscalapi
|
|
3
|
-
Version: 4.0.
|
|
3
|
+
Version: 4.0.450
|
|
4
4
|
Summary: Genera facturas CFDI válidas ante el SAT consumiendo el API de https://www.fiscalapi.com
|
|
5
5
|
Home-page: https://www.fiscalapi.com
|
|
6
6
|
Author: Fiscalapi
|
|
@@ -87,13 +87,14 @@ Dynamic: summary
|
|
|
87
87
|
- **Búsqueda de información** en catálogos del SAT con filtros avanzados
|
|
88
88
|
- **Acceso y búsqueda** en catálogos completos
|
|
89
89
|
|
|
90
|
-
## 🎫 Gestión de Timbres y Créditos de
|
|
90
|
+
## 🎫 Gestión de Timbres, Créditos de Validación y Créditos de Ticket
|
|
91
91
|
- **Gestión de folios fiscales**: compra timbres a FiscalAPI y transfiere/retira a las personas de tu organización según tus reglas de negocio
|
|
92
92
|
- **Listar transacciones** del ledger con paginación
|
|
93
93
|
- **Consultar transacciones** por ID
|
|
94
94
|
- **Transferir timbres** entre personas
|
|
95
95
|
- **Retirar timbres** de una persona
|
|
96
96
|
- **Transferir y retirar créditos de validación SAT** con `credit_type=CreditType.VALIDATION`
|
|
97
|
+
- **Transferir y retirar créditos de ticket** con `credit_type=CreditType.TICKET`; su saldo se lee en `balances` de la persona
|
|
97
98
|
|
|
98
99
|
## 🛡️ Validaciones SAT
|
|
99
100
|
- **Estructura del XML** contra el Anexo 20 y los complementos declarados
|
|
@@ -437,6 +438,25 @@ params = StampTransactionParams(
|
|
|
437
438
|
api_response = client.stamps.transfer_stamps(params)
|
|
438
439
|
```
|
|
439
440
|
|
|
441
|
+
Los créditos de ticket se transfieren igual, con `credit_type=CreditType.TICKET`, y su saldo se lee en `balances` de la persona (un tipo ausente tiene saldo 0):
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
from fiscalapi import CreditType, StampTransactionParams
|
|
445
|
+
|
|
446
|
+
params = StampTransactionParams(
|
|
447
|
+
from_person_id="3f3478b4-60fd-459e-8bfc-f8239fc96257",
|
|
448
|
+
to_person_id="96b46762-d246-4a67-a562-510a25dbafa9",
|
|
449
|
+
amount=100,
|
|
450
|
+
comments="Asignación de créditos de ticket",
|
|
451
|
+
credit_type=CreditType.TICKET
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
api_response = client.stamps.transfer_stamps(params)
|
|
455
|
+
|
|
456
|
+
person = client.people.get_by_id("96b46762-d246-4a67-a562-510a25dbafa9").data
|
|
457
|
+
ticket_balance = next((b.available for b in person.balances or [] if b.credit_type == CreditType.TICKET), 0)
|
|
458
|
+
```
|
|
459
|
+
|
|
440
460
|
### 10. Factura de Comercio Exterior
|
|
441
461
|
|
|
442
462
|
El complemento se envía en `Invoice.complement.comercio_exterior`. Puede combinarse con carta porte en el mismo comprobante.
|
|
@@ -568,6 +588,63 @@ if api_response.succeeded:
|
|
|
568
588
|
|
|
569
589
|
Tras una firma exitosa, todas las personas del tenant con el RFC del certificado quedan con `manifest_status_id` en `Signed`; `client.people.get_by_id(...)` lo refleja.
|
|
570
590
|
|
|
591
|
+
### 12. Recibir webhooks
|
|
592
|
+
|
|
593
|
+
FiscalAPI firma cada entrega con [Standard Webhooks](https://www.standardwebhooks.com) y el secreto `whsec_…` de su webhook. `webhooks.verify` comprueba la firma (en tiempo constante) y que `webhook-timestamp` no difiera más de 5 minutos del reloj del receptor, y devuelve el `WebhookEvent`. La firma es la única prueba de que la entrega viene de FiscalAPI.
|
|
594
|
+
|
|
595
|
+
**Verifique el cuerpo crudo**: pase los bytes tal como llegaron (`request.get_data()` en Flask, `request.body` en Django, `await request.body()` en FastAPI), antes de deserializarlos. Un cuerpo deserializado y vuelto a serializar (por ejemplo, `json.dumps(request.json)`) cambia sus bytes y la firma deja de coincidir.
|
|
596
|
+
|
|
597
|
+
```python
|
|
598
|
+
import os
|
|
599
|
+
import queue
|
|
600
|
+
import threading
|
|
601
|
+
|
|
602
|
+
from flask import Flask, request
|
|
603
|
+
|
|
604
|
+
from fiscalapi import FiscalApiClient, FiscalApiSettings, WebhookEventTypes, WebhookVerificationException, webhooks
|
|
605
|
+
|
|
606
|
+
app = Flask(__name__)
|
|
607
|
+
client = FiscalApiClient(settings=FiscalApiSettings(api_url="...", api_key="...", tenant="..."))
|
|
608
|
+
webhook_secret = os.environ["FISCALAPI_WEBHOOK_SECRET"] # whsec_… del webhook, desde su configuración segura
|
|
609
|
+
|
|
610
|
+
# La cola en memoria es ilustrativa: en producción use una cola durable (Celery, RQ, SQS…).
|
|
611
|
+
events = queue.Queue()
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
@app.post("/webhooks/fiscalapi")
|
|
615
|
+
def receive_fiscalapi_webhook():
|
|
616
|
+
try:
|
|
617
|
+
event = webhooks.verify(request.headers, request.get_data(), webhook_secret)
|
|
618
|
+
except WebhookVerificationException:
|
|
619
|
+
return "", 400 # firma inválida, header faltante o timestamp fuera de tolerancia
|
|
620
|
+
|
|
621
|
+
# Responda 2xx rápido y procese después: FiscalAPI corta cada intento a los 15 s y lo reintenta.
|
|
622
|
+
events.put(event)
|
|
623
|
+
return "", 200
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def process_events():
|
|
627
|
+
while True:
|
|
628
|
+
event = events.get()
|
|
629
|
+
# La entrega es al menos una vez: event.id (el header webhook-id) se repite en cada reintento; descarte los ya
|
|
630
|
+
# procesados. El orden de llegada no está garantizado: el estado autoritativo se lee del recurso.
|
|
631
|
+
if event.type == WebhookEventTypes.INVOICE_CANCELED:
|
|
632
|
+
invoice = client.invoices.get_by_id(event.data.related_object.id)
|
|
633
|
+
elif event.type == WebhookEventTypes.DOWNLOAD_REQUEST_COMPLETED:
|
|
634
|
+
download_request = client.download_requests.get_by_id(event.data.related_object.id)
|
|
635
|
+
# Un tipo que no conoce se ignora: el catálogo puede crecer.
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
threading.Thread(target=process_events, daemon=True).start()
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
- `WebhookEvent` es el mismo sobre para todo evento: `id`, `type` (texto; compárelo con las constantes de `WebhookEventTypes`), `created_at` (`datetime` en UTC), `person_id` (`None` si el recurso no tiene persona) y `data` con `related_object` (`id`, `type`, `url`), `status` y `previous_status`.
|
|
642
|
+
- `related_object.url` es la ruta relativa del GET v4 del recurso (`/api/v4/invoices/{id}`); para leerlo, use el servicio de su tipo con `related_object.id`.
|
|
643
|
+
- El botón "Enviar prueba" del webhook (`POST /api/v4/webhooks/{id}/test`) envía `WebhookEventTypes.WEBHOOK_TEST` (`webhook.test`), cuyo `related_object` es el webhook mismo (`type` `webhook`, `url` `/api/v4/webhooks/{id}`), sin persona ni estados; no se suscribe.
|
|
644
|
+
- Durante una rotación del secreto, FiscalAPI firma con el nuevo y con el anterior por 24 h: pase ambos, `webhooks.verify(request.headers, body, [secreto_nuevo, secreto_anterior])`.
|
|
645
|
+
- La tolerancia es configurable (`webhooks.verify(request.headers, body, secreto, timedelta(minutes=2))`); el valor por omisión es `webhooks.DEFAULT_TOLERANCE`.
|
|
646
|
+
- Un secreto mal copiado (no es base64), una lista de secretos vacía o una tolerancia negativa lanzan `ValueError`, y un argumento `None` lanza `TypeError`, con la posición del secreto (nunca su valor): es un error de configuración, no de la entrega.
|
|
647
|
+
|
|
571
648
|
## 📋 Operaciones Principales
|
|
572
649
|
|
|
573
650
|
- **Facturas (CFDI)**
|
|
@@ -576,17 +653,22 @@ Tras una firma exitosa, todas las personas del tenant con el RFC del certificado
|
|
|
576
653
|
Alta y administración de personas, gestión de certificados (CSD).
|
|
577
654
|
- **Productos y Servicios**
|
|
578
655
|
Administración de catálogos de productos, búsqueda en catálogos SAT.
|
|
579
|
-
- **Timbres y créditos de
|
|
580
|
-
Listar transacciones, transferir y retirar timbres o créditos de
|
|
656
|
+
- **Timbres, créditos de validación y créditos de ticket**
|
|
657
|
+
Listar transacciones, transferir y retirar timbres, créditos de validación o créditos de ticket entre personas.
|
|
581
658
|
- **Validaciones SAT**
|
|
582
659
|
Validar estructura, certificado, sellos, estatus en el SAT y listas negras 69-B de un CFDI o un RFC.
|
|
583
660
|
- **Carta manifiesto**
|
|
584
661
|
Firmar la carta manifiesto de una persona con su e.firma (FIEL) y obtener el PDF resultante.
|
|
662
|
+
- **Tickets (Auto CFDI)**
|
|
663
|
+
Ingerir ventas pagadas del punto de venta con Idempotency-Key opcional, consultarlas con filtros, reimprimir su código y cancelarlas.
|
|
664
|
+
- **Webhooks**
|
|
665
|
+
Verificación de la firma Standard Webhooks de las entregas y lectura del evento (`webhooks.verify`, `WebhookEvent`).
|
|
585
666
|
|
|
586
667
|
## 📂 Más Ejemplos
|
|
587
668
|
|
|
588
669
|
- [Gestión de Timbres](examples/ejemplos-timbres.py)
|
|
589
670
|
- [Validaciones SAT](examples/ejemplos-validaciones-sat.py)
|
|
671
|
+
- [Tickets de Auto CFDI](examples/ejemplos-tickets.py)
|
|
590
672
|
- [Complementos de Pago](examples/ejemplos-facturas-de-complemento-pago.py)
|
|
591
673
|
- [Facturas de Nómina](examples/ejemplos-facturas-de-nomina.py)
|
|
592
674
|
- [Impuestos Locales (Por Valores)](examples/ejemplos-factura-impuestos-locales-valores.py)
|
|
@@ -50,13 +50,14 @@
|
|
|
50
50
|
- **Búsqueda de información** en catálogos del SAT con filtros avanzados
|
|
51
51
|
- **Acceso y búsqueda** en catálogos completos
|
|
52
52
|
|
|
53
|
-
## 🎫 Gestión de Timbres y Créditos de
|
|
53
|
+
## 🎫 Gestión de Timbres, Créditos de Validación y Créditos de Ticket
|
|
54
54
|
- **Gestión de folios fiscales**: compra timbres a FiscalAPI y transfiere/retira a las personas de tu organización según tus reglas de negocio
|
|
55
55
|
- **Listar transacciones** del ledger con paginación
|
|
56
56
|
- **Consultar transacciones** por ID
|
|
57
57
|
- **Transferir timbres** entre personas
|
|
58
58
|
- **Retirar timbres** de una persona
|
|
59
59
|
- **Transferir y retirar créditos de validación SAT** con `credit_type=CreditType.VALIDATION`
|
|
60
|
+
- **Transferir y retirar créditos de ticket** con `credit_type=CreditType.TICKET`; su saldo se lee en `balances` de la persona
|
|
60
61
|
|
|
61
62
|
## 🛡️ Validaciones SAT
|
|
62
63
|
- **Estructura del XML** contra el Anexo 20 y los complementos declarados
|
|
@@ -400,6 +401,25 @@ params = StampTransactionParams(
|
|
|
400
401
|
api_response = client.stamps.transfer_stamps(params)
|
|
401
402
|
```
|
|
402
403
|
|
|
404
|
+
Los créditos de ticket se transfieren igual, con `credit_type=CreditType.TICKET`, y su saldo se lee en `balances` de la persona (un tipo ausente tiene saldo 0):
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
from fiscalapi import CreditType, StampTransactionParams
|
|
408
|
+
|
|
409
|
+
params = StampTransactionParams(
|
|
410
|
+
from_person_id="3f3478b4-60fd-459e-8bfc-f8239fc96257",
|
|
411
|
+
to_person_id="96b46762-d246-4a67-a562-510a25dbafa9",
|
|
412
|
+
amount=100,
|
|
413
|
+
comments="Asignación de créditos de ticket",
|
|
414
|
+
credit_type=CreditType.TICKET
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
api_response = client.stamps.transfer_stamps(params)
|
|
418
|
+
|
|
419
|
+
person = client.people.get_by_id("96b46762-d246-4a67-a562-510a25dbafa9").data
|
|
420
|
+
ticket_balance = next((b.available for b in person.balances or [] if b.credit_type == CreditType.TICKET), 0)
|
|
421
|
+
```
|
|
422
|
+
|
|
403
423
|
### 10. Factura de Comercio Exterior
|
|
404
424
|
|
|
405
425
|
El complemento se envía en `Invoice.complement.comercio_exterior`. Puede combinarse con carta porte en el mismo comprobante.
|
|
@@ -531,6 +551,63 @@ if api_response.succeeded:
|
|
|
531
551
|
|
|
532
552
|
Tras una firma exitosa, todas las personas del tenant con el RFC del certificado quedan con `manifest_status_id` en `Signed`; `client.people.get_by_id(...)` lo refleja.
|
|
533
553
|
|
|
554
|
+
### 12. Recibir webhooks
|
|
555
|
+
|
|
556
|
+
FiscalAPI firma cada entrega con [Standard Webhooks](https://www.standardwebhooks.com) y el secreto `whsec_…` de su webhook. `webhooks.verify` comprueba la firma (en tiempo constante) y que `webhook-timestamp` no difiera más de 5 minutos del reloj del receptor, y devuelve el `WebhookEvent`. La firma es la única prueba de que la entrega viene de FiscalAPI.
|
|
557
|
+
|
|
558
|
+
**Verifique el cuerpo crudo**: pase los bytes tal como llegaron (`request.get_data()` en Flask, `request.body` en Django, `await request.body()` en FastAPI), antes de deserializarlos. Un cuerpo deserializado y vuelto a serializar (por ejemplo, `json.dumps(request.json)`) cambia sus bytes y la firma deja de coincidir.
|
|
559
|
+
|
|
560
|
+
```python
|
|
561
|
+
import os
|
|
562
|
+
import queue
|
|
563
|
+
import threading
|
|
564
|
+
|
|
565
|
+
from flask import Flask, request
|
|
566
|
+
|
|
567
|
+
from fiscalapi import FiscalApiClient, FiscalApiSettings, WebhookEventTypes, WebhookVerificationException, webhooks
|
|
568
|
+
|
|
569
|
+
app = Flask(__name__)
|
|
570
|
+
client = FiscalApiClient(settings=FiscalApiSettings(api_url="...", api_key="...", tenant="..."))
|
|
571
|
+
webhook_secret = os.environ["FISCALAPI_WEBHOOK_SECRET"] # whsec_… del webhook, desde su configuración segura
|
|
572
|
+
|
|
573
|
+
# La cola en memoria es ilustrativa: en producción use una cola durable (Celery, RQ, SQS…).
|
|
574
|
+
events = queue.Queue()
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
@app.post("/webhooks/fiscalapi")
|
|
578
|
+
def receive_fiscalapi_webhook():
|
|
579
|
+
try:
|
|
580
|
+
event = webhooks.verify(request.headers, request.get_data(), webhook_secret)
|
|
581
|
+
except WebhookVerificationException:
|
|
582
|
+
return "", 400 # firma inválida, header faltante o timestamp fuera de tolerancia
|
|
583
|
+
|
|
584
|
+
# Responda 2xx rápido y procese después: FiscalAPI corta cada intento a los 15 s y lo reintenta.
|
|
585
|
+
events.put(event)
|
|
586
|
+
return "", 200
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def process_events():
|
|
590
|
+
while True:
|
|
591
|
+
event = events.get()
|
|
592
|
+
# La entrega es al menos una vez: event.id (el header webhook-id) se repite en cada reintento; descarte los ya
|
|
593
|
+
# procesados. El orden de llegada no está garantizado: el estado autoritativo se lee del recurso.
|
|
594
|
+
if event.type == WebhookEventTypes.INVOICE_CANCELED:
|
|
595
|
+
invoice = client.invoices.get_by_id(event.data.related_object.id)
|
|
596
|
+
elif event.type == WebhookEventTypes.DOWNLOAD_REQUEST_COMPLETED:
|
|
597
|
+
download_request = client.download_requests.get_by_id(event.data.related_object.id)
|
|
598
|
+
# Un tipo que no conoce se ignora: el catálogo puede crecer.
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
threading.Thread(target=process_events, daemon=True).start()
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
- `WebhookEvent` es el mismo sobre para todo evento: `id`, `type` (texto; compárelo con las constantes de `WebhookEventTypes`), `created_at` (`datetime` en UTC), `person_id` (`None` si el recurso no tiene persona) y `data` con `related_object` (`id`, `type`, `url`), `status` y `previous_status`.
|
|
605
|
+
- `related_object.url` es la ruta relativa del GET v4 del recurso (`/api/v4/invoices/{id}`); para leerlo, use el servicio de su tipo con `related_object.id`.
|
|
606
|
+
- El botón "Enviar prueba" del webhook (`POST /api/v4/webhooks/{id}/test`) envía `WebhookEventTypes.WEBHOOK_TEST` (`webhook.test`), cuyo `related_object` es el webhook mismo (`type` `webhook`, `url` `/api/v4/webhooks/{id}`), sin persona ni estados; no se suscribe.
|
|
607
|
+
- Durante una rotación del secreto, FiscalAPI firma con el nuevo y con el anterior por 24 h: pase ambos, `webhooks.verify(request.headers, body, [secreto_nuevo, secreto_anterior])`.
|
|
608
|
+
- La tolerancia es configurable (`webhooks.verify(request.headers, body, secreto, timedelta(minutes=2))`); el valor por omisión es `webhooks.DEFAULT_TOLERANCE`.
|
|
609
|
+
- Un secreto mal copiado (no es base64), una lista de secretos vacía o una tolerancia negativa lanzan `ValueError`, y un argumento `None` lanza `TypeError`, con la posición del secreto (nunca su valor): es un error de configuración, no de la entrega.
|
|
610
|
+
|
|
534
611
|
## 📋 Operaciones Principales
|
|
535
612
|
|
|
536
613
|
- **Facturas (CFDI)**
|
|
@@ -539,17 +616,22 @@ Tras una firma exitosa, todas las personas del tenant con el RFC del certificado
|
|
|
539
616
|
Alta y administración de personas, gestión de certificados (CSD).
|
|
540
617
|
- **Productos y Servicios**
|
|
541
618
|
Administración de catálogos de productos, búsqueda en catálogos SAT.
|
|
542
|
-
- **Timbres y créditos de
|
|
543
|
-
Listar transacciones, transferir y retirar timbres o créditos de
|
|
619
|
+
- **Timbres, créditos de validación y créditos de ticket**
|
|
620
|
+
Listar transacciones, transferir y retirar timbres, créditos de validación o créditos de ticket entre personas.
|
|
544
621
|
- **Validaciones SAT**
|
|
545
622
|
Validar estructura, certificado, sellos, estatus en el SAT y listas negras 69-B de un CFDI o un RFC.
|
|
546
623
|
- **Carta manifiesto**
|
|
547
624
|
Firmar la carta manifiesto de una persona con su e.firma (FIEL) y obtener el PDF resultante.
|
|
625
|
+
- **Tickets (Auto CFDI)**
|
|
626
|
+
Ingerir ventas pagadas del punto de venta con Idempotency-Key opcional, consultarlas con filtros, reimprimir su código y cancelarlas.
|
|
627
|
+
- **Webhooks**
|
|
628
|
+
Verificación de la firma Standard Webhooks de las entregas y lectura del evento (`webhooks.verify`, `WebhookEvent`).
|
|
548
629
|
|
|
549
630
|
## 📂 Más Ejemplos
|
|
550
631
|
|
|
551
632
|
- [Gestión de Timbres](examples/ejemplos-timbres.py)
|
|
552
633
|
- [Validaciones SAT](examples/ejemplos-validaciones-sat.py)
|
|
634
|
+
- [Tickets de Auto CFDI](examples/ejemplos-tickets.py)
|
|
553
635
|
- [Complementos de Pago](examples/ejemplos-facturas-de-complemento-pago.py)
|
|
554
636
|
- [Facturas de Nómina](examples/ejemplos-facturas-de-nomina.py)
|
|
555
637
|
- [Impuestos Locales (Por Valores)](examples/ejemplos-factura-impuestos-locales-valores.py)
|
|
@@ -75,6 +75,28 @@ from .models.sat_validation_models import (
|
|
|
75
75
|
SatValidationStatus,
|
|
76
76
|
SatValidationResult,
|
|
77
77
|
)
|
|
78
|
+
# Modelos de tickets (Auto CFDI)
|
|
79
|
+
from .models.ticket_models import (
|
|
80
|
+
TicketStatus,
|
|
81
|
+
TicketEventType,
|
|
82
|
+
TicketActorType,
|
|
83
|
+
TicketIssuer,
|
|
84
|
+
TicketItemTax,
|
|
85
|
+
TicketItem,
|
|
86
|
+
TicketEvent,
|
|
87
|
+
Ticket,
|
|
88
|
+
TicketIngestionResult,
|
|
89
|
+
TicketCodeResult,
|
|
90
|
+
TicketListFilters,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Modelos de webhooks
|
|
94
|
+
from .models.webhook_models import (
|
|
95
|
+
WebhookEventTypes,
|
|
96
|
+
WebhookRelatedObject,
|
|
97
|
+
WebhookEventData,
|
|
98
|
+
WebhookEvent,
|
|
99
|
+
)
|
|
78
100
|
|
|
79
101
|
# Modelos de dominio
|
|
80
102
|
from .models.fiscalapi_models import (
|
|
@@ -175,10 +197,15 @@ from .services.tax_file_service import TaxFileService
|
|
|
175
197
|
from .services.sat_validation_service import SatValidationService
|
|
176
198
|
from .services.stamp_service import StampService
|
|
177
199
|
from .services.manifest_service import ManifestService
|
|
200
|
+
from .services.ticket_service import TicketService
|
|
178
201
|
|
|
179
202
|
# Cliente principal
|
|
180
203
|
from .services.fiscalapi_client import FiscalApiClient
|
|
181
204
|
|
|
205
|
+
# Verificación de webhooks (fiscalapi.webhooks.verify)
|
|
206
|
+
from . import webhooks
|
|
207
|
+
from .webhooks import WebhookVerificationException
|
|
208
|
+
|
|
182
209
|
__all__ = [
|
|
183
210
|
# Modelos base
|
|
184
211
|
"ApiResponse",
|
|
@@ -311,6 +338,23 @@ __all__ = [
|
|
|
311
338
|
"SatValidationTypeStatus",
|
|
312
339
|
"SatValidationStatus",
|
|
313
340
|
"SatValidationResult",
|
|
341
|
+
# Ticket models (Auto CFDI)
|
|
342
|
+
"TicketStatus",
|
|
343
|
+
"TicketEventType",
|
|
344
|
+
"TicketActorType",
|
|
345
|
+
"TicketIssuer",
|
|
346
|
+
"TicketItemTax",
|
|
347
|
+
"TicketItem",
|
|
348
|
+
"TicketEvent",
|
|
349
|
+
"Ticket",
|
|
350
|
+
"TicketIngestionResult",
|
|
351
|
+
"TicketCodeResult",
|
|
352
|
+
"TicketListFilters",
|
|
353
|
+
# Webhook models
|
|
354
|
+
"WebhookEventTypes",
|
|
355
|
+
"WebhookRelatedObject",
|
|
356
|
+
"WebhookEventData",
|
|
357
|
+
"WebhookEvent",
|
|
314
358
|
# Servicios
|
|
315
359
|
"BaseService",
|
|
316
360
|
"ApiKeyService",
|
|
@@ -327,6 +371,10 @@ __all__ = [
|
|
|
327
371
|
"SatValidationService",
|
|
328
372
|
"StampService",
|
|
329
373
|
"ManifestService",
|
|
374
|
+
"TicketService",
|
|
330
375
|
# Cliente principal
|
|
331
376
|
"FiscalApiClient",
|
|
377
|
+
# Webhooks
|
|
378
|
+
"webhooks",
|
|
379
|
+
"WebhookVerificationException",
|
|
332
380
|
]
|
|
@@ -61,6 +61,27 @@ from .sat_validation_models import (
|
|
|
61
61
|
SatValidationStatus,
|
|
62
62
|
SatValidationResult,
|
|
63
63
|
)
|
|
64
|
+
from .ticket_models import (
|
|
65
|
+
# Ticket models (Auto CFDI)
|
|
66
|
+
TicketStatus,
|
|
67
|
+
TicketEventType,
|
|
68
|
+
TicketActorType,
|
|
69
|
+
TicketIssuer,
|
|
70
|
+
TicketItemTax,
|
|
71
|
+
TicketItem,
|
|
72
|
+
TicketEvent,
|
|
73
|
+
Ticket,
|
|
74
|
+
TicketIngestionResult,
|
|
75
|
+
TicketCodeResult,
|
|
76
|
+
TicketListFilters,
|
|
77
|
+
)
|
|
78
|
+
from .webhook_models import (
|
|
79
|
+
# Webhook models
|
|
80
|
+
WebhookEventTypes,
|
|
81
|
+
WebhookRelatedObject,
|
|
82
|
+
WebhookEventData,
|
|
83
|
+
WebhookEvent,
|
|
84
|
+
)
|
|
64
85
|
from .fiscalapi_models import (
|
|
65
86
|
# Product models
|
|
66
87
|
ProductTax,
|
|
@@ -275,4 +296,21 @@ __all__ = [
|
|
|
275
296
|
"SatValidationTypeStatus",
|
|
276
297
|
"SatValidationStatus",
|
|
277
298
|
"SatValidationResult",
|
|
299
|
+
# Ticket models (Auto CFDI)
|
|
300
|
+
"TicketStatus",
|
|
301
|
+
"TicketEventType",
|
|
302
|
+
"TicketActorType",
|
|
303
|
+
"TicketIssuer",
|
|
304
|
+
"TicketItemTax",
|
|
305
|
+
"TicketItem",
|
|
306
|
+
"TicketEvent",
|
|
307
|
+
"Ticket",
|
|
308
|
+
"TicketIngestionResult",
|
|
309
|
+
"TicketCodeResult",
|
|
310
|
+
"TicketListFilters",
|
|
311
|
+
# Webhook models
|
|
312
|
+
"WebhookEventTypes",
|
|
313
|
+
"WebhookRelatedObject",
|
|
314
|
+
"WebhookEventData",
|
|
315
|
+
"WebhookEvent",
|
|
278
316
|
]
|
|
@@ -11,6 +11,7 @@ class ApiResponse(BaseModel, Generic[T]):
|
|
|
11
11
|
data: Optional[T] = Field(default=None, alias="data")
|
|
12
12
|
http_status_code: Optional[int] = Field(default=None, alias="httpStatusCode")
|
|
13
13
|
trace_identifier: Optional[str] = Field(default=None, alias="traceIdentifier", description="Identificador de la petición en los registros de FiscalAPI. Viene en los errores (por ejemplo, un 500 o un 502): inclúyalo al escribir a soporte@fiscalapi.com.")
|
|
14
|
+
error_data: Optional[Any] = Field(default=None, alias="errorData", description="El data crudo de una respuesta de error (dict o lista tal cual), por ejemplo existingTicketId en el 409 de tickets.create o expectedTotal y computedTotal en su 422. None en las respuestas exitosas y en los errores sin data; data no cambia (en un error sigue en None).")
|
|
14
15
|
|
|
15
16
|
model_config = ConfigDict(populate_by_name=True)
|
|
16
17
|
|
|
@@ -4,7 +4,7 @@ from pydantic import BaseModel, ConfigDict, Field
|
|
|
4
4
|
from fiscalapi.models.common_models import BaseDto, CatalogDto
|
|
5
5
|
from fiscalapi.models.carta_porte_models import CartaPorteComplement
|
|
6
6
|
from fiscalapi.models.comercio_exterior_models import ComercioExteriorComplement
|
|
7
|
-
from typing import Optional
|
|
7
|
+
from typing import Optional, Union
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
|
|
10
10
|
# products models
|
|
@@ -54,7 +54,7 @@ class CreditBalance(BaseModel):
|
|
|
54
54
|
Un tipo ausente tiene saldo 0; un tipo que este SDK no conoce conserva su número en credit_type.
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
|
-
credit_type: int = Field(default=..., alias="creditType", description="Tipo de crédito: 1 timbres, 2 créditos de validación.")
|
|
57
|
+
credit_type: int = Field(default=..., alias="creditType", description="Tipo de crédito: 1 timbres, 2 créditos de validación, 3 créditos de ticket.")
|
|
58
58
|
available: int = Field(default=..., alias="available", description="Saldo disponible.")
|
|
59
59
|
|
|
60
60
|
model_config = ConfigDict(populate_by_name=True)
|
|
@@ -304,6 +304,7 @@ class InvoiceItem(BaseDto):
|
|
|
304
304
|
customs_info: Optional[list[ItemCustomsInfo]] = Field(default=None, alias="customsInfo", description="Información aduanera.")
|
|
305
305
|
property_info: Optional[list[ItemPropertyInfo]] = Field(default=None, alias="propertyInfo", description="Información predial.")
|
|
306
306
|
parts: Optional[list[ItemPart]] = Field(default=None, alias="parts", description="Partes del concepto.")
|
|
307
|
+
ticket_id: Optional[str] = Field(default=None, alias="ticketId", description="Id del ticket de Auto CFDI que este concepto factura. Solo en una factura global de ingreso (tipo I con global_information) con el emisor por referencia (issuer.id); un ticket puede ir en varios conceptos, uno por grupo de objeto de impuesto e impuestos. En None no se envía.")
|
|
307
308
|
|
|
308
309
|
model_config = ConfigDict(populate_by_name=True)
|
|
309
310
|
|
|
@@ -1011,12 +1012,13 @@ class Xml(BaseDto):
|
|
|
1011
1012
|
class CreditType(IntEnum):
|
|
1012
1013
|
"""Tipo de crédito de una transacción de timbres.
|
|
1013
1014
|
|
|
1014
|
-
Los saldos nunca se mezclan: STAMP afecta ``Person.available_balance
|
|
1015
|
-
``Person.available_validation_balance``.
|
|
1015
|
+
Los saldos nunca se mezclan: STAMP afecta ``Person.available_balance``, VALIDATION afecta
|
|
1016
|
+
``Person.available_validation_balance`` y TICKET afecta el saldo de tipo TICKET en ``Person.balances``.
|
|
1016
1017
|
"""
|
|
1017
1018
|
|
|
1018
1019
|
STAMP = 1
|
|
1019
1020
|
VALIDATION = 2
|
|
1021
|
+
TICKET = 3
|
|
1020
1022
|
|
|
1021
1023
|
|
|
1022
1024
|
class StampTransactionType(IntEnum):
|
|
@@ -1055,7 +1057,9 @@ class StampTransaction(BaseDto):
|
|
|
1055
1057
|
transaction_status: Optional[int] = Field(default=None, alias="transactionStatus", description="Estado de la transacción.")
|
|
1056
1058
|
reference_id: Optional[str] = Field(default=None, alias="referenceId", description="ID de referencia de la transacción.")
|
|
1057
1059
|
comments: Optional[str] = Field(default=None, alias="comments", description="Comentarios de la transacción.")
|
|
1058
|
-
|
|
1060
|
+
# left_to_right: un tipo conocido llega como miembro de CreditType y uno que este SDK todavía no conoce
|
|
1061
|
+
# conserva su número (int) en lugar de lanzar ValidationError y perder la respuesta completa.
|
|
1062
|
+
credit_type: Optional[Union[CreditType, int]] = Field(default=None, alias="creditType", union_mode="left_to_right", description="Tipo de crédito que mueve la transacción: timbres, créditos de validación o créditos de ticket. Un tipo que este SDK no conoce conserva su número (int).")
|
|
1059
1063
|
|
|
1060
1064
|
model_config = ConfigDict(populate_by_name=True)
|
|
1061
1065
|
|
|
@@ -1066,6 +1070,6 @@ class StampTransactionParams(BaseModel):
|
|
|
1066
1070
|
to_person_id: str = Field(default=..., alias="toPersonId", description="ID de la persona destino.")
|
|
1067
1071
|
amount: int = Field(default=..., alias="amount", description="Cantidad de timbres a transferir.")
|
|
1068
1072
|
comments: Optional[str] = Field(default=None, alias="comments", description="Comentarios de la transferencia.")
|
|
1069
|
-
credit_type: CreditType = Field(default=CreditType.STAMP, alias="creditType", description="Tipo de crédito a transferir: STAMP (timbres, por defecto)
|
|
1073
|
+
credit_type: CreditType = Field(default=CreditType.STAMP, alias="creditType", description="Tipo de crédito a transferir: STAMP (timbres, por defecto), VALIDATION (créditos de validación SAT) o TICKET (créditos de ticket).")
|
|
1070
1074
|
|
|
1071
1075
|
model_config = ConfigDict(populate_by_name=True)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Modelos de tickets de Auto CFDI (endpoints /api/v4/tickets).
|
|
2
|
+
|
|
3
|
+
Un ticket es una venta pagada del punto de venta que el receptor puede autofacturar o que entra a una factura global.
|
|
4
|
+
Ingerirlo cobra 1 crédito de ticket. Los conceptos e impuestos llevan los nombres de los conceptos de las facturas.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import date, datetime
|
|
8
|
+
from decimal import Decimal
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Optional, Union
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TicketStatus(str, Enum):
|
|
15
|
+
"""Estados de un ticket, como llegan en ``Ticket.status`` (texto camelCase)."""
|
|
16
|
+
|
|
17
|
+
PENDING = "pending"
|
|
18
|
+
INVOICING = "invoicing"
|
|
19
|
+
INVOICED = "invoiced"
|
|
20
|
+
GLOBALIZING = "globalizing"
|
|
21
|
+
GLOBALIZED = "globalized"
|
|
22
|
+
CANCELLED = "cancelled"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TicketEventType(str, Enum):
|
|
26
|
+
"""Tipos de hecho de la historia de un ticket, como llegan en ``TicketEvent.type``."""
|
|
27
|
+
|
|
28
|
+
INGESTED = "ingested"
|
|
29
|
+
INVOICING_STARTED = "invoicingStarted"
|
|
30
|
+
INVOICED = "invoiced"
|
|
31
|
+
INVOICE_REJECTED = "invoiceRejected"
|
|
32
|
+
OUTCOME_UNKNOWN = "outcomeUnknown"
|
|
33
|
+
LEASE_EXPIRED = "leaseExpired"
|
|
34
|
+
GLOBALIZING = "globalizing"
|
|
35
|
+
GLOBALIZED = "globalized"
|
|
36
|
+
RELEASED_FROM_GLOBAL = "releasedFromGlobal"
|
|
37
|
+
CANCELLED = "cancelled"
|
|
38
|
+
RECOVERY_FAILED = "recoveryFailed"
|
|
39
|
+
RECOVERED = "recovered"
|
|
40
|
+
RESENT = "resent"
|
|
41
|
+
RETURNED_TO_PENDING = "returnedToPending"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TicketActorType(str, Enum):
|
|
45
|
+
"""Quién actuó en un hecho del ticket, como llega en ``TicketEvent.actor_type``."""
|
|
46
|
+
|
|
47
|
+
TENANT = "tenant"
|
|
48
|
+
PORTAL = "portal"
|
|
49
|
+
SYSTEM = "system"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TicketIssuer(BaseModel):
|
|
53
|
+
"""Emisor de un ticket: una persona del tenant, por su id."""
|
|
54
|
+
|
|
55
|
+
id: Optional[str] = Field(default=None, alias="id", description="Id de la persona emisora.")
|
|
56
|
+
|
|
57
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TicketItemTax(BaseModel):
|
|
61
|
+
"""Impuesto de un concepto del ticket."""
|
|
62
|
+
|
|
63
|
+
tax_code: Optional[str] = Field(default=None, alias="taxCode", description="Impuesto: 001 (ISR), 002 (IVA) o 003 (IEPS).")
|
|
64
|
+
tax_type_code: Optional[str] = Field(default=None, alias="taxTypeCode", description="Tipo de factor: Tasa, Cuota o Exento.")
|
|
65
|
+
tax_rate: Optional[Decimal] = Field(default=None, alias="taxRate", description="Tasa o cuota; se envía tal cual, sin redondear.")
|
|
66
|
+
tax_flag_code: Optional[str] = Field(default=None, alias="taxFlagCode", description="T (traslado) o R (retención).")
|
|
67
|
+
|
|
68
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TicketItem(BaseModel):
|
|
72
|
+
"""Concepto de un ticket, con los nombres de los conceptos de las facturas (sin terceros, aduanas, predial ni partes)."""
|
|
73
|
+
|
|
74
|
+
item_code: Optional[str] = Field(default=None, alias="itemCode", description="Clave de producto o servicio (c_ClaveProdServ).")
|
|
75
|
+
item_sku: Optional[str] = Field(default=None, alias="itemSku", description="Número de identificación (SKU) del producto en el punto de venta.")
|
|
76
|
+
quantity: Optional[Decimal] = Field(default=None, alias="quantity", description="Cantidad.")
|
|
77
|
+
unit_of_measurement_code: Optional[str] = Field(default=None, alias="unitOfMeasurementCode", description="Clave de unidad (c_ClaveUnidad).")
|
|
78
|
+
description: Optional[str] = Field(default=None, alias="description", description="Descripción.")
|
|
79
|
+
unit_price: Optional[Decimal] = Field(default=None, alias="unitPrice", description="Valor unitario.")
|
|
80
|
+
discount: Optional[Decimal] = Field(default=None, alias="discount", description="Descuento del concepto. Opcional.")
|
|
81
|
+
tax_object_code: Optional[str] = Field(default=None, alias="taxObjectCode", description="Objeto de impuesto (c_ObjetoImp): 02 exige impuestos; 01, 03, 04 y 05 no admiten.")
|
|
82
|
+
item_taxes: Optional[list[TicketItemTax]] = Field(default=None, alias="itemTaxes", description="Impuestos del concepto.")
|
|
83
|
+
|
|
84
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TicketEvent(BaseModel):
|
|
88
|
+
"""Un hecho de la historia del ticket (solo lectura; llega con ``details=True``).
|
|
89
|
+
|
|
90
|
+
``type`` y ``actor_type`` llegan como miembro del enum si el SDK los conoce y como texto si no.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
type: Optional[Union[TicketEventType, str]] = Field(default=None, alias="type", union_mode="left_to_right", description="Tipo de hecho; uno que el SDK no conoce llega como texto.")
|
|
94
|
+
occurred_at: Optional[datetime] = Field(default=None, alias="occurredAt", description="Momento del hecho.")
|
|
95
|
+
actor_type: Optional[Union[TicketActorType, str]] = Field(default=None, alias="actorType", union_mode="left_to_right", description="Quién actuó; uno que el SDK no conoce llega como texto.")
|
|
96
|
+
invoice_id: Optional[str] = Field(default=None, alias="invoiceId", description="Factura del hecho; None si no aplica.")
|
|
97
|
+
|
|
98
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Ticket(BaseModel):
|
|
102
|
+
"""Ticket de Auto CFDI: se envía en ``tickets.create`` y llega en ``tickets.get_list`` y ``tickets.get_by_id``.
|
|
103
|
+
|
|
104
|
+
Los miembros de solo lectura (id, person_id, importes calculados, status, vigencia, facturas, created_at y events)
|
|
105
|
+
los asigna el API: al crear quedan en None y no se envían.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
id: Optional[str] = Field(default=None, alias="id", description="Id del ticket. Solo lectura.")
|
|
109
|
+
issuer: Optional[TicketIssuer] = Field(default=None, alias="issuer", description="Emisor por referencia. Se envía al crear; no llega en las consultas.")
|
|
110
|
+
person_id: Optional[str] = Field(default=None, alias="personId", description="Persona emisora del tenant. Solo lectura.")
|
|
111
|
+
folio: Optional[str] = Field(default=None, alias="folio", description="Folio del punto de venta (no es el folio del CFDI).")
|
|
112
|
+
series: Optional[str] = Field(default=None, alias="series", description="Serie del punto de venta. Opcional.")
|
|
113
|
+
payment_date: Optional[datetime] = Field(default=None, alias="paymentDate", description="Momento del pago en hora civil de México; un datetime sin zona viaja como yyyy-MM-ddTHH:mm:ss.")
|
|
114
|
+
payment_form_code: Optional[str] = Field(default=None, alias="paymentFormCode", description="Forma de pago (c_FormaPago), por ejemplo 01 (efectivo).")
|
|
115
|
+
currency_code: Optional[str] = Field(default=None, alias="currencyCode", description="Moneda (c_Moneda). Opcional: sin ella, el API usa MXN.")
|
|
116
|
+
exchange_rate: Optional[Decimal] = Field(default=None, alias="exchangeRate", description="Tipo de cambio. Opcional.")
|
|
117
|
+
expedition_zip_code: Optional[str] = Field(default=None, alias="expeditionZipCode", description="Código postal de expedición. Opcional: sin él, el API usa el del emisor.")
|
|
118
|
+
subtotal: Optional[Decimal] = Field(default=None, alias="subtotal", description="Subtotal calculado por el API. Solo lectura.")
|
|
119
|
+
total_transferred_taxes: Optional[Decimal] = Field(default=None, alias="totalTransferredTaxes", description="Total de impuestos trasladados calculado por el API. Solo lectura.")
|
|
120
|
+
total_withheld_taxes: Optional[Decimal] = Field(default=None, alias="totalWithheldTaxes", description="Total de impuestos retenidos calculado por el API. Solo lectura.")
|
|
121
|
+
total: Optional[Decimal] = Field(default=None, alias="total", description="Total que cobró el punto de venta: debe cuadrar al centavo con el calculado (si no, 422).")
|
|
122
|
+
status: Optional[Union[TicketStatus, str]] = Field(default=None, alias="status", union_mode="left_to_right", description="Estado del ticket. Solo lectura; uno que el SDK no conoce llega como texto.")
|
|
123
|
+
self_invoice_eligible: Optional[bool] = Field(default=None, alias="selfInvoiceEligible", description="Si hoy el receptor puede autofacturarlo. Solo lectura.")
|
|
124
|
+
eligible_until: Optional[datetime] = Field(default=None, alias="eligibleUntil", description="Último momento de la ventana de autofactura; None si no cierra. Solo lectura.")
|
|
125
|
+
invoice_id: Optional[str] = Field(default=None, alias="invoiceId", description="Factura individual (estado invoiced). Solo lectura.")
|
|
126
|
+
global_invoice_id: Optional[str] = Field(default=None, alias="globalInvoiceId", description="Factura global (estado globalized). Solo lectura.")
|
|
127
|
+
created_at: Optional[datetime] = Field(default=None, alias="createdAt", description="Fecha de alta. Solo lectura.")
|
|
128
|
+
items: Optional[list[TicketItem]] = Field(default=None, alias="items", description="Conceptos. Obligatorios al crear; en las consultas solo llegan con details=True.")
|
|
129
|
+
events: Optional[list[TicketEvent]] = Field(default=None, alias="events", description="Historia del ticket en orden. Solo lectura; solo llega con details=True.")
|
|
130
|
+
|
|
131
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class TicketIngestionResult(BaseModel):
|
|
135
|
+
"""Respuesta de ``tickets.create``: la misma forma en el 201 de un ticket nuevo y en el 200 de una repetición con la
|
|
136
|
+
misma Idempotency-Key y el mismo cuerpo."""
|
|
137
|
+
|
|
138
|
+
id: Optional[str] = Field(default=None, alias="id", description="Id del ticket.")
|
|
139
|
+
ticket_code: Optional[str] = Field(default=None, alias="ticketCode", description="Código sin guiones, como lo lleva el QR; se imprime en grupos (XXXX-XXXX-XXXX).")
|
|
140
|
+
qr_url: Optional[str] = Field(default=None, alias="qrUrl", description="URL del portal de autofactura que abre el ticket.")
|
|
141
|
+
status: Optional[Union[TicketStatus, str]] = Field(default=None, alias="status", union_mode="left_to_right", description="Estado actual (al ingerir, pending); uno que el SDK no conoce llega como texto.")
|
|
142
|
+
self_invoice_eligible: Optional[bool] = Field(default=None, alias="selfInvoiceEligible", description="Si hoy el receptor puede autofacturarlo.")
|
|
143
|
+
eligible_until: Optional[datetime] = Field(default=None, alias="eligibleUntil", description="Último momento de la ventana de autofactura; None si no cierra.")
|
|
144
|
+
subtotal: Optional[Decimal] = Field(default=None, alias="subtotal", description="Subtotal calculado.")
|
|
145
|
+
total_transferred_taxes: Optional[Decimal] = Field(default=None, alias="totalTransferredTaxes", description="Total de impuestos trasladados calculado.")
|
|
146
|
+
total_withheld_taxes: Optional[Decimal] = Field(default=None, alias="totalWithheldTaxes", description="Total de impuestos retenidos calculado.")
|
|
147
|
+
total: Optional[Decimal] = Field(default=None, alias="total", description="Total calculado (cuadra con el enviado).")
|
|
148
|
+
|
|
149
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TicketCodeResult(BaseModel):
|
|
153
|
+
"""Código de un ticket y la URL de su QR, para reimprimirlo (GET /api/v4/tickets/{id}/code)."""
|
|
154
|
+
|
|
155
|
+
ticket_code: Optional[str] = Field(default=None, alias="ticketCode", description="Código sin guiones, el mismo de la ingesta.")
|
|
156
|
+
qr_url: Optional[str] = Field(default=None, alias="qrUrl", description="URL del portal de autofactura que abre el ticket.")
|
|
157
|
+
|
|
158
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class TicketListFilters(BaseModel):
|
|
162
|
+
"""Filtros de ``tickets.get_list`` (GET /api/v4/tickets). Cada filtro aplica solo si se asigna; los que quedan en
|
|
163
|
+
None no se envían. ``from_`` lleva el guion bajo porque ``from`` es palabra reservada de Python."""
|
|
164
|
+
|
|
165
|
+
status: Optional[list[Union[TicketStatus, str]]] = Field(default=None, alias="status", description="Uno o más estados; se envían repitiendo la llave status.")
|
|
166
|
+
person_id: Optional[str] = Field(default=None, alias="personId", description="Persona emisora.")
|
|
167
|
+
details: Optional[bool] = Field(default=None, alias="details", description="Con true, cada ticket trae sus conceptos con impuestos (sin la historia).")
|
|
168
|
+
global_invoice_id: Optional[str] = Field(default=None, alias="globalInvoiceId", description="Factura global que ampara los tickets.")
|
|
169
|
+
from_: Optional[date] = Field(default=None, alias="from", description="Primer día del pago (incluido), en fecha civil de México.")
|
|
170
|
+
to: Optional[date] = Field(default=None, alias="to", description="Último día del pago (incluido), en fecha civil de México.")
|
|
171
|
+
page_number: Optional[int] = Field(default=None, alias="pageNumber", description="Página, desde 1. Sin ella, el API devuelve la primera.")
|
|
172
|
+
page_size: Optional[int] = Field(default=None, alias="pageSize", description="Tamaño de página (1 a 50). Sin él, el API usa 10.")
|
|
173
|
+
|
|
174
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
175
|
+
|
|
176
|
+
def to_query_params(self) -> list[tuple[str, str]]:
|
|
177
|
+
"""Los filtros asignados como pares de la query string: ``status`` se repite por cada estado, y ``details``
|
|
178
|
+
solo viaja como ``true`` (``False`` no se envía)."""
|
|
179
|
+
params: list[tuple[str, str]] = []
|
|
180
|
+
for key, value in self.model_dump(mode="json", by_alias=True, exclude_none=True).items():
|
|
181
|
+
values = value if isinstance(value, list) else [value]
|
|
182
|
+
params.extend((key, "true" if item is True else str(item)) for item in values if item is not False)
|
|
183
|
+
return params
|