use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::auth::Signer;
use crate::config::WxPayConfig;
use crate::error::WxPayResult;
use crate::http::{HttpClient, HttpMethod};
use crate::services::transport::{ServiceTransport, TransportObserver};
#[derive(Debug, Clone, Serialize)]
pub struct QueryFilter {
#[serde(skip_serializing_if = "Option::is_none")]
pub transaction_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub out_trade_no: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mchid: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct QueryByTransactionIdRequest {
pub transaction_id: String,
pub mchid: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct QueryByOutTradeNoRequest {
pub out_trade_no: String,
pub mchid: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct CloseOrderRequest {
pub mchid: String,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct CloseOrderResponse {}
#[derive(Debug, Clone, Deserialize)]
pub struct Transaction {
pub appid: String,
pub mchid: String,
pub out_trade_no: Option<String>,
pub transaction_id: String,
pub trade_state: String,
#[serde(rename = "trade_type")]
pub trade_type: Option<String>,
#[serde(rename = "trade_state_desc")]
pub trade_state_desc: Option<String>,
pub amount: Option<QueryAmount>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct QueryAmount {
pub total: u64,
pub currency: Option<String>,
#[serde(rename = "payer_total")]
pub payer_total: Option<u64>,
}
#[allow(dead_code)]
pub struct QueryService {
config: Arc<WxPayConfig>,
http_client: Arc<dyn HttpClient>,
signer: Arc<dyn Signer>,
transport: ServiceTransport,
}
impl QueryService {
pub fn new(
config: Arc<WxPayConfig>,
http_client: Arc<dyn HttpClient>,
signer: Arc<dyn Signer>,
) -> Self {
Self::new_with_observer(config.clone(), http_client.clone(), signer.clone(), None)
}
pub fn new_with_observer(
config: Arc<WxPayConfig>,
http_client: Arc<dyn HttpClient>,
signer: Arc<dyn Signer>,
transport_observer: Option<Arc<dyn TransportObserver>>,
) -> Self {
Self {
config: config.clone(),
http_client: http_client.clone(),
signer: signer.clone(),
transport: ServiceTransport::new_with_observer(
config,
http_client,
signer,
transport_observer,
),
}
}
pub async fn by_transaction_id(&self, transaction_id: &str) -> WxPayResult<Transaction> {
let request = QueryByTransactionIdRequest {
transaction_id: transaction_id.to_string(),
mchid: self.config.merchant_id.clone(),
};
self.by_transaction_id_request(&request).await
}
pub async fn by_transaction_id_request(
&self,
request: &QueryByTransactionIdRequest,
) -> WxPayResult<Transaction> {
self.by_transaction_id_with_request(request).await
}
pub async fn query_order_by_id(&self, transaction_id: &str) -> WxPayResult<Transaction> {
self.by_transaction_id(transaction_id).await
}
async fn by_transaction_id_with_request(
&self,
request: &QueryByTransactionIdRequest,
) -> WxPayResult<Transaction> {
let path = format!(
"/v3/pay/transactions/id/{}?mchid={}",
request.transaction_id, request.mchid
);
self.transport
.request(HttpMethod::Get, &path, None, "query.by_transaction_id")
.await
}
pub async fn by_out_trade_no(&self, out_trade_no: &str) -> WxPayResult<Transaction> {
let request = QueryByOutTradeNoRequest {
out_trade_no: out_trade_no.to_string(),
mchid: self.config.merchant_id.clone(),
};
self.by_out_trade_no_request(&request).await
}
pub async fn by_out_trade_no_request(
&self,
request: &QueryByOutTradeNoRequest,
) -> WxPayResult<Transaction> {
self.by_out_trade_no_with_request(request).await
}
pub async fn query_order_by_out_trade_no(
&self,
out_trade_no: &str,
) -> WxPayResult<Transaction> {
self.by_out_trade_no(out_trade_no).await
}
async fn by_out_trade_no_with_request(
&self,
request: &QueryByOutTradeNoRequest,
) -> WxPayResult<Transaction> {
let path = format!(
"/v3/pay/transactions/out-trade-no/{}?mchid={}",
request.out_trade_no, request.mchid
);
self.transport
.request(HttpMethod::Get, &path, None, "query.by_out_trade_no")
.await
}
pub async fn by_filter(&self, filter: &QueryFilter) -> WxPayResult<Transaction> {
let transaction_id = filter.transaction_id.as_deref();
let out_trade_no = filter.out_trade_no.as_deref();
let mchid = filter
.mchid
.clone()
.unwrap_or_else(|| self.config.merchant_id.clone());
match (transaction_id, out_trade_no) {
(Some(transaction_id), _) => {
let request = QueryByTransactionIdRequest {
transaction_id: transaction_id.to_string(),
mchid,
};
self.by_transaction_id_request(&request).await
}
(None, Some(out_trade_no)) => {
let request = QueryByOutTradeNoRequest {
out_trade_no: out_trade_no.to_string(),
mchid,
};
self.by_out_trade_no_request(&request).await
}
_ => Err(crate::error::WxPayError::invalid_parameter(
"by_filter 需要提供 transaction_id 或 out_trade_no",
)),
}
}
pub async fn close(&self, out_trade_no: &str) -> WxPayResult<CloseOrderResponse> {
let request = CloseOrderRequest {
mchid: self.config.merchant_id.clone(),
};
self.close_with_request(out_trade_no, &request).await
}
pub async fn close_with_request(
&self,
out_trade_no: &str,
request: &CloseOrderRequest,
) -> WxPayResult<CloseOrderResponse> {
let path = format!("/v3/pay/transactions/out-trade-no/{}/close", out_trade_no);
let body = serde_json::to_string(request)?;
self.transport
.request_default(HttpMethod::Post, &path, Some(&body), "query.close")
.await
}
}
impl std::fmt::Debug for QueryService {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("QueryService").finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_query_by_transaction_id_request_serialization() {
let request = QueryByTransactionIdRequest {
transaction_id: "4200000001".to_string(),
mchid: "1900000109".to_string(),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("4200000001"));
assert!(json.contains("1900000109"));
}
#[test]
fn test_query_by_out_trade_no_request_serialization() {
let request = QueryByOutTradeNoRequest {
out_trade_no: "out_trade_no_001".to_string(),
mchid: "1900000109".to_string(),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("out_trade_no_001"));
assert!(json.contains("1900000109"));
}
#[test]
fn test_go_style_query_alias_signatures_exist() {
let _ = QueryService::query_order_by_id;
let _ = QueryService::query_order_by_out_trade_no;
}
}