cpro-client 0.1.0
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 +7 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +42 -0
- data/CHANGELOG.md +29 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +79 -0
- data/LICENSE.txt +21 -0
- data/README.md +310 -0
- data/Rakefile +25 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/cpro-client.gemspec +32 -0
- data/lib/cpro/account.rb +32 -0
- data/lib/cpro/archive.rb +44 -0
- data/lib/cpro/auth/token.rb +36 -0
- data/lib/cpro/auth/token_provider.rb +59 -0
- data/lib/cpro/cdv/message.rb +230 -0
- data/lib/cpro/cdv/statut.rb +80 -0
- data/lib/cpro/client.rb +44 -0
- data/lib/cpro/configuration.rb +89 -0
- data/lib/cpro/connection.rb +19 -0
- data/lib/cpro/entities/base.rb +71 -0
- data/lib/cpro/entities/changement_statut.rb +27 -0
- data/lib/cpro/entities/depot_flux.rb +16 -0
- data/lib/cpro/entities/etablissement.rb +96 -0
- data/lib/cpro/entities/facture.rb +85 -0
- data/lib/cpro/entities/ligne_annuaire.rb +42 -0
- data/lib/cpro/entities/lignes_annuaire.rb +45 -0
- data/lib/cpro/entities/motif_rejet.rb +24 -0
- data/lib/cpro/entities/note_statut.rb +24 -0
- data/lib/cpro/entities/resultat_recherche.rb +55 -0
- data/lib/cpro/entities/statut_flux.rb +54 -0
- data/lib/cpro/entities/unite_legale.rb +47 -0
- data/lib/cpro/enveloppe.rb +73 -0
- data/lib/cpro/errors.rb +25 -0
- data/lib/cpro/facturx.rb +53 -0
- data/lib/cpro/middleware/authentication.rb +51 -0
- data/lib/cpro/middleware/raise_error.rb +50 -0
- data/lib/cpro/resources/annuaire.rb +110 -0
- data/lib/cpro/resources/base.rb +61 -0
- data/lib/cpro/resources/factures.rb +65 -0
- data/lib/cpro/resources/flux.rb +60 -0
- data/lib/cpro/version.rb +5 -0
- data/lib/cpro-client.rb +3 -0
- data/lib/cpro.rb +56 -0
- metadata +106 -0
data/lib/cpro/facturx.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
module Cpro
|
|
7
|
+
# Inspection minimale d'un PDF/A-3 Factur-X, sans dépendance à un analyseur PDF : de quoi
|
|
8
|
+
# attraper le mauvais fichier avant de le déposer, pas de quoi valider la facture.
|
|
9
|
+
module Facturx
|
|
10
|
+
ENTETE_PDF = "%PDF"
|
|
11
|
+
RACINE_CII = "CrossIndustryInvoice"
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def pdf?(contenu)
|
|
16
|
+
contenu.to_s.byteslice(0, ENTETE_PDF.bytesize) == ENTETE_PDF
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Les pièces jointes d'un PDF/A-3 sont presque toujours compressées en FlateDecode : on
|
|
20
|
+
# décompresse les flux jusqu'à retrouver la racine CII. Un XML stocké sans compression
|
|
21
|
+
# échapperait à cette recherche, d'où `xml_absent?` qui ne conclut que sur le cas certain.
|
|
22
|
+
def xml(contenu)
|
|
23
|
+
octets = contenu.to_s.b
|
|
24
|
+
octets.enum_for(:scan, /stream\r?\n/).each do
|
|
25
|
+
debut = Regexp.last_match.end(0)
|
|
26
|
+
fin = octets.index("endstream", debut) || next
|
|
27
|
+
decompresse = inflate(octets[debut...fin]) || next
|
|
28
|
+
return decompresse if decompresse.include?(RACINE_CII)
|
|
29
|
+
end
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def xml?(contenu)
|
|
34
|
+
!xml(contenu).nil?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Montant TTC déclaré par le XML embarqué (BT-112), à comparer par l'appelant à ce qu'il
|
|
38
|
+
# attend. La gem ne lit pas le PDF lisible et ne peut donc rien confronter elle-même.
|
|
39
|
+
def montant_total(contenu)
|
|
40
|
+
document = xml(contenu) or return nil
|
|
41
|
+
valeur = document[%r{<ram:GrandTotalAmount[^>]*>([^<]+)</ram:GrandTotalAmount>}, 1]
|
|
42
|
+
valeur && BigDecimal(valeur.strip)
|
|
43
|
+
rescue ArgumentError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def inflate(flux)
|
|
48
|
+
Zlib::Inflate.inflate(flux)
|
|
49
|
+
rescue Zlib::Error
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Middleware
|
|
5
|
+
# Injecte les deux niveaux d'authentification Chorus Pro : le jeton OAuth2 PISTE, commun à
|
|
6
|
+
# l'application, et le compte technique `cpro-account`, porté par requête via le contexte
|
|
7
|
+
# Faraday puisqu'il peut changer d'un appel à l'autre.
|
|
8
|
+
class Authentication < Faraday::Middleware
|
|
9
|
+
ACCOUNT_HEADER = "cpro-account"
|
|
10
|
+
|
|
11
|
+
def initialize(app, token_provider:)
|
|
12
|
+
super(app)
|
|
13
|
+
@token_provider = token_provider
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(env)
|
|
17
|
+
body = env.body
|
|
18
|
+
retried = false
|
|
19
|
+
|
|
20
|
+
begin
|
|
21
|
+
reset(env, body)
|
|
22
|
+
apply(env)
|
|
23
|
+
@app.call(env)
|
|
24
|
+
rescue AuthenticationError
|
|
25
|
+
raise if retried
|
|
26
|
+
|
|
27
|
+
retried = true
|
|
28
|
+
@token_provider.invalidate!
|
|
29
|
+
retry
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Le corps de `env` porte la requête à l'aller et la réponse au retour : il faut le restaurer
|
|
36
|
+
# avant de rejouer l'appel, sinon la seconde tentative posterait la réponse de la première.
|
|
37
|
+
def reset(env, body)
|
|
38
|
+
env.body = body
|
|
39
|
+
env.response = nil
|
|
40
|
+
env.status = nil
|
|
41
|
+
env.response_headers = nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def apply(env)
|
|
45
|
+
env.request_headers["Authorization"] = "Bearer #{@token_provider.token.value}"
|
|
46
|
+
account = env.request.context && env.request.context[:account]
|
|
47
|
+
env.request_headers[ACCOUNT_HEADER] = account.header_value if account
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Middleware
|
|
5
|
+
class RaiseError < Faraday::Middleware
|
|
6
|
+
ERRORS = {
|
|
7
|
+
400 => RequestError,
|
|
8
|
+
401 => AuthenticationError,
|
|
9
|
+
403 => AuthorizationError,
|
|
10
|
+
404 => NotFoundError,
|
|
11
|
+
406 => RequestError,
|
|
12
|
+
415 => RequestError,
|
|
13
|
+
422 => RequestError,
|
|
14
|
+
429 => RateLimitError
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
CORRELATION_HEADER = "x-correlationid"
|
|
18
|
+
|
|
19
|
+
def on_complete(env)
|
|
20
|
+
return if env.status < 400
|
|
21
|
+
|
|
22
|
+
raise error_class(env.status).new(
|
|
23
|
+
message_for(env),
|
|
24
|
+
status: env.status,
|
|
25
|
+
body: env.body,
|
|
26
|
+
correlation_id: env.response_headers && env.response_headers[CORRELATION_HEADER]
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def error_class(status)
|
|
33
|
+
ERRORS[status] || (status >= 500 ? ServerError : ApiError)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def message_for(env)
|
|
37
|
+
detail = detail_for(env.body) || env.reason_phrase
|
|
38
|
+
[env.status, detail].compact.join(" ")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def detail_for(body)
|
|
42
|
+
return body["message"] || body["error_description"] || body["error"] if body.is_a?(Hash)
|
|
43
|
+
return nil if body.nil?
|
|
44
|
+
|
|
45
|
+
text = body.to_s.strip
|
|
46
|
+
text.empty? ? nil : text[0, 500]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Resources
|
|
5
|
+
class Annuaire < Base
|
|
6
|
+
TOTAL_KEY = "nombre_total_resultats"
|
|
7
|
+
|
|
8
|
+
SIRET_LENGTH = 14
|
|
9
|
+
SIREN_LENGTH = 9
|
|
10
|
+
|
|
11
|
+
# Chaque champ filtrable n'admet qu'un seul opérateur dans le swagger : l'appelant fournit
|
|
12
|
+
# `siret: "702042755"` et l'opérateur attendu est déduit d'ici.
|
|
13
|
+
OPERATEURS_SIRET = {
|
|
14
|
+
"siret" => "contient",
|
|
15
|
+
"siren" => "contient",
|
|
16
|
+
"denomination" => "contient",
|
|
17
|
+
"localite" => "contient",
|
|
18
|
+
"codePostal" => "contient",
|
|
19
|
+
"lignesAdresse" => "contient",
|
|
20
|
+
"subDivisionPays" => "contient",
|
|
21
|
+
"typeEtablissement" => "strict",
|
|
22
|
+
"etatAdministratif" => "strict"
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
OPERATEURS_SIREN = {
|
|
26
|
+
"siren" => "contient",
|
|
27
|
+
"raisonSociale" => "contient",
|
|
28
|
+
"typeEntite" => "strict",
|
|
29
|
+
"etatAdministratif" => "strict"
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
def find_by_siret(siret, **options)
|
|
33
|
+
path = "siret/code-insee:#{normalize(siret, SIRET_LENGTH, "SIRET")}"
|
|
34
|
+
Entities::Etablissement.new(get(path, query(options)))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def find_by_siren(siren, **options)
|
|
38
|
+
path = "siren/code-insee:#{normalize(siren, SIREN_LENGTH, "SIREN")}"
|
|
39
|
+
Entities::UniteLegale.new(get(path, query(options)))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# `filtres` est un mot-clé et non un positionnel : avec `search_siret(filtres, **options)`,
|
|
43
|
+
# un appel de la forme `search_siret(siren: "702042755")` verrait tous ses mots-clés partir
|
|
44
|
+
# dans `options` sous Ruby 3 et lèverait une ArgumentError.
|
|
45
|
+
def search_siret(filtres: {}, **options)
|
|
46
|
+
payload = post("siret/recherche", search_body(OPERATEURS_SIRET, filtres, options))
|
|
47
|
+
Entities::ResultatRecherche.new(payload, item_class: Entities::Etablissement, total_key: TOTAL_KEY)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def search_siren(filtres: {}, **options)
|
|
51
|
+
payload = post("siren/recherche", search_body(OPERATEURS_SIREN, filtres, options))
|
|
52
|
+
Entities::ResultatRecherche.new(payload, item_class: Entities::UniteLegale, total_key: TOTAL_KEY)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def healthcheck
|
|
56
|
+
get("healthcheck")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def normalize(value, length, label)
|
|
62
|
+
digits = value.to_s.gsub(/\s+/, "")
|
|
63
|
+
return digits if digits.match?(/\A\d{#{length}}\z/)
|
|
64
|
+
|
|
65
|
+
raise ArgumentError, "#{label} invalide : #{value.inspect} (#{length} chiffres attendus)"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# `champs` et `inclure` sont déclarés `explode: false` : ils voyagent en liste séparée par
|
|
69
|
+
# des virgules, et non répétés.
|
|
70
|
+
def query(options)
|
|
71
|
+
{
|
|
72
|
+
champs: join(options[:champs]),
|
|
73
|
+
inclure: join(options[:inclure]),
|
|
74
|
+
limite: options[:limite],
|
|
75
|
+
ignorer: options[:ignorer]
|
|
76
|
+
}.compact
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def join(value)
|
|
80
|
+
return nil if value.nil?
|
|
81
|
+
|
|
82
|
+
Array(value).join(",")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def search_body(operateurs, filtres, options)
|
|
86
|
+
compact_body(
|
|
87
|
+
"filtres" => build_filtres(operateurs, filtres),
|
|
88
|
+
"champs" => Array(options[:champs]).map(&:to_s),
|
|
89
|
+
"inclure" => Array(options[:inclure]).map(&:to_s),
|
|
90
|
+
"tris" => build_tris(options[:tris]),
|
|
91
|
+
"limite" => options[:limite],
|
|
92
|
+
"ignorer" => options[:ignorer]
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def build_filtres(operateurs, filtres)
|
|
97
|
+
(filtres || {}).each_with_object({}) do |(champ, valeur), acc|
|
|
98
|
+
key = champ.to_s
|
|
99
|
+
operateur = operateurs[key]
|
|
100
|
+
unless operateur
|
|
101
|
+
raise ArgumentError,
|
|
102
|
+
"filtre inconnu : #{champ.inspect} (attendus : #{operateurs.keys.join(", ")})"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
acc[key] = { "op" => operateur, "valeur" => valeur.to_s }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Resources
|
|
5
|
+
class Base
|
|
6
|
+
def initialize(client, connection)
|
|
7
|
+
@client = client
|
|
8
|
+
@connection = connection
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
attr_reader :client, :connection
|
|
14
|
+
|
|
15
|
+
def get(path, params = {})
|
|
16
|
+
request(:get, path, params: params)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def post(path, body)
|
|
20
|
+
request(:post, path, body: body)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Le swagger Annuaire ne déclare pas cpro-account, mais l'API le rejette en 400 sans lui,
|
|
24
|
+
# healthcheck compris : toutes les ressources G2B l'exigent (vérifié en sandbox).
|
|
25
|
+
def request(method, path, params: nil, body: nil)
|
|
26
|
+
response = connection.public_send(method) do |request|
|
|
27
|
+
request.url(path, params || {})
|
|
28
|
+
request.body = body unless body.nil?
|
|
29
|
+
request.options.context = { account: required_account }
|
|
30
|
+
end
|
|
31
|
+
response.body
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def required_account
|
|
35
|
+
account = client.account
|
|
36
|
+
return account if account
|
|
37
|
+
|
|
38
|
+
raise ConfigurationError,
|
|
39
|
+
"un compte technique est requis pour #{self.class.name} : " \
|
|
40
|
+
"passez-le via Cpro::Client#with_account ou Cpro::Configuration#account"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def normalize_uid(uid, label)
|
|
44
|
+
value = uid.respond_to?(:uid) ? uid.uid : uid
|
|
45
|
+
raise ArgumentError, "#{label} manquant" if value.nil? || value.to_s.empty?
|
|
46
|
+
|
|
47
|
+
value.to_s
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_tris(tris)
|
|
51
|
+
return nil if tris.nil? || tris.empty?
|
|
52
|
+
|
|
53
|
+
tris.map { |champ, ordre| { "champ" => champ.to_s, "ordre" => ordre.to_s } }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def compact_body(body)
|
|
57
|
+
body.reject { |_, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Resources
|
|
5
|
+
class Factures < Base
|
|
6
|
+
# Contrairement à l'Annuaire, les filtres sont plats : `{"nomFlux" => "CPP004…"}` et non
|
|
7
|
+
# `{"op" => …, "valeur" => …}`. Les valeurs partent telles quelles, `codeTypeFacture` étant
|
|
8
|
+
# un entier.
|
|
9
|
+
FILTRES = %w[
|
|
10
|
+
numeroFacture dateEmissionFactureDu dateEmissionFactureAu codeTypeFacture idObjetFacture
|
|
11
|
+
siretVendeur siretAgentVendeur siretAcheteur sirenAcheteur siretTiersFacturant
|
|
12
|
+
dateStatutFactureDu dateStatutFactureAu nomFlux
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
TOTAL_KEY = "nombreTotalResultats"
|
|
16
|
+
|
|
17
|
+
def search(filtres:, **options)
|
|
18
|
+
payload = post("facture/recherche", search_body(filtres, options))
|
|
19
|
+
Entities::ResultatRecherche.new(payload, item_class: Entities::Facture, total_key: TOTAL_KEY)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Cinématique documentée après un dépôt : le nomFlux rendu par `Flux#status` mène aux
|
|
23
|
+
# factures que le flux contenait.
|
|
24
|
+
def search_by_nom_flux(nom_flux, **options)
|
|
25
|
+
search(filtres: { nomFlux: nom_flux.respond_to?(:nom_flux) ? nom_flux.nom_flux : nom_flux }, **options)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def find(uid)
|
|
29
|
+
payload = get("facture/#{normalize_uid(uid, "identifiant de facture")}")
|
|
30
|
+
payload = {} unless payload.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
Entities::Facture.new(payload["facture"], historique: payload["historique"])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def healthcheck
|
|
36
|
+
get("healthcheck")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def search_body(filtres, options)
|
|
42
|
+
compact_body(
|
|
43
|
+
"filtres" => build_filtres(filtres),
|
|
44
|
+
"champs" => Array(options[:champs]).map(&:to_s),
|
|
45
|
+
"tris" => build_tris(options[:tris]),
|
|
46
|
+
"limite" => options[:limite],
|
|
47
|
+
"ignorer" => options[:ignorer]
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build_filtres(filtres)
|
|
52
|
+
raise ArgumentError, "au moins un filtre est requis" if filtres.nil? || filtres.empty?
|
|
53
|
+
|
|
54
|
+
filtres.each_with_object({}) do |(champ, valeur), acc|
|
|
55
|
+
key = champ.to_s
|
|
56
|
+
unless FILTRES.include?(key)
|
|
57
|
+
raise ArgumentError, "filtre inconnu : #{champ.inspect} (attendus : #{FILTRES.join(", ")})"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
acc[key] = valeur
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Resources
|
|
5
|
+
class Flux < Base
|
|
6
|
+
def deposit_facturx(source, name: nil)
|
|
7
|
+
depot(source, :facturx, name: name) { |contenu| verifier_facturx(contenu) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def deposit_ubl(source, name: nil)
|
|
11
|
+
depot(source, :ubl, name: name)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Émettre un statut de cycle de vie : le fichier attendu est un CDAR D22B conforme à
|
|
15
|
+
# l'Annexe A de la norme AFNOR XP Z12-012. La gem le transporte, elle ne le fabrique pas.
|
|
16
|
+
def deposit_cdv(source, name: nil)
|
|
17
|
+
depot(source, :cdv, name: name)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def deposit_ereporting(source, name: nil)
|
|
21
|
+
depot(source, :ereporting, name: name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def emettre_statut(facture, statut, **options)
|
|
25
|
+
message = Cdv::Message.new(facture: facture, statut: statut, **options)
|
|
26
|
+
depot(StringIO.new(message.to_xml), :cdv, name: message.nom_fichier)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def status(uid)
|
|
30
|
+
status!(uid)
|
|
31
|
+
rescue NotFoundError
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def status!(uid)
|
|
36
|
+
Entities::StatutFlux.new(get("flux/#{normalize_uid(uid, "identifiant de flux")}"))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def healthcheck
|
|
40
|
+
get("healthcheck")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def depot(source, code_interface, name:)
|
|
46
|
+
enveloppe = Enveloppe.from(source, code_interface: code_interface, name: name)
|
|
47
|
+
yield enveloppe.content if block_given?
|
|
48
|
+
Entities::DepotFlux.new(post("flux", enveloppe.payload))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def verifier_facturx(contenu)
|
|
52
|
+
raise ArgumentError, "le fichier déposé n'est pas un PDF" unless Facturx.pdf?(contenu)
|
|
53
|
+
return if Facturx.xml?(contenu)
|
|
54
|
+
|
|
55
|
+
raise ArgumentError, "aucun XML Factur-X n'a été trouvé dans le PDF : vérifiez qu'il " \
|
|
56
|
+
"s'agit bien d'un PDF/A-3 avec sa pièce jointe factur-x.xml"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/cpro/version.rb
ADDED
data/lib/cpro-client.rb
ADDED
data/lib/cpro.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
|
|
5
|
+
require_relative "cpro/version"
|
|
6
|
+
require_relative "cpro/errors"
|
|
7
|
+
require_relative "cpro/configuration"
|
|
8
|
+
require_relative "cpro/account"
|
|
9
|
+
require_relative "cpro/auth/token"
|
|
10
|
+
require_relative "cpro/auth/token_provider"
|
|
11
|
+
require_relative "cpro/middleware/authentication"
|
|
12
|
+
require_relative "cpro/middleware/raise_error"
|
|
13
|
+
require_relative "cpro/connection"
|
|
14
|
+
require_relative "cpro/entities/base"
|
|
15
|
+
require_relative "cpro/entities/ligne_annuaire"
|
|
16
|
+
require_relative "cpro/entities/lignes_annuaire"
|
|
17
|
+
require_relative "cpro/entities/unite_legale"
|
|
18
|
+
require_relative "cpro/entities/etablissement"
|
|
19
|
+
require_relative "cpro/entities/note_statut"
|
|
20
|
+
require_relative "cpro/entities/motif_rejet"
|
|
21
|
+
require_relative "cpro/entities/changement_statut"
|
|
22
|
+
require_relative "cpro/entities/facture"
|
|
23
|
+
require_relative "cpro/entities/resultat_recherche"
|
|
24
|
+
require_relative "cpro/entities/depot_flux"
|
|
25
|
+
require_relative "cpro/entities/statut_flux"
|
|
26
|
+
require_relative "cpro/cdv/statut"
|
|
27
|
+
require_relative "cpro/cdv/message"
|
|
28
|
+
require_relative "cpro/facturx"
|
|
29
|
+
require_relative "cpro/archive"
|
|
30
|
+
require_relative "cpro/enveloppe"
|
|
31
|
+
require_relative "cpro/resources/base"
|
|
32
|
+
require_relative "cpro/resources/annuaire"
|
|
33
|
+
require_relative "cpro/resources/flux"
|
|
34
|
+
require_relative "cpro/resources/factures"
|
|
35
|
+
require_relative "cpro/client"
|
|
36
|
+
|
|
37
|
+
module Cpro
|
|
38
|
+
class << self
|
|
39
|
+
def configuration
|
|
40
|
+
@configuration ||= Configuration.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def configure
|
|
44
|
+
yield configuration
|
|
45
|
+
configuration
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def client(**options)
|
|
49
|
+
Client.new(configuration, **options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def reset!
|
|
53
|
+
@configuration = nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cpro-client
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Hôtentic
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-27 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
description: 'Client Ruby des API G2B de Chorus Pro (PPF) : authentification OAuth2
|
|
28
|
+
PISTE, consultation de l''annuaire par SIRET ou SIREN, et dépôt de factures Factur-X.'
|
|
29
|
+
email:
|
|
30
|
+
- contact@hotentic.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- ".gitignore"
|
|
36
|
+
- ".rubocop.yml"
|
|
37
|
+
- CHANGELOG.md
|
|
38
|
+
- CODE_OF_CONDUCT.md
|
|
39
|
+
- Gemfile
|
|
40
|
+
- Gemfile.lock
|
|
41
|
+
- LICENSE.txt
|
|
42
|
+
- README.md
|
|
43
|
+
- Rakefile
|
|
44
|
+
- bin/console
|
|
45
|
+
- bin/setup
|
|
46
|
+
- cpro-client.gemspec
|
|
47
|
+
- lib/cpro-client.rb
|
|
48
|
+
- lib/cpro.rb
|
|
49
|
+
- lib/cpro/account.rb
|
|
50
|
+
- lib/cpro/archive.rb
|
|
51
|
+
- lib/cpro/auth/token.rb
|
|
52
|
+
- lib/cpro/auth/token_provider.rb
|
|
53
|
+
- lib/cpro/cdv/message.rb
|
|
54
|
+
- lib/cpro/cdv/statut.rb
|
|
55
|
+
- lib/cpro/client.rb
|
|
56
|
+
- lib/cpro/configuration.rb
|
|
57
|
+
- lib/cpro/connection.rb
|
|
58
|
+
- lib/cpro/entities/base.rb
|
|
59
|
+
- lib/cpro/entities/changement_statut.rb
|
|
60
|
+
- lib/cpro/entities/depot_flux.rb
|
|
61
|
+
- lib/cpro/entities/etablissement.rb
|
|
62
|
+
- lib/cpro/entities/facture.rb
|
|
63
|
+
- lib/cpro/entities/ligne_annuaire.rb
|
|
64
|
+
- lib/cpro/entities/lignes_annuaire.rb
|
|
65
|
+
- lib/cpro/entities/motif_rejet.rb
|
|
66
|
+
- lib/cpro/entities/note_statut.rb
|
|
67
|
+
- lib/cpro/entities/resultat_recherche.rb
|
|
68
|
+
- lib/cpro/entities/statut_flux.rb
|
|
69
|
+
- lib/cpro/entities/unite_legale.rb
|
|
70
|
+
- lib/cpro/enveloppe.rb
|
|
71
|
+
- lib/cpro/errors.rb
|
|
72
|
+
- lib/cpro/facturx.rb
|
|
73
|
+
- lib/cpro/middleware/authentication.rb
|
|
74
|
+
- lib/cpro/middleware/raise_error.rb
|
|
75
|
+
- lib/cpro/resources/annuaire.rb
|
|
76
|
+
- lib/cpro/resources/base.rb
|
|
77
|
+
- lib/cpro/resources/factures.rb
|
|
78
|
+
- lib/cpro/resources/flux.rb
|
|
79
|
+
- lib/cpro/version.rb
|
|
80
|
+
homepage: https://hotentic.com
|
|
81
|
+
licenses:
|
|
82
|
+
- MIT
|
|
83
|
+
metadata:
|
|
84
|
+
homepage_uri: https://hotentic.com
|
|
85
|
+
source_code_uri: https://github.com/hotentic/cpro-client
|
|
86
|
+
changelog_uri: https://github.com/hotentic/cpro-client/releases
|
|
87
|
+
post_install_message:
|
|
88
|
+
rdoc_options: []
|
|
89
|
+
require_paths:
|
|
90
|
+
- lib
|
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: 2.7.0
|
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '0'
|
|
101
|
+
requirements: []
|
|
102
|
+
rubygems_version: 3.1.6
|
|
103
|
+
signing_key:
|
|
104
|
+
specification_version: 4
|
|
105
|
+
summary: A ruby client for the Chorus Pro API
|
|
106
|
+
test_files: []
|