use chrono::prelude::Local;
use openssl::base64;
use serde::{Deserialize, Serialize};
use crate::client::Mpesa;
use crate::constants::CommandId;
use crate::environment::ApiEnvironment;
use crate::errors::{MpesaError, MpesaResult};
static DEFAULT_PASSKEY: &str = "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919";
#[derive(Debug, Serialize)]
struct MpesaExpressRequestPayload<'mpesa> {
#[serde(rename(serialize = "BusinessShortCode"))]
business_short_code: &'mpesa str,
#[serde(rename(serialize = "Password"))]
password: &'mpesa str,
#[serde(rename(serialize = "Timestamp"))]
timestamp: &'mpesa str,
#[serde(rename(serialize = "TransactionType"))]
transaction_type: CommandId,
#[serde(rename(serialize = "Amount"))]
amount: f64,
#[serde(rename(serialize = "PartyA"), skip_serializing_if = "Option::is_none")]
party_a: Option<&'mpesa str>,
#[serde(rename(serialize = "PartyB"), skip_serializing_if = "Option::is_none")]
party_b: Option<&'mpesa str>,
#[serde(rename(serialize = "PhoneNumber"))]
phone_number: &'mpesa str,
#[serde(rename(serialize = "CallBackURL"))]
call_back_url: &'mpesa str,
#[serde(rename(serialize = "AccountReference"))]
account_reference: &'mpesa str,
#[serde(rename(serialize = "TransactionDesc"))]
transaction_desc: &'mpesa str,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MpesaExpressRequestResponse {
#[serde(rename(deserialize = "CheckoutRequestID"))]
pub checkout_request_id: String,
#[serde(rename(deserialize = "CustomerMessage"))]
pub customer_message: String,
#[serde(rename(deserialize = "MerchantRequestID"))]
pub merchant_request_id: String,
#[serde(rename(deserialize = "ResponseCode"))]
pub response_code: String,
#[serde(rename(deserialize = "ResponseDescription"))]
pub response_description: String,
}
pub struct MpesaExpressRequestBuilder<'mpesa, Env: ApiEnvironment> {
business_short_code: &'mpesa str,
client: &'mpesa Mpesa<Env>,
transaction_type: Option<CommandId>,
amount: Option<f64>,
party_a: Option<&'mpesa str>,
party_b: Option<&'mpesa str>,
phone_number: Option<&'mpesa str>,
callback_url: Option<&'mpesa str>,
account_ref: Option<&'mpesa str>,
transaction_desc: Option<&'mpesa str>,
pass_key: Option<&'mpesa str>,
}
impl<'mpesa, Env: ApiEnvironment> MpesaExpressRequestBuilder<'mpesa, Env> {
pub fn new(
client: &'mpesa Mpesa<Env>,
business_short_code: &'mpesa str,
) -> MpesaExpressRequestBuilder<'mpesa, Env> {
MpesaExpressRequestBuilder {
client,
business_short_code,
transaction_type: None,
transaction_desc: None,
amount: None,
party_a: None,
party_b: None,
phone_number: None,
callback_url: None,
account_ref: None,
pass_key: None,
}
}
pub fn business_short_code(&'mpesa self) -> &'mpesa str {
self.business_short_code
}
fn get_pass_key(&'mpesa self) -> &'mpesa str {
if let Some(key) = self.pass_key {
return key;
}
DEFAULT_PASSKEY
}
fn generate_password_and_timestamp(&self) -> (String, String) {
let timestamp = Local::now().format("%Y%m%d%H%M%S").to_string();
let encoded_password = base64::encode_block(
format!(
"{}{}{}",
self.business_short_code(),
self.get_pass_key(),
timestamp
)
.as_bytes(),
);
(encoded_password, timestamp)
}
pub fn pass_key(mut self, pass_key: &'mpesa str) -> MpesaExpressRequestBuilder<'mpesa, Env> {
self.pass_key = Some(pass_key);
self
}
pub fn amount<Number: Into<f64>>(
mut self,
amount: Number,
) -> MpesaExpressRequestBuilder<'mpesa, Env> {
self.amount = Some(amount.into());
self
}
pub fn phone_number(
mut self,
phone_number: &'mpesa str,
) -> MpesaExpressRequestBuilder<'mpesa, Env> {
self.phone_number = Some(phone_number);
self
}
pub fn callback_url(
mut self,
callback_url: &'mpesa str,
) -> MpesaExpressRequestBuilder<'mpesa, Env> {
self.callback_url = Some(callback_url);
self
}
pub fn party_a(mut self, party_a: &'mpesa str) -> MpesaExpressRequestBuilder<'mpesa, Env> {
self.party_a = Some(party_a);
self
}
pub fn party_b(mut self, party_b: &'mpesa str) -> MpesaExpressRequestBuilder<'mpesa, Env> {
self.party_b = Some(party_b);
self
}
pub fn account_ref(
mut self,
account_ref: &'mpesa str,
) -> MpesaExpressRequestBuilder<'mpesa, Env> {
self.account_ref = Some(account_ref);
self
}
pub fn transaction_type(
mut self,
command_id: CommandId,
) -> MpesaExpressRequestBuilder<'mpesa, Env> {
self.transaction_type = Some(command_id);
self
}
pub fn transaction_desc(
mut self,
description: &'mpesa str,
) -> MpesaExpressRequestBuilder<'mpesa, Env> {
self.transaction_desc = Some(description);
self
}
#[allow(clippy::or_fun_call)]
#[allow(clippy::unnecessary_lazy_evaluations)]
pub async fn send(self) -> MpesaResult<MpesaExpressRequestResponse> {
let url = format!(
"{}/mpesa/stkpush/v1/processrequest",
self.client.environment.base_url()
);
let (password, timestamp) = self.generate_password_and_timestamp();
let payload = MpesaExpressRequestPayload {
business_short_code: self.business_short_code,
password: &password,
timestamp: ×tamp,
amount: self
.amount
.ok_or(MpesaError::Message("amount is required"))?,
party_a: if self.party_a.is_some() {
self.party_a
} else {
self.phone_number
},
party_b: if self.party_b.is_some() {
self.party_b
} else {
Some(self.business_short_code)
},
phone_number: self
.phone_number
.ok_or(MpesaError::Message("phone_number is required"))?,
call_back_url: self
.callback_url
.ok_or(MpesaError::Message("callback_url is required"))?,
account_reference: self.account_ref.unwrap_or_else(|| stringify!(None)),
transaction_type: self
.transaction_type
.unwrap_or_else(|| CommandId::CustomerPayBillOnline),
transaction_desc: self.transaction_desc.unwrap_or_else(|| stringify!(None)),
};
let response = self
.client
.http_client
.post(&url)
.bearer_auth(self.client.auth().await?)
.json(&payload)
.send()
.await?;
if response.status().is_success() {
let value = response.json::<_>().await?;
return Ok(value);
}
let value = response.json().await?;
Err(MpesaError::MpesaExpressRequestError(value))
}
}