factpulse 1.0.9__py3-none-any.whl → 3.0.7__py3-none-any.whl

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.

Potentially problematic release.


This version of factpulse might be problematic. Click here for more details.

Files changed (269) hide show
  1. factpulse/__init__.py +275 -203
  2. factpulse/api/__init__.py +5 -3
  3. factpulse/api/afnorpdppa_api.py +559 -9
  4. factpulse/api/afnorpdppa_directory_service_api.py +4313 -66
  5. factpulse/api/afnorpdppa_flow_service_api.py +23 -23
  6. factpulse/api/chorus_pro_api.py +362 -404
  7. factpulse/api/{signature_lectronique_api.py → document_conversion_api.py} +519 -371
  8. factpulse/api/health_api.py +526 -0
  9. factpulse/api/invoice_processing_api.py +3437 -0
  10. factpulse/api/pdfxml_verification_api.py +1719 -0
  11. factpulse/api/{sant_api.py → user_api.py} +18 -17
  12. factpulse/api_client.py +6 -6
  13. factpulse/configuration.py +6 -4
  14. factpulse/exceptions.py +8 -5
  15. factpulse/models/__init__.py +133 -99
  16. factpulse/models/acknowledgment_status.py +38 -0
  17. factpulse/models/additional_document.py +115 -0
  18. factpulse/models/afnor_credentials.py +106 -0
  19. factpulse/models/afnor_destination.py +127 -0
  20. factpulse/models/afnor_health_check_response.py +91 -0
  21. factpulse/models/afnor_result.py +105 -0
  22. factpulse/models/allowance_charge.py +147 -0
  23. factpulse/models/allowance_reason_code.py +42 -0
  24. factpulse/models/allowance_total_amount.py +145 -0
  25. factpulse/models/amount.py +139 -0
  26. factpulse/models/amount_due.py +139 -0
  27. factpulse/models/api_error.py +104 -0
  28. factpulse/models/async_task_status.py +97 -0
  29. factpulse/models/base_amount.py +145 -0
  30. factpulse/models/bounding_box_schema.py +100 -0
  31. factpulse/models/celery_status.py +40 -0
  32. factpulse/models/certificate_info_response.py +24 -24
  33. factpulse/models/charge_total_amount.py +145 -0
  34. factpulse/models/chorus_pro_destination.py +108 -0
  35. factpulse/models/chorus_pro_result.py +101 -0
  36. factpulse/models/contact.py +113 -0
  37. factpulse/models/convert_error_response.py +105 -0
  38. factpulse/models/convert_pending_input_response.py +114 -0
  39. factpulse/models/convert_resume_request.py +87 -0
  40. factpulse/models/convert_success_response.py +126 -0
  41. factpulse/models/convert_validation_failed_response.py +120 -0
  42. factpulse/models/delivery_party.py +121 -0
  43. factpulse/models/destination.py +27 -27
  44. factpulse/models/document_type_info.py +91 -0
  45. factpulse/models/electronic_address.py +90 -0
  46. factpulse/models/enriched_invoice_info.py +133 -0
  47. factpulse/models/error_level.py +37 -0
  48. factpulse/models/error_source.py +43 -0
  49. factpulse/models/extraction_info.py +93 -0
  50. factpulse/models/factur_x_invoice.py +320 -0
  51. factpulse/models/factur_x_profile.py +39 -0
  52. factpulse/models/factur_xpdf_info.py +91 -0
  53. factpulse/models/facture_electronique_rest_api_schemas_chorus_pro_chorus_pro_credentials.py +95 -0
  54. factpulse/models/facture_electronique_rest_api_schemas_processing_chorus_pro_credentials.py +115 -0
  55. factpulse/models/field_status.py +40 -0
  56. factpulse/models/file_info.py +94 -0
  57. factpulse/models/files_info.py +106 -0
  58. factpulse/models/flow_direction.py +37 -0
  59. factpulse/models/flow_profile.py +38 -0
  60. factpulse/models/flow_summary.py +131 -0
  61. factpulse/models/flow_syntax.py +40 -0
  62. factpulse/models/flow_type.py +40 -0
  63. factpulse/models/generate_certificate_request.py +26 -26
  64. factpulse/models/generate_certificate_response.py +15 -15
  65. factpulse/models/get_chorus_pro_id_request.py +100 -0
  66. factpulse/models/get_chorus_pro_id_response.py +98 -0
  67. factpulse/models/get_invoice_request.py +98 -0
  68. factpulse/models/get_invoice_response.py +142 -0
  69. factpulse/models/get_structure_request.py +100 -0
  70. factpulse/models/get_structure_response.py +142 -0
  71. factpulse/models/global_allowance_amount.py +139 -0
  72. factpulse/models/gross_unit_price.py +145 -0
  73. factpulse/models/http_validation_error.py +2 -2
  74. factpulse/models/incoming_invoice.py +196 -0
  75. factpulse/models/incoming_supplier.py +144 -0
  76. factpulse/models/invoice_format.py +38 -0
  77. factpulse/models/invoice_line.py +354 -0
  78. factpulse/models/invoice_line_allowance_amount.py +145 -0
  79. factpulse/models/invoice_note.py +94 -0
  80. factpulse/models/invoice_references.py +194 -0
  81. factpulse/models/invoice_status.py +96 -0
  82. factpulse/models/invoice_totals.py +177 -0
  83. factpulse/models/invoice_totals_prepayment.py +145 -0
  84. factpulse/models/invoice_type_code.py +51 -0
  85. factpulse/models/invoicing_framework.py +110 -0
  86. factpulse/models/invoicing_framework_code.py +39 -0
  87. factpulse/models/line_net_amount.py +145 -0
  88. factpulse/models/line_total_amount.py +145 -0
  89. factpulse/models/mandatory_note_schema.py +124 -0
  90. factpulse/models/manual_rate.py +139 -0
  91. factpulse/models/manual_vat_rate.py +139 -0
  92. factpulse/models/missing_field.py +107 -0
  93. factpulse/models/operation_nature.py +49 -0
  94. factpulse/models/output_format.py +37 -0
  95. factpulse/models/page_dimensions_schema.py +89 -0
  96. factpulse/models/payee.py +168 -0
  97. factpulse/models/payment_card.py +99 -0
  98. factpulse/models/payment_means.py +41 -0
  99. factpulse/models/pdf_validation_result_api.py +169 -0
  100. factpulse/models/pdp_credentials.py +20 -13
  101. factpulse/models/percentage.py +145 -0
  102. factpulse/models/postal_address.py +134 -0
  103. factpulse/models/price_allowance_amount.py +145 -0
  104. factpulse/models/price_basis_quantity.py +145 -0
  105. factpulse/models/processing_options.py +94 -0
  106. factpulse/models/product_characteristic.py +89 -0
  107. factpulse/models/product_classification.py +101 -0
  108. factpulse/models/quantity.py +139 -0
  109. factpulse/models/recipient.py +167 -0
  110. factpulse/models/rounding_amount.py +145 -0
  111. factpulse/models/scheme_id.py +14 -8
  112. factpulse/models/search_flow_request.py +143 -0
  113. factpulse/models/search_flow_response.py +101 -0
  114. factpulse/models/search_services_response.py +101 -0
  115. factpulse/models/search_structure_request.py +119 -0
  116. factpulse/models/search_structure_response.py +101 -0
  117. factpulse/models/signature_info.py +6 -6
  118. factpulse/models/signature_info_api.py +122 -0
  119. factpulse/models/signature_parameters.py +133 -0
  120. factpulse/models/simplified_invoice_data.py +124 -0
  121. factpulse/models/structure_info.py +14 -14
  122. factpulse/models/structure_parameters.py +91 -0
  123. factpulse/models/structure_service.py +93 -0
  124. factpulse/models/submission_mode.py +38 -0
  125. factpulse/models/submit_complete_invoice_request.py +116 -0
  126. factpulse/models/submit_complete_invoice_response.py +145 -0
  127. factpulse/models/submit_flow_request.py +123 -0
  128. factpulse/models/submit_flow_response.py +109 -0
  129. factpulse/models/submit_gross_amount.py +139 -0
  130. factpulse/models/submit_invoice_request.py +176 -0
  131. factpulse/models/submit_invoice_response.py +103 -0
  132. factpulse/models/submit_net_amount.py +139 -0
  133. factpulse/models/submit_vat_amount.py +139 -0
  134. factpulse/models/supplementary_attachment.py +95 -0
  135. factpulse/models/supplier.py +225 -0
  136. factpulse/models/task_response.py +87 -0
  137. factpulse/models/tax_representative.py +95 -0
  138. factpulse/models/taxable_amount.py +139 -0
  139. factpulse/models/total_gross_amount.py +139 -0
  140. factpulse/models/total_net_amount.py +139 -0
  141. factpulse/models/total_vat_amount.py +139 -0
  142. factpulse/models/unit_net_price.py +139 -0
  143. factpulse/models/unit_of_measure.py +41 -0
  144. factpulse/models/validation_error.py +2 -2
  145. factpulse/models/validation_error_detail.py +107 -0
  146. factpulse/models/validation_error_loc_inner.py +2 -2
  147. factpulse/models/validation_error_response.py +87 -0
  148. factpulse/models/validation_info.py +105 -0
  149. factpulse/models/validation_success_response.py +87 -0
  150. factpulse/models/vat_accounting_code.py +39 -0
  151. factpulse/models/vat_amount.py +139 -0
  152. factpulse/models/vat_category.py +44 -0
  153. factpulse/models/vat_line.py +140 -0
  154. factpulse/models/vat_point_date_code.py +38 -0
  155. factpulse/models/vat_rate.py +145 -0
  156. factpulse/models/verification_success_response.py +135 -0
  157. factpulse/models/verified_field_schema.py +129 -0
  158. factpulse/rest.py +9 -4
  159. factpulse-3.0.7.dist-info/METADATA +292 -0
  160. factpulse-3.0.7.dist-info/RECORD +168 -0
  161. factpulse-3.0.7.dist-info/top_level.txt +2 -0
  162. factpulse_helpers/__init__.py +96 -0
  163. factpulse_helpers/client.py +2111 -0
  164. factpulse_helpers/exceptions.py +253 -0
  165. factpulse/api/processing_endpoints_unifis_api.py +0 -592
  166. factpulse/api/traitement_facture_api.py +0 -3439
  167. factpulse/api/utilisateur_api.py +0 -282
  168. factpulse/models/adresse_electronique.py +0 -90
  169. factpulse/models/adresse_postale.py +0 -120
  170. factpulse/models/body_ajouter_fichier_api_v1_chorus_pro_transverses_ajouter_fichier_post.py +0 -104
  171. factpulse/models/body_completer_facture_api_v1_chorus_pro_factures_completer_post.py +0 -104
  172. factpulse/models/body_lister_services_structure_api_v1_chorus_pro_structures_id_structure_cpp_services_get.py +0 -102
  173. factpulse/models/body_rechercher_factures_destinataire_api_v1_chorus_pro_factures_rechercher_destinataire_post.py +0 -104
  174. factpulse/models/body_rechercher_factures_fournisseur_api_v1_chorus_pro_factures_rechercher_fournisseur_post.py +0 -104
  175. factpulse/models/body_recycler_facture_api_v1_chorus_pro_factures_recycler_post.py +0 -104
  176. factpulse/models/body_telecharger_groupe_factures_api_v1_chorus_pro_factures_telecharger_groupe_post.py +0 -104
  177. factpulse/models/body_traiter_facture_recue_api_v1_chorus_pro_factures_traiter_facture_recue_post.py +0 -104
  178. factpulse/models/body_valideur_consulter_facture_api_v1_chorus_pro_factures_valideur_consulter_post.py +0 -104
  179. factpulse/models/body_valideur_rechercher_factures_api_v1_chorus_pro_factures_valideur_rechercher_post.py +0 -104
  180. factpulse/models/body_valideur_traiter_facture_api_v1_chorus_pro_factures_valideur_traiter_post.py +0 -104
  181. factpulse/models/cadre_de_facturation.py +0 -102
  182. factpulse/models/categorie_tva.py +0 -44
  183. factpulse/models/chorus_pro_credentials.py +0 -95
  184. factpulse/models/code_cadre_facturation.py +0 -39
  185. factpulse/models/code_raison_reduction.py +0 -42
  186. factpulse/models/consulter_facture_request.py +0 -98
  187. factpulse/models/consulter_facture_response.py +0 -142
  188. factpulse/models/consulter_structure_request.py +0 -100
  189. factpulse/models/consulter_structure_response.py +0 -142
  190. factpulse/models/credentials_afnor.py +0 -106
  191. factpulse/models/credentials_chorus_pro.py +0 -115
  192. factpulse/models/destinataire.py +0 -116
  193. factpulse/models/destination_afnor.py +0 -127
  194. factpulse/models/destination_chorus_pro.py +0 -108
  195. factpulse/models/direction_flux.py +0 -37
  196. factpulse/models/donnees_facture_simplifiees.py +0 -124
  197. factpulse/models/facture_enrichie_info_input.py +0 -123
  198. factpulse/models/facture_enrichie_info_output.py +0 -133
  199. factpulse/models/facture_factur_x.py +0 -173
  200. factpulse/models/flux_resume.py +0 -131
  201. factpulse/models/format_sortie.py +0 -37
  202. factpulse/models/fournisseur.py +0 -146
  203. factpulse/models/information_signature_api.py +0 -122
  204. factpulse/models/ligne_de_poste.py +0 -188
  205. factpulse/models/ligne_de_poste_montant_remise_ht.py +0 -145
  206. factpulse/models/ligne_de_poste_montant_total_ligne_ht.py +0 -145
  207. factpulse/models/ligne_de_poste_taux_tva_manuel.py +0 -145
  208. factpulse/models/ligne_de_tva.py +0 -118
  209. factpulse/models/mode_depot.py +0 -38
  210. factpulse/models/mode_paiement.py +0 -41
  211. factpulse/models/montant_ht_total.py +0 -139
  212. factpulse/models/montant_total.py +0 -138
  213. factpulse/models/montant_total_acompte.py +0 -145
  214. factpulse/models/montant_total_montant_remise_globale_ttc.py +0 -145
  215. factpulse/models/montant_ttc_total.py +0 -139
  216. factpulse/models/montant_tva.py +0 -139
  217. factpulse/models/montantapayer.py +0 -139
  218. factpulse/models/montantbaseht.py +0 -139
  219. factpulse/models/montanthttotal.py +0 -139
  220. factpulse/models/montantttctotal.py +0 -139
  221. factpulse/models/montanttva.py +0 -139
  222. factpulse/models/montanttva1.py +0 -139
  223. factpulse/models/montantunitaireht.py +0 -139
  224. factpulse/models/obtenir_id_chorus_pro_request.py +0 -100
  225. factpulse/models/obtenir_id_chorus_pro_response.py +0 -98
  226. factpulse/models/options_processing.py +0 -94
  227. factpulse/models/parametres_signature.py +0 -133
  228. factpulse/models/parametres_structure.py +0 -91
  229. factpulse/models/pdf_factur_x_info.py +0 -91
  230. factpulse/models/piece_jointe_complementaire.py +0 -95
  231. factpulse/models/profil_api.py +0 -39
  232. factpulse/models/profil_flux.py +0 -38
  233. factpulse/models/quantite.py +0 -139
  234. factpulse/models/quota_info.py +0 -95
  235. factpulse/models/rechercher_services_response.py +0 -101
  236. factpulse/models/rechercher_structure_request.py +0 -119
  237. factpulse/models/rechercher_structure_response.py +0 -101
  238. factpulse/models/references.py +0 -124
  239. factpulse/models/reponse_healthcheck_afnor.py +0 -91
  240. factpulse/models/reponse_recherche_flux.py +0 -101
  241. factpulse/models/reponse_soumission_flux.py +0 -109
  242. factpulse/models/reponse_tache.py +0 -87
  243. factpulse/models/reponse_validation_erreur.py +0 -87
  244. factpulse/models/reponse_validation_succes.py +0 -87
  245. factpulse/models/requete_recherche_flux.py +0 -143
  246. factpulse/models/requete_soumission_flux.py +0 -123
  247. factpulse/models/resultat_afnor.py +0 -105
  248. factpulse/models/resultat_chorus_pro.py +0 -101
  249. factpulse/models/resultat_validation_pdfapi.py +0 -169
  250. factpulse/models/service_structure.py +0 -93
  251. factpulse/models/soumettre_facture_complete_request.py +0 -116
  252. factpulse/models/soumettre_facture_complete_response.py +0 -145
  253. factpulse/models/soumettre_facture_request.py +0 -164
  254. factpulse/models/soumettre_facture_response.py +0 -103
  255. factpulse/models/statut_acquittement.py +0 -38
  256. factpulse/models/statut_facture.py +0 -96
  257. factpulse/models/statut_tache.py +0 -99
  258. factpulse/models/syntaxe_flux.py +0 -40
  259. factpulse/models/tauxmanuel.py +0 -139
  260. factpulse/models/type_facture.py +0 -37
  261. factpulse/models/type_flux.py +0 -40
  262. factpulse/models/type_tva.py +0 -39
  263. factpulse/models/unite.py +0 -41
  264. factpulse/models/utilisateur.py +0 -128
  265. factpulse-1.0.9.dist-info/METADATA +0 -182
  266. factpulse-1.0.9.dist-info/RECORD +0 -131
  267. factpulse-1.0.9.dist-info/top_level.txt +0 -1
  268. {factpulse-1.0.9.dist-info → factpulse-3.0.7.dist-info}/WHEEL +0 -0
  269. {factpulse-1.0.9.dist-info → factpulse-3.0.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,253 @@
1
+ """Custom exceptions for the FactPulse client.
2
+
3
+ This module defines an exception hierarchy aligned with the FactPulse API error format
4
+ (APIError, ValidationErrorDetail) compliant with AFNOR XP Z12-013 standard.
5
+
6
+ Exception hierarchy:
7
+ - FactPulseError (base)
8
+ ├── FactPulseAuthError (401)
9
+ ├── FactPulseValidationError (400, 422) - with structured details
10
+ ├── FactPulsePollingTimeout (polling timeout)
11
+ ├── FactPulseNotFoundError (404)
12
+ ├── FactPulseServiceUnavailableError (503)
13
+ └── FactPulseAPIError (generic with error_code)
14
+ """
15
+ from dataclasses import dataclass
16
+ from typing import Any, Dict, List, Optional
17
+
18
+
19
+ class FactPulseError(Exception):
20
+ """Base class for all FactPulse errors."""
21
+ pass
22
+
23
+
24
+ class FactPulseAuthError(FactPulseError):
25
+ """FactPulse authentication error (401).
26
+
27
+ Raised when:
28
+ - Invalid email/password
29
+ - Expired or invalid JWT token
30
+ - client_uid not found
31
+ """
32
+ def __init__(self, message: str = "Authentication required"):
33
+ self.message = message
34
+ super().__init__(message)
35
+
36
+
37
+ class FactPulsePollingTimeout(FactPulseError):
38
+ """Timeout while polling an asynchronous task."""
39
+ def __init__(self, task_id: str, timeout: int):
40
+ self.task_id = task_id
41
+ self.timeout = timeout
42
+ super().__init__(f"Timeout ({timeout}ms) reached for task {task_id}")
43
+
44
+
45
+ @dataclass
46
+ class ValidationErrorDetail:
47
+ """Validation error detail in AFNOR format.
48
+
49
+ Aligned with the AcknowledgementDetail schema from AFNOR XP Z12-013 standard.
50
+
51
+ Attributes:
52
+ level: Severity level ('Error' or 'Warning')
53
+ item: Identifier of the affected element (BR-FR rule, field, XPath)
54
+ reason: Error description
55
+ source: Error source (schematron, pydantic, pdfa, afnor, chorus_pro)
56
+ code: Unique error code (e.g., SCHEMATRON_BR_FR_01)
57
+ """
58
+ level: str = ""
59
+ item: str = ""
60
+ reason: str = ""
61
+ source: Optional[str] = None
62
+ code: Optional[str] = None
63
+
64
+ def __str__(self) -> str:
65
+ item = self.item or "unknown"
66
+ reason = self.reason or "Unknown error"
67
+ source_str = f" [{self.source}]" if self.source else ""
68
+ return f"[{item}]{source_str} {reason}"
69
+
70
+
71
+ class FactPulseValidationError(FactPulseError):
72
+ """Validation error with structured details (400, 422).
73
+
74
+ Contains a list of ValidationErrorDetail for diagnostics.
75
+
76
+ Attributes:
77
+ errors: List of detailed errors
78
+ error_code: API error code (e.g., VALIDATION_FAILED, SCHEMATRON_VALIDATION_FAILED)
79
+ """
80
+ def __init__(
81
+ self,
82
+ message: str,
83
+ errors: Optional[List[ValidationErrorDetail]] = None,
84
+ error_code: str = "VALIDATION_FAILED",
85
+ ):
86
+ self.errors = errors or []
87
+ self.error_code = error_code
88
+ if self.errors:
89
+ details = "\n".join(f" - {e}" for e in self.errors)
90
+ message = f"{message}\n\nDetails:\n{details}"
91
+ super().__init__(message)
92
+
93
+
94
+ class FactPulseNotFoundError(FactPulseError):
95
+ """Resource not found (404).
96
+
97
+ Attributes:
98
+ resource: Resource type (invoice, structure, flow, client)
99
+ identifier: Resource identifier
100
+ """
101
+ def __init__(self, resource: str, identifier: str = ""):
102
+ self.resource = resource
103
+ self.identifier = identifier
104
+ message = f"{resource.capitalize()} not found"
105
+ if identifier:
106
+ message = f"{resource.capitalize()} '{identifier}' not found"
107
+ super().__init__(message)
108
+
109
+
110
+ class FactPulseServiceUnavailableError(FactPulseError):
111
+ """External service unavailable (503).
112
+
113
+ Attributes:
114
+ service_name: Service name (AFNOR PDP, Chorus Pro, Django)
115
+ original_error: Original exception (optional)
116
+ """
117
+ def __init__(self, service_name: str, original_error: Optional[Exception] = None):
118
+ self.service_name = service_name
119
+ self.original_error = original_error
120
+ message = f"Service {service_name} is unavailable"
121
+ if original_error:
122
+ message = f"{message}: {str(original_error)}"
123
+ super().__init__(message)
124
+
125
+
126
+ class FactPulseAPIError(FactPulseError):
127
+ """Generic API error with structured error code.
128
+
129
+ Used for errors not covered by specific exceptions.
130
+
131
+ Attributes:
132
+ status_code: HTTP response status code
133
+ error_code: API error code (e.g., INTERNAL_ERROR)
134
+ error_message: API error message
135
+ details: Optional details (ValidationErrorDetail)
136
+ """
137
+ def __init__(
138
+ self,
139
+ status_code: int,
140
+ error_code: str,
141
+ error_message: str,
142
+ details: Optional[List[ValidationErrorDetail]] = None,
143
+ ):
144
+ self.status_code = status_code
145
+ self.error_code = error_code
146
+ self.error_message = error_message
147
+ self.details = details or []
148
+ super().__init__(f"[{error_code}] {error_message}")
149
+
150
+
151
+ def parse_api_error(response_json: Dict[str, Any], status_code: int = 400) -> FactPulseError:
152
+ """Parse an API error response and return the appropriate exception.
153
+
154
+ This function parses the unified FactPulse API error format
155
+ (APIError with errorCode, errorMessage, details) and returns
156
+ the appropriate Python exception.
157
+
158
+ Args:
159
+ response_json: Error response JSON (dict)
160
+ status_code: HTTP response status code
161
+
162
+ Returns:
163
+ Appropriate exception based on status_code and error_code
164
+
165
+ Example:
166
+ >>> response = requests.post(url, json=data)
167
+ >>> if response.status_code >= 400:
168
+ ... error = parse_api_error(response.json(), response.status_code)
169
+ ... raise error
170
+ """
171
+ # Extract API error fields
172
+ # Support both formats: camelCase (API) and snake_case
173
+ error_code = response_json.get("errorCode") or response_json.get("error_code") or "UNKNOWN_ERROR"
174
+ error_message = response_json.get("errorMessage") or response_json.get("error_message") or "Unknown error"
175
+ details_raw = response_json.get("details") or []
176
+
177
+ # Sometimes the error is wrapped in a "detail" field
178
+ if "detail" in response_json and isinstance(response_json["detail"], dict):
179
+ detail = response_json["detail"]
180
+ error_code = detail.get("error") or detail.get("errorCode") or error_code
181
+ error_message = detail.get("message") or detail.get("errorMessage") or error_message
182
+ details_raw = detail.get("details") or details_raw
183
+
184
+ # Parse details into ValidationErrorDetail
185
+ details = []
186
+ for d in details_raw:
187
+ if isinstance(d, dict):
188
+ details.append(ValidationErrorDetail(
189
+ level=d.get("level", "Error"),
190
+ item=d.get("item", ""),
191
+ reason=d.get("reason", ""),
192
+ source=d.get("source"),
193
+ code=d.get("code"),
194
+ ))
195
+
196
+ # Return appropriate exception based on status_code
197
+ if status_code == 401:
198
+ return FactPulseAuthError(error_message)
199
+ elif status_code == 404:
200
+ # Try to extract resource from message
201
+ resource = "resource"
202
+ if "client" in error_message.lower():
203
+ resource = "client"
204
+ elif "flux" in error_message.lower() or "flow" in error_message.lower():
205
+ resource = "flow"
206
+ elif "facture" in error_message.lower() or "invoice" in error_message.lower():
207
+ resource = "invoice"
208
+ elif "structure" in error_message.lower():
209
+ resource = "structure"
210
+ return FactPulseNotFoundError(resource)
211
+ elif status_code == 503:
212
+ service_name = "API"
213
+ if "afnor" in error_message.lower() or "pdp" in error_message.lower():
214
+ service_name = "AFNOR PDP"
215
+ elif "chorus" in error_message.lower():
216
+ service_name = "Chorus Pro"
217
+ return FactPulseServiceUnavailableError(service_name)
218
+ elif status_code in (400, 422) and details:
219
+ return FactPulseValidationError(error_message, details, error_code)
220
+ else:
221
+ return FactPulseAPIError(status_code, error_code, error_message, details)
222
+
223
+
224
+ def api_exception_to_validation_error(api_exception) -> FactPulseValidationError:
225
+ """Convert an SDK-generated ApiException to FactPulseValidationError.
226
+
227
+ The openapi-generator SDK generates ApiException objects that are not
228
+ very user-friendly. This function converts them to FactPulse exceptions
229
+ with intelligent error parsing.
230
+
231
+ Args:
232
+ api_exception: ApiException from the generated SDK
233
+
234
+ Returns:
235
+ FactPulseValidationError with structured details
236
+ """
237
+ import json
238
+
239
+ status_code = getattr(api_exception, "status", 400)
240
+ body = getattr(api_exception, "body", "{}")
241
+
242
+ try:
243
+ response_json = json.loads(body) if isinstance(body, str) else body
244
+ except (json.JSONDecodeError, TypeError):
245
+ response_json = {"errorMessage": str(api_exception)}
246
+
247
+ error = parse_api_error(response_json, status_code)
248
+
249
+ # Convert to FactPulseValidationError if not already
250
+ if isinstance(error, FactPulseValidationError):
251
+ return error
252
+ else:
253
+ return FactPulseValidationError(str(error))