Class: FactPulse::Helpers::FactPulseClient
- Inherits:
-
Object
- Object
- FactPulse::Helpers::FactPulseClient
- Defined in:
- lib/factpulse/helpers/client.rb
Instance Attribute Summary collapse
-
#afnor_credentials ⇒ Object
readonly
Returns the value of attribute afnor_credentials.
-
#chorus_credentials ⇒ Object
readonly
Returns the value of attribute chorus_credentials.
Class Method Summary collapse
Instance Method Summary collapse
- #afnor_credentials_for_api ⇒ Object
- #chorus_credentials_for_api ⇒ Object
-
#consulter_facture_chorus(identifiant_facture_cpp) ⇒ Object
Consulte le statut d'une facture Chorus Pro.
-
#consulter_structure_chorus(id_structure_cpp) ⇒ Object
Consulte les détails d'une structure Chorus Pro.
- #ensure_authenticated(force_refresh: false) ⇒ Object
-
#generer_certificat_test(**options) ⇒ Object
Génère un certificat de test (NON PRODUCTION).
-
#generer_facturx(facture_data, pdf_source, profil: 'EN16931', format_sortie: 'pdf', sync: true, timeout: nil) ⇒ Object
Génère une facture Factur-X à partir d'un dict/hash et d'un PDF source.
-
#generer_facturx_complet(facture, pdf_source_path, **options) ⇒ Object
Génère un PDF Factur-X complet avec validation, signature et soumission optionnelles.
- #get_afnor_credentials ⇒ Object
-
#get_chorus_pro_credentials ⇒ Object
Alias plus courts.
-
#healthcheck_afnor ⇒ Object
Vérifie la disponibilité du Flow Service AFNOR.
-
#initialize(email:, password:, api_url: nil, client_uid: nil, chorus_credentials: nil, afnor_credentials: nil, polling_interval: nil, polling_timeout: nil, max_retries: nil) ⇒ FactPulseClient
constructor
A new instance of FactPulseClient.
-
#lister_codes_routage_afnor(siren) ⇒ Object
Liste les codes de routage disponibles pour un SIREN.
-
#lister_services_structure_chorus(id_structure_cpp) ⇒ Object
Liste les services d'une structure Chorus Pro.
-
#obtenir_id_chorus_depuis_siret(siret, type_identifiant: 'SIRET') ⇒ Object
Obtient l'ID Chorus Pro d'une structure depuis son SIRET.
- #poll_task(task_id, timeout: nil, interval: nil) ⇒ Object
-
#rechercher_flux_afnor(**criteria) ⇒ Object
Recherche des flux de facturation AFNOR.
-
#rechercher_siren_afnor(siren) ⇒ Object
Recherche une entreprise par SIREN dans l'annuaire AFNOR.
-
#rechercher_siret_afnor(siret) ⇒ Object
Recherche une entreprise par SIRET dans l'annuaire AFNOR.
-
#rechercher_structure_chorus(identifiant_structure: nil, raison_sociale: nil, type_identifiant: 'SIRET', restreindre_privees: true) ⇒ Object
Recherche des structures sur Chorus Pro.
- #reset_auth ⇒ Object
-
#signer_pdf(pdf_path, **options) ⇒ Object
Signe un PDF avec le certificat configuré côté serveur.
-
#soumettre_facture_afnor(pdf_path, flow_name, **options) ⇒ Object
Soumet une facture à une PDP via l'API AFNOR.
-
#soumettre_facture_chorus(facture_data) ⇒ Object
Soumet une facture à Chorus Pro.
-
#telecharger_flux_afnor(flow_id) ⇒ Object
Télécharge le fichier PDF d'un flux AFNOR.
-
#valider_pdf_facturx(pdf_path, profil: 'EN16931') ⇒ Object
Valide un PDF Factur-X.
-
#valider_signature_pdf(pdf_path) ⇒ Object
Valide la signature d'un PDF signé.
-
#valider_xml_facturx(xml_content, profil: 'EN16931') ⇒ Object
Valide un XML Factur-X.
Constructor Details
#initialize(email:, password:, api_url: nil, client_uid: nil, chorus_credentials: nil, afnor_credentials: nil, polling_interval: nil, polling_timeout: nil, max_retries: nil) ⇒ FactPulseClient
Returns a new instance of FactPulseClient.
135 136 137 138 139 140 141 |
# File 'lib/factpulse/helpers/client.rb', line 135 def initialize(email:, password:, api_url: nil, client_uid: nil, chorus_credentials: nil, afnor_credentials: nil, polling_interval: nil, polling_timeout: nil, max_retries: nil) @email, @password = email, password; @api_url = (api_url || 'https://factpulse.fr').chomp('/') @client_uid, @polling_interval, @polling_timeout, @max_retries = client_uid, polling_interval || 2000, polling_timeout || 120000, max_retries || 1 @chorus_credentials, @afnor_credentials = chorus_credentials, afnor_credentials @access_token = @refresh_token = @token_expires_at = nil end |
Instance Attribute Details
#afnor_credentials ⇒ Object (readonly)
Returns the value of attribute afnor_credentials.
133 134 135 |
# File 'lib/factpulse/helpers/client.rb', line 133 def afnor_credentials @afnor_credentials end |
#chorus_credentials ⇒ Object (readonly)
Returns the value of attribute chorus_credentials.
133 134 135 |
# File 'lib/factpulse/helpers/client.rb', line 133 def chorus_credentials @chorus_credentials end |
Class Method Details
.format_montant(m) ⇒ Object
179 |
# File 'lib/factpulse/helpers/client.rb', line 179 def self.format_montant(m); MontantHelpers.montant(m); end |
Instance Method Details
#afnor_credentials_for_api ⇒ Object
144 |
# File 'lib/factpulse/helpers/client.rb', line 144 def afnor_credentials_for_api; @afnor_credentials&.to_h; end |
#chorus_credentials_for_api ⇒ Object
143 |
# File 'lib/factpulse/helpers/client.rb', line 143 def chorus_credentials_for_api; @chorus_credentials&.to_h; end |
#consulter_facture_chorus(identifiant_facture_cpp) ⇒ Object
Consulte le statut d'une facture Chorus Pro.
445 446 447 |
# File 'lib/factpulse/helpers/client.rb', line 445 def consulter_facture_chorus(identifiant_facture_cpp) make_chorus_request('POST', '/factures/consulter', { 'identifiant_facture_cpp' => identifiant_facture_cpp }) end |
#consulter_structure_chorus(id_structure_cpp) ⇒ Object
Consulte les détails d'une structure Chorus Pro.
425 426 427 |
# File 'lib/factpulse/helpers/client.rb', line 425 def consulter_structure_chorus(id_structure_cpp) make_chorus_request('POST', '/structures/consulter', { 'id_structure_cpp' => id_structure_cpp }) end |
#ensure_authenticated(force_refresh: false) ⇒ Object
149 150 151 152 153 154 155 156 157 158 |
# File 'lib/factpulse/helpers/client.rb', line 149 def ensure_authenticated(force_refresh: false) now = (Time.now.to_f * 1000).to_i if force_refresh || @access_token.nil? || (@token_expires_at && now >= @token_expires_at) payload = { 'username' => @email, 'password' => @password }; payload['client_uid'] = @client_uid if @client_uid response = http_post(URI("#{@api_url}/api/token/"), payload) raise FactPulseAuthError, "Auth failed" unless response.is_a?(Net::HTTPSuccess) tokens = JSON.parse(response.body); @access_token, @refresh_token = tokens['access'], tokens['refresh'] @token_expires_at = now + (28 * 60 * 1000) end end |
#generer_certificat_test(**options) ⇒ Object
Génère un certificat de test (NON PRODUCTION).
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 |
# File 'lib/factpulse/helpers/client.rb', line 533 def generer_certificat_test(**) ensure_authenticated uri = URI("#{@api_url}/api/v1/traitement/generer-certificat-test") body = { 'cn' => [:cn] || 'Test Organisation', 'organisation' => [:organisation] || 'Test Organisation', 'email' => [:email] || '[email protected]', 'duree_jours' => [:duree_jours] || 365, 'taille_cle' => [:taille_cle] || 2048 } response = http_post_json(uri, body) raise FactPulseValidationError.new("Error: #{response.code}") unless response.is_a?(Net::HTTPSuccess) JSON.parse(response.body) rescue {} end |
#generer_facturx(facture_data, pdf_source, profil: 'EN16931', format_sortie: 'pdf', sync: true, timeout: nil) ⇒ Object
Génère une facture Factur-X à partir d'un dict/hash et d'un PDF source.
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/factpulse/helpers/client.rb', line 182 def generer_facturx(facture_data, pdf_source, profil: 'EN16931', format_sortie: 'pdf', sync: true, timeout: nil) # Conversion des données en JSON string json_data = case facture_data when String then facture_data when Hash then JSON.generate(facture_data) else if facture_data.respond_to?(:to_h) JSON.generate(facture_data.to_h) elsif facture_data.respond_to?(:to_hash) JSON.generate(facture_data.to_hash) else raise FactPulseValidationError.new("Type de données non supporté: #{facture_data.class}") end end # Lecture du PDF source pdf_content = case pdf_source when String then File.binread(pdf_source) when File then pdf_source.read else if pdf_source.respond_to?(:read) pdf_source.read else raise FactPulseValidationError.new("Type de PDF non supporté: #{pdf_source.class}") end end pdf_filename = pdf_source.is_a?(String) ? File.basename(pdf_source) : 'facture.pdf' ensure_authenticated uri = URI("#{@api_url}/api/v1/traitement/generer-facture") # Construire la requête multipart boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}" body = build_multipart_body(boundary, [ { name: 'donnees_facture', content: json_data }, { name: 'profil', content: profil }, { name: 'format_sortie', content: format_sortie }, { name: 'source_pdf', content: pdf_content, filename: pdf_filename, content_type: 'application/pdf' } ]) response = http_multipart_post(uri, body, boundary) if response.code == '401' reset_auth; ensure_authenticated; response = http_multipart_post(uri, body, boundary) end unless response.is_a?(Net::HTTPSuccess) error_data = JSON.parse(response.body) rescue { 'detail' => response.body } raise FactPulseValidationError.new("Erreur API: #{error_data['detail'] || response.body}") end data = JSON.parse(response.body) if sync && data['id_tache'] result = poll_task(data['id_tache'], timeout: timeout) if result['contenu_b64'] return Base64.decode64(result['contenu_b64']) elsif result['contenu_xml'] return result['contenu_xml'] end raise FactPulseValidationError.new("Résultat inattendu: #{result.keys.join(', ')}") end data end |
#generer_facturx_complet(facture, pdf_source_path, **options) ⇒ Object
Génère un PDF Factur-X complet avec validation, signature et soumission optionnelles.
554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 |
# File 'lib/factpulse/helpers/client.rb', line 554 def generer_facturx_complet(facture, pdf_source_path, **) profil = [:profil] || 'EN16931' valider = .fetch(:valider, true) signer = .fetch(:signer, false) soumettre_afnor = .fetch(:soumettre_afnor, false) timeout = [:timeout] || 120000 result = {} # 1. Génération pdf_bytes = generer_facturx(facture, pdf_source_path, profil: profil, format_sortie: 'pdf', sync: true, timeout: timeout) result[:pdf_bytes] = pdf_bytes # Créer un fichier temporaire pour les opérations suivantes temp_file = Tempfile.new(['facturx_', '.pdf']) begin temp_file.binmode temp_file.write(pdf_bytes) temp_file.flush # 2. Validation if valider validation = valider_pdf_facturx(temp_file.path, profil: profil) result[:validation] = validation unless validation['est_conforme'] if [:output_path] File.binwrite([:output_path], pdf_bytes) result[:pdf_path] = [:output_path] end return result end end # 3. Signature if signer pdf_bytes = signer_pdf(temp_file.path, **) result[:pdf_bytes] = pdf_bytes result[:signature] = { 'signe' => true } temp_file.rewind temp_file.write(pdf_bytes) temp_file.flush end # 4. Soumission AFNOR if soumettre_afnor numero_facture = facture['numeroFacture'] || facture['numero_facture'] || 'FACTURE' flow_name = [:afnor_flow_name] || "Facture #{numero_facture}" tracking_id = [:afnor_tracking_id] || numero_facture afnor_result = soumettre_facture_afnor(temp_file.path, flow_name, tracking_id: tracking_id) result[:afnor] = afnor_result end # Sauvegarde finale if [:output_path] File.binwrite([:output_path], pdf_bytes) result[:pdf_path] = [:output_path] end ensure temp_file.close temp_file.unlink end result end |
#get_afnor_credentials ⇒ Object
147 |
# File 'lib/factpulse/helpers/client.rb', line 147 def get_afnor_credentials; afnor_credentials_for_api; end |
#get_chorus_pro_credentials ⇒ Object
Alias plus courts
146 |
# File 'lib/factpulse/helpers/client.rb', line 146 def get_chorus_pro_credentials; chorus_credentials_for_api; end |
#healthcheck_afnor ⇒ Object
Vérifie la disponibilité du Flow Service AFNOR.
364 365 366 |
# File 'lib/factpulse/helpers/client.rb', line 364 def healthcheck_afnor make_afnor_request('GET', '/flow/v1/healthcheck') end |
#lister_codes_routage_afnor(siren) ⇒ Object
Liste les codes de routage disponibles pour un SIREN.
381 382 383 |
# File 'lib/factpulse/helpers/client.rb', line 381 def lister_codes_routage_afnor(siren) make_afnor_request('GET', "/directory/siren/#{siren}/routing-codes") end |
#lister_services_structure_chorus(id_structure_cpp) ⇒ Object
Liste les services d'une structure Chorus Pro.
435 436 437 |
# File 'lib/factpulse/helpers/client.rb', line 435 def lister_services_structure_chorus(id_structure_cpp) make_chorus_request('GET', "/structures/#{id_structure_cpp}/services") end |
#obtenir_id_chorus_depuis_siret(siret, type_identifiant: 'SIRET') ⇒ Object
Obtient l'ID Chorus Pro d'une structure depuis son SIRET.
430 431 432 |
# File 'lib/factpulse/helpers/client.rb', line 430 def obtenir_id_chorus_depuis_siret(siret, type_identifiant: 'SIRET') make_chorus_request('POST', '/structures/obtenir-id-depuis-siret', { 'siret' => siret, 'type_identifiant' => type_identifiant }) end |
#poll_task(task_id, timeout: nil, interval: nil) ⇒ Object
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/factpulse/helpers/client.rb', line 162 def poll_task(task_id, timeout: nil, interval: nil) timeout_ms, interval_ms = timeout || @polling_timeout, interval || @polling_interval start_time, current_interval = (Time.now.to_f * 1000).to_i, interval_ms.to_f loop do raise FactPulsePollingTimeout.new(task_id, timeout_ms) if (Time.now.to_f * 1000).to_i - start_time > timeout_ms ensure_authenticated; response = http_get(URI("#{@api_url}/api/v1/traitement/taches/#{task_id}/statut")) reset_auth and next if response.code == '401' data = JSON.parse(response.body) return data['resultat'] || {} if data['statut'] == 'SUCCESS' if data['statut'] == 'FAILURE' r = data['resultat'] || {} raise FactPulseValidationError.new("Task #{task_id} failed: #{r['message_erreur'] || '?'}", (r['erreurs'] || []).map { |e| ValidationErrorDetail.from_hash(e) }) end sleep(current_interval / 1000.0); current_interval = [current_interval * 1.5, 10000].min end end |
#rechercher_flux_afnor(**criteria) ⇒ Object
Recherche des flux de facturation AFNOR.
345 346 347 348 349 350 351 352 353 354 355 |
# File 'lib/factpulse/helpers/client.rb', line 345 def rechercher_flux_afnor(**criteria) search_body = { 'offset' => criteria[:offset] || 0, 'limit' => criteria[:limit] || 25, 'where' => {} } search_body['where']['trackingId'] = criteria[:tracking_id] if criteria[:tracking_id] search_body['where']['status'] = criteria[:status] if criteria[:status] make_afnor_request('POST', '/flow/v1/flows/search', json_data: search_body) end |
#rechercher_siren_afnor(siren) ⇒ Object
Recherche une entreprise par SIREN dans l'annuaire AFNOR.
376 377 378 |
# File 'lib/factpulse/helpers/client.rb', line 376 def rechercher_siren_afnor(siren) make_afnor_request('GET', "/directory/siren/#{siren}") end |
#rechercher_siret_afnor(siret) ⇒ Object
Recherche une entreprise par SIRET dans l'annuaire AFNOR.
371 372 373 |
# File 'lib/factpulse/helpers/client.rb', line 371 def rechercher_siret_afnor(siret) make_afnor_request('GET', "/directory/siret/#{siret}") end |
#rechercher_structure_chorus(identifiant_structure: nil, raison_sociale: nil, type_identifiant: 'SIRET', restreindre_privees: true) ⇒ Object
Recherche des structures sur Chorus Pro.
415 416 417 418 419 420 421 422 |
# File 'lib/factpulse/helpers/client.rb', line 415 def rechercher_structure_chorus(identifiant_structure: nil, raison_sociale: nil, type_identifiant: 'SIRET', restreindre_privees: true) body = { 'restreindre_structures_privees' => restreindre_privees } body['identifiant_structure'] = identifiant_structure if identifiant_structure body['raison_sociale_structure'] = if body['type_identifiant_structure'] = type_identifiant if type_identifiant make_chorus_request('POST', '/structures/rechercher', body) end |
#reset_auth ⇒ Object
160 |
# File 'lib/factpulse/helpers/client.rb', line 160 def reset_auth; @access_token = @refresh_token = @token_expires_at = nil; end |
#signer_pdf(pdf_path, **options) ⇒ Object
Signe un PDF avec le certificat configuré côté serveur.
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 |
# File 'lib/factpulse/helpers/client.rb', line 507 def signer_pdf(pdf_path, **) ensure_authenticated uri = URI("#{@api_url}/api/v1/traitement/signer-pdf") pdf_content = File.binread(pdf_path) parts = [ { name: 'fichier_pdf', content: pdf_content, filename: File.basename(pdf_path), content_type: 'application/pdf' }, { name: 'use_pades_lt', content: ([:use_pades_lt] ? 'true' : 'false') }, { name: 'use_timestamp', content: (.key?(:use_timestamp) ? ([:use_timestamp] ? 'true' : 'false') : 'true') } ] parts << { name: 'raison', content: [:raison] } if [:raison] parts << { name: 'localisation', content: [:localisation] } if [:localisation] parts << { name: 'contact', content: [:contact] } if [:contact] boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}" body = build_multipart_body(boundary, parts) response = http_multipart_post(uri, body, boundary) raise FactPulseValidationError.new("Signature error: #{response.code}") unless response.is_a?(Net::HTTPSuccess) result = JSON.parse(response.body) rescue {} raise FactPulseValidationError.new("Invalid signature response") unless result['pdf_signe_base64'] Base64.decode64(result['pdf_signe_base64']) end |
#soumettre_facture_afnor(pdf_path, flow_name, **options) ⇒ Object
Soumet une facture à une PDP via l'API AFNOR.
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 |
# File 'lib/factpulse/helpers/client.rb', line 326 def soumettre_facture_afnor(pdf_path, flow_name, **) pdf_content = File.binread(pdf_path) sha256 = Digest::SHA256.hexdigest(pdf_content) flow_info = { 'name' => flow_name, 'flowSyntax' => [:flow_syntax] || 'CII', 'flowProfile' => [:flow_profile] || 'EN16931', 'sha256' => sha256 } flow_info['trackingId'] = [:tracking_id] if [:tracking_id] make_afnor_request('POST', '/flow/v1/flows', multipart: [ { name: 'file', content: pdf_content, filename: File.basename(pdf_path), content_type: 'application/pdf' }, { name: 'flowInfo', content: JSON.generate(flow_info), content_type: 'application/json' } ]) end |
#soumettre_facture_chorus(facture_data) ⇒ Object
Soumet une facture à Chorus Pro.
440 441 442 |
# File 'lib/factpulse/helpers/client.rb', line 440 def soumettre_facture_chorus(facture_data) make_chorus_request('POST', '/factures/soumettre', facture_data) end |
#telecharger_flux_afnor(flow_id) ⇒ Object
Télécharge le fichier PDF d'un flux AFNOR.
358 359 360 361 |
# File 'lib/factpulse/helpers/client.rb', line 358 def telecharger_flux_afnor(flow_id) result = make_afnor_request('GET', "/flow/v1/flows/#{flow_id}") result['_raw'] || '' end |
#valider_pdf_facturx(pdf_path, profil: 'EN16931') ⇒ Object
Valide un PDF Factur-X.
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 |
# File 'lib/factpulse/helpers/client.rb', line 454 def valider_pdf_facturx(pdf_path, profil: 'EN16931') ensure_authenticated uri = URI("#{@api_url}/api/v1/traitement/valider-pdf-facturx") pdf_content = File.binread(pdf_path) boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}" body = build_multipart_body(boundary, [ { name: 'fichier_pdf', content: pdf_content, filename: File.basename(pdf_path), content_type: 'application/pdf' }, { name: 'profil', content: profil } ]) response = http_multipart_post(uri, body, boundary) raise FactPulseValidationError.new("Validation error: #{response.code}") unless response.is_a?(Net::HTTPSuccess) JSON.parse(response.body) rescue {} end |
#valider_signature_pdf(pdf_path) ⇒ Object
Valide la signature d'un PDF signé.
487 488 489 490 491 492 493 494 495 496 497 498 499 500 |
# File 'lib/factpulse/helpers/client.rb', line 487 def valider_signature_pdf(pdf_path) ensure_authenticated uri = URI("#{@api_url}/api/v1/traitement/valider-signature-pdf") pdf_content = File.binread(pdf_path) boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}" body = build_multipart_body(boundary, [ { name: 'fichier_pdf', content: pdf_content, filename: File.basename(pdf_path), content_type: 'application/pdf' } ]) response = http_multipart_post(uri, body, boundary) raise FactPulseValidationError.new("Validation error: #{response.code}") unless response.is_a?(Net::HTTPSuccess) JSON.parse(response.body) rescue {} end |
#valider_xml_facturx(xml_content, profil: 'EN16931') ⇒ Object
Valide un XML Factur-X.
471 472 473 474 475 476 477 478 479 480 481 482 483 484 |
# File 'lib/factpulse/helpers/client.rb', line 471 def valider_xml_facturx(xml_content, profil: 'EN16931') ensure_authenticated uri = URI("#{@api_url}/api/v1/traitement/valider-xml") boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}" body = build_multipart_body(boundary, [ { name: 'fichier_xml', content: xml_content, filename: 'facture.xml', content_type: 'application/xml' }, { name: 'profil', content: profil } ]) response = http_multipart_post(uri, body, boundary) raise FactPulseValidationError.new("Validation error: #{response.code}") unless response.is_a?(Net::HTTPSuccess) JSON.parse(response.body) rescue {} end |