fiscalapi 4.0.433__tar.gz → 4.0.439__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.
Files changed (46) hide show
  1. {fiscalapi-4.0.433/fiscalapi.egg-info → fiscalapi-4.0.439}/PKG-INFO +27 -4
  2. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/README.md +26 -3
  3. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/__init__.py +28 -0
  4. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/models/__init__.py +26 -0
  5. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/models/common_models.py +1 -0
  6. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/models/fiscalapi_models.py +10 -6
  7. fiscalapi-4.0.439/fiscalapi/models/ticket_models.py +183 -0
  8. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/__init__.py +2 -0
  9. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/base_service.py +2 -0
  10. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/fiscalapi_client.py +2 -0
  11. fiscalapi-4.0.439/fiscalapi/services/ticket_service.py +102 -0
  12. {fiscalapi-4.0.433 → fiscalapi-4.0.439/fiscalapi.egg-info}/PKG-INFO +27 -4
  13. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi.egg-info/SOURCES.txt +5 -1
  14. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/setup.py +1 -1
  15. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/tests/test_cycle_contract.py +12 -11
  16. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/tests/test_error_trace_identifier.py +1 -1
  17. fiscalapi-4.0.439/tests/test_invoice_item_ticket_id.py +167 -0
  18. fiscalapi-4.0.439/tests/test_response_tolerance.py +193 -0
  19. fiscalapi-4.0.439/tests/test_ticket_service.py +368 -0
  20. fiscalapi-4.0.433/tests/test_response_tolerance.py +0 -143
  21. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/LICENSE.txt +0 -0
  22. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/MANIFEST.in +0 -0
  23. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/models/carta_porte_models.py +0 -0
  24. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/models/comercio_exterior_models.py +0 -0
  25. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/models/manifest_models.py +0 -0
  26. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/models/sat_validation_models.py +0 -0
  27. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/py.typed +0 -0
  28. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/api_key_service.py +0 -0
  29. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/catalog_service.py +0 -0
  30. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/download_catalog_service.py +0 -0
  31. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/download_request_service.py +0 -0
  32. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/download_rule_service.py +0 -0
  33. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/employee_service.py +0 -0
  34. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/employer_service.py +0 -0
  35. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/invoice_service.py +0 -0
  36. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/manifest_service.py +0 -0
  37. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/people_service.py +0 -0
  38. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/product_service.py +0 -0
  39. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/sat_validation_service.py +0 -0
  40. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/stamp_service.py +0 -0
  41. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi/services/tax_file_service.py +0 -0
  42. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi.egg-info/dependency_links.txt +0 -0
  43. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi.egg-info/requires.txt +0 -0
  44. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/fiscalapi.egg-info/top_level.txt +0 -0
  45. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/pyproject.toml +0 -0
  46. {fiscalapi-4.0.433 → fiscalapi-4.0.439}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fiscalapi
3
- Version: 4.0.433
3
+ Version: 4.0.439
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 Validación
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.
@@ -576,17 +596,20 @@ Tras una firma exitosa, todas las personas del tenant con el RFC del certificado
576
596
  Alta y administración de personas, gestión de certificados (CSD).
577
597
  - **Productos y Servicios**
578
598
  Administración de catálogos de productos, búsqueda en catálogos SAT.
579
- - **Timbres y créditos de validación**
580
- Listar transacciones, transferir y retirar timbres o créditos de validación entre personas.
599
+ - **Timbres, créditos de validación y créditos de ticket**
600
+ Listar transacciones, transferir y retirar timbres, créditos de validación o créditos de ticket entre personas.
581
601
  - **Validaciones SAT**
582
602
  Validar estructura, certificado, sellos, estatus en el SAT y listas negras 69-B de un CFDI o un RFC.
583
603
  - **Carta manifiesto**
584
604
  Firmar la carta manifiesto de una persona con su e.firma (FIEL) y obtener el PDF resultante.
