Skip to main content

koprogo_api/infrastructure/external/
signature_provider_universign.rs

1//! Adaptateur Universign — prestataire tiers, repli pour les signataires
2//! non-BE (Story 4.4, ADR-0014 §4).
3//!
4//! Deux modes :
5//! - `new(...)` — appelle un vrai endpoint Universign via le contrat REST
6//!   commun (`signature_provider_common::RestSignatureProvider`).
7//! - `simulated(...)` — **adaptateur simulé utilisable en développement**
8//!   (DoD Story 4.4) : aucun appel réseau, la signature est immédiate et
9//!   déterministe. À ne jamais construire en production — sert à faire
10//!   tourner un flux de signature complet en local ou en tests sans dépendre
11//!   d'un vrai compte Universign.
12
13use super::signature_provider_common::{compute_document_hmac, RestSignatureProvider, RetryPolicy};
14use crate::application::ports::electronic_signature_provider::{
15    ElectronicSignatureProvider, SignatureProviderError, SignatureRequest, SignatureRequestAck,
16};
17use crate::domain::plateforme::{QualifiedSignature, SignatureProviderKind};
18use async_trait::async_trait;
19use chrono::Utc;
20use uuid::Uuid;
21
22pub enum UniversignSignatureProvider {
23    Http(RestSignatureProvider),
24    /// Dev-only : signe immédiatement en mémoire, sans réseau.
25    Simulated {
26        hmac_secret: Vec<u8>,
27    },
28}
29
30impl UniversignSignatureProvider {
31    pub fn new(base_url: String, hmac_secret: Vec<u8>) -> Self {
32        Self::Http(RestSignatureProvider::new(
33            SignatureProviderKind::Universign,
34            base_url,
35            hmac_secret,
36        ))
37    }
38
39    /// Prestataire simulé utilisable en développement (DoD Story 4.4).
40    pub fn simulated(hmac_secret: Vec<u8>) -> Self {
41        Self::Simulated { hmac_secret }
42    }
43
44    /// Permet aux tests d'utiliser un délai de réessai quasi nul (mode Http).
45    pub fn with_retry_policy(self, retry_policy: RetryPolicy) -> Self {
46        match self {
47            Self::Http(inner) => Self::Http(inner.with_retry_policy(retry_policy)),
48            simulated => simulated,
49        }
50    }
51}
52
53/// Référence simulée : encode `document_id` et `subject_user_id` — le mode
54/// simulé ne persiste rien entre `request_signature` et `fetch_signature`,
55/// il reconstitue l'état à partir de la référence elle-même.
56fn simulated_reference(document_id: Uuid, subject_user_id: Uuid) -> String {
57    format!("sim:{document_id}:{subject_user_id}")
58}
59
60fn parse_simulated_reference(reference: &str) -> Result<(Uuid, Uuid), SignatureProviderError> {
61    let invalid = || {
62        SignatureProviderError::InvalidResponse(
63            SignatureProviderKind::Universign.to_string(),
64            format!("référence simulée invalide : {reference}"),
65        )
66    };
67    let rest = reference.strip_prefix("sim:").ok_or_else(invalid)?;
68    let mut parts = rest.split(':');
69    let document_id = parts
70        .next()
71        .and_then(|s| Uuid::parse_str(s).ok())
72        .ok_or_else(invalid)?;
73    let subject_user_id = parts
74        .next()
75        .and_then(|s| Uuid::parse_str(s).ok())
76        .ok_or_else(invalid)?;
77    Ok((document_id, subject_user_id))
78}
79
80#[async_trait]
81impl ElectronicSignatureProvider for UniversignSignatureProvider {
82    fn kind(&self) -> SignatureProviderKind {
83        SignatureProviderKind::Universign
84    }
85
86    async fn request_signature(
87        &self,
88        request: SignatureRequest,
89    ) -> Result<SignatureRequestAck, SignatureProviderError> {
90        match self {
91            Self::Http(inner) => inner.request_signature(request).await,
92            Self::Simulated { hmac_secret } => {
93                let document_hash = compute_document_hmac(hmac_secret, &request.document_bytes);
94                Ok(SignatureRequestAck {
95                    provider_reference: simulated_reference(
96                        request.document_id,
97                        request.subject_user_id,
98                    ),
99                    document_hash,
100                })
101            }
102        }
103    }
104
105    async fn fetch_signature(
106        &self,
107        provider_reference: &str,
108        expected_document_hash: &str,
109    ) -> Result<QualifiedSignature, SignatureProviderError> {
110        match self {
111            Self::Http(inner) => {
112                inner
113                    .fetch_signature(provider_reference, expected_document_hash)
114                    .await
115            }
116            Self::Simulated { .. } => {
117                let (document_id, subject_user_id) = parse_simulated_reference(provider_reference)?;
118                QualifiedSignature::new(
119                    SignatureProviderKind::Universign,
120                    subject_user_id,
121                    document_id,
122                    expected_document_hash.to_string(),
123                    provider_reference.to_string(),
124                    Utc::now(),
125                )
126                .map_err(|e| {
127                    SignatureProviderError::InvalidResponse(
128                        SignatureProviderKind::Universign.to_string(),
129                        e.to_string(),
130                    )
131                })
132            }
133        }
134    }
135}