alat/modules/bills.rs
1//! Bills payment & airtime/data — pay billers and top up phones.
2//!
3//! Two related products, both published on the Playground sandbox and both
4//! gated by the **`access` header** (supply it via
5//! [`Config::with_access_key`](crate::Config::with_access_key)):
6//!
7//! - **Bills Payment** (`/bills-payment`): list billers → validate the customer
8//! identifier → pay → (optionally) check transaction status.
9//! - **Airtime & Data** (`/airtime-data`): list data plans → purchase airtime or
10//! data with a single client account.
11//!
12//! All of these are *asynchronous*: a successful call returns a **pending**
13//! result and the final status is delivered to your callback URL. Use the
14//! check-status calls to poll if needed.
15//!
16//! # Ecosystem concepts
17//! - **Biller**: a merchant that accepts payments (power, TV, internet, etc.).
18//! - **Package**: a specific payable product of a biller (e.g. a TV bouquet).
19//! - **Customer validation**: confirming an identifier (meter no., smartcard,
20//! phone) exists at the biller before debiting the payer.
21
22use crate::client::Client;
23use crate::envelope::Envelope;
24use crate::error::Result;
25use serde::{Deserialize, Serialize};
26
27// ===========================================================================
28// Bills payment (/bills-payment) — gated by the `access` header
29// ===========================================================================
30
31/// A payable product offered by a biller.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct BillPackage {
35 /// Package id — pass this as `packageId` when validating/paying.
36 pub id: i64,
37 /// Id of the biller this package belongs to.
38 pub biller_id: i64,
39 /// Package display name.
40 pub name: String,
41 /// Whether the payer may choose the amount (vs. a fixed price).
42 pub is_amount_editable: bool,
43 /// Default/expected amount.
44 pub amount: f64,
45 /// Minimum payable amount (when editable).
46 pub min_amount: f64,
47 /// Maximum payable amount (when editable).
48 pub max_amount: f64,
49}
50
51/// A biller within a category.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct Biller {
55 /// Biller id.
56 pub id: i64,
57 /// Biller display name.
58 pub name: String,
59 /// Stable identifier string for the biller.
60 pub identifier: Option<String>,
61 /// Short code for the biller.
62 pub short_code: Option<String>,
63 /// Whether the biller is currently acquired/active (API key `isAquired`).
64 #[serde(rename = "isAquired")]
65 pub is_acquired: bool,
66 /// Whether the biller requires customer validation before payment.
67 pub required_validation: bool,
68 /// Flat charge associated with the biller.
69 pub charge: f64,
70 /// Payment-flow discriminator used by the platform.
71 pub flow: i64,
72 /// The biller's payable packages.
73 #[serde(default)]
74 pub packages: Vec<BillPackage>,
75}
76
77/// A bill category grouping billers (e.g. "Cable TV", "Electricity").
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct BillCategory {
81 /// Category id.
82 pub id: i64,
83 /// Category display name.
84 pub name: String,
85 /// Billers in this category.
86 #[serde(default)]
87 pub billers: Vec<Biller>,
88}
89
90/// Request body to validate a customer's identifier at a biller.
91///
92/// Server type: `ValidationRequest`.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct ValidateCustomerRequest {
96 /// Your channel id, as issued by Wema.
97 pub channel_id: String,
98 /// The customer identifier to validate (meter no., smartcard, phone, …).
99 pub identifier: String,
100 /// The package being paid for (see [`BillPackage::id`]).
101 pub package_id: i64,
102}
103
104/// Result of validating a customer identifier.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct CustomerValidation {
108 /// Whether the identifier is valid at the biller.
109 pub is_validated: bool,
110 /// The resolved customer name, when available.
111 pub customer_name: Option<String>,
112 /// Additional message from the biller.
113 pub message: Option<String>,
114 /// Extra validation context (biller-specific).
115 pub validation_info: Option<String>,
116 /// Outstanding/credit limit, where the biller reports one.
117 pub credit_limit: Option<f64>,
118}
119
120/// Request body to pay a bill from a client account.
121///
122/// Server type: `PayBillClientRequest`.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct PayBillRequest {
126 /// Your channel/client id.
127 pub client_id: String,
128 /// The 10-digit NUBAN to debit.
129 pub customer_account: String,
130 /// Amount to pay.
131 pub amount: f64,
132 /// Service charge to apply.
133 pub charge: f64,
134 /// Your unique transaction reference (idempotency key).
135 pub transaction_reference: String,
136 /// The package being paid for (see [`BillPackage::id`]).
137 pub package_id: i64,
138 /// The customer identifier (meter no., smartcard, …).
139 pub customer_identifier: String,
140 /// Customer email (for receipts/notifications).
141 pub customer_email: String,
142 /// Customer phone number.
143 pub customer_phone_number: String,
144 /// Customer name.
145 pub customer_name: String,
146 /// Channel-encrypted security info, where required by your integration.
147 pub security_info: String,
148}
149
150/// The `result` of a payment/purchase (`GlobalResponseEnvelope`).
151///
152/// Shared by bill payment and airtime/data purchase. Some fields appear only for
153/// certain operations (e.g. [`value`](Self::value) for airtime), hence `Option`.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct PaymentResult {
157 /// Transaction status string (often a *pending* state on acceptance).
158 pub status: Option<String>,
159 /// Status message.
160 pub message: Option<String>,
161 /// Transaction narration.
162 pub narration: Option<String>,
163 /// Your transaction reference (echoed back).
164 pub transaction_reference: Option<String>,
165 /// The platform's transaction reference, for status queries.
166 pub platform_transaction_reference: Option<String>,
167 /// Settlement/switch transaction STAN.
168 pub transaction_stan: Option<String>,
169 /// Operation-specific value (e.g. token/units for airtime).
170 pub value: Option<String>,
171 /// Original transaction date (API key `orinalTxnTransactionDate`).
172 #[serde(rename = "orinalTxnTransactionDate")]
173 pub original_transaction_date: Option<String>,
174}
175
176/// Request body to check a bill transaction's status.
177///
178/// Server type: `CheckBillsTransactionStatusRequest`.
179#[derive(Debug, Clone, Serialize, Deserialize)]
180#[serde(rename_all = "camelCase")]
181pub struct CheckTransactionStatusRequest {
182 /// The transaction reference to query.
183 pub transaction_reference: String,
184}
185
186/// The status of a previously submitted transaction.
187#[derive(Debug, Clone, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase")]
189pub struct TransactionStatus {
190 /// The queried transaction reference.
191 pub transaction_reference: Option<String>,
192 /// Status code, encoded as an integer enum by the platform (e.g. pending /
193 /// successful / failed). Map it according to your channel's documentation.
194 pub transaction_status: i64,
195}
196
197// ===========================================================================
198// Airtime & data (/airtime-data) — gated by the `access` header
199// ===========================================================================
200
201/// A single data package within a network's plan list.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase")]
204pub struct DataPackage {
205 /// Package id — pass this as `packageCode` when purchasing.
206 pub id: i64,
207 /// Package display name.
208 pub name: String,
209 /// Package price.
210 pub amount: f64,
211 /// Data allowance description (e.g. `"5GB"`).
212 pub data_plan: Option<String>,
213 /// Validity period (API key `validity_Period`, e.g. `"30 days"`).
214 #[serde(rename = "validity_Period")]
215 pub validity_period: Option<String>,
216 /// Whether this plan is enabled for Buy-Now-Pay-Later.
217 pub enabled_for_bnpl: Option<bool>,
218 /// Free-text description.
219 pub description: Option<String>,
220}
221
222/// Data plans for a single network provider.
223#[derive(Debug, Clone, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct DataPlanCategory {
226 /// Category id.
227 pub id: i64,
228 /// Network provider name (e.g. `"MTN"`, `"Airtel"`, `"Glo"`, `"9mobile"`).
229 pub network_provider: String,
230 /// The available data packages for this provider.
231 #[serde(default)]
232 pub data_packages: Vec<DataPackage>,
233}
234
235/// Request body to purchase airtime from a single client account.
236///
237/// Server type: `AirtimeForClientReqModel`.
238#[derive(Debug, Clone, Serialize, Deserialize)]
239#[serde(rename_all = "camelCase")]
240pub struct PurchaseAirtimeRequest {
241 /// Your unique transaction reference.
242 pub transaction_reference: String,
243 /// The 10-digit NUBAN to debit.
244 pub account_number: String,
245 /// The network operator (e.g. `"MTN"`).
246 pub network: String,
247 /// Recipient phone number to credit.
248 pub phone_number: String,
249 /// Airtime amount.
250 pub amount: f64,
251 /// Channel-encrypted security info, where required.
252 pub security_info: String,
253 /// Your channel/client id.
254 pub client_id: String,
255}
256
257/// Request body to purchase a data bundle from a single client account.
258///
259/// Server type: `DataForClientReqModel`.
260#[derive(Debug, Clone, Serialize, Deserialize)]
261#[serde(rename_all = "camelCase")]
262pub struct PurchaseDataRequest {
263 /// Your unique transaction reference.
264 pub transaction_reference: String,
265 /// The 10-digit NUBAN to debit.
266 pub account_number: String,
267 /// Recipient phone number to credit.
268 pub phone_number: String,
269 /// The data package code (see [`DataPackage::id`]).
270 pub package_code: i64,
271 /// The package amount.
272 pub amount: f64,
273 /// The network operator (e.g. `"MTN"`).
274 pub network: String,
275 /// Channel-encrypted security info, where required.
276 pub security_info: String,
277 /// Your channel/client id.
278 pub client_id: String,
279}
280
281impl Client {
282 /// Lists every bill category, biller, and package available to your channel.
283 ///
284 /// Requires the `access` header. `GET /bills-payment/api/BillsPayment/GetAllBills`
285 pub async fn get_all_bills(&self) -> Result<Vec<BillCategory>> {
286 let headers = self.access_headers(true)?;
287 self.get_json::<Envelope<Vec<BillCategory>>>(
288 "bills-payment/api/BillsPayment/GetAllBills",
289 &[],
290 &headers,
291 )
292 .await?
293 .into_result()
294 }
295
296 /// Validates a customer identifier at a biller before payment.
297 ///
298 /// Requires the `access` header. `POST /bills-payment/api/BillsPayment/ValidateCustomer`
299 pub async fn validate_customer(
300 &self,
301 request: &ValidateCustomerRequest,
302 ) -> Result<CustomerValidation> {
303 let headers = self.access_headers(true)?;
304 self.post_json::<_, Envelope<CustomerValidation>>(
305 "bills-payment/api/BillsPayment/ValidateCustomer",
306 request,
307 &headers,
308 )
309 .await?
310 .into_result()
311 }
312
313 /// Pays a bill from a client account (returns a pending result).
314 ///
315 /// Requires the `access` header. `POST /bills-payment/api/Shared/PayBill`
316 pub async fn pay_bill(&self, request: &PayBillRequest) -> Result<PaymentResult> {
317 let headers = self.access_headers(true)?;
318 self.post_json::<_, Envelope<PaymentResult>>(
319 "bills-payment/api/Shared/PayBill",
320 request,
321 &headers,
322 )
323 .await?
324 .into_result()
325 }
326
327 /// Checks the status of a previously submitted bill transaction.
328 ///
329 /// Requires the `access` header.
330 /// `POST /bills-payment/api/PartnerPayment/checktransactionstatus`
331 pub async fn check_bill_transaction_status(
332 &self,
333 request: &CheckTransactionStatusRequest,
334 ) -> Result<TransactionStatus> {
335 let headers = self.access_headers(true)?;
336 self.post_json::<_, Envelope<TransactionStatus>>(
337 "bills-payment/api/PartnerPayment/checktransactionstatus",
338 request,
339 &headers,
340 )
341 .await?
342 .into_result()
343 }
344
345 /// Lists data plans across all networks.
346 ///
347 /// The `access` header is optional here but sent if configured.
348 /// `GET /airtime-data/api/Data/GetDataPlans`
349 pub async fn get_data_plans(&self) -> Result<Vec<DataPlanCategory>> {
350 let headers = self.access_headers(false)?;
351 self.get_json::<Envelope<Vec<DataPlanCategory>>>(
352 "airtime-data/api/Data/GetDataPlans",
353 &[],
354 &headers,
355 )
356 .await?
357 .into_result()
358 }
359
360 /// Purchases airtime from a single client account (returns a pending result).
361 ///
362 /// Requires the `access` header.
363 /// `POST /airtime-data/api/Airtime/Client/PurchaseAirtime`
364 pub async fn purchase_airtime(&self, request: &PurchaseAirtimeRequest) -> Result<PaymentResult> {
365 let headers = self.access_headers(true)?;
366 self.post_json::<_, Envelope<PaymentResult>>(
367 "airtime-data/api/Airtime/Client/PurchaseAirtime",
368 request,
369 &headers,
370 )
371 .await?
372 .into_result()
373 }
374
375 /// Purchases a data bundle from a single client account (returns a pending result).
376 ///
377 /// The `access` header is optional here but sent if configured.
378 /// `POST /airtime-data/api/Data/Client/PurchaseData`
379 pub async fn purchase_data(&self, request: &PurchaseDataRequest) -> Result<PaymentResult> {
380 let headers = self.access_headers(false)?;
381 self.post_json::<_, Envelope<PaymentResult>>(
382 "airtime-data/api/Data/Client/PurchaseData",
383 request,
384 &headers,
385 )
386 .await?
387 .into_result()
388 }
389}