factpulse 3.0.37 → 4.0.1

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 (370) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -12
  3. data/Gemfile.lock +1 -1
  4. data/README.md +136 -138
  5. data/docs/AllowanceCharge.md +1 -1
  6. data/docs/Amount2.md +15 -0
  7. data/docs/CDARCycleDeVieApi.md +154 -30
  8. data/docs/ChorusProCredentials.md +8 -8
  9. data/docs/ChorusProDestination.md +1 -1
  10. data/docs/ClientActivateResponse.md +22 -0
  11. data/docs/ClientCreateRequest.md +22 -0
  12. data/docs/ClientDetail.md +38 -0
  13. data/docs/ClientListResponse.md +24 -0
  14. data/docs/ClientManagementApi.md +575 -0
  15. data/docs/ClientSummary.md +38 -0
  16. data/docs/ClientUpdateRequest.md +22 -0
  17. data/docs/DownloadsApi.md +0 -144
  18. data/docs/EncaisseeRequest.md +36 -0
  19. data/docs/FactureElectroniqueRestApiSchemasCdarValidationErrorResponse.md +24 -0
  20. data/docs/FactureElectroniqueRestApiSchemasProcessingChorusProCredentials.md +26 -0
  21. data/docs/GetChorusProIdRequest.md +1 -1
  22. data/docs/GetInvoiceRequest.md +1 -1
  23. data/docs/GetStructureRequest.md +1 -1
  24. data/docs/PDPConfigResponse.md +44 -0
  25. data/docs/PDPConfigUpdateRequest.md +28 -0
  26. data/docs/PaymentAmountByRate.md +1 -1
  27. data/docs/Recipient.md +1 -1
  28. data/docs/RefuseeRequest.md +36 -0
  29. data/docs/SearchStructureRequest.md +1 -1
  30. data/docs/SecretStatus.md +20 -0
  31. data/docs/SimplifiedCDARResponse.md +26 -0
  32. data/docs/SubmitCDARRequest.md +9 -1
  33. data/docs/SubmitCDARXMLRequest.md +9 -1
  34. data/docs/SubmitCompleteInvoiceResponse.md +2 -2
  35. data/docs/SubmitInvoiceRequest.md +1 -1
  36. data/docs/Supplier.md +1 -1
  37. data/docs/ValidateCDARResponse.md +2 -2
  38. data/docs/ValidationErrorResponse.md +2 -8
  39. data/factpulse.gemspec +1 -1
  40. data/lib/factpulse/api/afnorpdppa_api.rb +1 -1
  41. data/lib/factpulse/api/afnorpdppa_directory_service_api.rb +1 -1
  42. data/lib/factpulse/api/afnorpdppa_flow_service_api.rb +1 -1
  43. data/lib/factpulse/api/cdar_cycle_de_vie_api.rb +159 -49
  44. data/lib/factpulse/api/chorus_pro_api.rb +1 -1
  45. data/lib/factpulse/api/client_management_api.rb +565 -0
  46. data/lib/factpulse/api/document_conversion_api.rb +1 -1
  47. data/lib/factpulse/api/downloads_api.rb +1 -130
  48. data/lib/factpulse/api/e_reporting_api.rb +1 -1
  49. data/lib/factpulse/api/health_api.rb +1 -1
  50. data/lib/factpulse/api/invoice_processing_api.rb +1 -1
  51. data/lib/factpulse/api/pdfxml_verification_api.rb +1 -1
  52. data/lib/factpulse/api/user_api.rb +1 -1
  53. data/lib/factpulse/api_client.rb +1 -1
  54. data/lib/factpulse/api_error.rb +1 -1
  55. data/lib/factpulse/api_model_base.rb +1 -1
  56. data/lib/factpulse/configuration.rb +1 -1
  57. data/lib/factpulse/helpers/client.rb +199 -754
  58. data/lib/factpulse/helpers/exceptions.rb +38 -14
  59. data/lib/factpulse/helpers/helpers.rb +8 -7
  60. data/lib/factpulse/models/acknowledgment_status.rb +1 -1
  61. data/lib/factpulse/models/action_code_info.rb +1 -1
  62. data/lib/factpulse/models/action_codes_response.rb +1 -1
  63. data/lib/factpulse/models/additional_document.rb +1 -1
  64. data/lib/factpulse/models/afnor_acknowledgement.rb +1 -1
  65. data/lib/factpulse/models/afnor_acknowledgement_detail.rb +1 -1
  66. data/lib/factpulse/models/afnor_address_edit.rb +1 -1
  67. data/lib/factpulse/models/afnor_address_patch.rb +1 -1
  68. data/lib/factpulse/models/afnor_address_put.rb +1 -1
  69. data/lib/factpulse/models/afnor_address_read.rb +1 -1
  70. data/lib/factpulse/models/afnor_algorithm.rb +1 -1
  71. data/lib/factpulse/models/afnor_contains_operator.rb +1 -1
  72. data/lib/factpulse/models/afnor_create_directory_line_body.rb +1 -1
  73. data/lib/factpulse/models/afnor_create_directory_line_body_addressing_information.rb +1 -1
  74. data/lib/factpulse/models/afnor_create_directory_line_body_period.rb +1 -1
  75. data/lib/factpulse/models/afnor_create_routing_code_body.rb +1 -1
  76. data/lib/factpulse/models/afnor_credentials.rb +1 -1
  77. data/lib/factpulse/models/afnor_destination.rb +1 -1
  78. data/lib/factpulse/models/afnor_diffusion_status.rb +1 -1
  79. data/lib/factpulse/models/afnor_directory_line_field.rb +1 -1
  80. data/lib/factpulse/models/afnor_directory_line_payload_history_legal_unit_facility_routing_code.rb +1 -1
  81. data/lib/factpulse/models/afnor_directory_line_payload_history_legal_unit_facility_routing_code_platform.rb +1 -1
  82. data/lib/factpulse/models/afnor_directory_line_payload_history_legal_unit_facility_routing_code_routing_code.rb +1 -1
  83. data/lib/factpulse/models/afnor_directory_line_post201_response.rb +1 -1
  84. data/lib/factpulse/models/afnor_directory_line_search_post200_response.rb +1 -1
  85. data/lib/factpulse/models/afnor_entity_type.rb +1 -1
  86. data/lib/factpulse/models/afnor_error.rb +1 -1
  87. data/lib/factpulse/models/afnor_facility_administrative_status.rb +1 -1
  88. data/lib/factpulse/models/afnor_facility_nature.rb +1 -1
  89. data/lib/factpulse/models/afnor_facility_payload_history.rb +1 -1
  90. data/lib/factpulse/models/afnor_facility_payload_history_ule_b2g_additional_data.rb +1 -1
  91. data/lib/factpulse/models/afnor_facility_payload_included.rb +1 -1
  92. data/lib/factpulse/models/afnor_facility_type.rb +1 -1
  93. data/lib/factpulse/models/afnor_flow.rb +1 -1
  94. data/lib/factpulse/models/afnor_flow_ack_status.rb +1 -1
  95. data/lib/factpulse/models/afnor_flow_direction.rb +1 -1
  96. data/lib/factpulse/models/afnor_flow_info.rb +1 -1
  97. data/lib/factpulse/models/afnor_flow_profile.rb +1 -1
  98. data/lib/factpulse/models/afnor_flow_syntax.rb +1 -1
  99. data/lib/factpulse/models/afnor_flow_type.rb +1 -1
  100. data/lib/factpulse/models/afnor_full_flow_info.rb +1 -1
  101. data/lib/factpulse/models/afnor_health_check_response.rb +1 -1
  102. data/lib/factpulse/models/afnor_legal_unit_administrative_status.rb +1 -1
  103. data/lib/factpulse/models/afnor_legal_unit_payload_history.rb +1 -1
  104. data/lib/factpulse/models/afnor_legal_unit_payload_included.rb +1 -1
  105. data/lib/factpulse/models/afnor_legal_unit_payload_included_no_siren.rb +1 -1
  106. data/lib/factpulse/models/afnor_platform_status.rb +1 -1
  107. data/lib/factpulse/models/afnor_processing_rule.rb +1 -1
  108. data/lib/factpulse/models/afnor_reason_code.rb +1 -1
  109. data/lib/factpulse/models/afnor_reason_code_enum.rb +1 -1
  110. data/lib/factpulse/models/afnor_recipient_platform_type.rb +1 -1
  111. data/lib/factpulse/models/afnor_result.rb +1 -1
  112. data/lib/factpulse/models/afnor_routing_code_administrative_status.rb +1 -1
  113. data/lib/factpulse/models/afnor_routing_code_field.rb +1 -1
  114. data/lib/factpulse/models/afnor_routing_code_payload_history_legal_unit_facility.rb +1 -1
  115. data/lib/factpulse/models/afnor_routing_code_post201_response.rb +1 -1
  116. data/lib/factpulse/models/afnor_routing_code_search.rb +1 -1
  117. data/lib/factpulse/models/afnor_routing_code_search_filters.rb +1 -1
  118. data/lib/factpulse/models/afnor_routing_code_search_filters_administrative_status.rb +1 -1
  119. data/lib/factpulse/models/afnor_routing_code_search_filters_routing_code_name.rb +1 -1
  120. data/lib/factpulse/models/afnor_routing_code_search_filters_routing_identifier.rb +1 -1
  121. data/lib/factpulse/models/afnor_routing_code_search_post200_response.rb +1 -1
  122. data/lib/factpulse/models/afnor_routing_code_search_sorting_inner.rb +1 -1
  123. data/lib/factpulse/models/afnor_search_directory_line.rb +1 -1
  124. data/lib/factpulse/models/afnor_search_directory_line_filters.rb +1 -1
  125. data/lib/factpulse/models/afnor_search_directory_line_filters_addressing_identifier.rb +1 -1
  126. data/lib/factpulse/models/afnor_search_directory_line_filters_addressing_suffix.rb +1 -1
  127. data/lib/factpulse/models/afnor_search_directory_line_sorting_inner.rb +1 -1
  128. data/lib/factpulse/models/afnor_search_flow_content.rb +1 -1
  129. data/lib/factpulse/models/afnor_search_flow_filters.rb +1 -1
  130. data/lib/factpulse/models/afnor_search_flow_params.rb +1 -1
  131. data/lib/factpulse/models/afnor_search_siren.rb +1 -1
  132. data/lib/factpulse/models/afnor_search_siren_filters.rb +1 -1
  133. data/lib/factpulse/models/afnor_search_siren_filters_administrative_status.rb +1 -1
  134. data/lib/factpulse/models/afnor_search_siren_filters_business_name.rb +1 -1
  135. data/lib/factpulse/models/afnor_search_siren_filters_entity_type.rb +1 -1
  136. data/lib/factpulse/models/afnor_search_siren_filters_siren.rb +1 -1
  137. data/lib/factpulse/models/afnor_search_siren_sorting_inner.rb +1 -1
  138. data/lib/factpulse/models/afnor_search_siret.rb +1 -1
  139. data/lib/factpulse/models/afnor_search_siret_filters.rb +1 -1
  140. data/lib/factpulse/models/afnor_search_siret_filters_address_lines.rb +1 -1
  141. data/lib/factpulse/models/afnor_search_siret_filters_administrative_status.rb +1 -1
  142. data/lib/factpulse/models/afnor_search_siret_filters_country_subdivision.rb +1 -1
  143. data/lib/factpulse/models/afnor_search_siret_filters_facility_type.rb +1 -1
  144. data/lib/factpulse/models/afnor_search_siret_filters_locality.rb +1 -1
  145. data/lib/factpulse/models/afnor_search_siret_filters_name.rb +1 -1
  146. data/lib/factpulse/models/afnor_search_siret_filters_postal_code.rb +1 -1
  147. data/lib/factpulse/models/afnor_search_siret_filters_siret.rb +1 -1
  148. data/lib/factpulse/models/afnor_search_siret_sorting_inner.rb +1 -1
  149. data/lib/factpulse/models/afnor_siren_field.rb +1 -1
  150. data/lib/factpulse/models/afnor_siren_search_post200_response.rb +1 -1
  151. data/lib/factpulse/models/afnor_siret_field.rb +1 -1
  152. data/lib/factpulse/models/afnor_siret_search_post200_response.rb +1 -1
  153. data/lib/factpulse/models/afnor_sorting_order.rb +1 -1
  154. data/lib/factpulse/models/afnor_strict_operator.rb +1 -1
  155. data/lib/factpulse/models/afnor_update_patch_directory_line_body.rb +1 -1
  156. data/lib/factpulse/models/afnor_update_patch_routing_code_body.rb +1 -1
  157. data/lib/factpulse/models/afnor_update_put_routing_code_body.rb +1 -1
  158. data/lib/factpulse/models/afnor_webhook_callback_content.rb +1 -1
  159. data/lib/factpulse/models/aggregated_payment_input.rb +1 -1
  160. data/lib/factpulse/models/aggregated_transaction_input.rb +1 -1
  161. data/lib/factpulse/models/allowance_charge.rb +2 -2
  162. data/lib/factpulse/models/allowance_charge_reason_code.rb +1 -1
  163. data/lib/factpulse/models/allowance_reason_code.rb +1 -1
  164. data/lib/factpulse/models/allowance_total_amount.rb +1 -1
  165. data/lib/factpulse/models/amount.rb +2 -2
  166. data/lib/factpulse/models/amount1.rb +2 -2
  167. data/lib/factpulse/models/amount2.rb +104 -0
  168. data/lib/factpulse/models/amount_due.rb +1 -1
  169. data/lib/factpulse/models/api_error.rb +1 -1
  170. data/lib/factpulse/models/api_profile.rb +1 -1
  171. data/lib/factpulse/models/async_task_status.rb +1 -1
  172. data/lib/factpulse/models/base_amount.rb +1 -1
  173. data/lib/factpulse/models/bounding_box_schema.rb +1 -1
  174. data/lib/factpulse/models/buyercountry.rb +1 -1
  175. data/lib/factpulse/models/celery_status.rb +1 -1
  176. data/lib/factpulse/models/certificate_info_response.rb +1 -1
  177. data/lib/factpulse/models/charge_total_amount.rb +1 -1
  178. data/lib/factpulse/models/chorus_pro_credentials.rb +95 -27
  179. data/lib/factpulse/models/chorus_pro_destination.rb +2 -2
  180. data/lib/factpulse/models/chorus_pro_result.rb +1 -1
  181. data/lib/factpulse/models/{facture_electronique_rest_api_schemas_validation_validation_error_response.rb → client_activate_response.rb} +76 -24
  182. data/lib/factpulse/models/client_create_request.rb +236 -0
  183. data/lib/factpulse/models/client_detail.rb +368 -0
  184. data/lib/factpulse/models/client_list_response.rb +249 -0
  185. data/lib/factpulse/models/client_summary.rb +368 -0
  186. data/lib/factpulse/models/client_update_request.rb +225 -0
  187. data/lib/factpulse/models/contact.rb +1 -1
  188. data/lib/factpulse/models/convert_resume_request.rb +1 -1
  189. data/lib/factpulse/models/convert_success_response.rb +1 -1
  190. data/lib/factpulse/models/convert_validation_failed_response.rb +1 -1
  191. data/lib/factpulse/models/country_code.rb +1 -1
  192. data/lib/factpulse/models/create_aggregated_report_request.rb +1 -1
  193. data/lib/factpulse/models/create_cdar_request.rb +1 -1
  194. data/lib/factpulse/models/create_e_reporting_request.rb +1 -1
  195. data/lib/factpulse/models/currency.rb +1 -1
  196. data/lib/factpulse/models/currency_code.rb +1 -1
  197. data/lib/factpulse/models/delivery_party.rb +1 -1
  198. data/lib/factpulse/models/destination.rb +1 -1
  199. data/lib/factpulse/models/doc_type.rb +1 -1
  200. data/lib/factpulse/models/document_type_info.rb +1 -1
  201. data/lib/factpulse/models/e_reporting_flow_type.rb +1 -1
  202. data/lib/factpulse/models/e_reporting_validation_error.rb +1 -1
  203. data/lib/factpulse/models/electronic_address.rb +1 -1
  204. data/lib/factpulse/models/encaisseamount.rb +1 -1
  205. data/lib/factpulse/models/encaisseamount1.rb +1 -1
  206. data/lib/factpulse/models/encaissee_request.rb +293 -0
  207. data/lib/factpulse/models/enriched_invoice_info.rb +1 -1
  208. data/lib/factpulse/models/error_level.rb +1 -1
  209. data/lib/factpulse/models/error_source.rb +1 -1
  210. data/lib/factpulse/models/extraction_info.rb +1 -1
  211. data/lib/factpulse/models/factur_x_invoice.rb +1 -1
  212. data/lib/factpulse/models/factur_xpdf_info.rb +1 -1
  213. data/lib/factpulse/models/{body_submit_cdar_xml_api_v1_cdar_submit_xml_post.rb → facture_electronique_rest_api_schemas_cdar_validation_error_response.rb} +68 -27
  214. data/lib/factpulse/models/facture_electronique_rest_api_schemas_ereporting_invoice_type_code.rb +1 -1
  215. data/lib/factpulse/models/{facture_electronique_rest_api_schemas_chorus_pro_chorus_pro_credentials.rb → facture_electronique_rest_api_schemas_processing_chorus_pro_credentials.rb} +30 -98
  216. data/lib/factpulse/models/field_status.rb +1 -1
  217. data/lib/factpulse/models/file_info.rb +1 -1
  218. data/lib/factpulse/models/files_info.rb +1 -1
  219. data/lib/factpulse/models/flow_direction.rb +1 -1
  220. data/lib/factpulse/models/flow_profile.rb +1 -1
  221. data/lib/factpulse/models/flow_summary.rb +1 -1
  222. data/lib/factpulse/models/flow_syntax.rb +1 -1
  223. data/lib/factpulse/models/flow_type.rb +1 -1
  224. data/lib/factpulse/models/generate_aggregated_report_response.rb +1 -1
  225. data/lib/factpulse/models/generate_cdar_response.rb +1 -1
  226. data/lib/factpulse/models/generate_certificate_request.rb +1 -1
  227. data/lib/factpulse/models/generate_certificate_response.rb +1 -1
  228. data/lib/factpulse/models/generate_e_reporting_response.rb +1 -1
  229. data/lib/factpulse/models/get_chorus_pro_id_request.rb +2 -2
  230. data/lib/factpulse/models/get_chorus_pro_id_response.rb +1 -1
  231. data/lib/factpulse/models/get_invoice_request.rb +2 -2
  232. data/lib/factpulse/models/get_invoice_response.rb +1 -1
  233. data/lib/factpulse/models/get_structure_request.rb +2 -2
  234. data/lib/factpulse/models/get_structure_response.rb +1 -1
  235. data/lib/factpulse/models/global_allowance_amount.rb +1 -1
  236. data/lib/factpulse/models/gross_unit_price.rb +1 -1
  237. data/lib/factpulse/models/http_validation_error.rb +1 -1
  238. data/lib/factpulse/models/incoming_invoice.rb +1 -1
  239. data/lib/factpulse/models/incoming_supplier.rb +1 -1
  240. data/lib/factpulse/models/invoice_format.rb +1 -1
  241. data/lib/factpulse/models/invoice_input.rb +1 -1
  242. data/lib/factpulse/models/invoice_line.rb +1 -1
  243. data/lib/factpulse/models/invoice_line_allowance_amount.rb +1 -1
  244. data/lib/factpulse/models/invoice_note.rb +1 -1
  245. data/lib/factpulse/models/invoice_payment_input.rb +1 -1
  246. data/lib/factpulse/models/invoice_references.rb +1 -1
  247. data/lib/factpulse/models/invoice_status.rb +1 -1
  248. data/lib/factpulse/models/invoice_totals.rb +1 -1
  249. data/lib/factpulse/models/invoice_totals_prepayment.rb +1 -1
  250. data/lib/factpulse/models/invoice_type_code.rb +1 -1
  251. data/lib/factpulse/models/invoice_type_code_output.rb +1 -1
  252. data/lib/factpulse/models/invoicing_framework.rb +1 -1
  253. data/lib/factpulse/models/invoicing_framework_code.rb +1 -1
  254. data/lib/factpulse/models/line_net_amount.rb +1 -1
  255. data/lib/factpulse/models/line_sub_type.rb +1 -1
  256. data/lib/factpulse/models/line_total_amount.rb +1 -1
  257. data/lib/factpulse/models/location_inner.rb +1 -1
  258. data/lib/factpulse/models/mandatory_note_schema.rb +1 -1
  259. data/lib/factpulse/models/manual_rate.rb +1 -1
  260. data/lib/factpulse/models/manual_vat_rate.rb +1 -1
  261. data/lib/factpulse/models/missing_field.rb +1 -1
  262. data/lib/factpulse/models/operation_nature.rb +1 -1
  263. data/lib/factpulse/models/output_format.rb +1 -1
  264. data/lib/factpulse/models/page_dimensions_schema.rb +1 -1
  265. data/lib/factpulse/models/payee.rb +1 -1
  266. data/lib/factpulse/models/payment_amount_by_rate.rb +2 -2
  267. data/lib/factpulse/models/payment_card.rb +1 -1
  268. data/lib/factpulse/models/payment_means.rb +1 -1
  269. data/lib/factpulse/models/pdf_validation_result_api.rb +1 -1
  270. data/lib/factpulse/models/pdp_config_response.rb +296 -0
  271. data/lib/factpulse/models/pdp_config_update_request.rb +271 -0
  272. data/lib/factpulse/models/pdp_credentials.rb +1 -1
  273. data/lib/factpulse/models/percentage.rb +1 -1
  274. data/lib/factpulse/models/postal_address.rb +1 -1
  275. data/lib/factpulse/models/price_allowance_amount.rb +1 -1
  276. data/lib/factpulse/models/price_basis_quantity.rb +1 -1
  277. data/lib/factpulse/models/processing_options.rb +1 -1
  278. data/lib/factpulse/models/processing_rule.rb +1 -1
  279. data/lib/factpulse/models/product_characteristic.rb +1 -1
  280. data/lib/factpulse/models/product_classification.rb +1 -1
  281. data/lib/factpulse/models/quantity.rb +1 -1
  282. data/lib/factpulse/models/rate.rb +1 -1
  283. data/lib/factpulse/models/rate1.rb +1 -1
  284. data/lib/factpulse/models/reason_code_info.rb +1 -1
  285. data/lib/factpulse/models/reason_codes_response.rb +1 -1
  286. data/lib/factpulse/models/recipient.rb +1 -3
  287. data/lib/factpulse/models/recipient_input.rb +1 -1
  288. data/lib/factpulse/models/refusee_request.rb +292 -0
  289. data/lib/factpulse/models/report_period.rb +1 -1
  290. data/lib/factpulse/models/report_sender.rb +1 -1
  291. data/lib/factpulse/models/rounding_amount.rb +1 -1
  292. data/lib/factpulse/models/schematron_validation_error.rb +1 -1
  293. data/lib/factpulse/models/scheme_id.rb +4 -4
  294. data/lib/factpulse/models/search_flow_request.rb +1 -1
  295. data/lib/factpulse/models/search_flow_response.rb +1 -1
  296. data/lib/factpulse/models/search_services_response.rb +1 -1
  297. data/lib/factpulse/models/search_structure_request.rb +2 -2
  298. data/lib/factpulse/models/search_structure_response.rb +1 -1
  299. data/lib/factpulse/models/{body_submit_cdar_api_v1_cdar_submit_post.rb → secret_status.rb} +46 -27
  300. data/lib/factpulse/models/sellercountry.rb +1 -1
  301. data/lib/factpulse/models/signature_info.rb +1 -1
  302. data/lib/factpulse/models/signature_info_api.rb +1 -1
  303. data/lib/factpulse/models/signature_parameters.rb +1 -1
  304. data/lib/factpulse/models/simplified_cdar_response.rb +274 -0
  305. data/lib/factpulse/models/simplified_invoice_data.rb +1 -1
  306. data/lib/factpulse/models/status_code_info.rb +1 -1
  307. data/lib/factpulse/models/status_codes_response.rb +1 -1
  308. data/lib/factpulse/models/structure_info.rb +1 -1
  309. data/lib/factpulse/models/structure_parameters.rb +1 -1
  310. data/lib/factpulse/models/structure_service.rb +1 -1
  311. data/lib/factpulse/models/submission_mode.rb +1 -1
  312. data/lib/factpulse/models/submit_aggregated_report_request.rb +1 -1
  313. data/lib/factpulse/models/submit_cdar_request.rb +45 -5
  314. data/lib/factpulse/models/submit_cdar_response.rb +1 -1
  315. data/lib/factpulse/models/submit_cdarxml_request.rb +46 -6
  316. data/lib/factpulse/models/submit_complete_invoice_request.rb +1 -1
  317. data/lib/factpulse/models/submit_complete_invoice_response.rb +17 -17
  318. data/lib/factpulse/models/submit_e_reporting_request.rb +1 -1
  319. data/lib/factpulse/models/submit_e_reporting_response.rb +1 -1
  320. data/lib/factpulse/models/submit_flow_request.rb +1 -1
  321. data/lib/factpulse/models/submit_flow_response.rb +1 -1
  322. data/lib/factpulse/models/submit_gross_amount.rb +1 -1
  323. data/lib/factpulse/models/submit_invoice_request.rb +2 -2
  324. data/lib/factpulse/models/submit_invoice_response.rb +1 -1
  325. data/lib/factpulse/models/submit_net_amount.rb +1 -1
  326. data/lib/factpulse/models/submit_vat_amount.rb +1 -1
  327. data/lib/factpulse/models/supplementary_attachment.rb +1 -1
  328. data/lib/factpulse/models/supplier.rb +1 -3
  329. data/lib/factpulse/models/task_response.rb +1 -1
  330. data/lib/factpulse/models/tax_breakdown_input.rb +1 -1
  331. data/lib/factpulse/models/tax_due_date_type.rb +1 -1
  332. data/lib/factpulse/models/tax_representative.rb +1 -1
  333. data/lib/factpulse/models/taxable_amount.rb +1 -1
  334. data/lib/factpulse/models/taxableamount.rb +1 -1
  335. data/lib/factpulse/models/taxamount.rb +1 -1
  336. data/lib/factpulse/models/taxamount1.rb +1 -1
  337. data/lib/factpulse/models/taxamount2.rb +1 -1
  338. data/lib/factpulse/models/taxexclusiveamount.rb +1 -1
  339. data/lib/factpulse/models/taxexclusiveamount1.rb +1 -1
  340. data/lib/factpulse/models/total_gross_amount.rb +1 -1
  341. data/lib/factpulse/models/total_net_amount.rb +1 -1
  342. data/lib/factpulse/models/total_vat_amount.rb +1 -1
  343. data/lib/factpulse/models/transaction_category.rb +1 -1
  344. data/lib/factpulse/models/transmission_type_code.rb +1 -1
  345. data/lib/factpulse/models/unit_net_price.rb +1 -1
  346. data/lib/factpulse/models/unit_of_measure.rb +1 -1
  347. data/lib/factpulse/models/validate_cdar_request.rb +1 -1
  348. data/lib/factpulse/models/validate_cdar_response.rb +3 -3
  349. data/lib/factpulse/models/validate_e_reporting_request.rb +1 -1
  350. data/lib/factpulse/models/validate_e_reporting_response.rb +1 -1
  351. data/lib/factpulse/models/validation_error.rb +1 -1
  352. data/lib/factpulse/models/validation_error_detail.rb +1 -1
  353. data/lib/factpulse/models/validation_error_response.rb +21 -68
  354. data/lib/factpulse/models/validation_info.rb +1 -1
  355. data/lib/factpulse/models/validation_success_response.rb +1 -1
  356. data/lib/factpulse/models/vat_accounting_code.rb +1 -1
  357. data/lib/factpulse/models/vat_amount.rb +1 -1
  358. data/lib/factpulse/models/vat_category.rb +1 -1
  359. data/lib/factpulse/models/vat_line.rb +1 -1
  360. data/lib/factpulse/models/vat_point_date_code.rb +1 -1
  361. data/lib/factpulse/models/vat_rate.rb +1 -1
  362. data/lib/factpulse/models/verification_success_response.rb +1 -1
  363. data/lib/factpulse/models/verified_field_schema.rb +1 -1
  364. data/lib/factpulse/version.rb +2 -2
  365. data/lib/factpulse.rb +17 -5
  366. metadata +34 -10
  367. data/docs/BodySubmitCdarApiV1CdarSubmitPost.md +0 -20
  368. data/docs/BodySubmitCdarXmlApiV1CdarSubmitXmlPost.md +0 -20
  369. data/docs/FactureElectroniqueRestApiSchemasChorusProChorusProCredentials.md +0 -26
  370. data/docs/FactureElectroniqueRestApiSchemasValidationValidationErrorResponse.md +0 -18
