Skip to main content

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}