use serde::{Deserialize, Serialize};
use crate::models::common::{Currency, Money, PayerType, TransactionStatus};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SearchTransactionRequest {
#[serde(rename = "trxID")]
pub trx_id: String,
}
impl SearchTransactionRequest {
#[must_use]
pub fn new(trx_id: impl Into<String>) -> Self {
Self {
trx_id: trx_id.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SearchTransactionResponse {
#[serde(rename = "trxID")]
pub trx_id: String,
#[serde(rename = "transactionStatus", default)]
pub transaction_status: TransactionStatus,
#[serde(rename = "transactionType", default)]
pub transaction_type: String,
#[serde(default)]
pub amount: Money,
#[serde(default)]
pub currency: Currency,
#[serde(rename = "customerMsisdn", default)]
pub customer_msisdn: String,
#[serde(rename = "organizationShortCode", default)]
pub organization_short_code: String,
#[serde(rename = "initiationTime", default)]
pub initiation_time: String,
#[serde(rename = "completedTime", default)]
pub completed_time: String,
#[serde(rename = "payerType", default)]
pub payer_type: PayerType,
#[serde(rename = "maxRefundableAmount", default)]
pub max_refundable_amount: Money,
#[serde(rename = "saleAmount", default)]
pub sale_amount: Money,
#[serde(rename = "serviceFee", default)]
pub service_fee: Money,
#[serde(rename = "payerAccount", default)]
pub payer_account: String,
#[serde(rename = "couponAmount", default)]
pub coupon_amount: Money,
#[serde(rename = "merchantShareAmount", default)]
pub merchant_share_amount: Money,
#[serde(rename = "creditedAmount", default)]
pub credited_amount: Money,
}
impl SearchTransactionResponse {
#[must_use]
pub fn is_coupon(&self) -> bool {
!self.coupon_amount.as_str().is_empty()
|| !self.merchant_share_amount.as_str().is_empty()
|| !self.credited_amount.as_str().is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::common::{Currency, PayerType, TransactionStatus};
#[test]
fn search_request_serialises() {
let req = SearchTransactionRequest::new("8A00ABCD");
let json: serde_json::Value = serde_json::to_value(&req).unwrap();
assert_eq!(json["trxID"], "8A00ABCD");
}
#[test]
fn search_response_parses_regular_transaction() {
let body = r#"{
"trxID": "8A00ABCD",
"transactionStatus": "Completed",
"transactionType": "Payment",
"amount": "100.00",
"currency": "BDT",
"customerMsisdn": "01700000000",
"organizationShortCode": "0123",
"initiationTime": "2026-06-22T09:00:00:000 GMT+06:00",
"completedTime": "2026-06-22T09:01:00:000 GMT+06:00",
"payerType": "Customer",
"maxRefundableAmount": "100.00",
"saleAmount": "100.00",
"serviceFee": "0.00",
"payerAccount": "123456789"
}"#;
let resp: SearchTransactionResponse = serde_json::from_str(body).unwrap();
assert_eq!(resp.trx_id, "8A00ABCD");
assert_eq!(resp.transaction_status, TransactionStatus::Completed);
assert_eq!(resp.currency, Currency::Bdt);
assert_eq!(resp.payer_type, PayerType::Customer);
assert_eq!(resp.amount.as_str(), "100.00");
assert!(!resp.is_coupon());
}
#[test]
fn search_response_parses_coupon_transaction() {
let body = r#"{
"trxID": "8A00EFGH",
"transactionStatus": "Completed",
"transactionType": "Payment",
"amount": "100.00",
"currency": "BDT",
"customerMsisdn": "01700000000",
"organizationShortCode": "0123",
"initiationTime": "2026-06-22T09:00:00:000 GMT+06:00",
"completedTime": "2026-06-22T09:01:00:000 GMT+06:00",
"payerType": "Customer",
"maxRefundableAmount": "100.00",
"saleAmount": "90.00",
"serviceFee": "0.00",
"payerAccount": "123456789",
"couponAmount": "10.00",
"merchantShareAmount": "85.00",
"creditedAmount": "95.00"
}"#;
let resp: SearchTransactionResponse = serde_json::from_str(body).unwrap();
assert!(resp.is_coupon());
assert_eq!(resp.coupon_amount.as_str(), "10.00");
assert_eq!(resp.merchant_share_amount.as_str(), "85.00");
assert_eq!(resp.credited_amount.as_str(), "95.00");
}
use proptest::prelude::*;
proptest! {
#[test]
fn search_transaction_response_roundtrip(
trx_id in ".*",
amount in ".*",
currency in proptest::sample::select(vec![Currency::Bdt]),
) {
let resp = SearchTransactionResponse {
trx_id,
transaction_status: TransactionStatus::Completed,
transaction_type: "Payment".to_string(),
amount: Money::new(amount),
currency,
customer_msisdn: String::new(),
organization_short_code: String::new(),
initiation_time: String::new(),
completed_time: String::new(),
payer_type: PayerType::Customer,
max_refundable_amount: Money::new(String::new()),
sale_amount: Money::new(String::new()),
service_fee: Money::new(String::new()),
payer_account: String::new(),
coupon_amount: Money::new(String::new()),
merchant_share_amount: Money::new(String::new()),
credited_amount: Money::new(String::new()),
};
let json = serde_json::to_string(&resp).unwrap();
let back: SearchTransactionResponse = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
prop_assert_eq!(json, json2);
}
}
}