@@ -1,826 +1,271 @@
1
1
  # frozen_string_literal: true
2
- require 'net/http'; require 'json'; require 'base64'; require 'uri'; require 'securerandom'; require 'digest'; require 'tempfile'
2
+ #
3
+ # FactPulse SDK - Thin HTTP wrapper with auto-polling.
4
+ #
5
+ # Usage:
6
+ # client = FactPulseClient.new('email', 'password', 'client_uid')
7
+ #
8
+ # # POST /api/v1/processing/invoices/submit-complete-async
9
+ # result = client.post('processing/invoices/submit-complete-async',
10
+ # invoiceData: {...},
11
+ # destination: { type: 'afnor' }
12
+ # )
13
+ # pdf_bytes = result['content'] # auto-decoded, auto-polled
14
+
15
+ require 'net/http'
16
+ require 'json'
17
+ require 'base64'
18
+ require 'uri'
3
19
 
4
20
  module FactPulse
5
- module Helpers
6
- # Chorus Pro credentials for Zero-Trust mode.
7
- # These credentials are passed in each request and are never stored server-side.
8
- class ChorusProCredentials
9
- attr_reader :piste_client_id, :piste_client_secret, :chorus_pro_login, :chorus_pro_password, :sandbox
10
- def initialize(piste_client_id:, piste_client_secret:, chorus_pro_login:, chorus_pro_password:, sandbox: true)
11
- @piste_client_id, @piste_client_secret = piste_client_id, piste_client_secret
12
- @chorus_pro_login, @chorus_pro_password, @sandbox = chorus_pro_login, chorus_pro_password, sandbox
13
- end
14
- def to_h
15
- { 'piste_client_id' => @piste_client_id, 'piste_client_secret' => @piste_client_secret,
16
- 'chorus_pro_login' => @chorus_pro_login, 'chorus_pro_password' => @chorus_pro_password, 'sandbox' => @sandbox }
17
- end
21
+ class Error < StandardError
22
+ attr_reader :status_code, :details
23
+
24
+ def initialize(message, status_code: nil, details: [])
25
+ super(message)
26
+ @status_code = status_code
27
+ @details = details
18
28
  end
