fiscalapi 4.0.418__tar.gz → 4.0.433__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 (41) hide show
  1. {fiscalapi-4.0.418/fiscalapi.egg-info → fiscalapi-4.0.433}/PKG-INFO +4 -4
  2. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/README.md +3 -3
  3. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/__init__.py +2 -0
  4. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/models/__init__.py +2 -0
  5. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/models/common_models.py +2 -1
  6. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/models/fiscalapi_models.py +40 -23
  7. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/base_service.py +2 -0
  8. {fiscalapi-4.0.418 → fiscalapi-4.0.433/fiscalapi.egg-info}/PKG-INFO +4 -4
  9. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi.egg-info/SOURCES.txt +4 -1
  10. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/pyproject.toml +3 -0
  11. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/setup.py +1 -1
  12. fiscalapi-4.0.433/tests/test_cycle_contract.py +190 -0
  13. fiscalapi-4.0.433/tests/test_error_trace_identifier.py +94 -0
  14. fiscalapi-4.0.433/tests/test_response_tolerance.py +143 -0
  15. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/LICENSE.txt +0 -0
  16. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/MANIFEST.in +0 -0
  17. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/models/carta_porte_models.py +0 -0
  18. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/models/comercio_exterior_models.py +0 -0
  19. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/models/manifest_models.py +0 -0
  20. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/models/sat_validation_models.py +0 -0
  21. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/py.typed +0 -0
  22. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/__init__.py +0 -0
  23. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/api_key_service.py +0 -0
  24. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/catalog_service.py +0 -0
  25. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/download_catalog_service.py +0 -0
  26. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/download_request_service.py +0 -0
  27. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/download_rule_service.py +0 -0
  28. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/employee_service.py +0 -0
  29. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/employer_service.py +0 -0
  30. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/fiscalapi_client.py +0 -0
  31. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/invoice_service.py +0 -0
  32. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/manifest_service.py +0 -0
  33. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/people_service.py +0 -0
  34. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/product_service.py +0 -0
  35. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/sat_validation_service.py +0 -0
  36. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/stamp_service.py +0 -0
  37. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/tax_file_service.py +0 -0
  38. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi.egg-info/dependency_links.txt +0 -0
  39. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi.egg-info/requires.txt +0 -0
  40. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi.egg-info/top_level.txt +0 -0
  41. {fiscalapi-4.0.418 → fiscalapi-4.0.433}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fiscalapi
3
- Version: 4.0.418
3
+ Version: 4.0.433
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
@@ -180,7 +180,7 @@ from fiscalapi import TaxFile
180
180
  # Subir certificado (CER)
