use crate::client::Client;
use crate::envelope::{Envelope, ServiceResponse};
use crate::error::{Error, Result};
use hmac::{Hmac, Mac};
use serde::{Deserialize, Deserializer, Serialize};
use sha2::Sha512;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Bank {
pub bank_name: String,
pub bank_code: String,
}
#[derive(Debug, Clone)]
pub struct BankList(pub Vec<Bank>);
impl<'de> Deserialize<'de> for BankList {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany {
Many(Vec<Bank>),
One(Bank),
}
Ok(match OneOrMany::deserialize(deserializer)? {
OneOrMany::Many(v) => BankList(v),
OneOrMany::One(b) => BankList(vec![b]),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChargeFee {
pub id: i64,
pub charge_fee_name: Option<String>,
pub transaction_type: i64,
pub charge: f64,
pub lower: f64,
pub upper: f64,
}
impl ChargeFee {
pub fn applies_to(&self, amount: f64) -> bool {
amount >= self.lower && amount <= self.upper
}
}
pub fn charge_for(bands: &[ChargeFee], amount: f64) -> Option<&ChargeFee> {
bands.iter().find(|b| b.applies_to(amount))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountNameEnquiry {
pub bank_code: Option<String>,
pub account_name: String,
pub account_number: Option<String>,
pub currency: Option<String>,
pub terms_and_conditions: Option<String>,
pub terms_and_conditions_url: Option<String>,
#[serde(default)]
pub charge_fee: Vec<ChargeFee>,
}
impl AccountNameEnquiry {
pub fn charge_for(&self, amount: f64) -> Option<&ChargeFee> {
charge_for(&self.charge_fee, amount)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NipCharges {
#[serde(default)]
pub charge_fees: Vec<ChargeFee>,
pub terms_and_conditions: Option<String>,
pub terms_and_conditions_url: Option<String>,
}
impl NipCharges {
pub fn charge_for(&self, amount: f64) -> Option<&ChargeFee> {
charge_for(&self.charge_fees, amount)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransferRequest {
pub amount: f64,
pub narration: String,
pub transaction_reference: String,
pub destination_bank_code: String,
pub destination_bank_name: String,
pub destination_account_number: String,
pub destination_account_name: String,
pub source_account_number: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransferResult {
#[serde(default)]
pub status: serde_json::Value,
pub message: Option<String>,
pub platform_transaction_reference: Option<String>,
}
impl Client {
pub fn compute_transfer_hash(&self, salt_key: &str, request: &TransferRequest) -> Result<String> {
if salt_key.is_empty() {
return Err(Error::Validation("transfer salt key must not be empty".into()));
}
type HmacSha512 = Hmac<Sha512>;
let data = format!(
"{}{}{}{}{}",
request.transaction_reference,
request.destination_bank_code,
request.destination_account_number,
request.source_account_number,
request.amount,
);
let mut mac = HmacSha512::new_from_slice(salt_key.as_bytes())
.map_err(|e| Error::Configuration(format!("HMAC init failed: {e}")))?;
mac.update(data.as_bytes());
Ok(hex::encode(mac.finalize().into_bytes()))
}
pub async fn get_bank_list(&self) -> Result<Vec<Bank>> {
let banks = self
.get_json::<ServiceResponse<BankList>>(
"funds-transfer-open/api/OpenApiTransfer/GetAllBanks",
&[],
&[],
)
.await?
.into_result()?;
Ok(banks.0)
}
pub async fn verify_account(
&self,
bank_code: &str,
account_number: &str,
channel_id: Option<&str>,
) -> Result<AccountNameEnquiry> {
let path = format!(
"funds-transfer-open/api/Shared/AccountNameEnquiry/{}/{}",
bank_code, account_number
);
let query: Vec<(&str, &str)> = match channel_id {
Some(c) => vec![("channelId", c)],
None => vec![],
};
self.get_json::<Envelope<AccountNameEnquiry>>(&path, &query, &[])
.await?
.into_result()
}
pub async fn transfer_funds(
&self,
salt_key: &str,
request: &TransferRequest,
) -> Result<TransferResult> {
let hash = self.compute_transfer_hash(salt_key, request)?;
self.post_json::<_, ServiceResponse<TransferResult>>(
"funds-transfer-open/api/OpenApiTransfer/transfer-fund-request",
request,
&[("hash", hash)],
)
.await?
.into_result()
}
pub async fn get_nip_charges(&self) -> Result<NipCharges> {
self.get_json::<Envelope<NipCharges>>("debit-wallet/api/Shared/GetNIPCharges", &[], &[])
.await?
.into_result()
}
}