605
+ - **Tickets (Auto CFDI)**
606
+ Ingerir ventas pagadas del punto de venta con Idempotency-Key opcional, consultarlas con filtros, reimprimir su código y cancelarlas.
585
607
 
586
608
  ## 📂 Más Ejemplos
587
609
 
588
610
  - [Gestión de Timbres](examples/ejemplos-timbres.py)
589
611
  - [Validaciones SAT](examples/ejemplos-validaciones-sat.py)
612
+ - [Tickets de Auto CFDI](examples/ejemplos-tickets.py)
590
613
  - [Complementos de Pago](examples/ejemplos-facturas-de-complemento-pago.py)
591
614
  - [Facturas de Nómina](examples/ejemplos-facturas-de-nomina.py)
592
615
  - [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 Validación
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.
@@ -539,17 +559,20 @@ Tras una firma exitosa, todas las personas del tenant con el RFC del certificado
539
559
  Alta y administración de personas, gestión de certificados (CSD).
540
560
  - **Productos y Servicios**
541
561
  Administración de catálogos de productos, búsqueda en catálogos SAT.
542
- - **Timbres y créditos de validación**
543
- Listar transacciones, transferir y retirar timbres o créditos de validación entre personas.
562
+ - **Timbres, créditos de validación y créditos de ticket**
563
+ Listar transacciones, transferir y retirar timbres, créditos de validación o créditos de ticket entre personas.
544
564
  - **Validaciones SAT**
545
565
  Validar estructura, certificado, sellos, estatus en el SAT y listas negras 69-B de un CFDI o un RFC.
546
566
  - **Carta manifiesto**
547
567
  Firmar la carta manifiesto de una persona con su e.firma (FIEL) y obtener el PDF resultante.
568
+ - **Tickets (Auto CFDI)**
569
+ Ingerir ventas pagadas del punto de venta con Idempotency-Key opcional, consultarlas con filtros, reimprimir su código y cancelarlas.
548
570
 
549
571
  ## 📂 Más Ejemplos
550
572
 
551
573
  - [Gestión de Timbres](examples/ejemplos-timbres.py)
552
574
  - [Validaciones SAT](examples/ejemplos-validaciones-sat.py)
575
+ - [Tickets de Auto CFDI](examples/ejemplos-tickets.py)
553
576
  - [Complementos de Pago](examples/ejemplos-facturas-de-complemento-pago.py)
554
577
  - [Facturas de Nómina](examples/ejemplos-facturas-de-nomina.py)
555
578
  - [Impuestos Locales (Por Valores)](examples/ejemplos-factura-impuestos-locales-valores.py)
@@ -75,6 +75,20 @@ 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
+ )
78
92
 
79
93
  # Modelos de dominio
