factpulse 2.0.16 → 2.0.18
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/lib/factpulse/helpers/client.rb +109 -1
- data/lib/factpulse/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8c3b0a899b4de47edbb4b0430d2ccfd74bccf4df25b94051e0dbc79dcae0e97
|
|
4
|
+
data.tar.gz: 84913bfdf248d67d1a0aeb3054173ddd106097288445266767097c80487dfa9c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8db1cb3ed9a898dbd8ee368fe2d47e659964f2c0f2f071063becced842f151c652297e8601ed65d41e54e1cd893d8ec04ba60beb8ea953b1d015535116492042
|
|
7
|
+
data.tar.gz: bf90ed0c9ce3d375f00b5357ac1d8d557f21a772ca2de6129620a04f614397a8b4403810e3c0fb990a0c5ee42414de12b20275228b681c870854491a48ce7cee
|
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.18] - 2025-11-26
|
|
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.18...HEAD
|
|
28
|
+
[2.0.18]: https://github.com/factpulse/sdk-ruby/releases/tag/v2.0.18
|
data/Gemfile.lock
CHANGED
|
@@ -113,7 +113,7 @@ module FactPulse
|
|
|
113
113
|
start_time, current_interval = (Time.now.to_f * 1000).to_i, interval_ms.to_f
|
|
114
114
|
loop do
|
|
115
115
|
raise FactPulsePollingTimeout.new(task_id, timeout_ms) if (Time.now.to_f * 1000).to_i - start_time > timeout_ms
|
|
116
|
-
ensure_authenticated; response = http_get(URI("#{@api_url}/api/
|
|
116
|
+
ensure_authenticated; response = http_get(URI("#{@api_url}/api/v1/traitement/taches/#{task_id}/statut"))
|
|
117
117
|
reset_auth and next if response.code == '401'
|
|
118
118
|
data = JSON.parse(response.body)
|
|
119
119
|
return data['resultat'] || {} if data['statut'] == 'SUCCESS'
|
|
@@ -127,6 +127,114 @@ module FactPulse
|
|
|
127
127
|
|
|
128
128
|
def self.format_montant(m); MontantHelpers.montant(m); end
|
|
129
129
|
|
|
130
|
+
# Génère une facture Factur-X à partir d'un dict/hash et d'un PDF source.
|
|
131
|
+
# Accepte un Hash, un String JSON, ou tout objet avec une méthode to_h/to_hash.
|
|
132
|
+
# @param facture_data [Hash, String, Object] Données de la facture
|
|
133
|
+
# @param pdf_source [String, File] Chemin vers le PDF source ou objet File
|
|
134
|
+
# @param profil [String] Profil Factur-X (MINIMUM, BASIC, EN16931, EXTENDED)
|
|
135
|
+
# @param format_sortie [String] Format de sortie (pdf, xml, both)
|
|
136
|
+
# @param sync [Boolean] Mode synchrone (true) ou asynchrone (false)
|
|
137
|
+
# @param timeout [Integer, nil] Timeout en ms pour le polling
|
|
138
|
+
# @return [String] Contenu binaire du PDF généré
|
|
139
|
+
def generer_facturx(facture_data, pdf_source, profil: 'EN16931', format_sortie: 'pdf', sync: true, timeout: nil)
|
|
140
|
+
# Conversion des données en JSON string
|
|
141
|
+
json_data = case facture_data
|
|
142
|
+
when String then facture_data
|
|
143
|
+
when Hash then JSON.generate(facture_data)
|
|
144
|
+
else
|
|
145
|
+
if facture_data.respond_to?(:to_h)
|
|
146
|
+
JSON.generate(facture_data.to_h)
|
|
147
|
+
elsif facture_data.respond_to?(:to_hash)
|
|
148
|
+
JSON.generate(facture_data.to_hash)
|
|
149
|
+
else
|
|
150
|
+
raise FactPulseValidationError.new("Type de données non supporté: #{facture_data.class}")
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Lecture du PDF source
|
|
155
|
+
pdf_content = case pdf_source
|
|
156
|
+
when String then File.binread(pdf_source)
|
|
157
|
+
when File then pdf_source.read
|
|
158
|
+
else
|
|
159
|
+
if pdf_source.respond_to?(:read)
|
|
160
|
+
pdf_source.read
|
|
161
|
+
else
|
|
162
|
+
raise FactPulseValidationError.new("Type de PDF non supporté: #{pdf_source.class}")
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
pdf_filename = pdf_source.is_a?(String) ? File.basename(pdf_source) : 'facture.pdf'
|
|
166
|
+
|
|
167
|
+
ensure_authenticated
|
|
168
|
+
uri = URI("#{@api_url}/api/v1/traitement/generer-facture")
|
|
169
|
+
|
|
170
|
+
# Construire la requête multipart
|
|
171
|
+
boundary = "----RubyFormBoundary#{SecureRandom.hex(16)}"
|
|
172
|
+
body = []
|
|
173
|
+
|
|
174
|
+
# Champ donnees_facture
|
|
175
|
+
body << "--#{boundary}\r\n"
|
|
176
|
+
body << "Content-Disposition: form-data; name=\"donnees_facture\"\r\n\r\n"
|
|
177
|
+
body << "#{json_data}\r\n"
|
|
178
|
+
|
|
179
|
+
# Champ profil
|
|
180
|
+
body << "--#{boundary}\r\n"
|
|
181
|
+
body << "Content-Disposition: form-data; name=\"profil\"\r\n\r\n"
|
|
182
|
+
body << "#{profil}\r\n"
|
|
183
|
+
|
|
184
|
+
# Champ format_sortie
|
|
185
|
+
body << "--#{boundary}\r\n"
|
|
186
|
+
body << "Content-Disposition: form-data; name=\"format_sortie\"\r\n\r\n"
|
|
187
|
+
body << "#{format_sortie}\r\n"
|
|
188
|
+
|
|
189
|
+
# Champ source_pdf (fichier)
|
|
190
|
+
body << "--#{boundary}\r\n"
|
|
191
|
+
body << "Content-Disposition: form-data; name=\"source_pdf\"; filename=\"#{pdf_filename}\"\r\n"
|
|
192
|
+
body << "Content-Type: application/pdf\r\n\r\n"
|
|
193
|
+
body << pdf_content
|
|
194
|
+
body << "\r\n"
|
|
195
|
+
|
|
196
|
+
body << "--#{boundary}--\r\n"
|
|
197
|
+
body_str = body.join
|
|
198
|
+
|
|
199
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
200
|
+
http.use_ssl = uri.scheme == 'https'
|
|
201
|
+
http.read_timeout = 120
|
|
202
|
+
|
|
203
|
+
request = Net::HTTP::Post.new(uri)
|
|
204
|
+
request['Authorization'] = "Bearer #{@access_token}"
|
|
205
|
+
request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
|
|
206
|
+
request.body = body_str
|
|
207
|
+
|
|
208
|
+
response = http.request(request)
|
|
209
|
+
|
|
210
|
+
if response.code == '401'
|
|
211
|
+
reset_auth
|
|
212
|
+
ensure_authenticated
|
|
213
|
+
request['Authorization'] = "Bearer #{@access_token}"
|
|
214
|
+
response = http.request(request)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
218
|
+
error_data = JSON.parse(response.body) rescue { 'detail' => response.body }
|
|
219
|
+
raise FactPulseValidationError.new("Erreur API: #{error_data['detail'] || response.body}")
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
data = JSON.parse(response.body)
|
|
223
|
+
|
|
224
|
+
if sync && data['id_tache']
|
|
225
|
+
result = poll_task(data['id_tache'], timeout: timeout)
|
|
226
|
+
if result['contenu_b64']
|
|
227
|
+
require 'base64'
|
|
228
|
+
return Base64.decode64(result['contenu_b64'])
|
|
229
|
+
elsif result['contenu_xml']
|
|
230
|
+
return result['contenu_xml']
|
|
231
|
+
end
|
|
232
|
+
raise FactPulseValidationError.new("Résultat inattendu: #{result.keys.join(', ')}")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
data
|
|
236
|
+
end
|
|
237
|
+
|
|
130
238
|
private
|
|
131
239
|
def http_post(uri, payload)
|
|
132
240
|
Net::HTTP.new(uri.host, uri.port).tap { |h| h.use_ssl = uri.scheme == 'https'; h.read_timeout = 30 }
|
data/lib/factpulse/version.rb
CHANGED