补齐微信虚拟支付查单与补偿
接入微信官方虚拟支付查单并缓存稳定接口令牌 让用户确认与订单过期补偿统一核对支付类型、状态、金额和单号 增加单笔历史订单的 dry-run 与确认指纹补单工具 隔离运行时身份与敏感 openid 文件并补充配置契约和回归测试
This commit is contained in:
@@ -145,6 +145,7 @@ pub struct AppConfig {
|
||||
pub wechat_mini_program_virtual_payment_offer_id: Option<String>,
|
||||
pub wechat_mini_program_virtual_payment_app_key: Option<String>,
|
||||
pub wechat_mini_program_virtual_payment_sandbox_app_key: Option<String>,
|
||||
pub wechat_mini_program_virtual_payment_query_order_endpoint: String,
|
||||
pub wechat_mini_program_message_token: Option<String>,
|
||||
pub wechat_mini_program_message_encoding_aes_key: Option<String>,
|
||||
pub wechat_mini_program_subscribe_message_enabled: bool,
|
||||
@@ -400,6 +401,8 @@ impl Default for AppConfig {
|
||||
wechat_mini_program_virtual_payment_offer_id: None,
|
||||
wechat_mini_program_virtual_payment_app_key: None,
|
||||
wechat_mini_program_virtual_payment_sandbox_app_key: None,
|
||||
wechat_mini_program_virtual_payment_query_order_endpoint:
|
||||
"https://api.weixin.qq.com/xpay/query_order".to_string(),
|
||||
wechat_mini_program_message_token: None,
|
||||
wechat_mini_program_message_encoding_aes_key: None,
|
||||
wechat_mini_program_subscribe_message_enabled: true,
|
||||
@@ -941,6 +944,11 @@ impl AppConfig {
|
||||
read_first_non_empty_env(&["WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY"]);
|
||||
config.wechat_mini_program_virtual_payment_sandbox_app_key =
|
||||
read_first_non_empty_env(&["WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY"]);
|
||||
if let Some(endpoint) =
|
||||
read_first_non_empty_env(&["WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_QUERY_ORDER_ENDPOINT"])
|
||||
{
|
||||
config.wechat_mini_program_virtual_payment_query_order_endpoint = endpoint;
|
||||
}
|
||||
config.wechat_mini_program_message_token =
|
||||
read_first_non_empty_env(&["WECHAT_MINIPROGRAM_MESSAGE_TOKEN"]);
|
||||
config.wechat_mini_program_message_encoding_aes_key =
|
||||
@@ -2083,6 +2091,7 @@ mod tests {
|
||||
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_OFFER_ID");
|
||||
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY");
|
||||
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY");
|
||||
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_QUERY_ORDER_ENDPOINT");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_TOKEN");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_ENABLED");
|
||||
@@ -2110,6 +2119,10 @@ mod tests {
|
||||
"WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY",
|
||||
"sandbox-app-key-001",
|
||||
);
|
||||
std::env::set_var(
|
||||
"WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_QUERY_ORDER_ENDPOINT",
|
||||
"http://127.0.0.1:18080/xpay/query_order",
|
||||
);
|
||||
std::env::set_var("WECHAT_MINIPROGRAM_MESSAGE_TOKEN", "message-token-001");
|
||||
std::env::set_var(
|
||||
"WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY",
|
||||
@@ -2181,6 +2194,10 @@ mod tests {
|
||||
);
|
||||
assert_eq!(config.wechat_mini_program_subscribe_message_state, "trial");
|
||||
assert_eq!(config.wechat_mini_program_virtual_payment_env, 1);
|
||||
assert_eq!(
|
||||
config.wechat_mini_program_virtual_payment_query_order_endpoint,
|
||||
"http://127.0.0.1:18080/xpay/query_order"
|
||||
);
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("WECHAT_PAY_ENABLED");
|
||||
@@ -2195,6 +2212,7 @@ mod tests {
|
||||
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_OFFER_ID");
|
||||
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY");
|
||||
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY");
|
||||
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_QUERY_ORDER_ENDPOINT");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_TOKEN");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_ENABLED");
|
||||
|
||||
@@ -4,12 +4,20 @@ use module_runtime::{
|
||||
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL,
|
||||
RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeOrderStatus,
|
||||
};
|
||||
use platform_wechat::WechatError;
|
||||
use platform_wechat::pay::{WechatPayError, WechatPayNotifyOrder};
|
||||
use shared_kernel::{offset_datetime_to_unix_micros, parse_rfc3339};
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::{state::AppState, wechat::pay::current_unix_micros};
|
||||
use crate::{
|
||||
state::AppState,
|
||||
wechat::pay::{
|
||||
build_wechat_virtual_payment_query_order_request, current_unix_micros,
|
||||
is_wechat_virtual_payment_order_paid, paid_at_micros_from_wechat_virtual_payment_order,
|
||||
validate_wechat_virtual_payment_order,
|
||||
},
|
||||
};
|
||||
|
||||
const PROFILE_RECHARGE_EXPIRATION_LISTENER_RECONNECT_DELAY: Duration = Duration::from_secs(5);
|
||||
const PROFILE_RECHARGE_EXPIRATION_CATCH_UP_LIMIT: u32 = 100;
|
||||
@@ -184,16 +192,7 @@ async fn process_expired_profile_recharge_order_once(
|
||||
}
|
||||
|
||||
if order.payment_channel == PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL {
|
||||
mark_profile_recharge_expiration_checked(
|
||||
state,
|
||||
&order.order_id,
|
||||
Some(current_unix_micros()),
|
||||
Some("VIRTUAL_UNQUERYABLE".to_string()),
|
||||
Some("wechat_mp_virtual has no v3 transaction query".to_string()),
|
||||
)
|
||||
.await?;
|
||||
state.publish_profile_recharge_order_update(order.order_id.clone());
|
||||
return Ok(());
|
||||
return process_expired_virtual_payment_order(state, order).await;
|
||||
}
|
||||
|
||||
let wechat_order = match state
|
||||
@@ -260,6 +259,65 @@ async fn process_expired_profile_recharge_order_once(
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_expired_virtual_payment_order(
|
||||
state: &AppState,
|
||||
order: &RuntimeProfileRechargeOrderRecord,
|
||||
) -> Result<(), ExpirationCompensationError> {
|
||||
let identity = state
|
||||
.wechat_auth_service()
|
||||
.get_identity_by_user_id(&order.user_id)
|
||||
.map_err(|error| {
|
||||
ExpirationCompensationError::Runtime(format!(
|
||||
"failed to read WeChat identity for virtual payment query: {error}"
|
||||
))
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
ExpirationCompensationError::Runtime(
|
||||
"virtual payment query requires the user's WeChat identity".to_string(),
|
||||
)
|
||||
})?;
|
||||
let query_request = build_wechat_virtual_payment_query_order_request(
|
||||
&state.config,
|
||||
identity.provider_uid,
|
||||
order.order_id.clone(),
|
||||
)?;
|
||||
let wechat_order = state
|
||||
.wechat_client()
|
||||
.query_virtual_payment_order(query_request)
|
||||
.await?;
|
||||
validate_wechat_virtual_payment_order(&order.order_id, order.amount_cents, &wechat_order)?;
|
||||
|
||||
if is_wechat_virtual_payment_order_paid(wechat_order.status) {
|
||||
let paid_at_micros = paid_at_micros_from_wechat_virtual_payment_order(&wechat_order);
|
||||
state
|
||||
.spacetime_client()
|
||||
.mark_profile_recharge_order_paid(
|
||||
order.order_id.clone(),
|
||||
paid_at_micros,
|
||||
wechat_order.wxpay_order_id.or(wechat_order.wx_order_id),
|
||||
)
|
||||
.await?;
|
||||
state.publish_profile_recharge_order_update(order.order_id.clone());
|
||||
info!(
|
||||
order_id = order.order_id.as_str(),
|
||||
virtual_payment_status = wechat_order.status,
|
||||
"expired virtual payment recharge order compensated as paid"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
mark_profile_recharge_expiration_checked(
|
||||
state,
|
||||
&order.order_id,
|
||||
Some(current_unix_micros()),
|
||||
Some(format!("VIRTUAL_STATUS_{}", wechat_order.status)),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
state.publish_profile_recharge_order_update(order.order_id.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mark_expired_profile_recharge_order_paid(
|
||||
state: &AppState,
|
||||
order: &RuntimeProfileRechargeOrderRecord,
|
||||
@@ -306,19 +364,9 @@ async fn mark_profile_recharge_expiration_checked(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn record_profile_recharge_expiration_error(
|
||||
state: &AppState,
|
||||
order_id: &str,
|
||||
error: String,
|
||||
) {
|
||||
if let Err(mark_error) = mark_profile_recharge_expiration_checked(
|
||||
state,
|
||||
order_id,
|
||||
None,
|
||||
None,
|
||||
Some(error),
|
||||
)
|
||||
.await
|
||||
async fn record_profile_recharge_expiration_error(state: &AppState, order_id: &str, error: String) {
|
||||
if let Err(mark_error) =
|
||||
mark_profile_recharge_expiration_checked(state, order_id, None, None, Some(error)).await
|
||||
{
|
||||
warn!(
|
||||
order_id,
|
||||
@@ -330,11 +378,18 @@ async fn record_profile_recharge_expiration_error(
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ExpirationCompensationError {
|
||||
Wechat(WechatError),
|
||||
WechatPay(WechatPayError),
|
||||
Spacetime(spacetime_client::SpacetimeClientError),
|
||||
Runtime(String),
|
||||
}
|
||||
|
||||
impl From<WechatError> for ExpirationCompensationError {
|
||||
fn from(error: WechatError) -> Self {
|
||||
Self::Wechat(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WechatPayError> for ExpirationCompensationError {
|
||||
fn from(error: WechatPayError) -> Self {
|
||||
Self::WechatPay(error)
|
||||
@@ -350,6 +405,7 @@ impl From<spacetime_client::SpacetimeClientError> for ExpirationCompensationErro
|
||||
impl std::fmt::Display for ExpirationCompensationError {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Wechat(error) => write!(formatter, "wechat error: {error}"),
|
||||
Self::WechatPay(error) => write!(formatter, "wechat pay error: {error}"),
|
||||
Self::Spacetime(error) => write!(formatter, "spacetime error: {error}"),
|
||||
Self::Runtime(message) => formatter.write_str(message),
|
||||
|
||||
@@ -89,11 +89,14 @@ use crate::{
|
||||
api_response::json_success_body,
|
||||
auth::AuthenticatedAccessToken,
|
||||
http_error::AppError,
|
||||
platform_errors::map_wechat_error,
|
||||
request_context::RequestContext,
|
||||
state::AppState,
|
||||
wechat::pay::{
|
||||
build_wechat_payment_request, build_wechat_web_payment_request, current_unix_micros,
|
||||
map_wechat_pay_error,
|
||||
build_wechat_payment_request, build_wechat_virtual_payment_query_order_request,
|
||||
build_wechat_web_payment_request, current_unix_micros,
|
||||
is_wechat_virtual_payment_order_paid, map_wechat_pay_error,
|
||||
paid_at_micros_from_wechat_virtual_payment_order, validate_wechat_virtual_payment_order,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -389,6 +392,52 @@ pub async fn confirm_wechat_profile_recharge_order(
|
||||
));
|
||||
}
|
||||
if order.payment_channel == PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL {
|
||||
let openid = resolve_wechat_identity_for_payment(&state, &order.user_id)
|
||||
.await
|
||||
.map_err(|error| runtime_profile_error_response(&request_context, error))?;
|
||||
let query_request = build_wechat_virtual_payment_query_order_request(
|
||||
&state.config,
|
||||
openid,
|
||||
order.order_id.clone(),
|
||||
)
|
||||
.map_err(|error| {
|
||||
runtime_profile_error_response(&request_context, map_wechat_error(error))
|
||||
})?;
|
||||
let wechat_order = state
|
||||
.wechat_client()
|
||||
.query_virtual_payment_order(query_request)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
runtime_profile_error_response(&request_context, map_wechat_error(error))
|
||||
})?;
|
||||
validate_wechat_virtual_payment_order(&order.order_id, order.amount_cents, &wechat_order)
|
||||
.map_err(|error| {
|
||||
runtime_profile_error_response(&request_context, map_wechat_error(error))
|
||||
})?;
|
||||
if !is_wechat_virtual_payment_order_paid(wechat_order.status) {
|
||||
return Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
build_wechat_profile_recharge_order_confirmation(center, order),
|
||||
));
|
||||
}
|
||||
|
||||
let paid_at_micros = paid_at_micros_from_wechat_virtual_payment_order(&wechat_order);
|
||||
let (center, order) = state
|
||||
.spacetime_client()
|
||||
.mark_profile_recharge_order_paid(
|
||||
wechat_order.order_id,
|
||||
paid_at_micros,
|
||||
wechat_order.wxpay_order_id.or(wechat_order.wx_order_id),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
runtime_profile_error_response(
|
||||
&request_context,
|
||||
map_runtime_profile_client_error(error),
|
||||
)
|
||||
})?;
|
||||
state.publish_profile_recharge_order_update(order.order_id.clone());
|
||||
|
||||
return Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
build_wechat_profile_recharge_order_confirmation(center, order),
|
||||
|
||||
@@ -1883,6 +1883,9 @@ fn build_wechat_client(config: &AppConfig) -> WechatClient {
|
||||
subscribe_message_endpoint: config
|
||||
.wechat_mini_program_subscribe_message_endpoint
|
||||
.clone(),
|
||||
virtual_payment_query_order_endpoint: config
|
||||
.wechat_mini_program_virtual_payment_query_order_endpoint
|
||||
.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ use platform_wechat::pay::{
|
||||
parse_virtual_payment_notify, parse_wechat_mini_program_message_push_payload,
|
||||
resolve_wechat_message_push_verify_response, verify_wechat_message_push_signature,
|
||||
};
|
||||
use platform_wechat::{
|
||||
WechatError, WechatVirtualPaymentOrder, WechatVirtualPaymentQueryOrderRequest,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use shared_kernel::offset_datetime_to_unix_micros;
|
||||
@@ -248,6 +251,80 @@ pub fn build_wechat_pay_config(config: &AppConfig) -> WechatPayConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_wechat_virtual_payment_query_order_request(
|
||||
config: &AppConfig,
|
||||
openid: String,
|
||||
order_id: String,
|
||||
) -> Result<WechatVirtualPaymentQueryOrderRequest, WechatError> {
|
||||
let app_key = match config.wechat_mini_program_virtual_payment_env {
|
||||
0 => config
|
||||
.wechat_mini_program_virtual_payment_app_key
|
||||
.as_deref(),
|
||||
1 => config
|
||||
.wechat_mini_program_virtual_payment_sandbox_app_key
|
||||
.as_deref(),
|
||||
env => {
|
||||
return Err(WechatError::InvalidConfig(format!(
|
||||
"微信虚拟支付查单 env 只允许 0 或 1,当前为 {env}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
WechatError::InvalidConfig(match config.wechat_mini_program_virtual_payment_env {
|
||||
1 => "微信虚拟支付沙箱 AppKey 未配置".to_string(),
|
||||
_ => "微信虚拟支付 AppKey 未配置".to_string(),
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(WechatVirtualPaymentQueryOrderRequest {
|
||||
openid,
|
||||
order_id,
|
||||
env: config.wechat_mini_program_virtual_payment_env,
|
||||
app_key: app_key.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate_wechat_virtual_payment_order(
|
||||
expected_order_id: &str,
|
||||
expected_amount_cents: u64,
|
||||
order: &WechatVirtualPaymentOrder,
|
||||
) -> Result<(), WechatError> {
|
||||
if order.order_id != expected_order_id {
|
||||
return Err(WechatError::Upstream(
|
||||
"微信虚拟支付查单返回的订单号与本地订单不一致".to_string(),
|
||||
));
|
||||
}
|
||||
if !matches!(order.order_type, 0 | 7) {
|
||||
return Err(WechatError::Upstream(
|
||||
"微信虚拟支付查单返回的不是可入账支付单".to_string(),
|
||||
));
|
||||
}
|
||||
if order.order_fee != expected_amount_cents {
|
||||
return Err(WechatError::Upstream(
|
||||
"微信虚拟支付查单返回的金额与本地订单不一致".to_string(),
|
||||
));
|
||||
}
|
||||
if !(0..=10).contains(&order.status) {
|
||||
return Err(WechatError::Upstream(
|
||||
"微信虚拟支付查单返回了未知订单状态".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_wechat_virtual_payment_order_paid(status: i64) -> bool {
|
||||
matches!(status, 2..=4)
|
||||
}
|
||||
|
||||
pub fn paid_at_micros_from_wechat_virtual_payment_order(order: &WechatVirtualPaymentOrder) -> i64 {
|
||||
order
|
||||
.paid_time
|
||||
.and_then(|seconds| seconds.checked_mul(1_000_000))
|
||||
.unwrap_or_else(current_unix_micros)
|
||||
}
|
||||
|
||||
pub fn map_wechat_pay_error(error: WechatPayError) -> AppError {
|
||||
match error {
|
||||
WechatPayError::Disabled => AppError::from_status(StatusCode::BAD_REQUEST)
|
||||
@@ -345,6 +422,82 @@ fn build_wechat_message_push_verify_error_response(error: WechatPayError) -> Res
|
||||
(StatusCode::BAD_REQUEST, message).into_response()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
build_wechat_virtual_payment_query_order_request, is_wechat_virtual_payment_order_paid,
|
||||
validate_wechat_virtual_payment_order,
|
||||
};
|
||||
use crate::config::AppConfig;
|
||||
use platform_wechat::WechatVirtualPaymentOrder;
|
||||
|
||||
#[test]
|
||||
fn virtual_payment_query_uses_the_key_for_the_selected_environment() {
|
||||
let config = AppConfig {
|
||||
wechat_mini_program_virtual_payment_app_key: Some("production-key".to_string()),
|
||||
wechat_mini_program_virtual_payment_sandbox_app_key: Some("sandbox-key".to_string()),
|
||||
wechat_mini_program_virtual_payment_env: 1,
|
||||
..AppConfig::default()
|
||||
};
|
||||
|
||||
let request = build_wechat_virtual_payment_query_order_request(
|
||||
&config,
|
||||
"openid-001".to_string(),
|
||||
"order-001".to_string(),
|
||||
)
|
||||
.expect("sandbox query request should build");
|
||||
|
||||
assert_eq!(request.app_key, "sandbox-key");
|
||||
assert_eq!(request.env, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn virtual_payment_query_only_treats_paid_and_delivery_states_as_paid() {
|
||||
assert!(!is_wechat_virtual_payment_order_paid(0));
|
||||
assert!(!is_wechat_virtual_payment_order_paid(1));
|
||||
assert!(is_wechat_virtual_payment_order_paid(2));
|
||||
assert!(is_wechat_virtual_payment_order_paid(3));
|
||||
assert!(is_wechat_virtual_payment_order_paid(4));
|
||||
assert!(!is_wechat_virtual_payment_order_paid(5));
|
||||
assert!(!is_wechat_virtual_payment_order_paid(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn virtual_payment_query_rejects_a_mismatched_amount_before_crediting() {
|
||||
let order = WechatVirtualPaymentOrder {
|
||||
order_id: "order-001".to_string(),
|
||||
status: 2,
|
||||
order_fee: 601,
|
||||
order_type: 0,
|
||||
paid_time: Some(1_777_111_300),
|
||||
wx_order_id: Some("wx-order-001".to_string()),
|
||||
wxpay_order_id: Some("wxpay-order-001".to_string()),
|
||||
};
|
||||
|
||||
assert!(validate_wechat_virtual_payment_order("order-001", 600, &order).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn virtual_payment_query_accepts_ios_payment_and_rejects_refunds() {
|
||||
let mut order = WechatVirtualPaymentOrder {
|
||||
order_id: "order-001".to_string(),
|
||||
status: 2,
|
||||
order_fee: 600,
|
||||
order_type: 7,
|
||||
paid_time: Some(1_777_111_300),
|
||||
wx_order_id: Some("wx-order-001".to_string()),
|
||||
wxpay_order_id: None,
|
||||
};
|
||||
|
||||
validate_wechat_virtual_payment_order("order-001", 600, &order)
|
||||
.expect("iOS payment order should be eligible for confirmation");
|
||||
for refund_type in [1, 8] {
|
||||
order.order_type = refund_type;
|
||||
assert!(validate_wechat_virtual_payment_order("order-001", 600, &order).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_virtual_payment_notify_error_response(
|
||||
error: WechatPayError,
|
||||
response_format: VirtualPaymentNotifyResponseFormat,
|
||||
|
||||
Reference in New Issue
Block a user