29
+ end
19
30
 
20
- # AFNOR PDP credentials for Zero-Trust mode.
21
- # The FactPulse API uses these credentials to authenticate with the AFNOR PDP.
22
- class AFNORCredentials
23
- attr_reader :flow_service_url, :token_url, :client_id, :client_secret, :directory_service_url
24
- def initialize(flow_service_url:, token_url:, client_id:, client_secret:, directory_service_url: nil)
25
- @flow_service_url, @token_url = flow_service_url, token_url
26
- @client_id, @client_secret, @directory_service_url = client_id, client_secret, directory_service_url
27
- end
28
- def to_h
29
- result = { 'flow_service_url' => @flow_service_url, 'token_url' => @token_url,
30
- 'client_id' => @client_id, 'client_secret' => @client_secret }
31
- result['directory_service_url'] = @directory_service_url if @directory_service_url
32
- result
33
- end
31
+ class Client
32
+ DEFAULT_API_URL = 'https://factpulse.fr'
33
+
34
+ def initialize(email, password, client_uid, api_url: DEFAULT_API_URL, timeout: 60, polling_timeout: 120)
35
+ @email = email
36
+ @password = password
37
+ @client_uid = client_uid
38
+ @api_url = api_url.chomp('/')
39
+ @timeout = timeout
40
+ @polling_timeout = polling_timeout
41
+ @token = nil
42
+ @token_expires_at = 0
43
+ @token_mutex = Mutex.new
34
44
  end
