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_facture_entrante_afnor(flow_id, include_document: false) ⇒ Hash
Récupère les métadonnées JSON d'un flux entrant (facture fournisseur).
-
#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.
463 464 465 |
# File 'lib/factpulse/helpers/client.rb', line 463 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.
443 444 445 |
# File 'lib/factpulse/helpers/client.rb', line 443 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).
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 |
# File 'lib/factpulse/helpers/client.rb', line 551 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.
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 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 |
# File 'lib/factpulse/helpers/client.rb', line 572 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.
382 383 384 |
# File 'lib/factpulse/helpers/client.rb', line 382 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.
399 400 401 |
# File 'lib/factpulse/helpers/client.rb', line 399 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.
453 454 455 |
# File 'lib/factpulse/helpers/client.rb', line 453 def lister_services_structure_chorus(id_structure_cpp) make_chorus_request('GET', "/structures/#{id_structure_cpp}/services") end |
#obtenir_facture_entrante_afnor(flow_id, include_document: false) ⇒ Hash
Récupère les métadonnées JSON d'un flux entrant (facture fournisseur). Télécharge un flux entrant depuis la PDP AFNOR et extrait les métadonnées de la facture vers un format JSON unifié. Supporte Factur-X, CII et UBL.
375 376 377 378 379 |
# File 'lib/factpulse/helpers/client.rb', line 375 def obtenir_facture_entrante_afnor(flow_id, include_document: false) endpoint = "/flux-entrants/#{flow_id}" endpoint += "?include_document=true" if include_document make_afnor_request('GET', endpoint) end |
#obtenir_id_chorus_depuis_siret(siret, type_identifiant: 'SIRET') ⇒ Object
Obtient l'ID Chorus Pro d'une structure depuis son SIRET.
448 449 450 |
# File 'lib/factpulse/helpers/client.rb', line 448 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.
394 395 396 |
# File 'lib/factpulse/helpers/client.rb', line 394 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.
389 390 391 |
# File 'lib/factpulse/helpers/client.rb', line 389 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.
433 434 435 436 437 438 439 440 |
# File 'lib/factpulse/helpers/client.rb', line 433 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.
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 |
# File 'lib/factpulse/helpers/client.rb', line 525 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.
458 459 460 |
# File 'lib/factpulse/helpers/client.rb', line 458 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.
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 |
# File 'lib/factpulse/helpers/client.rb', line 472 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é.
505 506 507 508 509 510 511 512 513 514 515 516 517 518 |
# File 'lib/factpulse/helpers/client.rb', line 505 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.
489 490 491 492 493 494 495 496 497 498 499 500 501 502 |
# File 'lib/factpulse/helpers/client.rb', line 489 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 |