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.
- {fiscalapi-4.0.418/fiscalapi.egg-info → fiscalapi-4.0.433}/PKG-INFO +4 -4
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/README.md +3 -3
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/__init__.py +2 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/models/__init__.py +2 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/models/common_models.py +2 -1
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/models/fiscalapi_models.py +40 -23
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/base_service.py +2 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433/fiscalapi.egg-info}/PKG-INFO +4 -4
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi.egg-info/SOURCES.txt +4 -1
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/pyproject.toml +3 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/setup.py +1 -1
- fiscalapi-4.0.433/tests/test_cycle_contract.py +190 -0
- fiscalapi-4.0.433/tests/test_error_trace_identifier.py +94 -0
- fiscalapi-4.0.433/tests/test_response_tolerance.py +143 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/LICENSE.txt +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/MANIFEST.in +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/models/carta_porte_models.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/models/comercio_exterior_models.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/models/manifest_models.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/models/sat_validation_models.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/py.typed +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/__init__.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/api_key_service.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/catalog_service.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/download_catalog_service.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/download_request_service.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/download_rule_service.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/employee_service.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/employer_service.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/fiscalapi_client.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/invoice_service.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/manifest_service.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/people_service.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/product_service.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/sat_validation_service.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/stamp_service.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi/services/tax_file_service.py +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi.egg-info/dependency_links.txt +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi.egg-info/requires.txt +0 -0
- {fiscalapi-4.0.418 → fiscalapi-4.0.433}/fiscalapi.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
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[
|
|
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[
|
|
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[
|
|
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
|
-
|
|
58
|
-
sat_tax_regime_id: Optional[
|
|
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[
|
|
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[
|
|
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
|
|
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="
|
|
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
|
|
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[
|
|
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(
|
|
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:
|
|
192
|
-
password: str = Field(default
|
|
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[
|
|
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[
|
|
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:
|
|
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[
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|