35
45
 
36
- # Helpers for creating simplified total amounts.
37
- module AmountHelpers
38
- def self.amount(value)
39
- return '0.00' if value.nil?
40
- return format('%.2f', value) if value.is_a?(Numeric)
41
- value.is_a?(String) ? value : '0.00'
42
- end
46
+ # POST request to /api/v1/{path} (JSON body)
47
+ def post(path, **data)
48
+ request('POST', path, data, retry_auth: true)
49
+ end
43
50
 
44
- def self.invoice_totals(excl_tax, vat, incl_tax, amount_due, discount_incl_tax: nil, discount_reason: nil, prepayment: nil)
45
- result = {
46
- 'totalExclTax' => amount(excl_tax), 'vatAmount' => amount(vat),
47
- 'totalInclTax' => amount(incl_tax), 'amountDue' => amount(amount_due)
48
- }
49
- result['globalDiscountInclTax'] = amount(discount_incl_tax) if discount_incl_tax
50
- result['globalDiscountReason'] = discount_reason if discount_reason
51
- result['prepayment'] = amount(prepayment) if prepayment
52
- result
53
- end
51
+ # POST request with multipart/form-data (for file uploads)
52
+ def post_multipart(path, data: {}, files: {})
53
+ request_multipart(path, data, files, retry_auth: true)
54
+ end
54
55
 
55
- # Creates an invoice line (aligned with InvoiceLine in models.py).
56
- def self.invoice_line(number, description, quantity, unit_price_excl_tax, line_total_excl_tax,
57
- vat_rate: '20.00', vat_category: 'S', unit: 'LUMP_SUM', **options)
58
- result = {
59
- 'number' => number, 'description' => description,
60
- 'quantity' => amount(quantity), 'unitPriceExclTax' => amount(unit_price_excl_tax),
61
- 'lineTotalExclTax' => amount(line_total_excl_tax), 'vatRateManual' => amount(vat_rate),
62
- 'vatCategory' => vat_category, 'unit' => unit
63
- }
64
- result['reference'] = options[:reference] if options[:reference]
65
- result['discountExclTax'] = amount(options[:discount_excl_tax]) if options[:discount_excl_tax]
66
- result['discountReasonCode'] = options[:discount_reason_code] if options[:discount_reason_code]
67
- result['discountReason'] = options[:discount_reason] if options[:discount_reason]
68
- result['periodStartDate'] = options[:period_start_date] if options[:period_start_date]
69
- result['periodEndDate'] = options[:period_end_date] if options[:period_end_date]
70
- result
71
- end
56
+ # GET request to /api/v1/{path}
57
+ def get(path, **params)
58
+ request('GET', path, params, retry_auth: true)
59
+ end
72
60
 
73
- # Creates a VAT line (aligned with VatLine in models.py).
74
- def self.vat_line(rate_manual, base_amount_excl_tax, vat_amount, category: 'S')
75
- {
76
- 'rateManual' => amount(rate_manual), 'baseAmountExclTax' => amount(base_amount_excl_tax),
77
- 'vatAmount' => amount(vat_amount), 'category' => category
78
- }
79
- end
61
+ private
80
62
 
81
- # Creates a postal address for the FactPulse API.
82
- def self.postal_address(line1, postal_code, city, country: 'FR', line2: nil, line3: nil)
83
- result = { 'line1' => line1, 'postalCode' => postal_code, 'city' => city, 'countryCode' => country }
84
- result['line2'] = line2 if line2
85
- result['line3'] = line3 if line3
86
- result
87
- end
63
+ def request(method, path, data, retry_auth:)
64
+ ensure_auth
65
+ url = "#{@api_url}/api/v1/#{path}"
66
+ uri = URI(url)
88
67
 
89
- # Creates an electronic address. scheme_id: "0009"=SIREN, "0225"=SIRET
90
- def self.electronic_address(identifier, scheme_id: '0009')
91
- { 'identifier' => identifier, 'schemeId' => scheme_id }
92
- end
68
+ http = Net::HTTP.new(uri.host, uri.port)
69
+ http.use_ssl = uri.scheme == 'https'
70
+ http.read_timeout = @timeout
93
71
 
94
- # Computes the French intra-community VAT number from a SIREN.
95
- def self.compute_vat_intra(siren)
96
- return nil if siren.nil? || siren.length != 9 || !siren.match?(/^\d+$/)
97
- cle = (12 + 3 * (siren.to_i % 97)) % 97
98
- format('FR%02d%s', cle, siren)
72
+ if method == 'POST'
73
+ req = Net::HTTP::Post.new(uri)
74
+ req['Content-Type'] = 'application/json'
75
+ req.body = JSON.generate(data)
76
+ else
77
+ uri.query = URI.encode_www_form(data) unless data.empty?
78
+ req = Net::HTTP::Get.new(uri)
99
79
  end
80
+ req['Authorization'] = "Bearer #{@token}"
100
81
 
101
- # Creates a supplier (issuer) with auto-computed SIREN, intra-EU VAT number and addresses.
102
- def self.supplier(name, siret, address_line1, postal_code, city, **options)
103
- siren = options[:siren] || (siret.length == 14 ? siret[0, 9] : nil)
104
- vat_intra = options[:vat_intra] || (siren ? compute_vat_intra(siren) : nil)
105
- result = {
106
- 'name' => name, 'supplierId' => options[:supplier_id] || 0, 'siret' => siret,
107
- 'electronicAddress' => electronic_address(siret, scheme_id: '0225'),
108
- 'postalAddress' => postal_address(address_line1, postal_code, city, country: options[:country] || 'FR', line2: options[:address_line2])
109
- }
110
- result['siren'] = siren if siren
111
- result['vatIntra'] = vat_intra if vat_intra
112
- result['iban'] = options[:iban] if options[:iban]
113
- result['supplierServiceId'] = options[:service_code] if options[:service_code]
114
- result['supplierBankCoordinatesCode'] = options[:bank_coordinates_code] if options[:bank_coordinates_code]
115
- result
116
- end
82
+ response = http.request(req)
117
83
 
