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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
require "date"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module Cpro
|
|
8
|
+
module Entities
|
|
9
|
+
class Base
|
|
10
|
+
attr_reader :payload
|
|
11
|
+
|
|
12
|
+
def initialize(payload = nil)
|
|
13
|
+
@payload = payload.is_a?(Hash) ? payload : {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_h
|
|
17
|
+
payload
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ==(other)
|
|
21
|
+
other.instance_of?(self.class) && other.payload == payload
|
|
22
|
+
end
|
|
23
|
+
alias eql? ==
|
|
24
|
+
|
|
25
|
+
def hash
|
|
26
|
+
[self.class, payload].hash
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def dig_payload(*keys)
|
|
32
|
+
keys.reduce(payload) { |value, key| value.is_a?(Hash) ? value[key] : nil }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
SANS_FUSEAU = /\A\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)?\z/.freeze
|
|
36
|
+
private_constant :SANS_FUSEAU
|
|
37
|
+
|
|
38
|
+
# Les API G2B datent en UTC mais omettent le marqueur de fuseau. Sans ce rattrapage,
|
|
39
|
+
# Time.parse lirait la valeur comme une heure locale et le même payload donnerait un instant
|
|
40
|
+
# différent selon le fuseau du serveur hôte. Vérifié en sandbox : un dépôt à 20:25:41 UTC est
|
|
41
|
+
# daté « 2026-08-14T20:26:02.125919 » par l'API.
|
|
42
|
+
def parse_time(value)
|
|
43
|
+
return nil if value.nil? || value.to_s.empty?
|
|
44
|
+
|
|
45
|
+
texte = value.to_s
|
|
46
|
+
texte = "#{texte}Z" if texte.match?(SANS_FUSEAU)
|
|
47
|
+
Time.parse(texte)
|
|
48
|
+
rescue ArgumentError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_date(value)
|
|
53
|
+
return nil if value.nil? || value.to_s.empty?
|
|
54
|
+
|
|
55
|
+
Date.parse(value.to_s)
|
|
56
|
+
rescue ArgumentError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Les montants arrivent en nombres JSON : on repasse par leur représentation textuelle pour
|
|
61
|
+
# éviter de traîner les approximations du flottant sur des sommes d'argent.
|
|
62
|
+
def parse_montant(value)
|
|
63
|
+
return nil if value.nil?
|
|
64
|
+
|
|
65
|
+
BigDecimal(value.to_s)
|
|
66
|
+
rescue ArgumentError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Entities
|
|
5
|
+
class ChangementStatut < Base
|
|
6
|
+
def statut
|
|
7
|
+
payload["statut"]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def date
|
|
11
|
+
@date ||= parse_time(payload["dateEtHeureMiseAJourStatut"])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def motifs_rejet
|
|
15
|
+
@motifs_rejet ||= Array(payload["detailStatut"]).map { |detail| MotifRejet.new(detail) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def rejet?
|
|
19
|
+
!motifs_rejet.empty?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
statut.to_s
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Entities
|
|
5
|
+
class Etablissement < Base
|
|
6
|
+
ETABLISSEMENT_PRINCIPAL = "P"
|
|
7
|
+
ETAT_ACTIF = "A"
|
|
8
|
+
|
|
9
|
+
def siret
|
|
10
|
+
payload["siret"]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def siren
|
|
14
|
+
payload["siren"]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def denomination
|
|
18
|
+
payload["denomination"]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def type_etablissement
|
|
22
|
+
payload["typeEtablissement"]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def principal?
|
|
26
|
+
type_etablissement == ETABLISSEMENT_PRINCIPAL
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def etat_administratif
|
|
30
|
+
payload["etatAdministratif"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def actif?
|
|
34
|
+
etat_administratif == ETAT_ACTIF
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def diffusible
|
|
38
|
+
payload["diffusible"]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def adresse
|
|
42
|
+
payload["adresse"] || {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def code_postal
|
|
46
|
+
adresse["codePostal"]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def localite
|
|
50
|
+
adresse["localite"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def unite_legale
|
|
54
|
+
return nil unless payload["uniteLegale"]
|
|
55
|
+
|
|
56
|
+
@unite_legale ||= UniteLegale.new(payload["uniteLegale"])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def publique?
|
|
60
|
+
unite_legale&.publique?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def donnees_b2g
|
|
64
|
+
payload["donneesB2gComplementaires"] || {}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def moa?
|
|
68
|
+
donnees_b2g["moa"]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def moa_uniquement?
|
|
72
|
+
donnees_b2g["moaUniquement"]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def gestion_engagement_juridique?
|
|
76
|
+
donnees_b2g["gestionEngagementJuridique"]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def gestion_engagement_juridique_ou_service?
|
|
80
|
+
donnees_b2g["gestionEngagementJuridiqueOuService"]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def gestion_code_service?
|
|
84
|
+
donnees_b2g["gestionCodeService"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def gestion_statut_mise_en_paiement?
|
|
88
|
+
donnees_b2g["gestionStatutMiseEnPaiement"]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def lignes_annuaire
|
|
92
|
+
@lignes_annuaire ||= LignesAnnuaire.new(payload["lignesAnnuaire"])
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Entities
|
|
5
|
+
class Facture < Base
|
|
6
|
+
STATUTS = %w[
|
|
7
|
+
DEPOSEE RECUE_DE_LA_PLATEFORME REFUSEE PAIEMENT_TRANSMIS ENCAISSEE REJETEE ANNULEE
|
|
8
|
+
EMISE_PAR_LA_PLATEFORME MISE_A_DISPOSITION PRISE_EN_CHARGE APPROUVEE APPROUVEE_PARTIELLEMENT
|
|
9
|
+
EN_LITIGE SUSPENDUE COMPLETEE VISEE ERREUR_ROUTAGE DEMANDE_DE_PAIEMENT_DIRECT
|
|
10
|
+
CHANGEMENT_DE_COMPTE_A_PAYER NON_AFFACTUREE AFFACTUREE AFFACTUREE_CONFIDENTIEL
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
CHAMPS = {
|
|
14
|
+
uid: "uidFacture",
|
|
15
|
+
numero: "numeroFacture",
|
|
16
|
+
code_type: "codeTypeFacture",
|
|
17
|
+
devise: "codeDeviseFacture",
|
|
18
|
+
objet: "idObjetFacture",
|
|
19
|
+
type_processus_metier: "typeProcessusMetier",
|
|
20
|
+
numero_facture_anterieure: "numeroFactureAnterieure",
|
|
21
|
+
raison_sociale_vendeur: "raisonSocialeVendeur",
|
|
22
|
+
siren_vendeur: "sirenVendeur",
|
|
23
|
+
siret_vendeur: "siretVendeur",
|
|
24
|
+
raison_sociale_agent_vendeur: "raisonSocialeAgentVendeur",
|
|
25
|
+
siren_agent_vendeur: "sirenAgentVendeur",
|
|
26
|
+
siret_agent_vendeur: "siretAgentVendeur",
|
|
27
|
+
raison_sociale_acheteur: "raisonSocialeAcheteur",
|
|
28
|
+
siren_acheteur: "sirenAcheteur",
|
|
29
|
+
siret_acheteur: "siretAcheteur",
|
|
30
|
+
id_adressage_acheteur: "idAdressageAcheteur",
|
|
31
|
+
siren_tiers_facturant: "sirenTiersFacturant",
|
|
32
|
+
siret_tiers_facturant: "siretTiersFacturant",
|
|
33
|
+
statut: "statutFacture",
|
|
34
|
+
nom_flux: "nomFlux",
|
|
35
|
+
uid_facture_principale: "uidFacturePrincipale"
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
MONTANTS = {
|
|
39
|
+
montant_total: "montantTotal",
|
|
40
|
+
montant_total_hors_tva: "montantTotalHorsTVA",
|
|
41
|
+
montant_tva: "montantTVA",
|
|
42
|
+
montant_paye: "montantPaye",
|
|
43
|
+
montant_a_payer: "montantAPayer"
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
CHAMPS.each { |name, key| define_method(name) { payload[key] } }
|
|
47
|
+
MONTANTS.each { |name, key| define_method(name) { parse_montant(payload[key]) } }
|
|
48
|
+
STATUTS.each { |valeur| define_method(:"#{valeur.downcase}?") { statut == valeur } }
|
|
49
|
+
|
|
50
|
+
def initialize(payload = nil, historique: nil)
|
|
51
|
+
super(payload)
|
|
52
|
+
@historique_payload = historique
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def date_emission
|
|
56
|
+
@date_emission ||= parse_date(payload["dateEmissionFacture"])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def date_echeance
|
|
60
|
+
@date_echeance ||= parse_date(payload["dateEcheanceFacture"])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def date_statut
|
|
64
|
+
@date_statut ||= parse_time(payload["dateStatutFacture"])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def multivendeurs?
|
|
68
|
+
payload["factureMultivendeurs"]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Renseigné par `Factures#find` seulement : la recherche ne retourne pas l'historique.
|
|
72
|
+
def historique
|
|
73
|
+
@historique ||= Array(@historique_payload).map { |changement| ChangementStatut.new(changement) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def motifs_rejet
|
|
77
|
+
historique.flat_map(&:motifs_rejet)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def to_s
|
|
81
|
+
[numero, statut].compact.join(" ")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Entities
|
|
5
|
+
class LigneAnnuaire < Base
|
|
6
|
+
PLATEFORME_ACTIVE = "Plateforme active"
|
|
7
|
+
PLATEFORME_EN_ATTENTE = "Plateforme en attente d'activation"
|
|
8
|
+
|
|
9
|
+
def identifiant_adressage
|
|
10
|
+
payload["identifiantAdressage"]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def siren
|
|
14
|
+
payload["siren"]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def siret
|
|
18
|
+
payload["siret"]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def identifiant_routage
|
|
22
|
+
payload["identifiantRoutage"]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def suffixe_adressage
|
|
26
|
+
payload["suffixeAdressage"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def statut_plateforme
|
|
30
|
+
payload["statutPlateforme"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def plateforme_active?
|
|
34
|
+
statut_plateforme == PLATEFORME_ACTIVE
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def plateforme_en_attente?
|
|
38
|
+
statut_plateforme == PLATEFORME_EN_ATTENTE
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Entities
|
|
5
|
+
class LignesAnnuaire < Base
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
def items
|
|
9
|
+
@items ||= Array(payload["items"]).map { |item| LigneAnnuaire.new(item) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def each(&block)
|
|
13
|
+
items.each(&block)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def nombre_total
|
|
17
|
+
payload["nombreTotal"]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def limite
|
|
21
|
+
payload["limite"]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def ignorer
|
|
25
|
+
payload["ignorer"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def size
|
|
29
|
+
items.size
|
|
30
|
+
end
|
|
31
|
+
alias length size
|
|
32
|
+
|
|
33
|
+
def empty?
|
|
34
|
+
items.empty?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Vrai tant que la page courante ne couvre pas la totalité des lignes rattachées.
|
|
38
|
+
def suite?
|
|
39
|
+
return false if nombre_total.nil?
|
|
40
|
+
|
|
41
|
+
(ignorer.to_i + size) < nombre_total
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Entities
|
|
5
|
+
class MotifRejet < Base
|
|
6
|
+
# Code du motif de rejet du CDV, selon la norme AFNOR.
|
|
7
|
+
def code
|
|
8
|
+
payload["codeMotifRejet"]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def libelle
|
|
12
|
+
payload["libelleMotifRejet"]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def notes
|
|
16
|
+
@notes ||= Array(payload["noteStatut"]).map { |note| NoteStatut.new(note) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_s
|
|
20
|
+
[code, libelle].compact.join(" — ")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Entities
|
|
5
|
+
class NoteStatut < Base
|
|
6
|
+
def code
|
|
7
|
+
payload["codeContenu"]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def contenu
|
|
11
|
+
payload["contenu"]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Nom du fichier ayant provoqué l'irrecevabilité, lorsque la note porte sur un flux.
|
|
15
|
+
def sujet
|
|
16
|
+
payload["sujet"]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_s
|
|
20
|
+
[code, contenu].compact.join(" — ")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Entities
|
|
5
|
+
class ResultatRecherche < Base
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
# Les deux API ne nomment pas le total de la même façon — `nombre_total_resultats` pour
|
|
9
|
+
# l'Annuaire, `nombreTotalResultats` pour Recherche Factures — d'où la clé explicite.
|
|
10
|
+
attr_reader :item_class, :total_key
|
|
11
|
+
|
|
12
|
+
def initialize(payload, item_class:, total_key:)
|
|
13
|
+
super(payload)
|
|
14
|
+
@item_class = item_class
|
|
15
|
+
@total_key = total_key
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def resultats
|
|
19
|
+
@resultats ||= Array(payload["resultats"]).map { |item| item_class.new(item) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def each(&block)
|
|
23
|
+
resultats.each(&block)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def nombre_total
|
|
27
|
+
payload[total_key]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Renseignés par Recherche Factures seulement : la recherche d'annuaire ne retourne pas ses
|
|
31
|
+
# paramètres de pagination. Combinés à `nombre_total`, ils permettent de paginer.
|
|
32
|
+
def limite
|
|
33
|
+
payload["limite"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ignores
|
|
37
|
+
payload["ignores"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def size
|
|
41
|
+
resultats.size
|
|
42
|
+
end
|
|
43
|
+
alias length size
|
|
44
|
+
|
|
45
|
+
def empty?
|
|
46
|
+
resultats.empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def ==(other)
|
|
50
|
+
super && other.item_class == item_class
|
|
51
|
+
end
|
|
52
|
+
alias eql? ==
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Entities
|
|
5
|
+
class StatutFlux < Base
|
|
6
|
+
RECEVABLE = "RECEVABLE"
|
|
7
|
+
IRRECEVABLE = "IRRECEVABLE"
|
|
8
|
+
EN_COURS = "ENTRANT_ENVELOPPE_VALIDE"
|
|
9
|
+
|
|
10
|
+
def statut
|
|
11
|
+
payload["statut"]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# 500 recevable, 501 irrecevable. Ajouté par Chorus Pro 5.2.2 (19/08/2026).
|
|
15
|
+
def code_statut
|
|
16
|
+
payload["codeStatut"]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def recevable?
|
|
20
|
+
statut == RECEVABLE
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def irrecevable?
|
|
24
|
+
statut == IRRECEVABLE
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def en_cours?
|
|
28
|
+
statut == EN_COURS
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Renseigné par le PPF uniquement lorsque le flux est recevable ; c'est cette valeur qui
|
|
32
|
+
# permet ensuite de retrouver la facture via l'API Recherche Factures G2B.
|
|
33
|
+
def nom_flux
|
|
34
|
+
payload["nomFlux"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def date_maj_statut
|
|
38
|
+
@date_maj_statut ||= parse_time(payload["dateMajStatut"])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def motifs_rejet
|
|
42
|
+
@motifs_rejet ||= Array(payload["detailStatut"]).map { |detail| MotifRejet.new(detail) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def rejet?
|
|
46
|
+
!motifs_rejet.empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s
|
|
50
|
+
statut.to_s
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
module Entities
|
|
5
|
+
class UniteLegale < Base
|
|
6
|
+
TYPE_PUBLIQUE = "Publique"
|
|
7
|
+
TYPE_PRIVEE = "Privée assujettie"
|
|
8
|
+
ETAT_ACTIF = "A"
|
|
9
|
+
|
|
10
|
+
def siren
|
|
11
|
+
payload["siren"]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def raison_sociale
|
|
15
|
+
payload["raisonSociale"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def type_entite
|
|
19
|
+
payload["typeEntite"]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def publique?
|
|
23
|
+
type_entite == TYPE_PUBLIQUE
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def privee?
|
|
27
|
+
type_entite == TYPE_PRIVEE
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def etat_administratif
|
|
31
|
+
payload["etatAdministratif"]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def actif?
|
|
35
|
+
etat_administratif == ETAT_ACTIF
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def diffusible
|
|
39
|
+
payload["diffusible"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def lignes_annuaire
|
|
43
|
+
@lignes_annuaire ||= LignesAnnuaire.new(payload["lignesAnnuaire"])
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "digest"
|
|
5
|
+
|
|
6
|
+
module Cpro
|
|
7
|
+
class Enveloppe
|
|
8
|
+
# Codes interfaces acceptés par POST /flux. Le dépôt de CDV emprunte le même endpoint que
|
|
9
|
+
# la facture : émettre un statut, c'est déposer un flux.
|
|
10
|
+
CODES_INTERFACE = {
|
|
11
|
+
ubl: "FSO3110A",
|
|
12
|
+
facturx: "FSO3117A",
|
|
13
|
+
ereporting: "FSO6000A",
|
|
14
|
+
cdv: "FSO3300A"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :content, :name, :code_interface
|
|
18
|
+
|
|
19
|
+
def self.from(source, code_interface:, name: nil)
|
|
20
|
+
if source.respond_to?(:read)
|
|
21
|
+
resolved = name || (File.basename(source.path) if source.respond_to?(:path))
|
|
22
|
+
new(content: source.read, name: resolved, code_interface: code_interface)
|
|
23
|
+
else
|
|
24
|
+
path = source.to_s
|
|
25
|
+
new(content: File.binread(path), name: name || File.basename(path),
|
|
26
|
+
code_interface: code_interface)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(content:, name:, code_interface:)
|
|
31
|
+
raise ArgumentError, "le contenu du flux est vide" if content.nil? || content.empty?
|
|
32
|
+
raise ArgumentError, "un nom de fichier est requis" if name.nil? || name.to_s.empty?
|
|
33
|
+
|
|
34
|
+
@content = content
|
|
35
|
+
@name = name.to_s
|
|
36
|
+
@code_interface = resolve_code_interface(code_interface)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def payload
|
|
40
|
+
{
|
|
41
|
+
"codeInterface" => code_interface,
|
|
42
|
+
"fichierFlux" => fichier_flux,
|
|
43
|
+
"checksum" => checksum
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def archive
|
|
48
|
+
@archive ||= Archive.gzip([{ name: name, content: content }])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fichier_flux
|
|
52
|
+
@fichier_flux ||= Base64.strict_encode64(archive)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Le swagger Dépôt flux G2B définit le checksum comme le sha256 « du fichier encodé », donc
|
|
56
|
+
# de la chaîne base64 et non des octets de l'archive. Confirmé en sandbox : un flux dont le
|
|
57
|
+
# checksum est calculé ainsi ressort RECEVABLE.
|
|
58
|
+
def checksum
|
|
59
|
+
@checksum ||= Digest::SHA256.hexdigest(fichier_flux)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def resolve_code_interface(value)
|
|
65
|
+
return CODES_INTERFACE.fetch(value) if CODES_INTERFACE.key?(value)
|
|
66
|
+
return value if CODES_INTERFACE.value?(value)
|
|
67
|
+
|
|
68
|
+
raise ArgumentError,
|
|
69
|
+
"code interface inconnu : #{value.inspect} " \
|
|
70
|
+
"(attendus : #{CODES_INTERFACE.keys.join(", ")})"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/cpro/errors.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ConfigurationError < Error; end
|
|
7
|
+
|
|
8
|
+
class ApiError < Error
|
|
9
|
+
attr_reader :status, :body, :correlation_id
|
|
10
|
+
|
|
11
|
+
def initialize(message, status: nil, body: nil, correlation_id: nil)
|
|
12
|
+
@status = status
|
|
13
|
+
@body = body
|
|
14
|
+
@correlation_id = correlation_id
|
|
15
|
+
super(message)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class RequestError < ApiError; end
|
|
20
|
+
class AuthenticationError < ApiError; end
|
|
21
|
+
class AuthorizationError < ApiError; end
|
|
22
|
+
class NotFoundError < ApiError; end
|
|
23
|
+
class RateLimitError < ApiError; end
|
|
24
|
+
class ServerError < ApiError; end
|
|
25
|
+
end
|