factpulse 2.0.27 → 2.0.29

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08e17543bd509ca1f42cec62ef065447dcdbe63b0ba1796c401abd15b8d6cf1b'
4
- data.tar.gz: 179440ae99589fed9dd53f8fedb477f7897101d787dd7cd67cb4da9c5e77de38
3
+ metadata.gz: 3b4230cb60b7f639cf36283a1a93fd182dc089c2ef0e362d0882610fe59bb7a9
4
+ data.tar.gz: 88b51912bafaf2e0ca9e07c4a14308527af8535678821f519cab2e1aa0c0d407
5
5
  SHA512:
6
- metadata.gz: 1254176b5d2882369f50a5d540c7dac3d41cd94fc09f42d4e756152536e29ce7e40003366748bf9254954ce7bac7c6d416f72c1bcb029d2fd3765cad5149c620
7
- data.tar.gz: 25d1ba3573617f698c76632d413be403b341f981d25d484c74c72d50323fcb804c7128ff953c565cc74a9d2cf12db396badbbd377721068576dc560ecbcd06f5
6
+ metadata.gz: 613c3ac7e937bfb01e917c45f3c7b59d70355cda350a294dfbe2fc82d70629fe8d707dcd09d7aa80654c101ca7a7ad8fd1f74da76906ff271dfb4c1847a61f49
7
+ data.tar.gz: 4a63439a540ab0304cf77d582ba291fdca9b84e715d05117b9a6be43c6ee8c878213ec5eb508ca39efb9600c1b2e61e8413d91c30f985ae9f2663ff72321bcc9
data/CHANGELOG.md CHANGED
@@ -7,7 +7,7 @@ et ce projet adhère au [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [2.0.27] - 2025-11-27
10
+ ## [2.0.29] - 2025-11-28
11
11
 
12
12
  ### Added
13
13
  - Version initiale du SDK ruby
@@ -24,5 +24,5 @@ et ce projet adhère au [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
24
24
  - Guide d'authentification JWT
25
25
  - Configuration avancée (timeout, proxy, debug)
26
26
 
27
- [Unreleased]: https://github.com/factpulse/sdk-ruby/compare/v2.0.27...HEAD
28
- [2.0.27]: https://github.com/factpulse/sdk-ruby/releases/tag/v2.0.27
27
+ [Unreleased]: https://github.com/factpulse/sdk-ruby/compare/v2.0.29...HEAD
28
+ [2.0.29]: https://github.com/factpulse/sdk-ruby/releases/tag/v2.0.29
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- factpulse (2.0.27)
4
+ factpulse (2.0.29)
5
5
  typhoeus (~> 1.0, >= 1.0.1)
6
6
 
7
7
  GEM
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- require 'net/http'; require 'json'; require 'base64'; require 'uri'; require 'securerandom'
2
+ require 'net/http'; require 'json'; require 'base64'; require 'uri'; require 'securerandom'; require 'digest'; require 'tempfile'
3
3
 
4
4
  module FactPulse
5
5
  module Helpers
@@ -58,7 +58,7 @@ module FactPulse
58
58
  result = {
59
59
  'numero' => numero, 'denomination' => denomination,
60
60
  'quantite' => montant(quantite), 'montantUnitaireHt' => montant(montant_unitaire_ht),
61
- 'montantTotalLigneHt' => montant(montant_total_ligne_ht), 'tauxTva' => montant(taux_tva),
61
+ 'montantTotalLigneHt' => montant(montant_total_ligne_ht), 'tauxTvaManuel' => montant(taux_tva),
62
62
  'categorieTva' => categorie_tva, 'unite' => unite
63
63
  }
64
64
  result['reference'] = options[:reference] if options[:reference]
@@ -111,7 +111,7 @@ module FactPulse
111
111
  result['numeroTvaIntra'] = numero_tva_intra if numero_tva_intra
112
112
  result['iban'] = options[:iban] if options[:iban]
113
113
  result['idServiceFournisseur'] = options[:code_service] if options[:code_service]
114
- result['codeCoordonnesBancairesFournisseur'] = options[:code_coordonnees_bancaires] if options[:code_coordonnees_bancaires]
114
+ result['codeCoordonneeBancairesFournisseur'] = options[:code_coordonnees_bancaires] if options[:code_coordonnees_bancaires]
115
115
  result
116
116
  end
117
117
 
@@ -179,14 +179,6 @@ module FactPulse
179
179
  def self.format_montant(m); MontantHelpers.montant(m); end
180
180
 
181
181
  # Génère une facture Factur-X à partir d'un dict/hash et d'un PDF source.
182
- # Accepte un Hash, un String JSON, ou tout objet avec une méthode to_h/to_hash.
183
- # @param facture_data [Hash, String, Object] Données de la facture
184
- # @param pdf_source [String, File] Chemin vers le PDF source ou objet File
185
- # @param profil [String] Profil Factur-X (MINIMUM, BASIC, EN16931, EXTENDED)
186
- # @param format_sortie [String] Format de sortie (pdf, xml, both)
187
- # @param sync [Boolean] Mode synchrone (true) ou asynchrone (false)
188
- # @param timeout [Integer, nil] Timeout en ms pour le polling
189
- # @return [String] Contenu binaire du PDF généré
190
182
  def generer_facturx(facture_data, pdf_source, profil: 'EN16931', format_sortie: 'pdf', sync: true, timeout: nil)
191
183
  # Conversion des données en JSON string
192
184
  json_data = case facture_data
@@ -220,49 +212,17 @@ module FactPulse
220
212
 
221
213
  # Construire la requête multipart
222
214
  boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}"
223
- body = []
215
+ body = build_multipart_body(boundary, [
216
+ { name: 'donnees_facture', content: json_data },
217
+ { name: 'profil', content: profil },
218
+ { name: 'format_sortie', content: format_sortie },
219
+ { name: 'source_pdf', content: pdf_content, filename: pdf_filename, content_type: 'application/pdf' }
220
+ ])
224
221
 
225
- # Champ donnees_facture
226
- body << "--#{boundary}\r\n"
227
- body << "Content-Disposition: form-data; name=\"donnees_facture\"\r\n\r\n"
228
- body << "#{json_data}\r\n"
229
-
230
- # Champ profil
231
- body << "--#{boundary}\r\n"
232
- body << "Content-Disposition: form-data; name=\"profil\"\r\n\r\n"
233
- body << "#{profil}\r\n"
234
-
235
- # Champ format_sortie
236
- body << "--#{boundary}\r\n"
237
- body << "Content-Disposition: form-data; name=\"format_sortie\"\r\n\r\n"
238
- body << "#{format_sortie}\r\n"
239
-
240
- # Champ source_pdf (fichier)
241
- body << "--#{boundary}\r\n"
242
- body << "Content-Disposition: form-data; name=\"source_pdf\"; filename=\"#{pdf_filename}\"\r\n"
243
- body << "Content-Type: application/pdf\r\n\r\n"
244
- body << pdf_content
245
- body << "\r\n"
246
-
247
- body << "--#{boundary}--\r\n"
248
- body_str = body.join
249
-
250
- http = Net::HTTP.new(uri.host, uri.port)
251
- http.use_ssl = uri.scheme == 'https'
252
- http.read_timeout = 120
253
-
254
- request = Net::HTTP::Post.new(uri)
255
- request['Authorization'] = "Bearer #{@access_token}"
256
- request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
257
- request.body = body_str
258
-
259
- response = http.request(request)
222
+ response = http_multipart_post(uri, body, boundary)
260
223
 
261
224
  if response.code == '401'
262
- reset_auth
263
- ensure_authenticated
264
- request['Authorization'] = "Bearer #{@access_token}"
265
- response = http.request(request)
225
+ reset_auth; ensure_authenticated; response = http_multipart_post(uri, body, boundary)
266
226
  end
267
227
 
268
228
  unless response.is_a?(Net::HTTPSuccess)
@@ -275,7 +235,6 @@ module FactPulse
275
235
  if sync && data['id_tache']
276
236
  result = poll_task(data['id_tache'], timeout: timeout)
277
237
  if result['contenu_b64']
278
- require 'base64'
279
238
  return Base64.decode64(result['contenu_b64'])
280
239
  elsif result['contenu_xml']
281
240
  return result['contenu_xml']
@@ -286,15 +245,430 @@ module FactPulse
286
245
  data
287
246
  end
288
247
 
248
+ # =========================================================================
249
+ # AFNOR PDP - Authentication et helpers internes
250
+ # =========================================================================
251
+
252
+ private def get_afnor_credentials_internal
253
+ return @afnor_credentials if @afnor_credentials
254
+
255
+ ensure_authenticated
256
+ response = http_get(URI("#{@api_url}/api/v1/afnor/credentials"))
257
+ raise FactPulseAuthError, "Failed to get AFNOR credentials" unless response.is_a?(Net::HTTPSuccess)
258
+ creds = JSON.parse(response.body)
259
+ AFNORCredentials.new(
260
+ flow_service_url: creds['flow_service_url'],
261
+ token_url: creds['token_url'],
262
+ client_id: creds['client_id'],
263
+ client_secret: creds['client_secret'],
264
+ directory_service_url: creds['directory_service_url']
265
+ )
266
+ end
267
+
268
+ private def get_afnor_token_and_url
269
+ credentials = get_afnor_credentials_internal
270
+ uri = URI("#{@api_url}/api/v1/afnor/oauth/token")
271
+ http = Net::HTTP.new(uri.host, uri.port)
272
+ http.use_ssl = uri.scheme == 'https'
273
+ request = Net::HTTP::Post.new(uri)
274
+ request['X-PDP-Token-URL'] = credentials.token_url
275
+ request.set_form_data(
276
+ 'grant_type' => 'client_credentials',
277
+ 'client_id' => credentials.client_id,
278
+ 'client_secret' => credentials.client_secret
279
+ )
280
+ response = http.request(request)
281
+ raise FactPulseAuthError, "AFNOR OAuth2 failed" unless response.is_a?(Net::HTTPSuccess)
282
+ token_data = JSON.parse(response.body)
283
+ raise FactPulseAuthError, "Invalid AFNOR OAuth2 response" unless token_data['access_token']
284
+ { token: token_data['access_token'], pdp_base_url: credentials.flow_service_url }
285
+ end
286
+
287
+ private def make_afnor_request(method, endpoint, json_data: nil, multipart: nil)
288
+ token_info = get_afnor_token_and_url
289
+ uri = URI("#{@api_url}/api/v1/afnor#{endpoint}")
290
+ http = Net::HTTP.new(uri.host, uri.port)
291
+ http.use_ssl = uri.scheme == 'https'
292
+ http.read_timeout = 60
293
+
294
+ request = case method.upcase
295
+ when 'GET' then Net::HTTP::Get.new(uri)
296
+ when 'POST' then Net::HTTP::Post.new(uri)
297
+ else raise "Unsupported method: #{method}"
298
+ end
299
+
300
+ request['Authorization'] = "Bearer #{token_info[:token]}"
301
+ request['X-PDP-Base-URL'] = token_info[:pdp_base_url]
302
+
303
+ if multipart
304
+ boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}"
305
+ request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
306
+ request.body = build_multipart_body(boundary, multipart)
307
+ elsif json_data
308
+ request['Content-Type'] = 'application/json'
309
+ request.body = JSON.generate(json_data)
310
+ end
311
+
312
+ response = http.request(request)
313
+ raise FactPulseValidationError.new("AFNOR error: #{response.code} - #{response.body}") unless response.is_a?(Net::HTTPSuccess)
314
+
315
+ content_type = response['Content-Type'] || ''
316
+ if content_type.include?('application/json')
317
+ JSON.parse(response.body) rescue {}
318
+ else
319
+ { '_raw' => response.body }
320
+ end
321
+ end
322
+
323
+ # ==================== AFNOR Flow Service ====================
324
+
325
+ # Soumet une facture à une PDP via l'API AFNOR.
326
+ def soumettre_facture_afnor(pdf_path, flow_name, **options)
327
+ pdf_content = File.binread(pdf_path)
328
+ sha256 = Digest::SHA256.hexdigest(pdf_content)
329
+
330
+ flow_info = {
331
+ 'name' => flow_name,
332
+ 'flowSyntax' => options[:flow_syntax] || 'CII',
333
+ 'flowProfile' => options[:flow_profile] || 'EN16931',
334
+ 'sha256' => sha256
335
+ }
336
+ flow_info['trackingId'] = options[:tracking_id] if options[:tracking_id]
337
+
338
+ make_afnor_request('POST', '/flow/v1/flows', multipart: [
339
+ { name: 'file', content: pdf_content, filename: File.basename(pdf_path), content_type: 'application/pdf' },
340
+ { name: 'flowInfo', content: JSON.generate(flow_info), content_type: 'application/json' }
341
+ ])
342
+ end
343
+
344
+ # Recherche des flux de facturation AFNOR.
345
+ def rechercher_flux_afnor(**criteria)
346
+ search_body = {
347
+ 'offset' => criteria[:offset] || 0,
348
+ 'limit' => criteria[:limit] || 25,
349
+ 'where' => {}
350
+ }
351
+ search_body['where']['trackingId'] = criteria[:tracking_id] if criteria[:tracking_id]
352
+ search_body['where']['status'] = criteria[:status] if criteria[:status]
353
+
354
+ make_afnor_request('POST', '/flow/v1/flows/search', json_data: search_body)
355
+ end
356
+
357
+ # Télécharge le fichier PDF d'un flux AFNOR.
358
+ def telecharger_flux_afnor(flow_id)
359
+ result = make_afnor_request('GET', "/flow/v1/flows/#{flow_id}")
360
+ result['_raw'] || ''
361
+ end
362
+
363
+ # Vérifie la disponibilité du Flow Service AFNOR.
364
+ def healthcheck_afnor
365
+ make_afnor_request('GET', '/flow/v1/healthcheck')
366
+ end
367
+
368
+ # ==================== AFNOR Directory ====================
369
+
370
+ # Recherche une entreprise par SIRET dans l'annuaire AFNOR.
371
+ def rechercher_siret_afnor(siret)
372
+ make_afnor_request('GET', "/directory/siret/#{siret}")
373
+ end
374
+
375
+ # Recherche une entreprise par SIREN dans l'annuaire AFNOR.
376
+ def rechercher_siren_afnor(siren)
377
+ make_afnor_request('GET', "/directory/siren/#{siren}")
378
+ end
379
+
380
+ # Liste les codes de routage disponibles pour un SIREN.
381
+ def lister_codes_routage_afnor(siren)
382
+ make_afnor_request('GET', "/directory/siren/#{siren}/routing-codes")
383
+ end
384
+
385
+ # =========================================================================
386
+ # Chorus Pro
387
+ # =========================================================================
388
+
389
+ private def make_chorus_request(method, endpoint, json_data = nil)
390
+ ensure_authenticated
391
+ uri = URI("#{@api_url}/api/v1/chorus-pro#{endpoint}")
392
+ http = Net::HTTP.new(uri.host, uri.port)
393
+ http.use_ssl = uri.scheme == 'https'
394
+ http.read_timeout = 60
395
+
396
+ body = json_data || {}
397
+ body['credentials'] = @chorus_credentials.to_h if @chorus_credentials
398
+
399
+ request = case method.upcase
400
+ when 'GET' then Net::HTTP::Get.new(uri)
401
+ when 'POST' then Net::HTTP::Post.new(uri)
402
+ else raise "Unsupported method: #{method}"
403
+ end
404
+
405
+ request['Authorization'] = "Bearer #{@access_token}"
406
+ request['Content-Type'] = 'application/json'
407
+ request.body = JSON.generate(body) if body.any?
408
+
409
+ response = http.request(request)
410
+ raise FactPulseValidationError.new("Chorus Pro error: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
411
+ JSON.parse(response.body) rescue {}
412
+ end
413
+
414
+ # Recherche des structures sur Chorus Pro.
415
+ def rechercher_structure_chorus(identifiant_structure: nil, raison_sociale: nil, type_identifiant: 'SIRET', restreindre_privees: true)
416
+ body = { 'restreindre_structures_privees' => restreindre_privees }
417
+ body['identifiant_structure'] = identifiant_structure if identifiant_structure
418
+ body['raison_sociale_structure'] = raison_sociale if raison_sociale
419
+ body['type_identifiant_structure'] = type_identifiant if type_identifiant
420
+
421
+ make_chorus_request('POST', '/structures/rechercher', body)
422
+ end
423
+
424
+ # Consulte les détails d'une structure Chorus Pro.
425
+ def consulter_structure_chorus(id_structure_cpp)
426
+ make_chorus_request('POST', '/structures/consulter', { 'id_structure_cpp' => id_structure_cpp })
427
+ end
428
+
429
+ # Obtient l'ID Chorus Pro d'une structure depuis son SIRET.
430
+ def obtenir_id_chorus_depuis_siret(siret, type_identifiant: 'SIRET')
431
+ make_chorus_request('POST', '/structures/obtenir-id-depuis-siret', { 'siret' => siret, 'type_identifiant' => type_identifiant })
432
+ end
433
+
434
+ # Liste les services d'une structure Chorus Pro.
435
+ def lister_services_structure_chorus(id_structure_cpp)
436
+ make_chorus_request('GET', "/structures/#{id_structure_cpp}/services")
437
+ end
438
+
439
+ # Soumet une facture à Chorus Pro.
440
+ def soumettre_facture_chorus(facture_data)
441
+ make_chorus_request('POST', '/factures/soumettre', facture_data)
442
+ end
443
+
444
+ # Consulte le statut d'une facture Chorus Pro.
445
+ def consulter_facture_chorus(identifiant_facture_cpp)
446
+ make_chorus_request('POST', '/factures/consulter', { 'identifiant_facture_cpp' => identifiant_facture_cpp })
447
+ end
448
+
449
+ # =========================================================================
450
+ # Validation
451
+ # =========================================================================
452
+
453
+ # Valide un PDF Factur-X.
454
+ def valider_pdf_facturx(pdf_path, profil: 'EN16931')
455
+ ensure_authenticated
456
+ uri = URI("#{@api_url}/api/v1/traitement/valider-pdf-facturx")
457
+ pdf_content = File.binread(pdf_path)
458
+
459
+ boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}"
460
+ body = build_multipart_body(boundary, [
461
+ { name: 'fichier_pdf', content: pdf_content, filename: File.basename(pdf_path), content_type: 'application/pdf' },
462
+ { name: 'profil', content: profil }
463
+ ])
464
+
465
+ response = http_multipart_post(uri, body, boundary)
466
+ raise FactPulseValidationError.new("Validation error: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
467
+ JSON.parse(response.body) rescue {}
468
+ end
469
+
470
+ # Valide un XML Factur-X.
471
+ def valider_xml_facturx(xml_content, profil: 'EN16931')
472
+ ensure_authenticated
473
+ uri = URI("#{@api_url}/api/v1/traitement/valider-xml")
474
+
475
+ boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}"
476
+ body = build_multipart_body(boundary, [
477
+ { name: 'fichier_xml', content: xml_content, filename: 'facture.xml', content_type: 'application/xml' },
478
+ { name: 'profil', content: profil }
479
+ ])
480
+
481
+ response = http_multipart_post(uri, body, boundary)
482
+ raise FactPulseValidationError.new("Validation error: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
483
+ JSON.parse(response.body) rescue {}
484
+ end
485
+
486
+ # Valide la signature d'un PDF signé.
487
+ def valider_signature_pdf(pdf_path)
488
+ ensure_authenticated
489
+ uri = URI("#{@api_url}/api/v1/traitement/valider-signature-pdf")
490
+ pdf_content = File.binread(pdf_path)
491
+
492
+ boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}"
493
+ body = build_multipart_body(boundary, [
494
+ { name: 'fichier_pdf', content: pdf_content, filename: File.basename(pdf_path), content_type: 'application/pdf' }
495
+ ])
496
+
497
+ response = http_multipart_post(uri, body, boundary)
498
+ raise FactPulseValidationError.new("Validation error: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
499
+ JSON.parse(response.body) rescue {}
500
+ end
501
+
502
+ # =========================================================================
503
+ # Signature
504
+ # =========================================================================
505
+
506
+ # Signe un PDF avec le certificat configuré côté serveur.
507
+ def signer_pdf(pdf_path, **options)
508
+ ensure_authenticated
509
+ uri = URI("#{@api_url}/api/v1/traitement/signer-pdf")
510
+ pdf_content = File.binread(pdf_path)
511
+
512
+ parts = [
513
+ { name: 'fichier_pdf', content: pdf_content, filename: File.basename(pdf_path), content_type: 'application/pdf' },
514
+ { name: 'use_pades_lt', content: (options[:use_pades_lt] ? 'true' : 'false') },
515
+ { name: 'use_timestamp', content: (options.key?(:use_timestamp) ? (options[:use_timestamp] ? 'true' : 'false') : 'true') }
516
+ ]
517
+ parts << { name: 'raison', content: options[:raison] } if options[:raison]
518
+ parts << { name: 'localisation', content: options[:localisation] } if options[:localisation]
519
+ parts << { name: 'contact', content: options[:contact] } if options[:contact]
520
+
521
+ boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}"
522
+ body = build_multipart_body(boundary, parts)
523
+
524
+ response = http_multipart_post(uri, body, boundary)
525
+ raise FactPulseValidationError.new("Signature error: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
526
+
527
+ result = JSON.parse(response.body) rescue {}
528
+ raise FactPulseValidationError.new("Invalid signature response") unless result['pdf_signe_base64']
529
+ Base64.decode64(result['pdf_signe_base64'])
530
+ end
531
+
532
+ # Génère un certificat de test (NON PRODUCTION).
533
+ def generer_certificat_test(**options)
534
+ ensure_authenticated
535
+ uri = URI("#{@api_url}/api/v1/traitement/generer-certificat-test")
536
+ body = {
537
+ 'cn' => options[:cn] || 'Test Organisation',
538
+ 'organisation' => options[:organisation] || 'Test Organisation',
539
+ 'email' => options[:email] || 'test@example.com',
540
+ 'duree_jours' => options[:duree_jours] || 365,
541
+ 'taille_cle' => options[:taille_cle] || 2048
542
+ }
543
+
544
+ response = http_post_json(uri, body)
545
+ raise FactPulseValidationError.new("Error: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
546
+ JSON.parse(response.body) rescue {}
547
+ end
548
+
549
+ # =========================================================================
550
+ # Workflow complet
551
+ # =========================================================================
552
+
553
+ # Génère un PDF Factur-X complet avec validation, signature et soumission optionnelles.
554
+ def generer_facturx_complet(facture, pdf_source_path, **options)
555
+ profil = options[:profil] || 'EN16931'
556
+ valider = options.fetch(:valider, true)
557
+ signer = options.fetch(:signer, false)
558
+ soumettre_afnor = options.fetch(:soumettre_afnor, false)
559
+ timeout = options[:timeout] || 120000
560
+
561
+ result = {}
562
+
563
+ # 1. Génération
564
+ pdf_bytes = generer_facturx(facture, pdf_source_path, profil: profil, format_sortie: 'pdf', sync: true, timeout: timeout)
565
+ result[:pdf_bytes] = pdf_bytes
566
+
567
+ # Créer un fichier temporaire pour les opérations suivantes
568
+ temp_file = Tempfile.new(['facturx_', '.pdf'])
569
+ begin
570
+ temp_file.binmode
571
+ temp_file.write(pdf_bytes)
572
+ temp_file.flush
573
+
574
+ # 2. Validation
575
+ if valider
576
+ validation = valider_pdf_facturx(temp_file.path, profil: profil)
577
+ result[:validation] = validation
578
+ unless validation['est_conforme']
579
+ if options[:output_path]
580
+ File.binwrite(options[:output_path], pdf_bytes)
581
+ result[:pdf_path] = options[:output_path]
582
+ end
583
+ return result
584
+ end
585
+ end
586
+
587
+ # 3. Signature
588
+ if signer
589
+ pdf_bytes = signer_pdf(temp_file.path, **options)
590
+ result[:pdf_bytes] = pdf_bytes
591
+ result[:signature] = { 'signe' => true }
592
+ temp_file.rewind
593
+ temp_file.write(pdf_bytes)
594
+ temp_file.flush
595
+ end
596
+
597
+ # 4. Soumission AFNOR
598
+ if soumettre_afnor
599
+ numero_facture = facture['numeroFacture'] || facture['numero_facture'] || 'FACTURE'
600
+ flow_name = options[:afnor_flow_name] || "Facture #{numero_facture}"
601
+ tracking_id = options[:afnor_tracking_id] || numero_facture
602
+ afnor_result = soumettre_facture_afnor(temp_file.path, flow_name, tracking_id: tracking_id)
603
+ result[:afnor] = afnor_result
604
+ end
605
+
606
+ # Sauvegarde finale
607
+ if options[:output_path]
608
+ File.binwrite(options[:output_path], pdf_bytes)
609
+ result[:pdf_path] = options[:output_path]
610
+ end
611
+ ensure
612
+ temp_file.close
613
+ temp_file.unlink
614
+ end
615
+
616
+ result
617
+ end
618
+
289
619
  private
620
+
290
621
  def http_post(uri, payload)
291
622
  Net::HTTP.new(uri.host, uri.port).tap { |h| h.use_ssl = uri.scheme == 'https'; h.read_timeout = 30 }
292
623
  .request(Net::HTTP::Post.new(uri).tap { |r| r['Content-Type'] = 'application/json'; r.body = JSON.generate(payload) })
293
624
  end
625
+
626
+ def http_post_json(uri, payload)
627
+ http = Net::HTTP.new(uri.host, uri.port)
628
+ http.use_ssl = uri.scheme == 'https'
629
+ http.read_timeout = 30
630
+ request = Net::HTTP::Post.new(uri)
631
+ request['Authorization'] = "Bearer #{@access_token}"
632
+ request['Content-Type'] = 'application/json'
633
+ request.body = JSON.generate(payload)
634
+ http.request(request)
635
+ end
636
+
294
637
  def http_get(uri)
295
638
  Net::HTTP.new(uri.host, uri.port).tap { |h| h.use_ssl = uri.scheme == 'https'; h.read_timeout = 30 }
296
639
  .request(Net::HTTP::Get.new(uri).tap { |r| r['Authorization'] = "Bearer #{@access_token}" })
297
640
  end
641
+
642
+ def http_multipart_post(uri, body, boundary)
643
+ http = Net::HTTP.new(uri.host, uri.port)
644
+ http.use_ssl = uri.scheme == 'https'
645
+ http.read_timeout = 120
646
+
647
+ request = Net::HTTP::Post.new(uri)
648
+ request['Authorization'] = "Bearer #{@access_token}"
649
+ request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
650
+ request.body = body
651
+ http.request(request)
652
+ end
653
+
654
+ def build_multipart_body(boundary, parts)
655
+ body_parts = []
656
+ parts.each do |part|
657
+ body_parts << "--#{boundary}\r\n"
658
+ if part[:filename]
659
+ body_parts << "Content-Disposition: form-data; name=\"#{part[:name]}\"; filename=\"#{part[:filename]}\"\r\n"
660
+ body_parts << "Content-Type: #{part[:content_type] || 'application/octet-stream'}\r\n\r\n"
661
+ else
662
+ body_parts << "Content-Disposition: form-data; name=\"#{part[:name]}\"\r\n"
663
+ body_parts << "Content-Type: #{part[:content_type]}\r\n" if part[:content_type]
664
+ body_parts << "\r\n"
665
+ end
666
+ body_parts << part[:content]
667
+ body_parts << "\r\n"
668
+ end
669
+ body_parts << "--#{boundary}--\r\n"
670
+ body_parts.join
671
+ end
298
672
  end
299
673
  end
300
674
  end
@@ -11,5 +11,5 @@ Generator version: 7.18.0-SNAPSHOT
11
11
  =end
12
12
 
13
13
  module FactPulse
14
- VERSION = '2.0.27'
14
+ VERSION = '2.0.29'
15
15
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factpulse
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.27
4
+ version: 2.0.29
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenAPI-Generator
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-27 00:00:00.000000000 Z
11
+ date: 2025-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: typhoeus
@@ -229,9 +229,9 @@ files:
229
229
  - lib/factpulse/api_error.rb
230
230
  - lib/factpulse/api_model_base.rb
231
231
  - lib/factpulse/configuration.rb
232
- - lib/factpulse/helpers.rb
233
232
  - lib/factpulse/helpers/client.rb
234
233
  - lib/factpulse/helpers/exceptions.rb
234
+ - lib/factpulse/helpers/helpers.rb
235
235
  - lib/factpulse/models/adresse_electronique.rb
236
236
  - lib/factpulse/models/adresse_postale.rb
237
237
  - lib/factpulse/models/api_error.rb
File without changes