118
- # Creates a recipient (customer) with auto-computed SIREN and addresses.
119
- def self.recipient(name, siret, address_line1, postal_code, city, **options)
120
- siren = options[:siren] || (siret.length == 14 ? siret[0, 9] : nil)
121
- result = {
122
- 'name' => name, 'siret' => siret,
123
- 'electronicAddress' => electronic_address(siret, scheme_id: '0225'),
124
- 'postalAddress' => postal_address(address_line1, postal_code, city, country: options[:country] || 'FR', line2: options[:address_line2])
125
- }
126
- result['siren'] = siren if siren
127
- result['executingServiceCode'] = options[:executing_service_code] if options[:executing_service_code]
128
- result
84
+ if response.code == '401' && retry_auth
85
+ invalidate_token
86
+ return request(method, path, data, retry_auth: false)
129
87
  end
130
88
 
131
- # Creates a beneficiary (factor) for factoring.
132
- #
133
- # The beneficiary (BG-10 / PayeeTradeParty) is used when payment
134
- # must be made to a third party different from the supplier, typically
135
- # a factor (factoring company).
136
- #
137
- # For factored invoices, you also need to:
138
- # - Use a factored document type (393, 396, 501, 502, 472, 473)
139
- # - Add an ACC note with the subrogation mention
140
- # - The beneficiary's IBAN will be used for payment
141
- #
142
- # @param name [String] Factor's business name (BT-59)
143
- # @param options [Hash] Options: :siret (BT-60), :siren (BT-61), :iban, :bic
144
- # @return [Hash] Dict ready to be used in a factored invoice
145
- #
146
- # @example
147
- # factor = beneficiary('FACTOR SAS',
148
- # siret: '30000000700033',
149
- # iban: 'FR76 3000 4000 0500 0012 3456 789'
150
- # )
151
- def self.beneficiary(name, **options)
152
- # Auto-compute SIREN from SIRET
153
- siret = options[:siret]
154
- siren = options[:siren] || (siret && siret.length == 14 ? siret[0, 9] : nil)
155
-
156
- result = { 'name' => name }
157
- result['siret'] = siret if siret
158
- result['siren'] = siren if siren
159
- result['iban'] = options[:iban] if options[:iban]
160
- result['bic'] = options[:bic] if options[:bic]
161
- result
162
- end
163
- end
89
+ result = parse_response(response)
164
90
 
165
- class FactPulseClient
166
- attr_reader :chorus_credentials, :afnor_credentials
167
-
168
- def initialize(email:, password:, api_url: nil, client_uid: nil, chorus_credentials: nil, afnor_credentials: nil,
169
- polling_interval: nil, polling_timeout: nil, max_retries: nil)
170
- @email, @password = email, password; @api_url = (api_url || 'https://factpulse.fr').chomp('/')
171
- @client_uid, @polling_interval, @polling_timeout, @max_retries = client_uid, polling_interval || 2000, polling_timeout || 120000, max_retries || 1
172
- @chorus_credentials, @afnor_credentials = chorus_credentials, afnor_credentials
173
- @access_token = @refresh_token = @token_expires_at = nil
91
+ # Auto-poll: support both taskId (camelCase) and task_id (snake_case)
92
+ if result.is_a?(Hash)
93
+ task_id = result['taskId'] || result['task_id']
94
+ result = poll(task_id) if task_id
174
95
  end
175
96
 
176
- def chorus_credentials_for_api; @chorus_credentials&.to_h; end
177
- def afnor_credentials_for_api; @afnor_credentials&.to_h; end
178
- # Shorter aliases
179
- def get_chorus_pro_credentials; chorus_credentials_for_api; end
180
- def get_afnor_credentials; afnor_credentials_for_api; end
181
-
182
- def ensure_authenticated(force_refresh: false)
183
- now = (Time.now.to_f * 1000).to_i
184
- if force_refresh || @access_token.nil? || (@token_expires_at && now >= @token_expires_at)
185
- payload = { 'username' => @email, 'password' => @password }; payload['client_uid'] = @client_uid if @client_uid
186
- response = http_post(URI("#{@api_url}/api/token/"), payload)
187
- raise FactPulseAuthError, "Auth failed" unless response.is_a?(Net::HTTPSuccess)
188
- tokens = JSON.parse(response.body); @access_token, @refresh_token = tokens['access'], tokens['refresh']
189
- @token_expires_at = now + (28 * 60 * 1000)
190
- end
97
+ # Auto-decode: support both content_b64 and contentB64
98
+ if result.is_a?(Hash)
99
+ b64_content = result.delete('content_b64') || result.delete('contentB64')
100
+ result['content'] = Base64.decode64(b64_content) if b64_content
191
101
  end
192
102
 
193
- def reset_auth; @access_token = @refresh_token = @token_expires_at = nil; end
194
-
195
- def poll_task(task_id, timeout: nil, interval: nil)
196
- timeout_ms, interval_ms = timeout || @polling_timeout, interval || @polling_interval
197
- start_time, current_interval = (Time.now.to_f * 1000).to_i, interval_ms.to_f
198
- loop do
199
- raise FactPulsePollingTimeout.new(task_id, timeout_ms) if (Time.now.to_f * 1000).to_i - start_time > timeout_ms
200
- ensure_authenticated; response = http_get(URI("#{@api_url}/api/v1/processing/tasks/#{task_id}/status"))
201
- reset_auth and next if response.code == '401'
202
- data = JSON.parse(response.body)
203
- return data['result'] || {} if data['status'] == 'SUCCESS'
204
- if data['status'] == 'FAILURE'
205
- # Format AFNOR: errorMessage, details
206
- r = data['result'] || {}
207
- raise FactPulseValidationError.new("Task #{task_id} failed: #{r['errorMessage'] || '?'}", (r['details'] || []).map { |e| ValidationErrorDetail.from_hash(e) })
208
- end
209
- sleep(current_interval / 1000.0); current_interval = [current_interval * 1.5, 10000].min
210
- end
211
- end
212
-
213
- def self.format_amount(m); AmountHelpers.amount(m); end
214
-
215
- # Generates a Factur-X invoice from a dict/hash and a source PDF.
216
- def generate_facturx(invoice_data, pdf_source, profile: 'EN16931', output_format: 'pdf', sync: true, timeout: nil)
217
- # Convert data to JSON string
218
- json_data = case invoice_data
219
- when String then invoice_data
220
- when Hash then JSON.generate(invoice_data)
221
- else
222
- if invoice_data.respond_to?(:to_h)
223
- JSON.generate(invoice_data.to_h)
224
- elsif invoice_data.respond_to?(:to_hash)
225
- JSON.generate(invoice_data.to_hash)
226
- else
227
- raise FactPulseValidationError.new("Unsupported data type: #{invoice_data.class}")
228
- end
229
- end
230
-
231
- # Read source PDF
232
- pdf_content = case pdf_source
233
- when String then File.binread(pdf_source)
234
- when File then pdf_source.read
235
- else
236
- if pdf_source.respond_to?(:read)
237
- pdf_source.read
238
- else
239
- raise FactPulseValidationError.new("Unsupported PDF type: #{pdf_source.class}")
240
- end
241
- end
242
- pdf_filename = pdf_source.is_a?(String) ? File.basename(pdf_source) : 'invoice.pdf'
243
-
244
- ensure_authenticated
245
- uri = URI("#{@api_url}/api/v1/processing/generate-invoice")
246
-
247
- # Build multipart request
248
- boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}"
249
- body = build_multipart_body(boundary, [
250
- { name: 'invoice_data', content: json_data, content_type: 'application/json' },
251
- { name: 'profile', content: profile },
252
- { name: 'output_format', content: output_format },
253
- { name: 'source_pdf', content: pdf_content, filename: pdf_filename, content_type: 'application/pdf' }
254
- ])
255
-
256
- response = http_multipart_post(uri, body, boundary)
257
-
258
- if response.code == '401'
259
- reset_auth; ensure_authenticated; response = http_multipart_post(uri, body, boundary)
260
- end
261
-
262
- unless response.is_a?(Net::HTTPSuccess)
263
- # Extract error details from response body
264
- error_msg = "API Error (#{response.code})"
265
- errors = []
266
-
267
- begin
268
- error_data = JSON.parse(response.body)
269
- # Format FastAPI/Pydantic: {"detail": [{"loc": [...], "msg": "...", "type": "..."}]}
270
- if error_data['detail'].is_a?(Array)
271
- error_msg = 'Validation error'
272
- error_data['detail'].each do |err|
273
- next unless err.is_a?(Hash)
274
- loc = (err['loc'] || []).map(&:to_s).join(' -> ')
275
- errors << ValidationErrorDetail.new(
276
- level: 'ERROR',
277
- item: loc,
278
- reason: err['msg'] || err.to_s,
279
- source: 'validation',
280
- code: err['type']
281
- )
282
- end
283
- elsif error_data['detail'].is_a?(String)
284
- error_msg = error_data['detail']
285
- elsif error_data['errorMessage']
286
- error_msg = error_data['errorMessage']
287
- end
288
- rescue JSON::ParserError
289
- error_msg = "API Error (#{response.code}): #{response.body}"
290
- end
103
+ result
104
+ end
291
105
 
292
- warn "API Error #{response.code}: #{response.body}"
293
- raise FactPulseValidationError.new(error_msg, errors)
294
- end
106
+ def request_multipart(path, data, files, retry_auth:)
107
+ ensure_auth
108
+ url = "#{@api_url}/api/v1/#{path}"
109
+ uri = URI(url)
295
110
 