80
94
  from .models.fiscalapi_models import (
@@ -175,6 +189,7 @@ from .services.tax_file_service import TaxFileService
175
189
  from .services.sat_validation_service import SatValidationService
176
190
  from .services.stamp_service import StampService
177
191
  from .services.manifest_service import ManifestService
192
+ from .services.ticket_service import TicketService
178
193
 
179
194
  # Cliente principal
180
195
  from .services.fiscalapi_client import FiscalApiClient
@@ -311,6 +326,18 @@ __all__ = [
311
326
  "SatValidationTypeStatus",
312
327
  "SatValidationStatus",
313
328
  "SatValidationResult",
329
+ # Ticket models (Auto CFDI)
330
+ "TicketStatus",
331
+ "TicketEventType",
332
+ "TicketActorType",
333
+ "TicketIssuer",
334
+ "TicketItemTax",
335
+ "TicketItem",
336
+ "TicketEvent",
337
+ "Ticket",
338
+ "TicketIngestionResult",
339
+ "TicketCodeResult",
340
+ "TicketListFilters",
314
341
  # Servicios
315
342
  "BaseService",
316
343
  "ApiKeyService",
@@ -327,6 +354,7 @@ __all__ = [
327
354
  "SatValidationService",
328
355
  "StampService",
329
356
  "ManifestService",
357
+ "TicketService",
330
358
  # Cliente principal
331
359
  "FiscalApiClient",
332
360
  ]
@@ -61,6 +61,20 @@ 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
+ )
64
78
  from .fiscalapi_models import (
65
79
  # Product models
66
80
  ProductTax,
@@ -275,4 +289,16 @@ __all__ = [
275
289
  "SatValidationTypeStatus",
276
290
  "SatValidationStatus",
277
291
  "SatValidationResult",
292
+ # Ticket models (Auto CFDI)
293
+ "TicketStatus",
294
+ "TicketEventType",
295
+ "TicketActorType",
296
+ "TicketIssuer",
297
+ "TicketItemTax",
298
+ "TicketItem",
299
+ "TicketEvent",
300
+ "Ticket",
301
+ "TicketIngestionResult",
302
+ "TicketCodeResult",
303
+ "TicketListFilters",
278
304
  ]
@@ -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`` y VALIDATION afecta
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
- credit_type: Optional[CreditType] = Field(default=None, alias="creditType", description="Tipo de crédito que mueve la transacción: timbres o créditos de validación.")
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) o VALIDATION (créditos de validación SAT).")
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
@@ -16,6 +16,7 @@ from .product_service import ProductService
16
16
  from .sat_validation_service import SatValidationService
17
17
  from .stamp_service import StampService
18
18
  from .tax_file_service import TaxFileService
19
+ from .ticket_service import TicketService
19
20
 
20
21
  __all__ = [
21
22
  "BaseService",
@@ -34,4 +35,5 @@ __all__ = [
34
35
  "SatValidationService",
35
36
  "StampService",
36
37
  "TaxFileService",
38
+ "TicketService",
37
39
  ]
@@ -114,6 +114,7 @@ class BaseService:
114
114
  message=generic_error.message,
115
115
  details=details_str,
116
116
  trace_identifier=generic_error.trace_identifier,
117
+ error_data=response_data.get("data"),
117
118
  data=None
118
119
  )
119
120
  except Exception:
@@ -125,6 +126,7 @@ class BaseService:
125
126
  message=generic_error.message or f"HTTP Error {status_code}",
126
127
  details=generic_error.details or raw_content,
127
128
  trace_identifier=generic_error.trace_identifier,
129
+ error_data=response_data.get("data"),
128
130
  data=None
129
131
  )
130
132
 
@@ -11,6 +11,7 @@ from fiscalapi.services.download_request_service import DownloadRequestService
11
11
  from fiscalapi.services.sat_validation_service import SatValidationService
12
12
  from fiscalapi.services.stamp_service import StampService
13
13
  from fiscalapi.services.manifest_service import ManifestService
14
+ from fiscalapi.services.ticket_service import TicketService
14
15
 
15
16
 
16
17
 
@@ -29,4 +30,5 @@ class FiscalApiClient:
29
30
  self.stamps = StampService(settings)
30
31
  self.sat_validations = SatValidationService(settings)
31
32
  self.manifests = ManifestService(settings)
33
+ self.tickets = TicketService(settings)
32
34
  self.settings = settings
