Skip to main content

gingr/
response.rs

1use crate::endpoint;
2use bytes::Bytes;
3use std::{collections::BTreeMap, fmt};
4
5#[derive(
6    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
7)]
8#[serde(transparent)]
9/// HTTP status wrapper used by Gingr transport and webhook acknowledgements.
10pub struct HttpStatus(u16);
11
12impl HttpStatus {
13    /// HTTP 200 acknowledgement used for accepted Gingr responses and webhooks.
14    pub const OK: Self = Self(200);
15    /// HTTP 403 status returned when Gingr rejects authorization or signature checks fail.
16    pub const FORBIDDEN: Self = Self(403);
17    /// HTTP 500 status used when downstream processing fails after a Gingr request.
18    pub const INTERNAL_SERVER_ERROR: Self = Self(500);
19
20    /// Wraps an already-observed Gingr identifier without claiming anything beyond provider provenance.
21    pub const fn new(value: u16) -> Self {
22        Self(value)
23    }
24
25    /// Returns the numeric HTTP status code.
26    pub const fn as_u16(self) -> u16 {
27        self.0
28    }
29
30    /// Classifies statuses that should trigger Gingr retry handling.
31    pub const fn is_gingr_retry_override_allowed(self) -> bool {
32        matches!(self.0, 100..=599) && self.0 != Self::OK.0 && self.0 != Self::FORBIDDEN.0
33    }
34}
35
36impl fmt::Display for HttpStatus {
37    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(formatter, "{}", self.0)
39    }
40}
41
42#[derive(Clone, Debug, PartialEq, Eq)]
43/// Raw Gingr response body paired with status for later DTO decoding.
44pub struct Raw {
45    status: HttpStatus,
46    body: Bytes,
47}
48
49impl Raw {
50    /// Constructs this typed Gingr boundary value after the caller has chosen the provider input to trust.
51    pub fn new(status: HttpStatus, body: impl Into<Bytes>) -> Self {
52        Self {
53            status,
54            body: body.into(),
55        }
56    }
57
58    /// Returns the HTTP status reported by Gingr.
59    pub fn status(&self) -> HttpStatus {
60        self.status
61    }
62
63    /// Returns the response body decoded from the Gingr transport.
64    pub fn body(&self) -> &[u8] {
65        &self.body
66    }
67}
68
69/// Provider response envelope boundary for Gingr provider payloads.
70pub mod provider {
71    use std::fmt;
72
73    #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
74    #[serde(transparent)]
75    /// Gingr error response DTO preserving provider diagnostics.
76    pub struct Error {
77        detail: String,
78    }
79
80    impl Error {
81        /// Constructs this typed Gingr boundary value after the caller has chosen the provider input to trust.
82        pub fn new(detail: impl Into<String>) -> Self {
83            Self {
84                detail: detail.into(),
85            }
86        }
87
88        /// Returns the provider error detail string, if Gingr supplied one.
89        pub fn detail(&self) -> &str {
90            &self.detail
91        }
92    }
93
94    impl fmt::Display for Error {
95        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96            formatter.write_str(&self.detail)
97        }
98    }
99
100    #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
101    /// Provider-specific webhook payload body retained for downstream DTO mapping.
102    pub struct Payload(pub serde_json::Value);
103
104    impl fmt::Display for Payload {
105        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106            formatter.write_str("<provider payload quarantined>")
107        }
108    }
109
110    #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
111    #[serde(transparent)]
112    /// Gingr owner email DTO used in response mapping.
113    pub struct Email(String);
114
115    impl Email {
116        /// Constructs this typed Gingr boundary value after the caller has chosen the provider input to trust.
117        pub fn new(value: impl Into<String>) -> Self {
118            Self(value.into())
119        }
120
121        /// Returns the normalized provider or storage string slice.
122        pub fn as_str(&self) -> &str {
123            &self.0
124        }
125    }
126
127    impl fmt::Display for Email {
128        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129            formatter.write_str(&self.0)
130        }
131    }
132}
133
134#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
135/// Raw Gingr webhook envelope before signature verification and required-field promotion.
136pub struct Envelope<T> {
137    /// Provider success flag from the Gingr envelope, retained as transport evidence rather than a domain outcome.
138    pub success: Option<bool>,
139    /// Provider error detail from the Gingr envelope, retained for diagnostics and retry decisions.
140    pub error: Option<provider::Error>,
141    /// Provider payload body carried inside the Gingr response envelope for later DTO decoding.
142    pub data: T,
143    #[serde(flatten)]
144    /// Extra provider fields preserved for audit and future mapping without becoming validated NVA facts.
145    pub unknown: serde_json::Map<String, serde_json::Value>,
146}
147
148#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
149/// Gingr owner response DTO before customer-domain mapping.
150pub struct OwnerRecord {
151    /// Provider record identifier observed in the Gingr payload.
152    pub id: endpoint::OwnerId,
153    #[serde(default)]
154    /// Owner first name observed in Gingr and used only as provider-sourced contact context.
155    pub first_name: Option<String>,
156    #[serde(default)]
157    /// Owner last name observed in Gingr and used only as provider-sourced contact context.
158    pub last_name: Option<String>,
159    #[serde(default)]
160    /// Email address observed from Gingr and carried as customer-contact evidence.
161    pub email: Option<provider::Email>,
162    #[serde(default, alias = "cell")]
163    /// Owner cell-phone value observed in Gingr and carried as contact evidence.
164    pub cell_phone: Option<String>,
165    #[serde(flatten)]
166    /// Extra provider fields preserved for audit and future mapping without becoming validated NVA facts.
167    pub unknown: BTreeMap<String, serde_json::Value>,
168}
169
170impl OwnerRecord {
171    /// Builds a readable owner name from first and last name fields.
172    pub fn display_name(&self) -> Option<String> {
173        let joined = [self.first_name.as_deref(), self.last_name.as_deref()]
174            .into_iter()
175            .flatten()
176            .map(str::trim)
177            .filter(|part| !part.is_empty())
178            .collect::<Vec<_>>()
179            .join(" ");
180        if joined.is_empty() {
181            None
182        } else {
183            Some(joined)
184        }
185    }
186}
187
188#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
189/// Gingr animal response DTO before pet-domain mapping.
190pub struct AnimalRecord {
191    /// Provider record identifier observed in the Gingr payload.
192    pub id: endpoint::AnimalId,
193    #[serde(default)]
194    /// Provider owner/customer identifier observed in the Gingr payload.
195    pub owner_id: Option<endpoint::OwnerId>,
196    #[serde(default)]
197    /// Provider display label retained for operator context; NVA-specific naming rules are applied downstream.
198    pub name: Option<String>,
199    #[serde(default)]
200    /// Provider species label for the animal; mapping code must validate any NVA pet-domain meaning separately.
201    pub species: Option<String>,
202    #[serde(default)]
203    /// Provider birthday string for the animal, retained raw because this crate does not validate age semantics.
204    pub birthday: Option<String>,
205    #[serde(flatten)]
206    /// Extra provider fields preserved for audit and future mapping without becoming validated NVA facts.
207    pub unknown: BTreeMap<String, serde_json::Value>,
208}
209
210#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
211/// Gingr reservation response DTO used to reconcile bookings and source evidence.
212pub struct ReservationRecord {
213    /// Provider record identifier observed in the Gingr payload.
214    pub id: endpoint::ReservationId,
215    #[serde(default)]
216    /// Provider owner/customer identifier observed in the Gingr payload.
217    pub owner_id: Option<endpoint::OwnerId>,
218    #[serde(default)]
219    /// Provider animal/pet identifier observed in the Gingr payload.
220    pub animal_id: Option<endpoint::AnimalId>,
221    #[serde(default)]
222    /// Provider status string preserved as source evidence until NVA validates a semantic status.
223    pub status: Option<String>,
224    #[serde(flatten)]
225    /// Extra provider fields preserved for audit and future mapping without becoming validated NVA facts.
226    pub unknown: BTreeMap<String, serde_json::Value>,
227}
228
229#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
230/// Gingr reference-data DTO for lookup tables such as locations or vets.
231pub struct ReferenceRecord {
232    /// Provider record identifier observed in the Gingr payload.
233    pub id: endpoint::ReferenceId,
234    #[serde(default)]
235    /// Provider display label retained for operator context; NVA-specific naming rules are applied downstream.
236    pub name: Option<String>,
237    #[serde(flatten)]
238    /// Extra provider fields preserved for audit and future mapping without becoming validated NVA facts.
239    pub unknown: BTreeMap<String, serde_json::Value>,
240}