alat/modules/wallet.rs
1//! Wallet creation & onboarding — open Tier-1 ALAT wallets programmatically.
2//!
3//! ALAT lets partners onboard customers into **Tier-1 wallets** using either a
4//! **BVN** or a **NIN** as the identity anchor. The two flows are mirror images
5//! of each other and live on two different products/base paths:
6//!
7//! | Flow | Product (base path) | Identity field |
8//! |------|--------------------------|----------------|
9//! | BVN | `/account-creation` | `bvn` |
10//! | NIN | `/wallet-creation` | `nin` |
11//!
12//! Both follow the same 3-step choreography:
13//!
14//! 1. **Initiate** — submit identity + phone + email. The bank sends a one-time
15//! password (OTP) to the customer's registered phone and returns an
16//! acknowledgement (the `trackingId` you pass to step 2 is issued out-of-band
17//! / via your callback in the live flow).
18//! 2. **Validate OTP** — submit `phone + otp + trackingId`. On success the wallet
19//! is enqueued for creation; the resulting NUBAN arrives via your callback URL
20//! (`Request = 1 WalletCreation`).
21//! 3. **Fetch details** — once created, look the account up by phone number.
22//!
23//! Helpers are provided for **resending the OTP** and for **debit-restriction
24//! (PND) management** on the created wallet.
25//!
26//! # Ecosystem concepts
27//! - **Tier-1 wallet**: a low-KYC account with CBN-mandated balance/transaction
28//! limits, openable purely digitally.
29//! - **BVN / NIN**: 11-digit identifiers (Bank Verification Number / National
30//! Identification Number) used as the KYC anchor.
31//! - **OTP**: a one-time password sent to the customer's phone to prove consent.
32//! - **PND ("Post No Debit")**: a restriction that blocks debits from an account.
33
34use crate::client::Client;
35use crate::envelope::{Acknowledgement, ApiResponse, StatusResponse};
36use crate::error::Result;
37use serde::{Deserialize, Serialize};
38
39// ===========================================================================
40// Request payloads
41// ===========================================================================
42
43/// Step 1 (BVN) — request body for `PostPartnershipAccountCreationWithBvn`.
44///
45/// Server type: `PatnershipRequestWithBvn`.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct CreateWalletWithBvnRequest {
49 /// Customer's phone number, as registered with the BVN.
50 pub phone_number: String,
51 /// Customer's email address (used for profiling/notifications).
52 pub email: String,
53 /// The customer's 11-digit Bank Verification Number.
54 pub bvn: String,
55}
56
57/// Step 1 (NIN) — request body for `GenerateWalletAccountForPartnerships/Request`.
58///
59/// Server type: `PatnershipRequestV3`.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct CreateWalletWithNinRequest {
63 /// Customer's phone number, as registered with the NIN.
64 pub phone_number: String,
65 /// Customer's email address.
66 pub email: String,
67 /// The customer's 11-digit National Identification Number.
68 pub nin: String,
69}
70
71/// Step 2 (BVN & NIN) — request body for OTP validation.
72///
73/// Server type: `PatnershipRequestV2`. Shared verbatim by both flows.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub struct ValidateOtpRequest {
77 /// Customer's phone number (the one the OTP was sent to).
78 pub phone_number: String,
79 /// The one-time password the customer received.
80 pub otp: String,
81 /// The tracking id correlating this validation with the step-1 request.
82 pub tracking_id: String,
83}
84
85/// Request body to re-send the onboarding OTP.
86///
87/// Server type: `ResendOtpModel`.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct ResendOtpRequest {
91 /// The tracking id from the step-1 response.
92 pub tracking_id: String,
93 /// The customer's phone number to re-send the OTP to.
94 pub phone_number: String,
95}
96
97/// Request body for debit-restriction (PND) management.
98///
99/// Server type: `PndRequest`.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct DebitRestrictionRequest {
103 /// The restriction action. The portal documents `"LiftPnd"` to remove a
104 /// restriction; the place-restriction value is the symmetric counterpart
105 /// (commonly `"PlacePnd"`). Confirm the exact tokens with Wema for your
106 /// channel before relying on placement.
107 pub pnd_type: String,
108 /// The 10-digit NUBAN to apply the restriction change to.
109 pub account_number: String,
110}
111
112// ===========================================================================
113// Response payloads
114// ===========================================================================
115
116/// The created wallet's details, returned by `GetPartnershipAccountDetails`.
117///
118/// This is the `data` object inside the `ResponseModel` envelope. Note the API
119/// returns the customer name as separate `firstName`/`lastName` fields rather
120/// than a single display name.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub struct PartnershipAccountDetails {
124 /// The generated 10-digit NUBAN account number.
125 pub account_number: String,
126 /// Customer's first name.
127 pub first_name: String,
128 /// Customer's last name.
129 pub last_name: String,
130 /// Customer's email address.
131 pub email: String,
132 /// Customer's phone number.
133 pub phone_number: String,
134}
135
136impl Client {
137 // ---- BVN flow (base path: /account-creation) --------------------------
138
139 /// **BVN · Step 1** — initiate wallet creation with a BVN.
140 ///
141 /// Triggers an OTP to the customer's phone. The returned [`Acknowledgement`]
142 /// confirms acceptance; proceed to [`validate_bvn_otp`](Client::validate_bvn_otp).
143 ///
144 /// `POST /account-creation/api/CustomerAccount/PostPartnershipAccountCreationWithBvn`
145 pub async fn create_wallet_with_bvn(
146 &self,
147 request: &CreateWalletWithBvnRequest,
148 ) -> Result<Acknowledgement> {
149 self.post_json::<_, StatusResponse>(
150 "account-creation/api/CustomerAccount/PostPartnershipAccountCreationWithBvn",
151 request,
152 &[],
153 )
154 .await?
155 .into_result()
156 }
157
158 /// **BVN · Step 2** — validate the OTP and enqueue wallet creation.
159 ///
160 /// On success the wallet is queued; the NUBAN is delivered to your callback
161 /// URL. Then fetch it with
162 /// [`get_bvn_partnership_account_details`](Client::get_bvn_partnership_account_details).
163 ///
164 /// `POST /account-creation/api/CustomerAccount/ValidateBVNandEnqueueAccountCreation`
165 pub async fn validate_bvn_otp(&self, request: &ValidateOtpRequest) -> Result<Acknowledgement> {
166 self.post_json::<_, StatusResponse>(
167 "account-creation/api/CustomerAccount/ValidateBVNandEnqueueAccountCreation",
168 request,
169 &[],
170 )
171 .await?
172 .into_result()
173 }
174
175 /// **BVN** — re-send the onboarding OTP.
176 ///
177 /// `POST /account-creation/api/CustomerAccount/ResendOtpRequest/ResendOtp`
178 pub async fn resend_bvn_otp(&self, request: &ResendOtpRequest) -> Result<Acknowledgement> {
179 self.post_json::<_, StatusResponse>(
180 "account-creation/api/CustomerAccount/ResendOtpRequest/ResendOtp",
181 request,
182 &[],
183 )
184 .await?
185 .into_result()
186 }
187
188 /// **BVN · Step 3** — fetch the created wallet's details by phone number.
189 ///
190 /// `GET /account-creation/api/CustomerAccount/GetPartnershipAccountDetails?phoneNumber=...`
191 pub async fn get_bvn_partnership_account_details(
192 &self,
193 phone_number: &str,
194 ) -> Result<PartnershipAccountDetails> {
195 self.get_json::<ApiResponse<PartnershipAccountDetails>>(
196 "account-creation/api/CustomerAccount/GetPartnershipAccountDetails",
197 &[("phoneNumber", phone_number)],
198 &[],
199 )
200 .await?
201 .into_result()
202 }
203
204 /// **BVN** — place or lift a debit restriction (PND) on the wallet.
205 ///
206 /// `POST /account-creation/api/CustomerAccount/PartnerDebitRestrictionManagement`
207 pub async fn set_bvn_debit_restriction(
208 &self,
209 request: &DebitRestrictionRequest,
210 ) -> Result<Acknowledgement> {
211 self.post_json::<_, StatusResponse>(
212 "account-creation/api/CustomerAccount/PartnerDebitRestrictionManagement",
213 request,
214 &[],
215 )
216 .await?
217 .into_result()
218 }
219
220 // ---- NIN flow (base path: /wallet-creation) ---------------------------
221
222 /// **NIN · Step 1** — initiate wallet creation with a NIN.
223 ///
224 /// `POST /wallet-creation/api/CustomerAccount/GenerateWalletAccountForPartnerships/Request`
225 pub async fn create_wallet_with_nin(
226 &self,
227 request: &CreateWalletWithNinRequest,
228 ) -> Result<Acknowledgement> {
229 self.post_json::<_, StatusResponse>(
230 "wallet-creation/api/CustomerAccount/GenerateWalletAccountForPartnerships/Request",
231 request,
232 &[],
233 )
234 .await?
235 .into_result()
236 }
237
238 /// **NIN · Step 2** — validate the OTP and enqueue wallet creation.
239 ///
240 /// `POST /wallet-creation/api/CustomerAccount/GenerateWalletAccountForPartnershipsV2/Otp`
241 pub async fn validate_nin_otp(&self, request: &ValidateOtpRequest) -> Result<Acknowledgement> {
242 self.post_json::<_, StatusResponse>(
243 "wallet-creation/api/CustomerAccount/GenerateWalletAccountForPartnershipsV2/Otp",
244 request,
245 &[],
246 )
247 .await?
248 .into_result()
249 }
250
251 /// **NIN** — re-send the onboarding OTP.
252 ///
253 /// `POST /wallet-creation/api/CustomerAccount/ResendOtpRequest/ResendOtp`
254 pub async fn resend_nin_otp(&self, request: &ResendOtpRequest) -> Result<Acknowledgement> {
255 self.post_json::<_, StatusResponse>(
256 "wallet-creation/api/CustomerAccount/ResendOtpRequest/ResendOtp",
257 request,
258 &[],
259 )
260 .await?
261 .into_result()
262 }
263
264 /// **NIN · Step 3** — fetch the created wallet's details by phone number.
265 ///
266 /// `GET /wallet-creation/api/CustomerAccount/GetPartnershipAccountDetails?phoneNumber=...`
267 pub async fn get_nin_partnership_account_details(
268 &self,
269 phone_number: &str,
270 ) -> Result<PartnershipAccountDetails> {
271 self.get_json::<ApiResponse<PartnershipAccountDetails>>(
272 "wallet-creation/api/CustomerAccount/GetPartnershipAccountDetails",
273 &[("phoneNumber", phone_number)],
274 &[],
275 )
276 .await?
277 .into_result()
278 }
279
280 /// **NIN** — place or lift a debit restriction (PND) on the wallet.
281 ///
282 /// `POST /wallet-creation/api/CustomerAccount/PartnerDebitRestrictionManagement`
283 pub async fn set_nin_debit_restriction(
284 &self,
285 request: &DebitRestrictionRequest,
286 ) -> Result<Acknowledgement> {
287 self.post_json::<_, StatusResponse>(
288 "wallet-creation/api/CustomerAccount/PartnerDebitRestrictionManagement",
289 request,
290 &[],
291 )
292 .await?
293 .into_result()
294 }
295}