@@ -0,0 +1,102 @@
1
+ """Servicio de tickets de Auto CFDI: ingesta, consulta con filtros, código para reimprimir y cancelación."""
2
+
3
+ from typing import Optional
4
+ from urllib.parse import urlencode
5
+
6
+ from fiscalapi.models.common_models import ApiResponse, PagedList
7
+ from fiscalapi.models.ticket_models import Ticket, TicketCodeResult, TicketIngestionResult, TicketListFilters
8
+ from fiscalapi.services.base_service import BaseService
9
+
10
+ IDEMPOTENCY_KEY_HEADER = "Idempotency-Key"
11
+
12
+
13
+ class TicketService(BaseService):
14
+ """Service for Auto CFDI tickets (endpoints /api/v4/tickets)."""
15
+
16
+ def create(self, ticket: Ticket, idempotency_key: Optional[str] = None) -> ApiResponse[TicketIngestionResult]:
17
+ """Ingest a ticket and charge 1 ticket credit. POST /api/v4/tickets
18
+
19
+ With ``idempotency_key`` (up to 64 characters) the ``Idempotency-Key`` header is sent: repeating the key with
20
+ the same body answers 200 with the original ticket without charging again; with another body, 422. Without it
21
+ the header is not sent. An active folio and series of the issuer answers 409.
22
+
23
+ Args:
24
+ ticket: Ticket to ingest.
25
+ idempotency_key: Value of the Idempotency-Key header, or None to not send it.
26
+
27
+ Returns:
28
+ ApiResponse containing the TicketIngestionResult (code and QR URL to print).
29
+
30
+ Raises:
31
+ ValueError: If ticket is None.
32
+ """
33
+ if ticket is None:
34
+ raise ValueError("Se requiere el ticket.")
35
+
36
+ headers = {} if idempotency_key is None else {IDEMPOTENCY_KEY_HEADER: idempotency_key}
37
+ return self.send_request("POST", "tickets", TicketIngestionResult, payload=ticket, headers=headers)
38
+
39
+ def get_list(self, filters: Optional[TicketListFilters] = None) -> ApiResponse[PagedList[Ticket]]:
40
+ """List the tenant's tickets with the given filters (none: all of them, first page). GET /api/v4/tickets
41
+
42
+ Args:
43
+ filters: Status (one or more; the ``status`` key repeats), person, payment date range and page.
44
+
45
+ Returns:
46
+ ApiResponse containing a PagedList of Ticket, ordered by payment date.
47
+ """
48
+ params = filters.to_query_params() if filters is not None else []
49
+ endpoint = f"tickets?{urlencode(params)}" if params else "tickets"
50
+ return self.send_request("GET", endpoint, PagedList[Ticket])
51
+
52
+ def get_by_id(self, ticket_id: str, details: bool = False) -> ApiResponse[Ticket]:
53
+ """Get a ticket; with ``details`` it also brings its items and history. GET /api/v4/tickets/{id}
54
+
55
+ Args:
56
+ ticket_id: Ticket id.
57
+ details: Include items and events.
58
+
59
+ Returns:
60
+ ApiResponse containing the Ticket.
61
+
62
+ Raises:
63
+ ValueError: If ticket_id is empty.
64
+ """
65
+ self._ensure_ticket_id(ticket_id)
66
+ return self.send_request("GET", f"tickets/{ticket_id}", Ticket, details=details)
67
+
68
+ def get_code(self, ticket_id: str) -> ApiResponse[TicketCodeResult]:
69
+ """Get the ticket code and its QR URL, to reprint it. GET /api/v4/tickets/{id}/code
70
+
71
+ Args:
72
+ ticket_id: Ticket id.
73
+
74
+ Returns:
75
+ ApiResponse containing the TicketCodeResult.
76
+
77
+ Raises:
78
+ ValueError: If ticket_id is empty.
79
+ """
80
+ self._ensure_ticket_id(ticket_id)
81
+ return self.send_request("GET", f"tickets/{ticket_id}/code", TicketCodeResult)
82
+
83
+ def cancel(self, ticket_id: str) -> ApiResponse[bool]:
84
+ """Cancel a pending ticket (409 in any other status); the ticket credit is not refunded. DELETE /api/v4/tickets/{id}
85
+
86
+ Args:
87
+ ticket_id: Ticket id.
88
+
89
+ Returns:
90
+ ApiResponse containing True if the ticket was cancelled.
91
+
92
+ Raises:
93
+ ValueError: If ticket_id is empty.
94
+ """
95
+ self._ensure_ticket_id(ticket_id)
96
+ return self.send_request("DELETE", f"tickets/{ticket_id}", bool)
97
+
98
+ @staticmethod
99
+ def _ensure_ticket_id(ticket_id: str) -> None:
100
+ """Guard against an empty id, which would otherwise hit the list (GET) or the collection (DELETE)."""
101
+ if not ticket_id or not ticket_id.strip():
102
+ raise ValueError("Se requiere el id del ticket.")