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}