296
- data = JSON.parse(response.body)
297
-
298
- if sync && data['taskId']
299
- result = poll_task(data['taskId'], timeout: timeout)
300
-
301
- # Check for business error (task succeeded but business result is ERROR)
302
- if result['status'] == 'ERROR'
303
- error_msg = result['errorMessage'] || 'Business error'
304
- errors = (result['details'] || []).map do |d|
305
- ValidationErrorDetail.new(
306
- d['level'] || 'ERROR',
307
- d['item'] || '',
308
- d['reason'] || '',
309
- d['source'],
310
- d['code']
311
- )
312
- end
313
- raise FactPulseValidationError.new(error_msg, errors)
314
- end
111
+ http = Net::HTTP.new(uri.host, uri.port)
112
+ http.use_ssl = uri.scheme == 'https'
113
+ http.read_timeout = @timeout
315
114
 
316
- if result['content_b64']
317
- return Base64.decode64(result['content_b64'])
318
- elsif result['content_xml']
319
- return result['content_xml']
320
- end
321
- raise FactPulseValidationError.new("Unexpected result: #{result.keys.join(', ')}")
322
- end
115
+ # Build multipart body
116
+ boundary = "----RubyFormBoundary#{rand(1_000_000)}"
117
+ body = []
323
118
 
324
- data
119
+ data.each do |key, value|
120
+ body << "--#{boundary}\r\n"
121
+ body << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
122
+ body << "#{value}\r\n"
325
123
  end
326
124
 
327
- # =========================================================================
328
- # AFNOR PDP - Authentication and internal helpers
329
- # =========================================================================
330
-
331
- private def get_afnor_credentials_internal
332
- return @afnor_credentials if @afnor_credentials
333
-
334
- ensure_authenticated
335
- response = http_get(URI("#{@api_url}/api/v1/afnor/credentials"))
336
- raise FactPulseAuthError, "Failed to get AFNOR credentials" unless response.is_a?(Net::HTTPSuccess)
337
- creds = JSON.parse(response.body)
338
- AFNORCredentials.new(
339
- flow_service_url: creds['flow_service_url'],
340
- token_url: creds['token_url'],
341
- client_id: creds['client_id'],
342
- client_secret: creds['client_secret'],
343
- directory_service_url: creds['directory_service_url']
344
- )
125
+ files.each do |key, content|
126
+ body << "--#{boundary}\r\n"
127
+ body << "Content-Disposition: form-data; name=\"#{key}\"; filename=\"#{key}\"\r\n"
128
+ body << "Content-Type: application/octet-stream\r\n\r\n"
129
+ body << content
130
+ body << "\r\n"
345
131
  end
346
132
 
347
- private def get_afnor_token_and_url
348
- credentials = get_afnor_credentials_internal
349
- uri = URI("#{@api_url}/api/v1/afnor/oauth/token")
350
- http = Net::HTTP.new(uri.host, uri.port)
351
- http.use_ssl = uri.scheme == 'https'
352
- request = Net::HTTP::Post.new(uri)
353
- request['X-PDP-Token-URL'] = credentials.token_url
354
- request.set_form_data(
355
- 'grant_type' => 'client_credentials',
356
- 'client_id' => credentials.client_id,
357
- 'client_secret' => credentials.client_secret
358
- )
359
- response = http.request(request)
360
- raise FactPulseAuthError, "AFNOR OAuth2 failed" unless response.is_a?(Net::HTTPSuccess)
361
- token_data = JSON.parse(response.body)
362
- raise FactPulseAuthError, "Invalid AFNOR OAuth2 response" unless token_data['access_token']
363
- { token: token_data['access_token'], pdp_base_url: credentials.flow_service_url }
364
- end
133
+ body << "--#{boundary}--\r\n"
365
134
 
