factpulse 2.0.28 → 2.0.30
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 +4 -4
- data/CHANGELOG.md +3 -3
- data/Gemfile.lock +1 -1
- data/README.md +146 -133
- data/lib/factpulse/helpers/client.rb +426 -52
- data/lib/factpulse/version.rb +1 -1
- metadata +3 -3
- /data/lib/factpulse/{helpers.rb → helpers/helpers.rb} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78ca875881399d5d04602d4298a5fee55f3f7249fa347fe10e7aeb2fcb0dfd9d
|
|
4
|
+
data.tar.gz: b6ec7a089da11bc8b23e5aa4d181c027b7195f4e1c55c7ef9c5e7c06104831e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c0ed4f46576b6b09ce2751926b947fad1a6200cb42c4c5f725df79984530b7c507761ed9fdef006671517749fce5f3f3bb327c4b0d67f1f244fb2d3f1787f93
|
|
7
|
+
data.tar.gz: e5ab5b82a5eb675daa30dbb8e129ba468616c41126ec5ee9aa17d7d907a4c17565472a86f513dfe456113de53b5adf005106200a7775348b52e045d69c74aadc
|
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.
|
|
10
|
+
## [2.0.30] - 2025-11-29
|
|
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.
|
|
28
|
-
[2.0.
|
|
27
|
+
[Unreleased]: https://github.com/factpulse/sdk-ruby/compare/v2.0.30...HEAD
|
|
28
|
+
[2.0.30]: https://github.com/factpulse/sdk-ruby/releases/tag/v2.0.30
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Client Ruby officiel pour l'API FactPulse - Facturation électronique française.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Fonctionnalités
|
|
6
6
|
|
|
7
7
|
- **Factur-X** : Génération et validation de factures électroniques (profils MINIMUM, BASIC, EN16931, EXTENDED)
|
|
8
8
|
- **Chorus Pro** : Intégration avec la plateforme de facturation publique française
|
|
9
9
|
- **AFNOR PDP/PA** : Soumission de flux conformes à la norme XP Z12-013
|
|
10
10
|
- **Signature électronique** : Signature PDF (PAdES-B-B, PAdES-B-T, PAdES-B-LT)
|
|
11
11
|
- **Client simplifié** : Authentification JWT et polling intégrés via `helpers`
|
|
12
|
-
- **Ruby 2.7+** : Compatible avec les versions modernes de Ruby
|
|
13
12
|
|
|
14
|
-
##
|
|
13
|
+
## Installation
|
|
15
14
|
|
|
16
15
|
```bash
|
|
17
16
|
gem install factpulse
|
|
@@ -23,9 +22,7 @@ Ou dans votre Gemfile :
|
|
|
23
22
|
gem 'factpulse'
|
|
24
23
|
```
|
|
25
24
|
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
### Méthode recommandée : Client simplifié avec helpers
|
|
25
|
+
## Démarrage rapide
|
|
29
26
|
|
|
30
27
|
Le module `helpers` offre une API simplifiée avec authentification et polling automatiques :
|
|
31
28
|
|
|
@@ -33,175 +30,191 @@ Le module `helpers` offre une API simplifiée avec authentification et polling a
|
|
|
33
30
|
require 'factpulse'
|
|
34
31
|
require 'factpulse/helpers'
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
include Factpulse::Helpers
|
|
34
|
+
|
|
35
|
+
# Créer le client
|
|
36
|
+
client = FactPulseClient.new(
|
|
37
|
+
'votre_email@example.com',
|
|
38
|
+
'votre_mot_de_passe'
|
|
40
39
|
)
|
|
41
40
|
|
|
42
|
-
#
|
|
41
|
+
# Construire la facture avec les helpers
|
|
43
42
|
facture_data = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
fournisseur:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
montant_ht_total: '1000.00',
|
|
68
|
-
montant_tva: '200.00',
|
|
69
|
-
montant_ttc_total: '1200.00',
|
|
70
|
-
montant_a_payer: '1200.00'
|
|
71
|
-
},
|
|
72
|
-
lignes_de_poste: [{
|
|
73
|
-
numero: 1,
|
|
74
|
-
denomination: 'Prestation de conseil',
|
|
75
|
-
quantite: '10.00',
|
|
76
|
-
unite: 'PIECE',
|
|
77
|
-
montant_unitaire_ht: '100.00'
|
|
78
|
-
}]
|
|
43
|
+
numeroFacture: 'FAC-2025-001',
|
|
44
|
+
dateFacture: '2025-01-15',
|
|
45
|
+
fournisseur: fournisseur(
|
|
46
|
+
'Mon Entreprise SAS',
|
|
47
|
+
'12345678901234',
|
|
48
|
+
'123 Rue Example',
|
|
49
|
+
'75001',
|
|
50
|
+
'Paris'
|
|
51
|
+
),
|
|
52
|
+
destinataire: destinataire(
|
|
53
|
+
'Client SARL',
|
|
54
|
+
'98765432109876',
|
|
55
|
+
'456 Avenue Test',
|
|
56
|
+
'69001',
|
|
57
|
+
'Lyon'
|
|
58
|
+
),
|
|
59
|
+
montantTotal: montant_total(1000.00, 200.00, 1200.00, 1200.00),
|
|
60
|
+
lignesDePoste: [
|
|
61
|
+
ligne_de_poste(1, 'Prestation de conseil', 10, 100.00, 1000.00)
|
|
62
|
+
],
|
|
63
|
+
lignesDeTva: [
|
|
64
|
+
ligne_de_tva(1000.00, 200.00)
|
|
65
|
+
]
|
|
79
66
|
}
|
|
80
67
|
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# Générer le PDF Factur-X (polling automatique)
|
|
85
|
-
pdf_bytes = client.generer_facturx(
|
|
86
|
-
facture_data,
|
|
87
|
-
pdf_source,
|
|
88
|
-
profil: 'EN16931',
|
|
89
|
-
format_sortie: 'pdf',
|
|
90
|
-
sync: true # Attend le résultat automatiquement
|
|
91
|
-
)
|
|
68
|
+
# Générer le PDF Factur-X
|
|
69
|
+
pdf_bytes = client.generer_facturx(facture_data, 'facture_source.pdf', 'EN16931')
|
|
92
70
|
|
|
93
|
-
# Sauvegarder
|
|
94
71
|
File.binwrite('facture_facturx.pdf', pdf_bytes)
|
|
95
72
|
```
|
|
96
73
|
|
|
97
|
-
|
|
74
|
+
## Helpers disponibles (module Factpulse::Helpers)
|
|
98
75
|
|
|
99
|
-
|
|
76
|
+
### montant(value)
|
|
77
|
+
|
|
78
|
+
Convertit une valeur en string formaté pour les montants monétaires.
|
|
100
79
|
|
|
101
80
|
```ruby
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
config.host = 'https://factpulse.fr/api/facturation'
|
|
123
|
-
config.access_token = token
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# 3. Appeler l'API
|
|
127
|
-
api = Factpulse::TraitementFactureApi.new
|
|
128
|
-
response = api.generer_facture_api_v1_traitement_generer_facture_post(
|
|
129
|
-
facture_data.to_json,
|
|
130
|
-
'EN16931',
|
|
131
|
-
'pdf',
|
|
132
|
-
File.open('facture_source.pdf', 'rb')
|
|
81
|
+
include Factpulse::Helpers
|
|
82
|
+
|
|
83
|
+
montant(1234.5) # "1234.50"
|
|
84
|
+
montant('1234.56') # "1234.56"
|
|
85
|
+
montant(nil) # "0.00"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### montant_total(ht, tva, ttc, a_payer, ...)
|
|
89
|
+
|
|
90
|
+
Crée un objet MontantTotal complet.
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
total = montant_total(
|
|
94
|
+
1000.00, # ht
|
|
95
|
+
200.00, # tva
|
|
96
|
+
1200.00, # ttc
|
|
97
|
+
1200.00, # a_payer
|
|
98
|
+
50.00, # remise_ttc (optionnel)
|
|
99
|
+
'Fidélité', # motif_remise (optionnel)
|
|
100
|
+
100.00 # acompte (optionnel)
|
|
133
101
|
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### ligne_de_poste(numero, denomination, quantite, montant_unitaire_ht, montant_total_ligne_ht, ...)
|
|
134
105
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
106
|
+
Crée une ligne de facturation.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
ligne = ligne_de_poste(
|
|
110
|
+
1,
|
|
111
|
+
'Prestation de conseil',
|
|
112
|
+
5,
|
|
113
|
+
200.00,
|
|
114
|
+
1000.00, # montant_total_ligne_ht requis
|
|
115
|
+
'S', # categorie_tva: S, Z, E, AE, K
|
|
116
|
+
'HEURE', # unite: FORFAIT, PIECE, HEURE, JOUR...
|
|
117
|
+
{
|
|
118
|
+
taux_tva: 'TVA20', # Ou taux_tva_manuel: '20.00'
|
|
119
|
+
reference: 'REF-001'
|
|
120
|
+
}
|
|
121
|
+
)
|
|
138
122
|
```
|
|
139
123
|
|
|
140
|
-
|
|
124
|
+
### ligne_de_tva(montant_base_ht, montant_tva, ...)
|
|
141
125
|
|
|
142
|
-
|
|
143
|
-
|----------------|----------|---------|
|
|
144
|
-
| Authentification | Manuelle | Automatique |
|
|
145
|
-
| Refresh token | Manuel | Automatique |
|
|
146
|
-
| Polling tâches async | Manuel | Automatique (backoff) |
|
|
147
|
-
| Retry sur 401 | Manuel | Automatique |
|
|
126
|
+
Crée une ligne de ventilation TVA.
|
|
148
127
|
|
|
149
|
-
|
|
128
|
+
```ruby
|
|
129
|
+
tva = ligne_de_tva(
|
|
130
|
+
1000.00, # montant_base_ht
|
|
131
|
+
200.00, # montant_tva
|
|
132
|
+
'S', # categorie: S, Z, E, AE, K
|
|
133
|
+
{ taux: 'TVA20' } # Ou taux_manuel: '20.00'
|
|
134
|
+
)
|
|
135
|
+
```
|
|
150
136
|
|
|
151
|
-
###
|
|
137
|
+
### adresse_postale(ligne1, code_postal, ville, ...)
|
|
152
138
|
|
|
153
|
-
|
|
139
|
+
Crée une adresse postale structurée.
|
|
154
140
|
|
|
155
141
|
```ruby
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
142
|
+
adresse = adresse_postale(
|
|
143
|
+
'123 Rue de la République',
|
|
144
|
+
'75001',
|
|
145
|
+
'Paris',
|
|
146
|
+
'FR', # pays (défaut: 'FR')
|
|
147
|
+
'Bâtiment A' # ligne2 (optionnel)
|
|
160
148
|
)
|
|
161
149
|
```
|
|
162
150
|
|
|
163
|
-
###
|
|
151
|
+
### fournisseur(nom, siret, adresse_ligne1, code_postal, ville, options)
|
|
152
|
+
|
|
153
|
+
Crée un fournisseur complet avec calcul automatique du SIREN et TVA intra.
|
|
164
154
|
|
|
165
155
|
```ruby
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
156
|
+
f = fournisseur(
|
|
157
|
+
'Ma Société SAS',
|
|
158
|
+
'12345678901234',
|
|
159
|
+
'123 Rue Example',
|
|
160
|
+
'75001',
|
|
161
|
+
'Paris',
|
|
162
|
+
{ iban: 'FR7630006000011234567890189' }
|
|
173
163
|
)
|
|
164
|
+
# SIREN et TVA intracommunautaire calculés automatiquement
|
|
174
165
|
```
|
|
175
166
|
|
|
176
|
-
|
|
167
|
+
### destinataire(nom, siret, adresse_ligne1, code_postal, ville, options)
|
|
177
168
|
|
|
178
|
-
|
|
169
|
+
Crée un destinataire (client) avec calcul automatique du SIREN.
|
|
179
170
|
|
|
180
171
|
```ruby
|
|
181
|
-
|
|
182
|
-
|
|
172
|
+
d = destinataire(
|
|
173
|
+
'Client SARL',
|
|
174
|
+
'98765432109876',
|
|
175
|
+
'456 Avenue Test',
|
|
176
|
+
'69001',
|
|
177
|
+
'Lyon'
|
|
178
|
+
)
|
|
179
|
+
```
|
|
183
180
|
|
|
184
|
-
|
|
185
|
-
montant = 1234.56
|
|
181
|
+
## Mode Zero-Trust (Chorus Pro / AFNOR)
|
|
186
182
|
|
|
187
|
-
|
|
188
|
-
montant = 1234
|
|
183
|
+
Pour passer vos propres credentials sans stockage côté serveur :
|
|
189
184
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
185
|
+
```ruby
|
|
186
|
+
include Factpulse::Helpers
|
|
187
|
+
|
|
188
|
+
chorus_creds = ChorusProCredentials.new(
|
|
189
|
+
'votre_client_id',
|
|
190
|
+
'votre_client_secret',
|
|
191
|
+
'votre_login',
|
|
192
|
+
'votre_password',
|
|
193
|
+
true # sandbox
|
|
194
|
+
)
|
|
193
195
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
+
afnor_creds = AFNORCredentials.new(
|
|
197
|
+
'https://api.pdp.fr/flow/v1',
|
|
198
|
+
'https://auth.pdp.fr/oauth/token',
|
|
199
|
+
'votre_client_id',
|
|
200
|
+
'votre_client_secret'
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
client = FactPulseClient.new(
|
|
204
|
+
'votre_email@example.com',
|
|
205
|
+
'votre_mot_de_passe',
|
|
206
|
+
nil, # api_url
|
|
207
|
+
nil, # client_uid
|
|
208
|
+
chorus_creds,
|
|
209
|
+
afnor_creds
|
|
210
|
+
)
|
|
196
211
|
```
|
|
197
212
|
|
|
198
|
-
##
|
|
213
|
+
## Ressources
|
|
199
214
|
|
|
200
215
|
- **Documentation API** : https://factpulse.fr/api/facturation/documentation
|
|
201
|
-
- **Code source** : https://github.com/factpulse/sdk-ruby
|
|
202
|
-
- **Issues** : https://github.com/factpulse/sdk-ruby/issues
|
|
203
216
|
- **Support** : contact@factpulse.fr
|
|
204
217
|
|
|
205
|
-
##
|
|
218
|
+
## Licence
|
|
206
219
|
|
|
207
220
|
MIT License - Copyright (c) 2025 FactPulse
|
|
@@ -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), '
|
|
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['
|
|
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
|
-
|
|
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
|
data/lib/factpulse/version.rb
CHANGED
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.
|
|
4
|
+
version: 2.0.30
|
|
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-
|
|
11
|
+
date: 2025-11-29 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
|