181
181
  certificado_csd = TaxFile(
182
182
  person_id="3f3478b4-60fd-459e-8bfc-f8239fc96257",
183
- tin="FUNK671228PH6",
183
+ tin="FUNK671228PH6", # RFC de la persona (opcional)
184
184
  base64_file="MIIFgDCCA2igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0NDYwDQYJKo...",
185
185
  file_type=0, # 0 para certificado, 1 para llave privada
186
186
  password="12345678a"
@@ -189,7 +189,7 @@ certificado_csd = TaxFile(
189
189
  # Subir llave privada (KEY)
190
190
  clave_privada_csd = TaxFile(
191
191
  person_id="3f3478b4-60fd-459e-8bfc-f8239fc96257",
192
- tin="FUNK671228PH6",
192
+ tin="FUNK671228PH6", # RFC de la persona (opcional)
193
193
  base64_file="MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAg...",
194
194
  file_type=1,
195
195
  password="12345678a"
@@ -566,7 +566,7 @@ if api_response.succeeded:
566
566
  print(api_response.data.base64_file) # PDF firmado en base64
567
567
  ```
568
568
 
569
- Tras una firma exitosa, `client.people.get_by_id(...)` refleja el nuevo estado en `manifest_status_id`.
569
+ 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
570
 
571
571
  ## 📋 Operaciones Principales
572
572
 
@@ -143,7 +143,7 @@ from fiscalapi import TaxFile
143
143
  # Subir certificado (CER)
144
144
  certificado_csd = TaxFile(
145
145
  person_id="3f3478b4-60fd-459e-8bfc-f8239fc96257",
146
- tin="FUNK671228PH6",
146
+ tin="FUNK671228PH6", # RFC de la persona (opcional)
147
147
  base64_file="MIIFgDCCA2igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0NDYwDQYJKo...",
148
148
  file_type=0, # 0 para certificado, 1 para llave privada
149
149
  password="12345678a"
@@ -152,7 +152,7 @@ certificado_csd = TaxFile(
152
152
  # Subir llave privada (KEY)
153
153
  clave_privada_csd = TaxFile(
154
154
  person_id="3f3478b4-60fd-459e-8bfc-f8239fc96257",
155
- tin="FUNK671228PH6",
155
+ tin="FUNK671228PH6", # RFC de la persona (opcional)
156
156
  base64_file="MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAg...",
157
157
  file_type=1,
158
158
  password="12345678a"
@@ -529,7 +529,7 @@ if api_response.succeeded:
529
529
  print(api_response.data.base64_file) # PDF firmado en base64
530
530
  ```
531
531
 
532
- Tras una firma exitosa, `client.people.get_by_id(...)` refleja el nuevo estado en `manifest_status_id`.
532
+ 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
533
 
534
534
  ## 📋 Operaciones Principales
535
535
 
@@ -82,6 +82,7 @@ from .models.fiscalapi_models import (
82
82
  ProductTax,
83
83
  Product,
84
84
  # Person models
85
+ CreditBalance,
85
86
  Person,
86
87
  EmployeeData,
87
88
  EmployerData,
@@ -190,6 +191,7 @@ __all__ = [
190
191
  "ProductTax",
191
192
  "Product",
192
193
  # Person models
194
+ "CreditBalance",
193
195
  "Person",
194
196
  "EmployeeData",
195
197
  "EmployerData",
@@ -66,6 +66,7 @@ from .fiscalapi_models import (
66
66
  ProductTax,
67
67
  Product,
68
68
  # Person models
69
+ CreditBalance,
69
70
  Person,
70
71
  EmployeeData,
71
72
  EmployerData,
@@ -188,6 +189,7 @@ __all__ = [
188
189
  "ProductTax",
189
190
  "Product",
190
191
  # Person models
192
+ "CreditBalance",
191
193
  "Person",
192
194
  "EmployeeData",
193
195
  "EmployerData",
@@ -10,6 +10,7 @@ class ApiResponse(BaseModel, Generic[T]):
10
10
  details: Optional[str] = Field(default=None, alias="details")
11
11
  data: Optional[T] = Field(default=None, alias="data")
12
12
  http_status_code: Optional[int] = Field(default=None, alias="httpStatusCode")
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.")
13
14
 
14
15
  model_config = ConfigDict(populate_by_name=True)
15
16
 
@@ -30,7 +31,7 @@ class ValidationFailure(BaseModel):
30
31
  """Modelo para errores de validación."""
31
32
  property_name: str = Field(alias="propertyName")
32
33
  error_message: str = Field(alias="errorMessage")
33
- attempted_value: Optional[Any] = Field(default=None, alias="attemptedValue")
34
+ attempted_value: Optional[Any] = Field(default=None, alias="attemptedValue", description="Valor recibido. El de un secreto (contraseñas, códigos, tokens, archivos y contraseñas de CSD/FIEL) llega como \"[masked: n]\", o \"[masked]\" si es un objeto o lista que lo contiene.")
34
35
 
35
36
  model_config = ConfigDict(populate_by_name=True)
36
37
 
@@ -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 Literal, Optional
7
+ from typing import Optional
8
8
  from datetime import datetime
9
9
 
10
10
  # products models
@@ -14,13 +14,13 @@ class ProductTax(BaseDto):
14
14
 
15
15
  rate: Decimal = Field(default=..., ge=0, le=1, alias="rate", description="Tasa de impuesto")
16
16
 
17
- tax_id: Optional[Literal["001", "002", "003"]] = Field(default=None, alias="taxId", description="Impuesto")
17
+ tax_id: Optional[str] = Field(default=None, alias="taxId", description="Impuesto: clave del catálogo c_Impuesto del SAT (\"001\" ISR, \"002\" IVA, \"003\" IEPS). La valida el API.")
18
18
  tax: Optional[CatalogDto] = Field(default=None, alias="tax", description="Impuesto expandido")
19
19
 
20
- tax_flag_id: Optional[Literal["T", "R"]] = Field(default=None, alias="taxFlagId", description="Traslado o Retención")
20
+ tax_flag_id: Optional[str] = Field(default=None, alias="taxFlagId", description="Naturaleza del impuesto: \"T\" (traslado) o \"R\" (retención). La valida el API.")
21
21
  tax_flag: Optional[CatalogDto] = Field(default=None, alias="taxFlag", description="Traslado o Retención expandido")
22
22
 
23
- tax_type_id: Optional[Literal["Tasa", "Cuota", "Exento"]] = Field(default=None, alias="taxTypeId", description="Tipo de impuesto")
23
+ tax_type_id: Optional[str] = Field(default=None, alias="taxTypeId", description="Tipo factor: clave del catálogo c_TipoFactor del SAT (\"Tasa\", \"Cuota\" o \"Exento\"). La valida el API.")
24
24
  tax_type: Optional[CatalogDto] = Field(default=None, alias="taxType", description="Tipo de impuesto expandido")
25
25
 
26
26
 
@@ -48,33 +48,49 @@ class Product(BaseDto):
48
48
 
49
49
  # people models
50
50
 
51
+ class CreditBalance(BaseModel):
52
+ """Saldo de una persona para un tipo de crédito (Person.balances).
53
+
54
+ Un tipo ausente tiene saldo 0; un tipo que este SDK no conoce conserva su número en credit_type.
55
+ """
56
+
57
+ credit_type: int = Field(default=..., alias="creditType", description="Tipo de crédito: 1 timbres, 2 créditos de validación.")
58
+ available: int = Field(default=..., alias="available", description="Saldo disponible.")
59
+
60
+ model_config = ConfigDict(populate_by_name=True)
61
+
62
+
51
63
  class Person(BaseDto):
52
64
  """Modelo persona en FiscalAPI."""
53
65
 
54
66
  legal_name: Optional[str] = Field(default=None, alias="legalName", description="Razón social de la persona sin régimen de capital.")
55
67
  email: Optional[str] = Field(default=None, alias="email", description="Correo electrónico de la persona. El API valida el formato.")
56
- password: Optional[str] = Field(default=None, alias="password", description="Contraseña para acceder al dashboard.")
57
- capital_regime: Optional[str] = Field(default=None, alias="CapitalRegime", description="[Deprecado] El API no expone este campo; se conserva por compatibilidad y se ignora.")
58
- sat_tax_regime_id: Optional[Literal["601", "603", "605", "606", "607", "608", "610", "611", "612", "614", "615", "616", "620", "621", "622", "623", "624", "625", "626"]] = Field(default=None, alias="satTaxRegimeId", description="Código del régimen fiscal del emisor.")
68
+ password: Optional[str] = Field(default=None, alias="password", description="Contraseña para acceder al dashboard. Requerida al crear; al actualizar, None o vacía conserva la actual. El API nunca la devuelve.")
69
+ phone_number: Optional[str] = Field(default=None, alias="phoneNumber", description="Teléfono de la persona.")
70
+ sat_tax_regime_id: Optional[str] = Field(default=None, alias="satTaxRegimeId", description="Código del régimen fiscal: clave del catálogo c_RegimenFiscal del SAT. La valida el API.")
59
71
  sat_tax_regime: Optional[CatalogDto] = Field(default=None, alias="satTaxRegime", description="Código del régimen fiscal expandido.")
60
- sat_cfdi_use_id: Optional[Literal["G01", "G02", "G03", "I01", "I02", "I03", "I04", "I05", "I06", "I07", "I08", "D01", "D02", "D03", "D04", "D05", "D06", "D07", "D08", "D09", "D10", "S01", "CP01", "CN01"]] = Field(default=None, alias="satCfdiUseId", description="Código de uso del CFDI.")
72
+ sat_cfdi_use_id: Optional[str] = Field(default=None, alias="satCfdiUseId", description="Código de uso del CFDI: clave del catálogo c_UsoCFDI del SAT. La valida el API.")
61
73
  sat_cfdi_use: Optional[CatalogDto] = Field(default=None, alias="satCfdiUse", description="Código de uso del CFDI expandido.")
62
- user_type_id: Optional[Literal["T","C", "U"]] = Field(default=None, alias="userTypeId", description="Tipo de persona.")
74
+ user_type_id: Optional[str] = Field(default=None, alias="userTypeId", description="Tipo de persona: C (cliente, por omisión al crear) o U (usuario). T (tenant) solo llega en respuestas: el API lo rechaza al crear y al actualizar solo lo acepta si la persona ya es T.")
63
75
  user_type: Optional[CatalogDto] = Field(default=None, alias="userType", description="Tipo de persona expandido.")
64
76
  tin: Optional[str] = Field(default=None, alias="tin", description="RFC del emisor (Tax Identification Number).")
65
77
  zip_code: Optional[str] = Field(default=None, alias="zipCode", description="Código postal del emisor.")
66
78
  curp: Optional[str] = Field(default=None, alias="curp", description="CURP de la persona.")
67
79
  base64_photo: Optional[str] = Field(default=None, alias="base64Photo", description="Foto de perfil en formato base64.")
68
- tax_password: Optional[str] = Field(default=None, alias="taxPassword", description="Contraseña de los certificados CSD del emisor.")
80
+ tax_password: Optional[str] = Field(default=None, alias="taxPassword", description="Contraseña de la llave privada (.key) que la persona guarda en su perfil. El API no la usa para sellar (al timbrar usa la de los certificados registrados o la de tax_credentials). Con valor solo para la propia persona y el owner del tenant; los demás reciben None. Al actualizar, None la conserva y \"\" la borra.")
69
81
  available_balance: Optional[Decimal] = Field(default=None, alias="availableBalance", description="Saldo disponible en la cuenta.")
70
- committed_balance: Optional[Decimal] = Field(default=None, alias="committedBalance", description="Saldo en tránsito.")
82
+ committed_balance: Optional[Decimal] = Field(default=None, alias="committedBalance", description="Campo heredado: el API ya no lo calcula y siempre vale 0 (solo lectura). Para el saldo use available_balance o balances.")
71
83
  available_validation_balance: Optional[int] = Field(default=None, alias="availableValidationBalance", description="Créditos de validación SAT disponibles en la cuenta.")
84
+ balances: Optional[list[CreditBalance]] = Field(default=None, alias="balances", description="Saldos por tipo de crédito (solo lectura). Solo aparecen los tipos que la persona ha tenido.")
72
85
  manifest_status_id: Optional[str] = Field(default=None, alias="manifestStatusId", description="Estado de la carta manifiesto de la persona.")
73
86
  manifest_status: Optional[CatalogDto] = Field(default=None, alias="manifestStatus", description="Estado de la carta manifiesto expandido.")
74
87
  tenant_id: Optional[str] = Field(default=None, alias="tenantId", description="ID del tenant al que pertenece el emisor.")
75
88
  tenant: Optional[CatalogDto] = Field(default=None, alias="tenant", description="Tenant expandido.")
89
+ is_owner: Optional[bool] = Field(default=None, alias="isOwner", description="True si la persona es el owner de su tenant (solo lectura).")
90
+ valid_to: Optional[datetime] = Field(default=None, alias="validTo", description="Fin de vigencia de la persona (solo lectura). La asigna el API; casi siempre es None y es informativa (no limita el timbrado ni el acceso al API).")
76
91
  country_id: Optional[str] = Field(default=None, alias="countryId", description="Código del país de residencia para extranjeros (catálogo c_Pais).")
77
92
  foreign_tin: Optional[str] = Field(default=None, alias="foreignTin", description="Número de identificación fiscal del extranjero.")
93
+ country: Optional[CatalogDto] = Field(default=None, alias="country", description="País de residencia fiscal expandido (solo lectura).")
78
94
 
79
95
  model_config = ConfigDict(populate_by_name=True)
80
96
 
@@ -132,10 +148,10 @@ class TaxFile(BaseDto):
132
148
  """Modelo TaxFile que representa un componente de un par CSD: certificado (.cer) o llave privada (.key)."""
133
149
 
134
150
  person_id: Optional[str] = Field(default=None, alias="personId", description="Id de la persona propietaria del certificado.")
135
- tin: Optional[str] = Field(default=None, alias="tin", description="RFC del propietario del certificado. Debe coincidir con el RFC del certificado.")
151
+ tin: Optional[str] = Field(default=None, alias="tin", description="RFC de la persona. Opcional al subir: si se omite, el API usa el RFC de la persona; si se envía, debe ser el RFC de la persona (sin distinguir mayúsculas) o el API responde 400 con la falla en Tin. El API siempre devuelve el RFC de la persona.")
136
152
  base64_file: Optional[str] = Field(default=None, alias="base64File", description="Archivo certificado o llave privada en formato base64.")
137
- file_type: Optional[Literal[0, 1]] = Field(default=None, alias="fileType", description="Tipo de archivo: 0 para certificado, 1 para llave privada.")
138
- password: Optional[str] = Field(default=None, alias="password", description="Contraseña de la llave privada.")
153
+ file_type: Optional[int] = Field(default=None, alias="fileType", description="Tipo de archivo: 0 certificado CSD (.cer), 1 llave privada CSD (.key), 2 certificado FIEL (.cer), 3 llave privada FIEL (.key). El API valida el valor.")
154
+ password: Optional[str] = Field(default=None, alias="password", description="Contraseña de la llave privada. Solo se envía al subir (tax_files.create()): en las consultas (get_list(), get_by_id() y la respuesta de create()) el API la devuelve vacía (''); solo get_default_values() la devuelve.")
139
155
  valid_from: Optional[datetime] = Field(default=None, alias="validFrom", description="Fecha de inicio de vigencia del certificado o llave privada.")
140
156
  valid_to: Optional[datetime] = Field(default=None, alias="validTo", description="Fecha de fin de vigencia del certificado o llave privada.")
141
157
  sequence: Optional[int] = Field(default=None, alias="sequence", description="Numero de secuencia que identifica el par entre certificado y llave privada.")
@@ -185,11 +201,11 @@ class InvoiceRecipientEmployeeData(BaseDto):
185
201
  model_config = ConfigDict(populate_by_name=True)
186
202
 
187
203
 
188
- class TaxCredential(BaseDto):
204
+ class TaxCredential(BaseModel):
189
205
  """Modelo para los sellos del emisor (archivos .cer y .key)."""
190
206
  base64_file: str = Field(default=..., alias="base64File", description="Archivo en formato base64.")
191
- file_type: Literal[0, 1] = Field(default=..., alias="fileType", description="Tipo de archivo: 0 para certificado, 1 para llave privada.")
192
- password: str = Field(default=..., alias="password", description="Contraseña del archivo .key independientemente de si es un archivo .cer o .key.")
207
+ file_type: int = Field(default=..., alias="fileType", description="Tipo de archivo: 0 para certificado, 1 para llave privada (el par CSD). El API valida el valor.")
208
+ password: Optional[str] = Field(default=None, alias="password", description="Contraseña de la llave privada. Requerida en el objeto .key (file_type 1); en el .cer el API no la usa.")
193
209
 
194
210
  model_config = ConfigDict(populate_by_name=True)
195
211
 
@@ -230,7 +246,7 @@ class ItemTax(BaseDto):
230
246
  tax_code: Optional[str] = Field(default=None, alias="taxCode", description="Código del impuesto.")
231
247
  tax_type_code: Optional[str] = Field(default=None, alias="taxTypeCode", description="Tipo de factor.")
232
248
  tax_rate: Optional[Decimal] = Field(default=None, alias="taxRate", description="Tasa del impuesto.")
233
- tax_flag_code: Optional[Literal["T", "R"]] = Field(default=None, alias="taxFlagCode", description="Código que indica la naturaleza del impuesto. (T)raslado o (R)etención.")
249
+ tax_flag_code: Optional[str] = Field(default=None, alias="taxFlagCode", description="Código que indica la naturaleza del impuesto. (T)raslado o (R)etención.")
234
250
 
235
251
  model_config = ConfigDict(populate_by_name=True)
236
252
 
@@ -313,7 +329,7 @@ class PaidInvoiceTax(BaseDto):
313
329
  tax_code: str = Field(default=..., alias="taxCode", description="Código del impuesto.")
314
330
  tax_type_code: str = Field(default=..., alias="taxTypeCode", description="Tipo de factor.")
315
331
  tax_rate: Decimal = Field(default=..., alias="taxRate", description="Tasa del impuesto.")
316
- tax_flag_code: Optional[Literal["T", "R"]] = Field(default=None, alias="taxFlagCode", description="Código que indica la naturaleza del impuesto. (T)raslado o (R)etención.")
332
+ tax_flag_code: Optional[str] = Field(default=None, alias="taxFlagCode", description="Código que indica la naturaleza del impuesto. (T)raslado o (R)etención.")
317
333
 
318
334
  model_config = ConfigDict(populate_by_name=True)
319
335
 
@@ -343,7 +359,7 @@ class InvoicePayment(BaseDto):
343
359
  payment_date: str = Field(default=..., alias="paymentDate", description="Fecha de pago.")
344
360
  payment_form_code: str = Field(default=..., alias="paymentFormCode", description="Código de la forma de pago.")
345
361
 
346
- currency_code: Literal ["MXN", "USD", "EUR"] = Field(default="MXN", alias="currencyCode", description="Código de la moneda utilizada en el pago.")
362
+ currency_code: str = Field(default="MXN", alias="currencyCode", description="Código de la moneda utilizada en el pago: una clave del catálogo c_Moneda del SAT, en mayúsculas, distinta de XXX.")
347
363
  exchange_rate: Optional[Decimal] = Field(default=Decimal("1"), alias="exchangeRate", description="Tipo de cambio FIX conforme a la moneda registrada en la factura. Si la moneda es MXN, el tipo de cambio debe ser 1..")
348
364
  amount: Decimal = Field(default=..., description="Monto del pago.")
349
365
  source_bank_tin: str = Field(default=..., alias="sourceBankTin", description="RFC del banco origen. (Rfc del banco emisor del pago)")
@@ -363,7 +379,7 @@ class LocalTax(BaseDto):
363
379
  tax_name: Optional[str] = Field(default=None, alias="taxName", description="Nombre del impuesto local.")
364
380
  tax_rate: Optional[Decimal] = Field(default=None, alias="taxRate", description="Tasa del impuesto local.")
365
381
  tax_amount: Optional[Decimal] = Field(default=None, alias="taxAmount", description="Monto del impuesto local.")
366
- tax_flag_code: Optional[Literal["T", "R"]] = Field(default=None, alias="taxFlagCode", description="Traslado o Retención.")
382
+ tax_flag_code: Optional[str] = Field(default=None, alias="taxFlagCode", description="Traslado o Retención.")
367
383
 
368
384
  model_config = ConfigDict(populate_by_name=True)
369
385
 
@@ -588,7 +604,7 @@ class CancelInvoiceRequest(BaseDto):
588
604
  id: Optional[str] = Field(default=None, alias="id", description="ID de la factura a cancelar. Obligatorio cuando se cancela por referencias.")
589
605
  invoice_uuid: Optional[str] = Field(default=None, alias="invoiceUuid", description="UUID de la factura a cancelar. Obligatorio cuando se cancela por valores.")
590
606
  tin: Optional[str] = Field(default=None, alias="tin", description="RFC del emisor de la factura. Obligatorio cuando se cancela por valores.")
591
- cancellation_reason_code: Literal["01", "02", "03", "04"] = Field(default=..., alias="cancellationReasonCode", description="Código del motivo de cancelación.")
607
+ cancellation_reason_code: str = Field(default=..., alias="cancellationReasonCode", description="Código del motivo de cancelación, catálogo c_MotivoCancelacion del SAT: 01, 02, 03 o 04. El API valida el valor.")
592
608
  replacement_uuid: Optional[str] = Field(default=None, alias="replacementUuid", description="UUID de la factura de reemplazo. Obligatorio si el motivo de cancelación es '01'.")
593
609
  tax_credentials: Optional[list[TaxCredential]] = Field(default=None, alias="taxCredentials", description="Sellos del emisor. Obligatorio cuando se cancela por valores.")
594
610
 
@@ -738,7 +754,8 @@ class DownloadRequest(BaseDto):
738
754
  download_request_status: Optional[CatalogDto] = Field(default=None, alias="downloadRequestStatus", description="Estado de la solicitud de descarga Fiscalapi.")
739
755
 
740
756
  last_attempt_date: Optional[datetime] = Field(default=None, alias="lastAttemptDate", description="Fecha del último intento para la solicitud asociada.")
741
- next_attempt_date: Optional[datetime] = Field(default=None, alias="nextAttemptDate", description="Fecha del siguiente intento para la solicitud asociada.")
757
+ # Obsoleto: el API ya no devuelve nextAttemptDate (siempre era null). Se eliminará en una versión mayor.
758
+ next_attempt_date: Optional[datetime] = Field(default=None, alias="nextAttemptDate", description="Obsoleto: el API ya no lo devuelve; siempre era null porque Fiscalapi no programa un siguiente intento por solicitud. Se eliminará en una versión mayor; use last_attempt_date.")
742
759
 
743
760
  invoice_count: Optional[int] = Field(default=None, alias="invoiceCount", description="Número de CFDIs encontrados para la solicitud cuando la solicitud ha terminado.")
744
761
  package_ids: Optional[list[str]] = Field(default_factory=list, alias="packageIds", description="Lista de IDs de paquetes disponibles para descarga cuando la solicitud ha terminado.")
@@ -113,6 +113,7 @@ class BaseService:
113
113
  http_status_code=status_code,
114
114
  message=generic_error.message,
115
115
  details=details_str,
116
+ trace_identifier=generic_error.trace_identifier,
116
117
  data=None
117
118
  )
118
119
  except Exception:
@@ -123,6 +124,7 @@ class BaseService:
123
124
  http_status_code=status_code,
124
125
  message=generic_error.message or f"HTTP Error {status_code}",
125
126
  details=generic_error.details or raw_content,
127
+ trace_identifier=generic_error.trace_identifier,
126
128
  data=None
127
129
  )
128
130
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fiscalapi
3
- Version: 4.0.418
3
+ Version: 4.0.433
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
@@ -180,7 +180,7 @@ from fiscalapi import TaxFile
180
180
  # Subir certificado (CER)
181
181
  certificado_csd = TaxFile(
182
182
  person_id="3f3478b4-60fd-459e-8bfc-f8239fc96257",
183
- tin="FUNK671228PH6",
183
+ tin="FUNK671228PH6", # RFC de la persona (opcional)
184
184
  base64_file="MIIFgDCCA2igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0NDYwDQYJKo...",
185
185
  file_type=0, # 0 para certificado, 1 para llave privada
186
186
  password="12345678a"
@@ -189,7 +189,7 @@ certificado_csd = TaxFile(
189
189
  # Subir llave privada (KEY)
190
190
  clave_privada_csd = TaxFile(
191
191
  person_id="3f3478b4-60fd-459e-8bfc-f8239fc96257",
192
- tin="FUNK671228PH6",
192
+ tin="FUNK671228PH6", # RFC de la persona (opcional)
193
193
  base64_file="MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAg...",
194
194
  file_type=1,
195
195
  password="12345678a"
@@ -566,7 +566,7 @@ if api_response.succeeded:
566
566
  print(api_response.data.base64_file) # PDF firmado en base64
567
567
  ```
568
568
 
569
- Tras una firma exitosa, `client.people.get_by_id(...)` refleja el nuevo estado en `manifest_status_id`.
569
+ 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
570
 
571
571
  ## 📋 Operaciones Principales
572
572
 
@@ -33,4 +33,7 @@ fiscalapi/services/people_service.py
33
33
  fiscalapi/services/product_service.py
34
34
  fiscalapi/services/sat_validation_service.py
35
35
  fiscalapi/services/stamp_service.py
36
- fiscalapi/services/tax_file_service.py
36
+ fiscalapi/services/tax_file_service.py
37
+ tests/test_cycle_contract.py
38
+ tests/test_error_trace_identifier.py
39
+ tests/test_response_tolerance.py
@@ -18,3 +18,6 @@ target-version = "py39"
18
18
  [tool.ruff.lint]
19
19
  select = ["E", "F", "W"]
20
20
  ignore = ["E501", "W293", "W291", "W292"]
21
+
22
+ [tool.pytest.ini_options]
23
+ testpaths = ["tests"]
@@ -2,7 +2,7 @@
2
2
  import os
3
3
  from setuptools import setup, find_packages
4
4
 
5
- VERSION = "4.0.418"
5
+ VERSION = "4.0.433"
6
6
 
7
7
  DESCRIPTION = "Genera facturas CFDI válidas ante el SAT consumiendo el API de https://www.fiscalapi.com"
8
8
 
@@ -0,0 +1,190 @@
1
+ """Cambios de código del SDK en el ciclo de deuda técnica 2 (Fase III, C22).
2
+
3
+ Los escenarios 1 a 3 viven también en los SDK de .NET, Node.js, PHP y Java (DEC-136), cuando el cambio aplica a cada uno:
4
+
5
+ 1. persona: ``valid_to`` de solo lectura y ``committed_balance`` heredado; ``capital_regime``, ``stripeCustomerId`` y
6
+ ``subscriptionStatus`` fuera del modelo (SDK-043, SDK-044);
7
+ 2. certificados: ``tin`` opcional al subir (SDK-045) y ``file_type`` 2 y 3 de la FIEL (SDK-058);
8
+ 3. certificados sin ``update()``: el API retiró PUT tax-files (SDK-051).
9
+
10
+ Los escenarios 4 y 5 son solo de Python: el SDK deja de restringir con ``Literal`` las claves que valida el API y las
11
+ envía tal cual, como los otros SDK (SDK-044, SDK-050, SDK-058; DEC-163 H2).
12
+ """
13
+ from datetime import datetime
14
+ from decimal import Decimal
15
+
16
+ import pytest
17
+
18
+ from fiscalapi import (
19
+ BaseDto,
20
+ CancelInvoiceRequest,
21
+ InvoicePayment,
22
+ Person,
23
+ ProductTax,
24
+ TaxCredential,
25
+ TaxFile,
26
+ )
27
+ from fiscalapi.models.fiscalapi_models import ItemTax, LocalTax, PaidInvoiceTax
28
+
29
+ BASE_URL = "https://sdk-tests.fiscalapi.invalid/api/v4"
30
+ PERSON_ID = "4b1c2d3e-0000-4000-8000-000000000002"
31
+ TAX_FILE_ID = "7a1b2c3d-0000-4000-8000-0000000000f1"
32
+ BASE64_CER = "MIIFsDCCA5igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MTYwDQYJKoZIhvcNAQELBQAw"
33
+ BASE64_KEY = "MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAgEAAoIBAQACAggAMBQG"
34
+
35
+ RETIRED_PERSON_KEYS = ("validTo", "capitalRegime", "CapitalRegime", "stripeCustomerId", "subscriptionStatus")
36
+
37
+
38
+ def _envelope(data) -> dict:
39
+ return {"data": data, "succeeded": True, "message": "", "details": "", "httpStatusCode": 200}
40
+
41
+
42
+ # 1. Persona (SDK-043, SDK-044)
43
+
44
+
45
+ def test_person_from_response_reads_valid_to_and_committed_balance_and_drops_the_retired_fields(client, fake_api, api_json):
46
+ fake_api.respond_with(api_json("person-valid-to.json"))
47
+
48
+ response = client.people.get_by_id(PERSON_ID)
49
+
50
+ person = response.data
51
+ assert response.succeeded is True
52
+ assert person.valid_to == datetime(2025, 7, 15, 10, 30)
53
+ assert person.committed_balance == 0
54
+ assert person.available_balance == 100
55
+ assert "capital_regime" not in Person.model_fields
56
+ dumped = person.model_dump(by_alias=True)
57
+ for key in ("capitalRegime", "CapitalRegime", "stripeCustomerId", "subscriptionStatus"):
58
+ assert key not in dumped
59
+
60
+
61
+ def test_person_create_sends_neither_valid_to_nor_the_retired_fields(client, fake_api, api_json):
62
+ fake_api.respond_with(api_json("person-valid-to.json"))
63
+ person = Person(legal_name="ESCUELA KEMPER URGATE", email="someone@example.com", password="UserPass123!", tin="EKU9003173C9")
64
+
65
+ client.people.create(person)
66
+
67
+ [request] = fake_api.requests
68
+ assert request["method"] == "POST"
69
+ assert request["url"] == f"{BASE_URL}/people"
70
+ assert request["json"]["legalName"] == "ESCUELA KEMPER URGATE"
71
+ for key in RETIRED_PERSON_KEYS:
72
+ assert key not in request["json"]
73
+
74
+
75
+ # 2. Certificados: tin opcional (SDK-045) y file_type de la FIEL (SDK-058)
76
+
77
+
78
+ @pytest.mark.parametrize("file_type", [2, 3])
79
+ def test_tax_file_create_without_tin_sends_the_file_type_as_a_number_and_no_tin(client, fake_api, file_type):
80
+ fake_api.respond_with(_envelope({"id": TAX_FILE_ID, "personId": PERSON_ID, "tin": "EKU9003173C9", "fileType": file_type, "sequence": 1}))
81
+ tax_file = TaxFile(person_id=PERSON_ID, base64_file=BASE64_CER, file_type=file_type, password="12345678a")
82
+
83
+ response = client.tax_files.create(tax_file)
84
+
85
+ assert response.succeeded is True
86
+ assert response.data.tin == "EKU9003173C9"
87
+ [request] = fake_api.requests
88
+ assert request["method"] == "POST"
89
+ assert request["url"] == f"{BASE_URL}/tax-files"
90
+ assert request["json"] == {"personId": PERSON_ID, "base64File": BASE64_CER, "fileType": file_type, "password": "12345678a"}
91
+
92
+
93
+ # 3. Certificados sin update() (SDK-051)
94
+
95
+
96
+ def test_tax_file_service_has_no_update(client):
97
+ assert not hasattr(client.tax_files, "update")
98
+
99
+
100
+ def test_tax_file_delete_sends_delete_to_the_tax_file(client, fake_api):
101
+ fake_api.respond_with(_envelope(True))
102
+
103
+ response = client.tax_files.delete(TAX_FILE_ID)
104
+
105
+ assert response.succeeded is True
106
+ assert response.data is True
107
+ [request] = fake_api.requests
108
+ assert request["method"] == "DELETE"
109
+ assert request["url"] == f"{BASE_URL}/tax-files/{TAX_FILE_ID}"
110
+ assert request["json"] is None
111
+
112
+
113
+ # 4. Cancelación por valores: TaxCredential sin BaseDto (SDK-044) y motivo como texto (SDK-058)
114
+
115
+
116
+ @pytest.mark.parametrize("reason_code", ["02", "05"])
117
+ def test_cancel_by_values_sends_the_reason_code_as_is_and_only_the_credential_fields(client, fake_api, reason_code):
118
+ fake_api.respond_with(_envelope({"base64CancellationAcknowledgement": "PD94", "invoiceUuids": {"UUID-1": "201"}}))
119
+ request_model = CancelInvoiceRequest(
120
+ invoice_uuid="UUID-1",
121
+ tin="EKU9003173C9",
122
+ cancellation_reason_code=reason_code,
123
+ tax_credentials=[
124
+ TaxCredential(base64_file=BASE64_CER, file_type=0),
125
+ TaxCredential(base64_file=BASE64_KEY, file_type=1, password="12345678a"),
126
+ ],
127
+ )
128
+
129
+ response = client.invoices.cancel(request_model)
130
+
131
+ assert response.succeeded is True
132
+ assert response.data.invoice_uuids == {"UUID-1": "201"}
133
+ [request] = fake_api.requests
134
+ assert request["method"] == "DELETE"
135
+ assert request["url"] == f"{BASE_URL}/invoices"
136
+ assert request["json"]["cancellationReasonCode"] == reason_code
137
+ assert request["json"]["taxCredentials"] == [
138
+ {"base64File": BASE64_CER, "fileType": 0},
139
+ {"base64File": BASE64_KEY, "fileType": 1, "password": "12345678a"},
140
+ ]
141
+
142
+
143
+ def test_tax_credential_is_not_a_base_dto():
144
+ credential = TaxCredential(base64_file=BASE64_CER, file_type=0)
145
+
146
+ assert not isinstance(credential, BaseDto)
147
+ assert set(TaxCredential.model_fields) == {"base64_file", "file_type", "password"}
148
+ assert credential.password is None
149
+
150
+
151
+ # 5. Claves de catálogo como texto: el SDK no las restringe, las valida el API (SDK-050, DEC-163 H2)
152
+
153
+
154
+ def test_catalog_keys_are_sent_as_is():
155
+ product_tax = ProductTax(rate=Decimal("0.16"), tax_id="004", tax_flag_id="X", tax_type_id="Otro")
156
+ person = Person(sat_tax_regime_id="699", sat_cfdi_use_id="Z99", user_type_id="U")
157
+
158
+ assert product_tax.model_dump(mode="json", by_alias=True, exclude_none=True) == {
159
+ "rate": "0.16",
160
+ "taxId": "004",
161
+ "taxFlagId": "X",
162
+ "taxTypeId": "Otro",
163
+ }
164
+ assert person.model_dump(mode="json", by_alias=True, exclude_none=True) == {
165
+ "satTaxRegimeId": "699",
166
+ "satCfdiUseId": "Z99",
167
+ "userTypeId": "U",
168
+ }
169
+ for model in (
170
+ ItemTax(tax_flag_code="X"),
171
+ PaidInvoiceTax(tax_code="002", tax_type_code="Tasa", tax_rate=Decimal("0.16"), tax_flag_code="X"),
172
+ LocalTax(tax_flag_code="X"),
173
+ ):
174
+ assert model.model_dump(by_alias=True)["taxFlagCode"] == "X"
175
+
176
+
177
+ def test_invoice_payment_accepts_any_currency_code_and_defaults_to_mxn():
178
+ common = dict(
179
+ payment_date="2026-10-08T10:00:00",
180
+ payment_form_code="03",
181
+ amount=Decimal("100"),
182
+ source_bank_tin="BSM970519DU8",
183
+ source_bank_account="1234567891",
184
+ target_bank_tin="BBA830831LJ2",
185
+ target_bank_account="0123456789",
186
+ paid_invoices=[],
187
+ )
188
+
189
+ assert InvoicePayment(currency_code="CAD", **common).model_dump(by_alias=True)["currencyCode"] == "CAD"
190
+ assert InvoicePayment(**common).currency_code == "MXN"
@@ -0,0 +1,94 @@
1
+ """SDK-060 (DEC-184, consistencia DEC-136 con Java): las respuestas de error exponen ``trace_identifier``.
2
+
3
+ Los textos de los 5xx del API piden enviar el traceIdentifier a soporte. El cambio es aditivo: ``message``,
4
+ ``details`` y ``http_status_code`` no cambian.
5
+ """
6
+ from fiscalapi import SignManifestRequest
7
+
8
+ TRACE_IDENTIFIER = "5C1E0B7A92D44F3E8A6B1D07F3C29E41"
9
+
10
+
11
+ def _sign_request() -> SignManifestRequest:
12
+ return SignManifestRequest(
13
+ base64_cer="base64CerHere...",
14
+ base64_key="base64KeyHere...",
15
+ password="passwordPhraseHere...",
16
+ )
17
+
18
+
19
+ def test_server_error_exposes_the_trace_identifier_and_keeps_message_and_details(client, fake_api):
20
+ fake_api.respond_with({
21
+ "data": None,
22
+ "succeeded": False,
23
+ "message": "Something went wrong on our end.",
24
+ "details": "Ocurrió un error inesperado en el servidor. Escríbenos a soporte@fiscalapi.com con el traceIdentifier de esta respuesta.",
25
+ "httpStatusCode": 500,
26
+ "traceIdentifier": TRACE_IDENTIFIER,
27
+ }, status_code=500)
28
+
29
+ response = client.manifests.sign(_sign_request())
30
+
31
+ assert response.succeeded is False
32
+ assert response.http_status_code == 500
33
+ assert response.message == "Something went wrong on our end."
34
+ assert response.details == (
35
+ "Ocurrió un error inesperado en el servidor. Escríbenos a soporte@fiscalapi.com con el traceIdentifier de esta respuesta."
36
+ )
37
+ assert response.trace_identifier == TRACE_IDENTIFIER
38
+ assert response.data is None
39
+
40
+
41
+ def test_client_error_exposes_the_trace_identifier(client, fake_api):
42
+ fake_api.respond_with({
43
+ "data": None,
44
+ "succeeded": False,
45
+ "message": "Sorry, it's not me, it's you.",
46
+ "details": "No existe un usuario con RFC EKU9003173C9 en el tenant actual.",
47
+ "httpStatusCode": 404,
48
+ "traceIdentifier": TRACE_IDENTIFIER,
49
+ }, status_code=404)
50
+
51
+ response = client.manifests.sign(_sign_request())
52
+
53
+ assert response.http_status_code == 404
54
+ assert response.message == "Sorry, it's not me, it's you."
55
+ assert response.details == "No existe un usuario con RFC EKU9003173C9 en el tenant actual."
56
+ assert response.trace_identifier == TRACE_IDENTIFIER
57
+
58
+
59
+ def test_validation_error_exposes_the_trace_identifier_and_keeps_the_joined_details(client, fake_api):
60
+ fake_api.respond_with({
61
+ "data": [{
62
+ "propertyName": "Password",
63
+ "errorMessage": "La contraseña de la llave privada (.key) es incorrecta o el archivo no es una llave privada.",
64
+ "attemptedValue": None,
65
+ }],
66
+ "succeeded": False,
67
+ "message": "Sorry, it's not me, it's you.",
68
+ "details": "One or more validation failures have occurred.",
69
+ "httpStatusCode": 400,
70
+ "traceIdentifier": TRACE_IDENTIFIER,
71
+ }, status_code=400)
72
+
73
+ response = client.manifests.sign(_sign_request())
74
+
75
+ assert response.http_status_code == 400
76
+ assert response.details == (
77
+ "Password: La contraseña de la llave privada (.key) es incorrecta o el archivo no es una llave privada."
78
+ )
79
+ assert response.trace_identifier == TRACE_IDENTIFIER
80
+
81
+
82
+ def test_error_without_trace_identifier_leaves_it_none(client, fake_api):
83
+ fake_api.respond_with({
84
+ "data": None,
85
+ "succeeded": False,
86
+ "message": "Too Many Requests",
87
+ "details": "x",
88
+ "httpStatusCode": 429,
89
+ }, status_code=429)
90
+
91
+ response = client.manifests.sign(_sign_request())
92
+
93
+ assert response.http_status_code == 429
94
+ assert response.trace_identifier is None
@@ -0,0 +1,143 @@
1
+ """Tolerancia del SDK a respuestas que el API devolverá en las próximas fases (SDK-001).
2
+
3
+ Son pruebas de caracterización: fijan el comportamiento ACTUAL. Una prueba cuyo nombre cita SDK-012
4
+ documenta un defecto conocido; SDK-012 la invierte al corregirlo. Los casos son:
5
+
6
+ 1. una transacción de timbres con ``creditType: 3`` (créditos de ticket, Fase 1);
7
+ 2. una persona con el campo nuevo ``availableTicketBalance`` (Fase 1);
8
+ 3. propiedades desconocidas en la envoltura, en ``data`` y en objetos anidados;
9
+ 4. ``globalInformation`` en la respuesta de una factura (BE-008).
10
+ """
11
+ from decimal import Decimal
12
+
13
+ import pytest
14
+ from pydantic import ValidationError
15
+
16
+ from fiscalapi import CreditType, GlobalInformation
17
+
18
+ TICKET_CREDIT_TYPE = 3
19
+
20
+
21
+ def _ticket_credit_type_errors(error: ValidationError) -> list:
22
+ return [e for e in error.errors() if e["loc"][-1] == "creditType" and e["input"] == TICKET_CREDIT_TYPE]
23
+
24
+
25
+ # 1. creditType: 3
26
+
27
+
28
+ def test_stamp_list_with_credit_type_3_raises_validation_error__fixed_by_SDK_012(client, fake_api, api_json):
29
+ fake_api.respond_with(api_json("stamps-page-credit-type-3.json"))
30
+
31
+ with pytest.raises(ValidationError) as raised:
32
+ client.stamps.get_list(1, 10)
33
+
34
+ assert len(_ticket_credit_type_errors(raised.value)) == 1
35
+ assert raised.value.error_count() == 1
36
+
37
+
38
+ def test_stamp_by_id_with_credit_type_3_raises_validation_error__fixed_by_SDK_012(client, fake_api, api_json):
39
+ fake_api.respond_with(api_json("stamp-credit-type-3.json"))
40
+
41
+ with pytest.raises(ValidationError) as raised:
42
+ client.stamps.get_by_id("8d0f6a52-6f1e-4c3a-9a10-000000000103")
43
+
44
+ assert len(_ticket_credit_type_errors(raised.value)) == 1
45
+ assert raised.value.error_count() == 1
46
+
47
+
48
+ def test_stamp_list_without_the_credit_type_3_item_deserializes_known_credit_types(client, fake_api, api_json):
49
+ page = api_json("stamps-page-credit-type-3.json")
50
+ page["data"]["items"] = [i for i in page["data"]["items"] if i["creditType"] != TICKET_CREDIT_TYPE]
51
+ fake_api.respond_with(page)
52
+
53
+ response = client.stamps.get_list(1, 10)
54
+
55
+ assert response.succeeded is True
56
+ assert [t.credit_type for t in response.data.items] == [CreditType.STAMP, CreditType.VALIDATION]
57
+ assert response.data.total_count == 3
58
+ assert fake_api.requested_urls == ["https://sdk-tests.fiscalapi.invalid/api/v4/stamps?pageNumber=1&pageSize=10"]
59
+
60
+
61
+ # 2. availableTicketBalance
62
+
63
+
64
+ def test_person_with_available_ticket_balance_deserializes_and_drops_the_field__exposed_by_SDK_012(
65
+ client, fake_api, api_json
66
+ ):
67
+ fake_api.respond_with(api_json("person-ticket-balance.json"))
68
+
69
+ response = client.people.get_by_id("4b1c2d3e-0000-4000-8000-000000000001")
70
+
71
+ person = response.data
72
+ assert response.succeeded is True
73
+ assert person.available_balance == Decimal("100")
74
+ assert person.committed_balance == Decimal("0")
75
+ assert person.available_validation_balance == 50
76
+ assert not hasattr(person, "available_ticket_balance")
77
+ assert "availableTicketBalance" not in person.model_dump(by_alias=True)
78
+
79
+
80
+ # 3. Propiedades desconocidas
81
+
82
+
83
+ def test_unknown_properties_on_a_stamp_transaction_are_ignored_and_known_fields_are_read(client, fake_api, api_json):
84
+ stamp = api_json("stamp-credit-type-3.json")
85
+ stamp["data"]["creditType"] = int(CreditType.VALIDATION)
86
+ fake_api.respond_with(stamp)
87
+
88
+ response = client.stamps.get_by_id("8d0f6a52-6f1e-4c3a-9a10-000000000103")
89
+
90
+ transaction = response.data
91
+ assert response.succeeded is True
92
+ assert transaction.credit_type == CreditType.VALIDATION
93
+ assert transaction.amount == 25
94
+ assert transaction.from_person.legal_name == "FISCALAPI"
95
+ assert "unknownScalarField" not in transaction.model_dump(by_alias=True)
96
+ assert "unknownNestedField" not in transaction.from_person.model_dump(by_alias=True)
97
+
98
+
99
+ def test_unknown_properties_on_the_envelope_the_page_and_a_person_are_ignored(client, fake_api, api_json):
100
+ fake_api.respond_with(api_json("person-ticket-balance.json"))
101
+
102
+ response = client.people.get_by_id("4b1c2d3e-0000-4000-8000-000000000001")
103
+
104
+ assert response.succeeded is True
105
+ assert response.http_status_code == 200
106
+ assert response.data.tin == "EKU9003173C9"
107
+ assert "unknownEnvelopeField" not in response.model_dump(by_alias=True)
108
+ assert "unknownObjectField" not in response.data.model_dump(by_alias=True)
109
+
110
+
111
+ # 4. globalInformation en la respuesta de una factura (BE-008)
112
+
113
+
114
+ def test_invoice_with_global_information_deserializes_it(client, fake_api, api_json):
115
+ fake_api.respond_with(api_json("invoice-global-information.json"))
116
+
117
+ response = client.invoices.get_by_id("3f2a9c1e-0b7d-4e55-9a61-2c8f0e4d7b10")
118
+
119
+ assert response.succeeded is True
120
+ assert response.data.global_information == GlobalInformation(periodicity_code="04", month_code="09", year=2026)
121
+ assert response.data.issuer.tin == "EKU9003173C9"
122
+
123
+
124
+ def test_invoice_with_null_global_information_deserializes_it_as_none(client, fake_api, api_json):
125
+ fake_api.respond_with(api_json("invoice-global-information-null.json"))
126
+
127
+ response = client.invoices.get_by_id("3f2a9c1e-0b7d-4e55-9a61-2c8f0e4d7b10")
128
+
129
+ assert response.succeeded is True
130
+ assert response.data.global_information is None
131
+ assert response.data.total == Decimal("1160")
132
+
133
+
134
+ def test_invoice_with_null_members_in_global_information_raises_validation_error__BE_008_must_not_emit_it(
135
+ client, fake_api, api_json
136
+ ):
137
+ fake_api.respond_with(api_json("invoice-global-information-null-members.json"))
138
+
139
+ with pytest.raises(ValidationError) as raised:
140
+ client.invoices.get_by_id("3f2a9c1e-0b7d-4e55-9a61-2c8f0e4d7b10")
141
+
142
+ failing_fields = sorted(e["loc"][-1] for e in raised.value.errors())
143
+ assert failing_fields == ["monthCode", "periodicityCode", "year"]
File without changes
File without changes
File without changes