366
- private def make_afnor_request(method, endpoint, json_data: nil, multipart: nil)
367
- token_info = get_afnor_token_and_url
368
- uri = URI("#{@api_url}/api/v1/afnor#{endpoint}")
369
- http = Net::HTTP.new(uri.host, uri.port)
370
- http.use_ssl = uri.scheme == 'https'
371
- http.read_timeout = 60
372
-
373
- request = case method.upcase
374
- when 'GET' then Net::HTTP::Get.new(uri)
375
- when 'POST' then Net::HTTP::Post.new(uri)
376
- else raise "Unsupported method: #{method}"
377
- end
378
-
379
- request['Authorization'] = "Bearer #{token_info[:token]}"
380
- request['X-PDP-Base-URL'] = token_info[:pdp_base_url]
381
-
382
- if multipart
383
- boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}"
384
- request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
385
- request.body = build_multipart_body(boundary, multipart)
386
- elsif json_data
387
- request['Content-Type'] = 'application/json'
388
- request.body = JSON.generate(json_data)
389
- end
135
+ req = Net::HTTP::Post.new(uri)
136
+ req['Authorization'] = "Bearer #{@token}"
137
+ req['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
138
+ req.body = body.join
390
139
 
391
- response = http.request(request)
392
- raise FactPulseValidationError.new("AFNOR error: #{response.code} - #{response.body}") unless response.is_a?(Net::HTTPSuccess)
140
+ response = http.request(req)
393
141
 
394
- content_type = response['Content-Type'] || ''
395
- if content_type.include?('application/json')
396
- JSON.parse(response.body) rescue {}
397
- else
398
- { '_raw' => response.body }
399
- end
142
+ if response.code == '401' && retry_auth
143
+ invalidate_token
144
+ return request_multipart(path, data, files, retry_auth: false)
400
145
  end
401
146
 
402
- # ==================== AFNOR Flow Service ====================
403
-
404
- # Submits an invoice to a PDP via the AFNOR API.
405
- def submit_invoice_afnor(pdf_path, flow_name, **options)
406
- pdf_content = File.binread(pdf_path)
407
- sha256 = Digest::SHA256.hexdigest(pdf_content)
408
-
409
- flow_info = {
410
- 'name' => flow_name,
411
- 'flowSyntax' => options[:flow_syntax] || 'CII',
412
- 'flowProfile' => options[:flow_profile] || 'EN16931',
413
- 'sha256' => sha256
414
- }
415
- flow_info['trackingId'] = options[:tracking_id] if options[:tracking_id]
416
-
417
- make_afnor_request('POST', '/flow/v1/flows', multipart: [
418
- { name: 'file', content: pdf_content, filename: File.basename(pdf_path), content_type: 'application/pdf' },
419
- { name: 'flowInfo', content: JSON.generate(flow_info), content_type: 'application/json' }
420
- ])
421
- end
147
+ result = parse_response(response)
422
148
 
423
- # Searches for AFNOR invoicing flows.
424
- def search_flows_afnor(**criteria)
425
- search_body = {
426
- 'offset' => criteria[:offset] || 0,
427
- 'limit' => criteria[:limit] || 25,
428
- 'where' => {}
429
- }
430
- search_body['where']['trackingId'] = criteria[:tracking_id] if criteria[:tracking_id]
431
- search_body['where']['status'] = criteria[:status] if criteria[:status]
432
-
433
- make_afnor_request('POST', '/flow/v1/flows/search', json_data: search_body)
149
+ # Auto-poll
150
+ if result.is_a?(Hash)
151
+ task_id = result['taskId'] || result['task_id']
152
+ result = poll(task_id) if task_id
434
153
  end
435
154
 
436
- # Downloads the PDF file of an AFNOR flow.
437
- def download_flow_afnor(flow_id)
438
- result = make_afnor_request('GET', "/flow/v1/flows/#{flow_id}")
439
- result['_raw'] || ''
155
+ # Auto-decode
156
+ if result.is_a?(Hash)
157
+ b64_content = result.delete('content_b64') || result.delete('contentB64')
158
+ result['content'] = Base64.decode64(b64_content) if b64_content
440
159
  end
441
160
 
442
- # Retrieves JSON metadata of an incoming flow (supplier invoice).
443
- # Downloads an incoming flow from the AFNOR PDP and extracts invoice
444
- # metadata into a unified JSON format. Supports Factur-X, CII and UBL.
445
- #
446
- # Note: This endpoint uses FactPulse JWT authentication (not AFNOR OAuth).
447
- # The FactPulse server handles calling the PDP with stored credentials.
448
- #
449
- # @param flow_id [String] Flow identifier (UUID)
450
- # @param include_document [Boolean] If true, includes the document in base64
451
- # @return [Hash] Invoice metadata (supplier, amounts, dates, etc.)
452
- #
453
- # @example
454
- # invoice = client.get_incoming_invoice_afnor("550e8400-...")
455
- # puts "Supplier: #{invoice['supplier']['name']}"
456
- # puts "Total incl. tax: #{invoice['total_incl_tax']} #{invoice['currency']}"
457
- def get_incoming_invoice_afnor(flow_id, include_document: false)
458
- ensure_authenticated
459
- uri = URI("#{@api_url}/api/v1/afnor/incoming-flows/#{flow_id}")
460
- uri.query = "include_document=true" if include_document
461
-
462
- http = Net::HTTP.new(uri.host, uri.port)
463
- http.use_ssl = uri.scheme == 'https'
464
- http.read_timeout = 60
465
-
466
- request = Net::HTTP::Get.new(uri)
467
- request['Authorization'] = "Bearer #{@access_token}"
468
-
469
- response = http.request(request)
470
- raise FactPulseValidationError.new("Incoming flow error: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
471
- JSON.parse(response.body) rescue {}
472
- end
473
-
474
- # Checks the availability of the AFNOR Flow Service.
475
- def healthcheck_afnor
476
- make_afnor_request('GET', '/flow/v1/healthcheck')
477
- end
161
+ result
162
+ end
478
163
 
479
- # ==================== AFNOR Directory ====================
164
+ def parse_response(response)
165
+ body = response.body
166
+ data = body && !body.empty? ? JSON.parse(body) : {}
480
167
 
481
- # Gets a company (legal unit) by SIRET in the AFNOR directory.
482
- # @param siret [String] 14-digit SIRET number
483
- # @return [Hash] Company information
484
- def get_siret_afnor(siret)
485
- make_afnor_request('GET', "/directory/v1/siret/code-insee:#{siret}")
486
- end
168
+ return data if response.is_a?(Net::HTTPSuccess)
487
169
 
488
- # Gets a company (legal unit) by SIREN in the AFNOR directory.
489
- # @param siren [String] 9-digit SIREN number
490
- # @return [Hash] Company information
491
- def get_siren_afnor(siren)
492
- make_afnor_request('GET', "/directory/v1/siren/code-insee:#{siren}")
493
- end
170
+ msg = "HTTP #{response.code}"
171
+ details = []
494
172
 
495
- # Searches for SIRENs (legal units) in the AFNOR directory.
496
- # @param criteria [Hash] Search criteria (filters, sorting, fields, limit)
497
- # @return [Hash] Search results
498
- def search_siren_afnor(**criteria)
499
- search_body = {
500
- 'limit' => criteria[:limit] || 25,
501
- 'filters' => criteria[:filters] || {}
502
- }
503
- make_afnor_request('POST', '/directory/v1/siren/search', json_data: search_body)
173
+ if data.is_a?(Hash)
174
+ if data['detail'].is_a?(Array)
175
+ details = data['detail']
176
+ msgs = data['detail'].map do |e|
177
+ loc = e['loc'] || []
178
+ "#{loc.last || '?'}: #{e['msg'] || '?'}"
179
+ end
180
+ msg = "Validation error: #{msgs.join('; ')}"
181
+ elsif data['detail'].is_a?(String)
182
+ msg = data['detail']
183
+ elsif data['errorMessage'].is_a?(String)
184
+ msg = data['errorMessage']
185
+ end
504
186
  end
505
187
 
506
- # Searches for routing codes in the AFNOR directory.
507
- # @param criteria [Hash] Search criteria (filters, sorting, fields, limit)
508
- # @return [Hash] Search results with routing codes
509
- def search_routing_codes_afnor(**criteria)
510
- search_body = {
511
- 'limit' => criteria[:limit] || 25,
512
- 'filters' => criteria[:filters] || {}
513
- }
514
- make_afnor_request('POST', '/directory/v1/routing-code/search', json_data: search_body)
515
- end
188
+ raise Error.new(msg, status_code: response.code.to_i, details: details)
189
+ end
516
190
 
517
- # Gets a routing code by SIRET and routing identifier.
518
- # @param siret [String] 14-digit SIRET number
519
- # @param routing_identifier [String] Routing code identifier
520
- # @return [Hash] Routing code information
521
- def get_routing_code_afnor(siret, routing_identifier)
522
- make_afnor_request('GET', "/directory/v1/routing-code/siret:#{siret}/code:#{routing_identifier}")
523
- end
191
+ def poll(task_id)
192
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
193
+ interval = 1.0
524
194
 
525
- # =========================================================================
526
- # Chorus Pro
527
- # =========================================================================
195
+ loop do
196
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
197
+ if elapsed >= @polling_timeout
198
+ raise Error.new("Polling timeout after #{@polling_timeout}s for task #{task_id}")
199
+ end
528
200
 
529
- private def make_chorus_request(method, endpoint, json_data = nil)
530
- ensure_authenticated
531
- uri = URI("#{@api_url}/api/v1/chorus-pro#{endpoint}")
201
+ ensure_auth
202
+ uri = URI("#{@api_url}/api/v1/processing/tasks/#{task_id}/status")
532
203
  http = Net::HTTP.new(uri.host, uri.port)
533
204
  http.use_ssl = uri.scheme == 'https'
534
- http.read_timeout = 60
535
-
536
- body = json_data || {}
537
- body['credentials'] = @chorus_credentials.to_h if @chorus_credentials
538
-
539
- request = case method.upcase
540
- when 'GET' then Net::HTTP::Get.new(uri)
541
- when 'POST' then Net::HTTP::Post.new(uri)
542
- else raise "Unsupported method: #{method}"
543
- end
205
+ http.read_timeout = @timeout
544
206
 
545
- request['Authorization'] = "Bearer #{@access_token}"
546
- request['Content-Type'] = 'application/json'
547
- request.body = JSON.generate(body) if body.any?
548
-
549
- response = http.request(request)
550
- raise FactPulseValidationError.new("Chorus Pro error: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
551
- JSON.parse(response.body) rescue {}
552
- end
553
-
554
- # Searches for structures on Chorus Pro.
555
- def rechercher_structure_chorus(identifiant_structure: nil, raison_sociale: nil, type_identifiant: 'SIRET', restreindre_privees: true)
556
- body = { 'restreindre_structures_privees' => restreindre_privees }
557
- body['identifiant_structure'] = identifiant_structure if identifiant_structure
558
- body['raison_sociale_structure'] = raison_sociale if raison_sociale
559
- body['type_identifiant_structure'] = type_identifiant if type_identifiant
560
-
561
- make_chorus_request('POST', '/structures/rechercher', body)
562
- end
563
-
564
- # Gets the details of a Chorus Pro structure.
565
- def consulter_structure_chorus(id_structure_cpp)
566
- make_chorus_request('POST', '/structures/consulter', { 'id_structure_cpp' => id_structure_cpp })
567
- end
568
-
569
- # Gets the Chorus Pro ID of a structure from its SIRET.
570
- def obtenir_id_chorus_depuis_siret(siret, type_identifiant: 'SIRET')
571
- make_chorus_request('POST', '/structures/obtenir-id-depuis-siret', { 'siret' => siret, 'type_identifiant' => type_identifiant })
572
- end
573
-
574
- # Lists the services of a Chorus Pro structure.
575
- def lister_services_structure_chorus(id_structure_cpp)
576
- make_chorus_request('GET', "/structures/#{id_structure_cpp}/services")
577
- end
578
-
579
- # Submits an invoice to Chorus Pro.
580
- # @param invoice_data [Hash] Invoice data with keys: numero_facture, date_facture, date_echeance_paiement,
581
- # id_structure_cpp, montant_ht_total, montant_tva, montant_ttc_total, etc.
582
- # @return [Hash] Response with identifiant_facture_cpp, numero_flux_depot, code_retour, libelle
583
- def submit_invoice_chorus(invoice_data)
584
- make_chorus_request('POST', '/factures/soumettre', invoice_data)
585
- end
586
- alias soumettre_facture_chorus submit_invoice_chorus
587
-
588
- # Gets the status of a Chorus Pro invoice.
589
- # @param invoice_cpp_id [Integer] Chorus Pro invoice ID
590
- # @return [Hash] Invoice status with statut_courant, numero_facture, date_facture, etc.
591
- def get_invoice_status_chorus(invoice_cpp_id)
592
- make_chorus_request('POST', '/factures/consulter', { 'identifiant_facture_cpp' => invoice_cpp_id })
593
- end
594
- alias consulter_facture_chorus get_invoice_status_chorus
595
-
596
- # =========================================================================
597
- # Validation
598
- # =========================================================================
599
-
600
- # Validates a Factur-X PDF.
601
- # @param pdf_path [String] Path to the PDF file
602
- # @param profile [String, nil] Factur-X profile (MINIMUM, BASIC, EN16931, EXTENDED). If nil, auto-detected.
603
- # @param use_verapdf [Boolean] Enable strict PDF/A validation with VeraPDF (default: false)
604
- def validate_facturx_pdf(pdf_path, profile: nil, use_verapdf: false)
605
- ensure_authenticated
606
- uri = URI("#{@api_url}/api/v1/processing/validate-facturx-pdf")
607
- pdf_content = File.binread(pdf_path)
608
-
609
- parts = [
610
- { name: 'pdf_file', content: pdf_content, filename: File.basename(pdf_path), content_type: 'application/pdf' },
611
- { name: 'use_verapdf', content: use_verapdf.to_s }
612
- ]
613
- parts << { name: 'profile', content: profile } if profile
614
-
615
- boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}"
616
- body = build_multipart_body(boundary, parts)
617
-
618
- response = http_multipart_post(uri, body, boundary)
619
- raise FactPulseValidationError.new("Validation error: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
620
- JSON.parse(response.body) rescue {}
621
- end
622
-
623
- # Validates a Factur-X XML.
624
- def validate_facturx_xml(xml_content, profile: 'EN16931')
625
- ensure_authenticated
626
- uri = URI("#{@api_url}/api/v1/processing/validate-xml")
627
-
628
- boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}"
629
- body = build_multipart_body(boundary, [
630
- { name: 'xml_file', content: xml_content, filename: 'invoice.xml', content_type: 'application/xml' },
631
- { name: 'profile', content: profile }
632
- ])
633
-
634
- response = http_multipart_post(uri, body, boundary)
635
- raise FactPulseValidationError.new("Validation error: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
636
- JSON.parse(response.body) rescue {}
637
- end
207
+ req = Net::HTTP::Get.new(uri)
208
+ req['Authorization'] = "Bearer #{@token}"
209
+ response = http.request(req)
638
210
 
639
- # Validates the signature of a signed PDF.
640
- def validate_pdf_signature(pdf_path)
641
- ensure_authenticated
642
- uri = URI("#{@api_url}/api/v1/processing/validate-pdf-signature")
643
- pdf_content = File.binread(pdf_path)
211
+ if response.code == '401'
212
+ invalidate_token
213
+ next
214
+ end
644
215
 
645
- boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}"
646
- body = build_multipart_body(boundary, [
647
- { name: 'pdf_file', content: pdf_content, filename: File.basename(pdf_path), content_type: 'application/pdf' }
648
- ])
216
+ data = parse_response(response)
217
+ status = data['status']
649
218
 
650
- response = http_multipart_post(uri, body, boundary)
651
- raise FactPulseValidationError.new("Validation error: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
652
- JSON.parse(response.body) rescue {}
653
- end
219
+ if status == 'SUCCESS'
220
+ result = data['result'] || {}
221
+ # Support both content_b64 and contentB64
222
+ b64_content = result.delete('content_b64') || result.delete('contentB64')
223
+ result['content'] = Base64.decode64(b64_content) if b64_content
224
+ return result
225
+ end
654
226
 
655
- # =========================================================================
656
- # Signature
657
- # =========================================================================
658
-
659
- # Signs a PDF with the server-configured certificate.
660
- def sign_pdf(pdf_path, **options)
661
- ensure_authenticated
662
- uri = URI("#{@api_url}/api/v1/processing/sign-pdf")
663
- pdf_content = File.binread(pdf_path)
664
-
665
- parts = [
666
- { name: 'pdf_file', content: pdf_content, filename: File.basename(pdf_path), content_type: 'application/pdf' },
667
- { name: 'use_pades_lt', content: (options[:use_pades_lt] ? 'true' : 'false') },
668
- { name: 'use_timestamp', content: (options.key?(:use_timestamp) ? (options[:use_timestamp] ? 'true' : 'false') : 'true') }
669
- ]
670
- parts << { name: 'reason', content: options[:reason] } if options[:reason]
671
- parts << { name: 'location', content: options[:location] } if options[:location]
672
- parts << { name: 'contact', content: options[:contact] } if options[:contact]
673
-
674
- boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}"
675
- body = build_multipart_body(boundary, parts)
676
-
677
- response = http_multipart_post(uri, body, boundary)
678
- raise FactPulseValidationError.new("Signature error: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
679
-
680
- result = JSON.parse(response.body) rescue {}
681
- raise FactPulseValidationError.new("Invalid signature response") unless result['pdf_signe_base64']
682
- Base64.decode64(result['pdf_signe_base64'])
683
- end
227
+ if status == 'FAILURE'
228
+ res = data['result'] || {}
229
+ raise Error.new(res['errorMessage'] || 'Task failed', details: res['details'] || [])
230
+ end
684
231
 
685
- # Generates a test certificate (NOT FOR PRODUCTION).
686
- def generate_test_certificate(**options)
687
- ensure_authenticated
688
- uri = URI("#{@api_url}/api/v1/processing/generate-test-certificate")
689
- body = {
690
- 'cn' => options[:cn] || 'Test Organisation',
691
- 'organisation' => options[:organisation] || 'Test Organisation',
692
- 'email' => options[:email] || 'test@example.com',
693
- 'validity_days' => options[:validity_days] || 365,
694
- 'key_size' => options[:key_size] || 2048
695
- }
696
-
697
- response = http_post_json(uri, body)
698
- raise FactPulseValidationError.new("Error: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
699
- JSON.parse(response.body) rescue {}
232
+ sleep([interval, @polling_timeout - elapsed].min)
233
+ interval = [interval * 1.5, 10].min
700
234
  end
235
+ end
701
236
 
702
- # =========================================================================
703
- # Workflow complet
704
- # =========================================================================
705
-
706
- # Generates a complete Factur-X PDF with optional validation, signature and submission.
707
- def generate_complete_facturx(invoice, pdf_source_path, **options)
708
- profile = options[:profile] || 'EN16931'
709
- validate = options.fetch(:validate, true)
710
- sign = options.fetch(:sign, false)
711
- submit_afnor = options.fetch(:submit_afnor, false)
712
- timeout = options[:timeout] || 120000
713
-
714
- result = {}
715
-
716
- # 1. Generation
717
- pdf_bytes = generate_facturx(invoice, pdf_source_path, profile: profile, output_format: 'pdf', sync: true, timeout: timeout)
718
- result[:pdf_bytes] = pdf_bytes
719
-
720
- # Create a temporary file for subsequent operations
721
- temp_file = Tempfile.new(['facturx_', '.pdf'])
722
- begin
723
- temp_file.binmode
724
- temp_file.write(pdf_bytes)
725
- temp_file.flush
726
-
727
- # 2. Validation
728
- if validate
729
- validation = validate_facturx_pdf(temp_file.path, profile: profile)
730
- result[:validation] = validation
731
- unless validation['isCompliant']
732
- if options[:output_path]
733
- File.binwrite(options[:output_path], pdf_bytes)
734
- result[:pdf_path] = options[:output_path]
735
- end
736
- return result
737
- end
738
- end
739
-
740
- # 3. Signature
741
- if sign
742
- pdf_bytes = sign_pdf(temp_file.path, **options)
743
- result[:pdf_bytes] = pdf_bytes
744
- result[:signature] = { 'signed' => true }
745
- temp_file.rewind
746
- temp_file.write(pdf_bytes)
747
- temp_file.flush
748
- end
749
-
750
- # 4. AFNOR submission
751
- if submit_afnor
752
- invoice_number = invoice['invoiceNumber'] || invoice['invoice_number'] || 'INVOICE'
753
- flow_name = options[:afnor_flow_name] || "Invoice #{invoice_number}"
754
- tracking_id = options[:afnor_tracking_id] || invoice_number
755
- afnor_result = submit_invoice_afnor(temp_file.path, flow_name, tracking_id: tracking_id)
756
- result[:afnor] = afnor_result
757
- end
758
-
759
- # Final save
760
- if options[:output_path]
761
- File.binwrite(options[:output_path], pdf_bytes)
762
- result[:pdf_path] = options[:output_path]
763
- end
764
- ensure
765
- temp_file.close
766
- temp_file.unlink
237
+ def ensure_auth
238
+ @token_mutex.synchronize do
239
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= @token_expires_at
240
+ refresh_token
767
241
  end
768
-
769
- result
770
242
  end
243
+ end
771
244
 
772
- private
245
+ def refresh_token
246
+ uri = URI("#{@api_url}/api/token/")
247
+ http = Net::HTTP.new(uri.host, uri.port)
248
+ http.use_ssl = uri.scheme == 'https'
249
+ http.read_timeout = @timeout
773
250
 
774
- def http_post(uri, payload)
775
- Net::HTTP.new(uri.host, uri.port).tap { |h| h.use_ssl = uri.scheme == 'https'; h.read_timeout = 30 }
776
- .request(Net::HTTP::Post.new(uri).tap { |r| r['Content-Type'] = 'application/json'; r.body = JSON.generate(payload) })
777
- end
251
+ req = Net::HTTP::Post.new(uri)
252
+ req['Content-Type'] = 'application/json'
253
+ req.body = JSON.generate(username: @email, password: @password, client_uid: @client_uid)
778
254
 
779
- def http_post_json(uri, payload)
780
- http = Net::HTTP.new(uri.host, uri.port)
781
- http.use_ssl = uri.scheme == 'https'
782
- http.read_timeout = 30
783
- request = Net::HTTP::Post.new(uri)
784
- request['Authorization'] = "Bearer #{@access_token}"
785
- request['Content-Type'] = 'application/json'
786
- request.body = JSON.generate(payload)
787
- http.request(request)
788
- end
255
+ response = http.request(req)
789
256
 
790
- def http_get(uri)
791
- Net::HTTP.new(uri.host, uri.port).tap { |h| h.use_ssl = uri.scheme == 'https'; h.read_timeout = 30 }
792
- .request(Net::HTTP::Get.new(uri).tap { |r| r['Authorization'] = "Bearer #{@access_token}" })
257
+ unless response.is_a?(Net::HTTPSuccess)
258
+ raise Error.new("Authentication failed: HTTP #{response.code}", status_code: response.code.to_i)
793
259
  end
794
260
 
795
- def http_multipart_post(uri, body, boundary)
796
- http = Net::HTTP.new(uri.host, uri.port)
797
- http.use_ssl = uri.scheme == 'https'
798
- http.read_timeout = 120
799
-
800
- request = Net::HTTP::Post.new(uri)
801
- request['Authorization'] = "Bearer #{@access_token}"
802
- request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
803
- request.body = body
804
- http.request(request)
805
- end
261
+ data = JSON.parse(response.body)
262
+ @token = data['access'] || raise(Error.new('Invalid auth response'))
263
+ @token_expires_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 28 * 60
264
+ end
806
265
 
807
- def build_multipart_body(boundary, parts)
808
- body_parts = []
809
- parts.each do |part|
810
- body_parts << "--#{boundary}\r\n"
811
- if part[:filename]
812
- body_parts << "Content-Disposition: form-data; name=\"#{part[:name]}\"; filename=\"#{part[:filename]}\"\r\n"
813
- body_parts << "Content-Type: #{part[:content_type] || 'application/octet-stream'}\r\n\r\n"
814
- else
815
- body_parts << "Content-Disposition: form-data; name=\"#{part[:name]}\"\r\n"
816
- body_parts << "Content-Type: #{part[:content_type]}\r\n" if part[:content_type]
817
- body_parts << "\r\n"
818
- end
819
- body_parts << part[:content]
820
- body_parts << "\r\n"
821
- end
822
- body_parts << "--#{boundary}--\r\n"
823
- body_parts.join
266
+ def invalidate_token
267
+ @token_mutex.synchronize do
268
+ @token_expires_at = 0
824
269
  end
825
270
  end
826
271
  end