Skip to main content

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}