Skip to main content

alat/
envelope.rs

1//! Response envelopes used across the ALAT API surface.
2//!
3//! Every ALAT endpoint wraps its payload in one of **three** envelope shapes.
4//! Modeling them generically here — rather than re-inventing a flat struct per
5//! endpoint — is what lets the SDK actually deserialize live responses, and it
6//! gives every call a uniform way to distinguish "the bank said no" (a business
7//! failure carried *inside* a `200 OK`) from transport/HTTP errors.
8//!
9//! | Envelope            | Shape (success flag)                                            | Used by |
10//! |---------------------|-----------------------------------------------------------------|---------|
11//! | [`ServiceResponse`] | `{ result, successful, message }`                               | account, transaction history, funds transfer |
12//! | [`Envelope`]        | `{ result, errorMessage, errorMessages, hasError, timeGenerated }` | name enquiry, bills, airtime |
13//! | [`ApiResponse`]     | `{ data, status, message, code, statusCode, errors }`           | wallet creation, statement (with payload) |
14//! | [`StatusResponse`]  | `{ status, message, code, statusCode, errors }` (no payload)    | wallet creation steps, debit restriction |
15//!
16//! Each wrapper exposes `into_result()` which returns the inner payload on
17//! success or an [`Error::Api`] carrying the bank's own message/code on failure.
18
19use crate::error::{Error, Result};
20use serde::Deserialize;
21
22/// The `ServiceResponse<T>` / `*Envelope` shape that uses a boolean
23/// [`successful`](Self::successful) flag.
24///
25/// Returned by account maintenance (`GetAccountV2`, `transhistoryV2`) and the
26/// funds-transfer product (`GetAllBanks`, `transfer-fund-request`). On the wire:
27/// `{ "result": <T>, "successful": true, "message": "..." }`.
28#[derive(Debug, Clone, Deserialize)]
29pub struct ServiceResponse<T> {
30    /// The business payload. `None` when the call failed or returned no body.
31    #[serde(default = "none")]
32    pub result: Option<T>,
33    /// Whether the operation succeeded. This is the authoritative success flag.
34    #[serde(default)]
35    pub successful: bool,
36    /// Optional human-readable status/diagnostic message.
37    #[serde(default)]
38    pub message: Option<String>,
39}
40
41impl<T> ServiceResponse<T> {
42    /// Unwraps to the inner payload, or an [`Error::Api`] if `successful` is
43    /// `false` (or the payload is missing on an otherwise "successful" reply).
44    pub fn into_result(self) -> Result<T> {
45        if self.successful {
46            self.result.ok_or_else(|| Error::Api {
47                message: self
48                    .message
49                    .unwrap_or_else(|| "successful response carried no `result` payload".into()),
50                code: None,
51                errors: Vec::new(),
52            })
53        } else {
54            Err(Error::Api {
55                message: self.message.unwrap_or_else(|| "request was not successful".into()),
56                code: None,
57                errors: Vec::new(),
58            })
59        }
60    }
61}
62
63/// The `*Envelope` shape that reports failure via [`has_error`](Self::has_error)
64/// plus error message lists.
65///
66/// Returned by name enquiry, bills, and airtime. On the wire:
67/// `{ "result": <T>, "errorMessage": "...", "errorMessages": ["..."],
68/// "hasError": false, "timeGenerated": "..." }`.
69#[derive(Debug, Clone, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub struct Envelope<T> {
72    /// The business payload. `None` when the call failed.
73    #[serde(default = "none")]
74    pub result: Option<T>,
75    /// A single summary error message (present when `has_error` is true).
76    #[serde(default)]
77    pub error_message: Option<String>,
78    /// Granular, field-level error messages.
79    #[serde(default)]
80    pub error_messages: Vec<String>,
81    /// Authoritative failure flag: `true` means the bank rejected the request.
82    #[serde(default)]
83    pub has_error: bool,
84    /// Server timestamp the response was generated (ISO-8601), if provided.
85    #[serde(default)]
86    pub time_generated: Option<String>,
87}
88
89impl<T> Envelope<T> {
90    /// Unwraps to the inner payload, or an [`Error::Api`] when `has_error` is
91    /// `true` (or the payload is missing).
92    pub fn into_result(self) -> Result<T> {
93        if self.has_error {
94            let message = self
95                .error_message
96                .filter(|m| !m.is_empty())
97                .or_else(|| self.error_messages.first().cloned())
98                .unwrap_or_else(|| "request returned an error".into());
99            return Err(Error::Api {
100                message,
101                code: None,
102                errors: self.error_messages,
103            });
104        }
105        self.result.ok_or_else(|| Error::Api {
106            message: "response reported no error but carried no `result` payload".into(),
107            code: None,
108            errors: Vec::new(),
109        })
110    }
111}
112
113/// The `ResponseModel` shape (with a payload) that reports success via a boolean
114/// [`status`](Self::status) and nests data under `data`.
115///
116/// Returned by statement and the wallet-creation account-details lookup. On the
117/// wire: `{ "data": <T>, "status": true, "message": "...", "code": "...",
118/// "statusCode": "...", "errors": [...] }`.
119#[derive(Debug, Clone, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct ApiResponse<T> {
122    /// The business payload, nested under `data`. `None` on failure.
123    #[serde(default = "none")]
124    pub data: Option<T>,
125    /// Authoritative success flag.
126    #[serde(default)]
127    pub status: bool,
128    /// Human-readable message.
129    #[serde(default)]
130    pub message: Option<String>,
131    /// Machine-readable result code (e.g. `"InvalidBvn"`, `"Success"`).
132    #[serde(default)]
133    pub code: Option<String>,
134    /// A secondary status code string (e.g. `"Continue"`).
135    #[serde(default)]
136    pub status_code: Option<String>,
137    /// Field-level error messages, when present.
138    #[serde(default)]
139    pub errors: Vec<String>,
140}
141
142impl<T> ApiResponse<T> {
143    /// Unwraps to the inner `data`, or an [`Error::Api`] when `status` is
144    /// `false` (or `data` is missing).
145    pub fn into_result(self) -> Result<T> {
146        if self.status {
147            self.data.ok_or_else(|| Error::Api {
148                message: self
149                    .message
150                    .unwrap_or_else(|| "successful response carried no `data` payload".into()),
151                code: self.code,
152                errors: self.errors,
153            })
154        } else {
155            Err(Error::Api {
156                message: self.message.unwrap_or_else(|| "request was not successful".into()),
157                code: self.code,
158                errors: self.errors,
159            })
160        }
161    }
162}
163
164/// The `ResponseModel` shape **without** a payload — a pure acknowledgement.
165///
166/// Returned by the wallet-creation steps (BVN/NIN onboarding, OTP validation)
167/// and debit-restriction management, which report only success + a message. On
168/// the wire: `{ "status": true, "message": "...", "code": "...",
169/// "statusCode": "...", "errors": [...] }`.
170#[derive(Debug, Clone, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct StatusResponse {
173    /// Authoritative success flag.
174    #[serde(default)]
175    pub status: bool,
176    /// Human-readable message describing the outcome.
177    #[serde(default)]
178    pub message: Option<String>,
179    /// Machine-readable result code (e.g. `"InvalidBvn"`).
180    #[serde(default)]
181    pub code: Option<String>,
182    /// A secondary status code string (e.g. `"Continue"`).
183    #[serde(default)]
184    pub status_code: Option<String>,
185    /// Field-level error messages, when present.
186    #[serde(default)]
187    pub errors: Vec<String>,
188}
189
190impl StatusResponse {
191    /// Converts into an [`Acknowledgement`] on success, or an [`Error::Api`] on
192    /// failure (`status == false`).
193    pub fn into_result(self) -> Result<Acknowledgement> {
194        if self.status {
195            Ok(Acknowledgement {
196                message: self.message,
197                code: self.code,
198            })
199        } else {
200            Err(Error::Api {
201                message: self.message.unwrap_or_else(|| "request was not successful".into()),
202                code: self.code,
203                errors: self.errors,
204            })
205        }
206    }
207}
208
209/// A successful, payload-less outcome from an ALAT operation.
210///
211/// Returned by "fire and acknowledge" calls — onboarding steps, OTP validation,
212/// debit-restriction changes — where the bank confirms acceptance but returns
213/// no resource. The asynchronous result (e.g. the generated NUBAN) arrives later
214/// via your registered callback URL.
215#[derive(Debug, Clone)]
216pub struct Acknowledgement {
217    /// The bank's confirmation message, if any.
218    pub message: Option<String>,
219    /// The bank's result code, if any (e.g. `"Success"`).
220    pub code: Option<String>,
221}
222
223/// serde default helper: `Option<T>` defaults to `None` without requiring
224/// `T: Default`.
225fn none<T>() -> Option<T> {
226    None
227}