use serde::{Deserialize, Serialize};
use crate::models::common::{Currency, Intent, Money, TransactionStatus};
pub const PAYMENT_MODE: &str = "0011";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreatePaymentRequest {
#[serde(rename = "mode")]
pub mode: 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>,
}
impl CreatePaymentRequest {
#[must_use]
pub fn new(
payer_reference: impl Into<String>,
callback_url: impl Into<String>,
amount: Money,
currency: Currency,
) -> Self {
Self {
mode: PAYMENT_MODE.to_string(),
payer_reference: payer_reference.into(),
callback_url: callback_url.into(),
amount,
currency,
intent: Intent::Authorization,
merchant_invoice_number: None,
}
}
#[must_use]
pub fn with_merchant_invoice_number(mut self, n: impl Into<String>) -> Self {
self.merchant_invoice_number = Some(n.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 = "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 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 = "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(rename = "paymentExecuteTime", default)]
pub payment_execute_time: String,
#[serde(default)]
pub currency: Currency,
#[serde(default)]
pub intent: Intent,
#[serde(default)]
pub amount: Money,
#[serde(rename = "transactionStatus", default)]
pub transaction_status: TransactionStatus,
}
#[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 = "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: TransactionStatus,
#[serde(rename = "paymentExecuteTime", default)]
pub payment_execute_time: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CaptureRequest {
#[serde(rename = "paymentID")]
pub payment_id: String,
}
impl CaptureRequest {
#[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 CaptureResponse {
#[serde(rename = "paymentId", alias = "paymentID")]
pub payment_id: String,
#[serde(rename = "createTime", default)]
pub create_time: String,
#[serde(rename = "updateTime", default)]
pub update_time: String,
#[serde(rename = "trxID", default)]
pub trx_id: String,
#[serde(rename = "transactionStatus", default)]
pub transaction_status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct VoidRequest {
#[serde(rename = "paymentID")]
pub payment_id: String,
}
impl VoidRequest {
#[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 VoidResponse {
#[serde(rename = "paymentId", alias = "paymentID")]
pub payment_id: String,
#[serde(rename = "createTime", default)]
pub create_time: String,
#[serde(rename = "updateTime", default)]
pub update_time: String,
#[serde(rename = "trxID", default)]
pub trx_id: String,
#[serde(rename = "transactionStatus", default)]
pub transaction_status: String,
}
#[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,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_payment_serialises_with_mode_0011_and_intent_authorization() {
let req = CreatePaymentRequest::new(
"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"], "0011");
assert_eq!(json["intent"], "authorization");
assert_eq!(json["payerReference"], "cust-1");
assert_eq!(json["callbackURL"], "https://example.test/cb");
assert_eq!(json["amount"], "50.00");
assert_eq!(json["currency"], "BDT");
assert!(json.get("merchantInvoiceNumber").is_none());
}
#[test]
fn create_payment_with_optional_fields_serialises() {
let req = CreatePaymentRequest::new(
"cust-1",
"https://example.test/cb",
Money::bdt("50.00"),
Currency::Bdt,
)
.with_merchant_invoice_number("INV-AC-1");
let json: serde_json::Value = serde_json::to_value(&req).unwrap();
assert_eq!(json["merchantInvoiceNumber"], "INV-AC-1");
}
#[test]
fn create_payment_response_parses_minimal() {
let body = r#"{
"paymentID": "TR0001",
"bkashURL": "https://example.test/bkash",
"callbackURL": "https://example.test/cb",
"paymentCreateTime": "2026-06-22T10:00:00:000 GMT+06:00",
"payerReference": "cust-1",
"orgShortCode": "0123",
"currency": "BDT",
"intent": "authorization",
"merchantInvoiceNumber": ""
}"#;
let resp: CreatePaymentResponse = serde_json::from_str(body).unwrap();
assert_eq!(resp.payment_id, "TR0001");
assert_eq!(resp.intent, Intent::Authorization);
assert_eq!(resp.bkash_url, "https://example.test/bkash");
}
#[test]
fn execute_payment_response_parses_minimal() {
let body = r#"{
"paymentID": "TR0001",
"trxID": "8A00ABCD",
"transactionStatus": "Authorized",
"amount": "50.00",
"currency": "BDT",
"intent": "authorization",
"paymentExecuteTime": "2026-06-22T10:01: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, TransactionStatus::Authorized);
assert_eq!(resp.amount.as_str(), "50.00");
}
#[test]
fn query_payment_request_serialises() {
let req = QueryPaymentRequest::new("TR0001");
let json: serde_json::Value = serde_json::to_value(&req).unwrap();
assert_eq!(json["paymentID"], "TR0001");
}
#[test]
fn query_payment_response_parses_authorized() {
let body = r#"{
"paymentID": "TR0001",
"trxID": "8A00ABCD",
"transactionStatus": "Authorized",
"amount": "50.00",
"currency": "BDT",
"intent": "authorization",
"payerReference": "cust-1",
"customerMsisdn": "01700000000",
"orgShortCode": "0123",
"callbackURL": "https://example.test/cb",
"merchantInvoiceNumber": "",
"paymentExecuteTime": "2026-06-22T10:01:00:000 GMT+06:00"
}"#;
let resp: QueryPaymentResponse = serde_json::from_str(body).unwrap();
assert_eq!(resp.payment_id, "TR0001");
assert_eq!(resp.transaction_status, TransactionStatus::Authorized);
}
#[test]
fn capture_request_serialises() {
let req = CaptureRequest::new("TR0001");
let json: serde_json::Value = serde_json::to_value(&req).unwrap();
assert_eq!(json["paymentID"], "TR0001");
}
#[test]
fn capture_response_accepts_lowercase_payment_id() {
let body = r#"{
"paymentId": "TR0001",
"createTime": "2026-06-22T10:02:00:000 GMT+06:00",
"updateTime": "2026-06-22T10:02:00:000 GMT+06:00",
"trxID": "8A00ABCD",
"transactionStatus": "Completed"
}"#;
let resp: CaptureResponse = serde_json::from_str(body).unwrap();
assert_eq!(resp.payment_id, "TR0001");
assert_eq!(resp.trx_id, "8A00ABCD");
assert_eq!(resp.transaction_status, "Completed");
}
#[test]
fn capture_response_accepts_uppercase_payment_id() {
let body = r#"{
"paymentID": "TR0001",
"createTime": "2026-06-22T10:02:00:000 GMT+06:00",
"updateTime": "2026-06-22T10:02:00:000 GMT+06:00",
"trxID": "8A00ABCD",
"transactionStatus": "Completed"
}"#;
let resp: CaptureResponse = serde_json::from_str(body).unwrap();
assert_eq!(resp.payment_id, "TR0001");
}
#[test]
fn void_request_serialises() {
let req = VoidRequest::new("TR0001");
let json: serde_json::Value = serde_json::to_value(&req).unwrap();
assert_eq!(json["paymentID"], "TR0001");
}
#[test]
fn void_response_accepts_lowercase_payment_id() {
let body = r#"{
"paymentId": "TR0001",
"createTime": "2026-06-22T10:02:00:000 GMT+06:00",
"updateTime": "2026-06-22T10:02:00:000 GMT+06:00",
"trxID": "8A00ABCD",
"transactionStatus": "Cancelled"
}"#;
let resp: VoidResponse = serde_json::from_str(body).unwrap();
assert_eq!(resp.payment_id, "TR0001");
assert_eq!(resp.transaction_status, "Cancelled");
}
#[test]
fn void_response_accepts_uppercase_payment_id() {
let body = r#"{
"paymentID": "TR0001",
"createTime": "2026-06-22T10:02:00:000 GMT+06:00",
"updateTime": "2026-06-22T10:02:00:000 GMT+06:00",
"trxID": "8A00ABCD",
"transactionStatus": "Cancelled"
}"#;
let resp: VoidResponse = serde_json::from_str(body).unwrap();
assert_eq!(resp.payment_id, "TR0001");
}
#[test]
fn search_transaction_response_parses() {
let body = r#"{
"trxID": "8A00ABCD",
"transactionStatus": "Authorized",
"transactionType": "Payment",
"amount": "100.00",
"currency": "BDT",
"customerMsisdn": "01700000000",
"organizationShortCode": "0123",
"initiationTime": "2026-06-22T09:00:00:000 GMT+06:00",
"completedTime": ""
}"#;
let resp: SearchTransactionResponse = serde_json::from_str(body).unwrap();
assert_eq!(resp.trx_id, "8A00ABCD");
assert_eq!(resp.transaction_status, TransactionStatus::Authorized);
assert_eq!(resp.amount.as_str(), "100.00");
}
use proptest::prelude::*;
proptest! {
#[test]
fn create_payment_request_roundtrip(
payer_ref in ".*",
callback in ".*",
amount in ".*",
) {
let req = CreatePaymentRequest::new(
payer_ref,
callback,
Money::new(amount),
Currency::Bdt,
);
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);
prop_assert_eq!(back.intent, Intent::Authorization);
prop_assert_eq!(back.mode, PAYMENT_MODE);
}
}
}