use serde::{Deserialize, Serialize};
use crate::models::common::{Currency, Intent, Money};
pub const PAYMENT_MODE: &str = "0001";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreatePaymentRequest {
#[serde(rename = "mode")]
pub mode: String,
#[serde(rename = "agreementID")]
pub agreement_id: String,
#[serde(rename = "payerReference")]
pub payer_reference: String,
#[serde(rename = "callbackURL")]
pub callback_url: String,
pub amount: Money,
pub currency: Currency,
pub intent: Intent,
#[serde(
rename = "merchantInvoiceNumber",
skip_serializing_if = "Option::is_none"
)]
pub merchant_invoice_number: Option<String>,
#[serde(
rename = "merchantAssociationInfo",
skip_serializing_if = "Option::is_none"
)]
pub merchant_association_info: Option<String>,
}
impl CreatePaymentRequest {
#[must_use]
pub fn new(
agreement_id: impl Into<String>,
payer_reference: impl Into<String>,
callback_url: impl Into<String>,
amount: Money,
currency: Currency,
) -> Self {
Self {
mode: PAYMENT_MODE.to_string(),
agreement_id: agreement_id.into(),
payer_reference: payer_reference.into(),
callback_url: callback_url.into(),
amount,
currency,
intent: Intent::Sale,
merchant_invoice_number: None,
merchant_association_info: None,
}
}
#[must_use]
pub fn with_merchant_invoice_number(mut self, n: impl Into<String>) -> Self {
self.merchant_invoice_number = Some(n.into());
self
}
#[must_use]
pub fn with_merchant_association_info(mut self, tlv: impl Into<String>) -> Self {
self.merchant_association_info = Some(tlv.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreatePaymentResponse {
#[serde(rename = "paymentID")]
pub payment_id: String,
#[serde(rename = "bkashURL", default)]
pub bkash_url: String,
#[serde(rename = "callbackURL", default)]
pub callback_url: String,
#[serde(rename = "paymentCreateTime", default)]
pub payment_create_time: String,
#[serde(rename = "agreementID", default)]
pub agreement_id: String,
#[serde(rename = "payerReference", default)]
pub payer_reference: String,
#[serde(rename = "orgShortCode", default)]
pub org_short_code: String,
#[serde(default)]
pub currency: Currency,
#[serde(default)]
pub intent: Intent,
#[serde(rename = "merchantInvoiceNumber", default)]
pub merchant_invoice_number: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ExecutePaymentRequest {
#[serde(rename = "paymentID")]
pub payment_id: String,
}
impl ExecutePaymentRequest {
#[must_use]
pub fn new(payment_id: impl Into<String>) -> Self {
Self {
payment_id: payment_id.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ExecutePaymentResponse {
#[serde(rename = "paymentID")]
pub payment_id: String,
#[serde(rename = "trxID", default)]
pub trx_id: String,
#[serde(rename = "customerMsisdn", default)]
pub customer_msisdn: String,
#[serde(rename = "payerReference", default)]
pub payer_reference: String,
#[serde(rename = "agreementID", default)]
pub agreement_id: String,
#[serde(rename = "orgShortCode", default)]
pub org_short_code: String,
#[serde(rename = "merchantInvoiceNumber", default)]
pub merchant_invoice_number: String,
#[serde(rename = "paymentExecuteTime", default)]
pub payment_execute_time: String,
#[serde(default)]
pub currency: Currency,
#[serde(default)]
pub intent: Intent,
#[serde(rename = "transactionStatus", default)]
pub transaction_status: String,
#[serde(default)]
pub amount: Money,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct QueryPaymentRequest {
#[serde(rename = "paymentID")]
pub payment_id: String,
}
impl QueryPaymentRequest {
#[must_use]
pub fn new(payment_id: impl Into<String>) -> Self {
Self {
payment_id: payment_id.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct QueryPaymentResponse {
#[serde(rename = "paymentID")]
pub payment_id: String,
#[serde(rename = "trxID", default)]
pub trx_id: String,
#[serde(rename = "customerMsisdn", default)]
pub customer_msisdn: String,
#[serde(rename = "payerReference", default)]
pub payer_reference: String,
#[serde(rename = "agreementID", default)]
pub agreement_id: String,
#[serde(rename = "orgShortCode", default)]
pub org_short_code: String,
#[serde(rename = "merchantInvoiceNumber", default)]
pub merchant_invoice_number: String,
#[serde(rename = "callbackURL", default)]
pub callback_url: String,
#[serde(default)]
pub currency: Currency,
#[serde(default)]
pub intent: Intent,
#[serde(default)]
pub amount: Money,
#[serde(rename = "transactionStatus", default)]
pub transaction_status: String,
#[serde(rename = "paymentExecuteTime", default)]
pub payment_execute_time: String,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::common::Currency;
#[test]
fn create_payment_serialises_with_mode_0001_and_intent_sale() {
let req = CreatePaymentRequest::new(
"AG0001",
"cust-1",
"https://example.test/cb",
Money::bdt("50.00"),
Currency::Bdt,
);
let json: serde_json::Value = serde_json::to_value(&req).unwrap();
assert_eq!(json["mode"], "0001");
assert_eq!(json["agreementID"], "AG0001");
assert_eq!(json["intent"], "sale");
assert_eq!(json["payerReference"], "cust-1");
assert_eq!(json["amount"], "50.00");
assert_eq!(json["currency"], "BDT");
assert!(json.get("merchantInvoiceNumber").is_none());
assert!(json.get("merchantAssociationInfo").is_none());
}
#[test]
fn create_payment_with_tlv_serialises() {
let req = CreatePaymentRequest::new(
"AG0001",
"cust-1",
"https://example.test/cb",
Money::bdt("50.00"),
Currency::Bdt,
)
.with_merchant_invoice_number("INV-2")
.with_merchant_association_info("tag1value1");
let json: serde_json::Value = serde_json::to_value(&req).unwrap();
assert_eq!(json["merchantInvoiceNumber"], "INV-2");
assert_eq!(json["merchantAssociationInfo"], "tag1value1");
}
#[test]
fn execute_payment_request_serialises() {
let req = ExecutePaymentRequest::new("TR0001");
let json: serde_json::Value = serde_json::to_value(&req).unwrap();
assert_eq!(json["paymentID"], "TR0001");
}
#[test]
fn execute_payment_response_parses_minimal() {
let body = r#"{
"paymentID": "TR0001",
"trxID": "8A00ABCD",
"transactionStatus": "Completed",
"amount": "50.00",
"currency": "BDT",
"intent": "sale",
"paymentExecuteTime": "2026-06-22T10:00:00:000 GMT+06:00"
}"#;
let resp: ExecutePaymentResponse = serde_json::from_str(body).unwrap();
assert_eq!(resp.payment_id, "TR0001");
assert_eq!(resp.trx_id, "8A00ABCD");
assert_eq!(resp.transaction_status, "Completed");
assert_eq!(resp.amount.as_str(), "50.00");
}
use proptest::prelude::*;
proptest! {
#[test]
fn create_payment_request_roundtrip(
agreement_id in ".*",
payer_ref in ".*",
callback in ".*",
amount in ".*",
intent in proptest::sample::select(vec![Intent::Sale, Intent::Authorization]),
) {
let req = CreatePaymentRequest {
mode: PAYMENT_MODE.to_string(),
agreement_id,
payer_reference: payer_ref,
callback_url: callback,
amount: Money::new(amount),
currency: Currency::Bdt,
intent,
merchant_invoice_number: None,
merchant_association_info: None,
};
let json = serde_json::to_string(&req).unwrap();
let back: CreatePaymentRequest = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
prop_assert_eq!(json, json2);
}
#[test]
fn create_payment_request_roundtrip_with_optional_fields(
inv in ".*",
tlv in ".*",
) {
let req = CreatePaymentRequest::new(
"AG-1",
"cust-1",
"https://example.test/cb",
Money::bdt("1.00"),
Currency::Bdt,
)
.with_merchant_invoice_number(inv)
.with_merchant_association_info(tlv);
let json = serde_json::to_string(&req).unwrap();
let back: CreatePaymentRequest = serde_json::from_str(&json).unwrap();
prop_assert_eq!(
back.merchant_invoice_number,
req.merchant_invoice_number
);
prop_assert_eq!(
back.merchant_association_info,
req.merchant_association_info
);
}
}
}