Merge branch 'master' into gptimage2-param-limit-in-backend
This commit is contained in:
@@ -16,6 +16,7 @@ use module_runtime::{
|
||||
build_runtime_profile_recharge_order_admin_list_input,
|
||||
build_runtime_profile_recharge_refund_hold_prepare_input,
|
||||
build_runtime_profile_recharge_refund_hold_preview_input,
|
||||
build_runtime_profile_recharge_refund_manual_review_resolve_input,
|
||||
build_runtime_profile_recharge_refund_settlement_plan,
|
||||
build_runtime_profile_wallet_manual_restriction_upsert_input,
|
||||
};
|
||||
@@ -27,7 +28,8 @@ use sha2::{Digest, Sha256};
|
||||
use shared_contracts::admin::{
|
||||
AdminProfileWalletPayload, AdminRechargeOrderEntryPayload, AdminRechargeOrderListQuery,
|
||||
AdminRechargeOrderListResponse, AdminRechargeRefundActionResponse,
|
||||
AdminRechargeRefundExecuteRequest, AdminRechargeRefundHoldPayload, AdminRechargeRefundPayload,
|
||||
AdminRechargeRefundExecuteRequest, AdminRechargeRefundHoldPayload,
|
||||
AdminRechargeRefundManualReviewResolveRequest, AdminRechargeRefundPayload,
|
||||
AdminRechargeRefundPreviewRequest, AdminRechargeRefundPreviewResponse,
|
||||
AdminRechargeRefundRegisterRequest, AdminUserDetailQuery, AdminUserDetailResponse,
|
||||
AdminUserSummaryPayload, AdminWalletManualRestrictionPayload, AdminWalletRestrictionRequest,
|
||||
@@ -448,6 +450,49 @@ pub async fn admin_register_recharge_refund(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn admin_resolve_recharge_refund_manual_review(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(admin): Extension<AuthenticatedAdmin>,
|
||||
Json(payload): Json<AdminRechargeRefundManualReviewResolveRequest>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let input = build_runtime_profile_recharge_refund_manual_review_resolve_input(
|
||||
payload.out_refund_no,
|
||||
admin.session().subject.clone(),
|
||||
payload.reason,
|
||||
)
|
||||
.map_err(|message| {
|
||||
error_response(
|
||||
&request_context,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_message(message),
|
||||
)
|
||||
})?;
|
||||
let (refund, _, _, resolution_code) = state
|
||||
.spacetime_client()
|
||||
.resolve_profile_recharge_refund_manual_review(input)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
error_response(
|
||||
&request_context,
|
||||
AppError::from_status(StatusCode::CONFLICT)
|
||||
.with_message("人工确认退款归属失败")
|
||||
.with_details(json!({"message": error.to_string()})),
|
||||
)
|
||||
})?;
|
||||
let entry = load_order(&state, &request_context, &refund.order_id).await?;
|
||||
let user = load_user_summary(&state, &entry.order.user_id);
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
AdminRechargeRefundActionResponse {
|
||||
out_refund_no: refund.out_refund_no,
|
||||
provider_status: refund.provider_status.as_str().to_string(),
|
||||
result_code: resolution_code,
|
||||
provider_status_unknown: false,
|
||||
order: map_order_entry(entry, user),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn admin_update_wallet_restriction(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -872,6 +917,9 @@ fn map_refund(refund: RuntimeProfileRechargeRefundSnapshot) -> AdminRechargeRefu
|
||||
unrecovered_points: refund.unrecovered_points,
|
||||
recovery_status: refund.recovery_status.as_str().to_string(),
|
||||
last_error_code: refund.last_error_code,
|
||||
manual_review_resolved_by_admin_user_id: refund.manual_review_resolved_by_admin_user_id,
|
||||
manual_review_resolution_reason: refund.manual_review_resolution_reason,
|
||||
manual_review_resolved_at_micros: refund.manual_review_resolved_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
admin_recharge::{
|
||||
admin_execute_recharge_refund, admin_get_user_detail, admin_list_recharge_orders,
|
||||
admin_preview_recharge_refund, admin_register_recharge_refund,
|
||||
admin_update_wallet_restriction,
|
||||
admin_resolve_recharge_refund_manual_review, admin_update_wallet_restriction,
|
||||
},
|
||||
runtime_profile::{
|
||||
admin_disable_profile_redeem_code, admin_disable_profile_task_config,
|
||||
@@ -275,6 +275,12 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
require_admin_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/admin/api/profile/recharge-refunds/manual-review/resolve",
|
||||
post(admin_resolve_recharge_refund_manual_review).route_layer(
|
||||
middleware::from_fn_with_state(state.clone(), require_admin_auth),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/admin/api/profile/users/detail",
|
||||
get(admin_get_user_detail).route_layer(middleware::from_fn_with_state(
|
||||
|
||||
@@ -54,7 +54,11 @@ pub(crate) struct PersistedWechatRefundObservation {
|
||||
|
||||
pub fn spawn_profile_recharge_refund_reconciliation_worker(state: AppState) {
|
||||
if !state.wechat_pay_refund_reconciliation_enabled() {
|
||||
debug!("wechat pay refund reconciliation worker is disabled");
|
||||
if state.wechat_pay_client().supports_refund_reconciliation() {
|
||||
warn!("real wechat pay is active but refund reconciliation worker is disabled");
|
||||
} else {
|
||||
debug!("wechat pay refund reconciliation worker is disabled");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if !state.wechat_pay_client().supports_refund_reconciliation() {
|
||||
@@ -62,6 +66,8 @@ pub fn spawn_profile_recharge_refund_reconciliation_worker(state: AppState) {
|
||||
return;
|
||||
}
|
||||
|
||||
info!("wechat pay refund reconciliation worker is enabled");
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut trade_bill_scan_slot = 0;
|
||||
let mut hold_not_found_counts = BTreeMap::<String, u8>::new();
|
||||
@@ -940,6 +946,9 @@ mod tests {
|
||||
recovery_status: RuntimeProfileRechargeRefundRecoveryStatus::NotApplicable,
|
||||
last_recovery_ledger_id: None,
|
||||
last_error_code: None,
|
||||
manual_review_resolved_by_admin_user_id: None,
|
||||
manual_review_resolution_reason: None,
|
||||
manual_review_resolved_at_micros: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -611,6 +611,24 @@ pub fn build_runtime_profile_recharge_refund_hold_list_input(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_recharge_refund_manual_review_resolve_input(
|
||||
out_refund_no: String,
|
||||
admin_user_id: String,
|
||||
reason: String,
|
||||
) -> Result<RuntimeProfileRechargeRefundManualReviewResolveInput, String> {
|
||||
Ok(RuntimeProfileRechargeRefundManualReviewResolveInput {
|
||||
out_refund_no: normalize_profile_recharge_refund_identifier(
|
||||
out_refund_no,
|
||||
"refund_manual_review.out_refund_no",
|
||||
)?,
|
||||
admin_user_id: normalize_profile_recharge_refund_identifier(
|
||||
admin_user_id,
|
||||
"refund_manual_review.admin_user_id",
|
||||
)?,
|
||||
reason: normalize_profile_recharge_refund_reason(reason, "refund_manual_review.reason")?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_admin_wallet_get_input(
|
||||
user_id: String,
|
||||
) -> Result<RuntimeProfileAdminWalletGetInput, String> {
|
||||
@@ -1432,6 +1450,28 @@ fn normalize_profile_task_id(value: String) -> Result<String, RuntimeProfileFiel
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn refund_manual_review_resolution_requires_auditable_fields() {
|
||||
let input = build_runtime_profile_recharge_refund_manual_review_resolve_input(
|
||||
" refund-1 ".to_string(),
|
||||
" admin-1 ".to_string(),
|
||||
" 已核对微信商户平台原始账单 ".to_string(),
|
||||
)
|
||||
.expect("manual review input should normalize");
|
||||
assert_eq!(input.out_refund_no, "refund-1");
|
||||
assert_eq!(input.admin_user_id, "admin-1");
|
||||
assert_eq!(input.reason, "已核对微信商户平台原始账单");
|
||||
|
||||
assert!(
|
||||
build_runtime_profile_recharge_refund_manual_review_resolve_input(
|
||||
"refund-1".to_string(),
|
||||
"admin-1".to_string(),
|
||||
" ".to_string(),
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invite_code_metadata_tags_alias_is_saved_as_user_tags() {
|
||||
let normalized = normalize_invite_code_metadata_json(
|
||||
|
||||
@@ -1435,6 +1435,9 @@ pub struct RuntimeProfileRechargeRefundSnapshot {
|
||||
pub recovery_status: RuntimeProfileRechargeRefundRecoveryStatus,
|
||||
pub last_recovery_ledger_id: Option<String>,
|
||||
pub last_error_code: Option<String>,
|
||||
pub manual_review_resolved_by_admin_user_id: Option<String>,
|
||||
pub manual_review_resolution_reason: Option<String>,
|
||||
pub manual_review_resolved_at_micros: Option<i64>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
@@ -1722,6 +1725,14 @@ pub struct RuntimeProfileRechargeRefundHoldListInput {
|
||||
pub limit: u32,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileRechargeRefundManualReviewResolveInput {
|
||||
pub out_refund_no: String,
|
||||
pub admin_user_id: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileAdminWalletGetInput {
|
||||
|
||||
@@ -709,6 +709,9 @@ pub struct AdminRechargeRefundPayload {
|
||||
pub unrecovered_points: u64,
|
||||
pub recovery_status: String,
|
||||
pub last_error_code: Option<String>,
|
||||
pub manual_review_resolved_by_admin_user_id: Option<String>,
|
||||
pub manual_review_resolution_reason: Option<String>,
|
||||
pub manual_review_resolved_at_micros: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -804,6 +807,13 @@ pub struct AdminRechargeRefundRegisterRequest {
|
||||
pub out_refund_no: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminRechargeRefundManualReviewResolveRequest {
|
||||
pub out_refund_no: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminWalletRestrictionRequest {
|
||||
@@ -854,7 +864,21 @@ pub struct AdminWalletRestrictionResponse {
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::AdminEditorShowcaseAssetPayload;
|
||||
use super::{
|
||||
AdminEditorShowcaseAssetPayload, AdminRechargeRefundManualReviewResolveRequest,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn refund_manual_review_resolution_request_uses_camel_case_contract() {
|
||||
let value = serde_json::to_value(AdminRechargeRefundManualReviewResolveRequest {
|
||||
out_refund_no: "refund-1".to_string(),
|
||||
reason: "已核对微信商户平台原始账单".to_string(),
|
||||
})
|
||||
.expect("request should serialize");
|
||||
|
||||
assert_eq!(value["outRefundNo"], json!("refund-1"));
|
||||
assert!(value.get("out_refund_no").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_showcase_asset_payload_serializes_author_fields_as_camel_case() {
|
||||
|
||||
@@ -161,6 +161,18 @@ impl From<module_runtime::RuntimeProfileRechargeRefundHoldListInput>
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeProfileRechargeRefundManualReviewResolveInput>
|
||||
for RuntimeProfileRechargeRefundManualReviewResolveInput
|
||||
{
|
||||
fn from(input: module_runtime::RuntimeProfileRechargeRefundManualReviewResolveInput) -> Self {
|
||||
Self {
|
||||
out_refund_no: input.out_refund_no,
|
||||
admin_user_id: input.admin_user_id,
|
||||
reason: input.reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeProfileAdminWalletGetInput> for RuntimeProfileAdminWalletGetInput {
|
||||
fn from(input: module_runtime::RuntimeProfileAdminWalletGetInput) -> Self {
|
||||
Self {
|
||||
@@ -1417,6 +1429,9 @@ pub(crate) fn map_runtime_profile_recharge_refund_snapshot(
|
||||
),
|
||||
last_recovery_ledger_id: snapshot.last_recovery_ledger_id,
|
||||
last_error_code: snapshot.last_error_code,
|
||||
manual_review_resolved_by_admin_user_id: snapshot.manual_review_resolved_by_admin_user_id,
|
||||
manual_review_resolution_reason: snapshot.manual_review_resolution_reason,
|
||||
manual_review_resolved_at_micros: snapshot.manual_review_resolved_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1012,6 +1012,7 @@ pub mod resolve_npc_interaction_reducer;
|
||||
pub mod resolve_npc_social_action_and_return_procedure;
|
||||
pub mod resolve_npc_social_action_input_type;
|
||||
pub mod resolve_npc_social_action_reducer;
|
||||
pub mod resolve_profile_recharge_refund_manual_review_and_return_procedure;
|
||||
pub mod resolve_treasure_interaction_and_return_procedure;
|
||||
pub mod resolve_treasure_interaction_reducer;
|
||||
pub mod restart_jump_hop_run_procedure;
|
||||
@@ -1112,6 +1113,7 @@ pub mod runtime_profile_recharge_refund_hold_release_input_type;
|
||||
pub mod runtime_profile_recharge_refund_hold_snapshot_type;
|
||||
pub mod runtime_profile_recharge_refund_hold_status_type;
|
||||
pub mod runtime_profile_recharge_refund_list_procedure_result_type;
|
||||
pub mod runtime_profile_recharge_refund_manual_review_resolve_input_type;
|
||||
pub mod runtime_profile_recharge_refund_observation_input_type;
|
||||
pub mod runtime_profile_recharge_refund_observation_source_type;
|
||||
pub mod runtime_profile_recharge_refund_procedure_result_type;
|
||||
@@ -2411,6 +2413,7 @@ pub use resolve_npc_interaction_reducer::resolve_npc_interaction;
|
||||
pub use resolve_npc_social_action_and_return_procedure::resolve_npc_social_action_and_return;
|
||||
pub use resolve_npc_social_action_input_type::ResolveNpcSocialActionInput;
|
||||
pub use resolve_npc_social_action_reducer::resolve_npc_social_action;
|
||||
pub use resolve_profile_recharge_refund_manual_review_and_return_procedure::resolve_profile_recharge_refund_manual_review_and_return;
|
||||
pub use resolve_treasure_interaction_and_return_procedure::resolve_treasure_interaction_and_return;
|
||||
pub use resolve_treasure_interaction_reducer::resolve_treasure_interaction;
|
||||
pub use restart_jump_hop_run_procedure::restart_jump_hop_run;
|
||||
@@ -2511,6 +2514,7 @@ pub use runtime_profile_recharge_refund_hold_release_input_type::RuntimeProfileR
|
||||
pub use runtime_profile_recharge_refund_hold_snapshot_type::RuntimeProfileRechargeRefundHoldSnapshot;
|
||||
pub use runtime_profile_recharge_refund_hold_status_type::RuntimeProfileRechargeRefundHoldStatus;
|
||||
pub use runtime_profile_recharge_refund_list_procedure_result_type::RuntimeProfileRechargeRefundListProcedureResult;
|
||||
pub use runtime_profile_recharge_refund_manual_review_resolve_input_type::RuntimeProfileRechargeRefundManualReviewResolveInput;
|
||||
pub use runtime_profile_recharge_refund_observation_input_type::RuntimeProfileRechargeRefundObservationInput;
|
||||
pub use runtime_profile_recharge_refund_observation_source_type::RuntimeProfileRechargeRefundObservationSource;
|
||||
pub use runtime_profile_recharge_refund_procedure_result_type::RuntimeProfileRechargeRefundProcedureResult;
|
||||
|
||||
@@ -33,6 +33,9 @@ pub struct ProfileRechargeRefund {
|
||||
pub recovery_status: RuntimeProfileRechargeRefundRecoveryStatus,
|
||||
pub last_recovery_ledger_id: Option<String>,
|
||||
pub last_error_code: Option<String>,
|
||||
pub manual_review_resolved_by_admin_user_id: Option<String>,
|
||||
pub manual_review_resolution_reason: Option<String>,
|
||||
pub manual_review_resolved_at: Option<__sdk::Timestamp>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for ProfileRechargeRefund {
|
||||
@@ -73,6 +76,12 @@ pub struct ProfileRechargeRefundCols {
|
||||
>,
|
||||
pub last_recovery_ledger_id: __sdk::__query_builder::Col<ProfileRechargeRefund, Option<String>>,
|
||||
pub last_error_code: __sdk::__query_builder::Col<ProfileRechargeRefund, Option<String>>,
|
||||
pub manual_review_resolved_by_admin_user_id:
|
||||
__sdk::__query_builder::Col<ProfileRechargeRefund, Option<String>>,
|
||||
pub manual_review_resolution_reason:
|
||||
__sdk::__query_builder::Col<ProfileRechargeRefund, Option<String>>,
|
||||
pub manual_review_resolved_at:
|
||||
__sdk::__query_builder::Col<ProfileRechargeRefund, Option<__sdk::Timestamp>>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for ProfileRechargeRefund {
|
||||
@@ -116,6 +125,18 @@ impl __sdk::__query_builder::HasCols for ProfileRechargeRefund {
|
||||
"last_recovery_ledger_id",
|
||||
),
|
||||
last_error_code: __sdk::__query_builder::Col::new(table_name, "last_error_code"),
|
||||
manual_review_resolved_by_admin_user_id: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"manual_review_resolved_by_admin_user_id",
|
||||
),
|
||||
manual_review_resolution_reason: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"manual_review_resolution_reason",
|
||||
),
|
||||
manual_review_resolved_at: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"manual_review_resolved_at",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_recharge_refund_manual_review_resolve_input_type::RuntimeProfileRechargeRefundManualReviewResolveInput;
|
||||
use super::runtime_profile_recharge_refund_procedure_result_type::RuntimeProfileRechargeRefundProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct ResolveProfileRechargeRefundManualReviewAndReturnArgs {
|
||||
pub input: RuntimeProfileRechargeRefundManualReviewResolveInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for ResolveProfileRechargeRefundManualReviewAndReturnArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `resolve_profile_recharge_refund_manual_review_and_return`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait resolve_profile_recharge_refund_manual_review_and_return {
|
||||
fn resolve_profile_recharge_refund_manual_review_and_return(
|
||||
&self,
|
||||
input: RuntimeProfileRechargeRefundManualReviewResolveInput,
|
||||
) {
|
||||
self.resolve_profile_recharge_refund_manual_review_and_return_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn resolve_profile_recharge_refund_manual_review_and_return_then(
|
||||
&self,
|
||||
input: RuntimeProfileRechargeRefundManualReviewResolveInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<RuntimeProfileRechargeRefundProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl resolve_profile_recharge_refund_manual_review_and_return for super::RemoteProcedures {
|
||||
fn resolve_profile_recharge_refund_manual_review_and_return_then(
|
||||
&self,
|
||||
input: RuntimeProfileRechargeRefundManualReviewResolveInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<RuntimeProfileRechargeRefundProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, RuntimeProfileRechargeRefundProcedureResult>(
|
||||
"resolve_profile_recharge_refund_manual_review_and_return",
|
||||
ResolveProfileRechargeRefundManualReviewAndReturnArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct RuntimeProfileRechargeRefundManualReviewResolveInput {
|
||||
pub out_refund_no: String,
|
||||
pub admin_user_id: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileRechargeRefundManualReviewResolveInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
+3
@@ -33,6 +33,9 @@ pub struct RuntimeProfileRechargeRefundSnapshot {
|
||||
pub recovery_status: RuntimeProfileRechargeRefundRecoveryStatus,
|
||||
pub last_recovery_ledger_id: Option<String>,
|
||||
pub last_error_code: Option<String>,
|
||||
pub manual_review_resolved_by_admin_user_id: Option<String>,
|
||||
pub manual_review_resolution_reason: Option<String>,
|
||||
pub manual_review_resolved_at_micros: Option<i64>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileRechargeRefundSnapshot {
|
||||
|
||||
@@ -963,6 +963,38 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn resolve_profile_recharge_refund_manual_review(
|
||||
&self,
|
||||
input: module_runtime::RuntimeProfileRechargeRefundManualReviewResolveInput,
|
||||
) -> Result<
|
||||
(
|
||||
module_runtime::RuntimeProfileRechargeRefundSnapshot,
|
||||
Option<module_runtime::RuntimeProfileRechargeOrderRefundSettlementSnapshot>,
|
||||
bool,
|
||||
String,
|
||||
),
|
||||
SpacetimeClientError,
|
||||
> {
|
||||
let procedure_input: RuntimeProfileRechargeRefundManualReviewResolveInput = input.into();
|
||||
self.call_after_connect(
|
||||
"resolve_profile_recharge_refund_manual_review_and_return",
|
||||
move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.resolve_profile_recharge_refund_manual_review_and_return_then(
|
||||
procedure_input,
|
||||
move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_runtime_profile_recharge_refund_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn admin_get_profile_wallet(
|
||||
&self,
|
||||
input: module_runtime::RuntimeProfileAdminWalletGetInput,
|
||||
|
||||
@@ -1345,6 +1345,20 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
||||
.or_insert(serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
if table_name == "profile_recharge_refund" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:退款人工复核审计字段晚于退款事实表加入,旧迁移包按尚未处理兼容。
|
||||
object
|
||||
.entry("manual_review_resolved_by_admin_user_id".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
object
|
||||
.entry("manual_review_resolution_reason".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
object
|
||||
.entry("manual_review_resolved_at".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
if table_name == "external_generation_job_summary" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:非阻断完成告警晚于轻量任务摘要表加入,旧迁移包按无告警兼容。
|
||||
@@ -1662,6 +1676,56 @@ mod migration_bootstrap_secret_tests {
|
||||
assert_eq!(normalized["warning_message"], serde_json::Value::Null);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_profile_recharge_refund_rows_import_with_empty_manual_review_audit() {
|
||||
let row = ProfileRechargeRefund {
|
||||
out_refund_no: "refund-legacy".to_string(),
|
||||
provider_refund_id: "wx-refund-legacy".to_string(),
|
||||
order_id: "order-legacy".to_string(),
|
||||
provider_transaction_id: "wx-transaction-legacy".to_string(),
|
||||
user_id: Some("user-legacy".to_string()),
|
||||
provider_status: RuntimeProfileRechargeRefundStatus::Success,
|
||||
total_cents: 600,
|
||||
refund_cents: 300,
|
||||
payer_total_cents: 600,
|
||||
payer_refund_cents: 300,
|
||||
success_at: Some(Timestamp::from_micros_since_unix_epoch(200)),
|
||||
first_observed_at: Timestamp::from_micros_since_unix_epoch(100),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(200),
|
||||
last_observation_source: RuntimeProfileRechargeRefundObservationSource::Query,
|
||||
last_observation_id: "observation-legacy".to_string(),
|
||||
order_settled_at: None,
|
||||
target_recovery_points: 30,
|
||||
recovered_points: 30,
|
||||
unrecovered_points: 0,
|
||||
recovery_status: RuntimeProfileRechargeRefundRecoveryStatus::Applied,
|
||||
last_recovery_ledger_id: Some("ledger-legacy".to_string()),
|
||||
last_error_code: None,
|
||||
manual_review_resolved_by_admin_user_id: None,
|
||||
manual_review_resolution_reason: None,
|
||||
manual_review_resolved_at: None,
|
||||
};
|
||||
let mut legacy_value = row_to_json(&row).expect("退款旧行应可序列化");
|
||||
let object = legacy_value
|
||||
.as_object_mut()
|
||||
.expect("退款迁移行应为 JSON object");
|
||||
object.remove("manual_review_resolved_by_admin_user_id");
|
||||
object.remove("manual_review_resolution_reason");
|
||||
object.remove("manual_review_resolved_at");
|
||||
|
||||
let normalized = normalize_migration_row("profile_recharge_refund", &legacy_value);
|
||||
let restored = row_from_json::<ProfileRechargeRefund>(
|
||||
"profile_recharge_refund",
|
||||
&normalized,
|
||||
&mut Vec::new(),
|
||||
)
|
||||
.expect("缺少人工复核字段的旧退款行应可导入");
|
||||
|
||||
assert!(restored.manual_review_resolved_by_admin_user_id.is_none());
|
||||
assert!(restored.manual_review_resolution_reason.is_none());
|
||||
assert!(restored.manual_review_resolved_at.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bootstrap_secret_sha256_is_stable_and_does_not_contain_plaintext() {
|
||||
let secret = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -506,6 +506,12 @@ pub struct ProfileRechargeRefund {
|
||||
pub(crate) recovery_status: RuntimeProfileRechargeRefundRecoveryStatus,
|
||||
pub(crate) last_recovery_ledger_id: Option<String>,
|
||||
pub(crate) last_error_code: Option<String>,
|
||||
#[default(None::<String>)]
|
||||
pub(crate) manual_review_resolved_by_admin_user_id: Option<String>,
|
||||
#[default(None::<String>)]
|
||||
pub(crate) manual_review_resolution_reason: Option<String>,
|
||||
#[default(None::<Timestamp>)]
|
||||
pub(crate) manual_review_resolved_at: Option<Timestamp>,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
@@ -1473,6 +1479,39 @@ pub fn release_profile_recharge_refund_hold_and_return(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn resolve_profile_recharge_refund_manual_review_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeProfileRechargeRefundManualReviewResolveInput,
|
||||
) -> RuntimeProfileRechargeRefundProcedureResult {
|
||||
let caller = ctx.sender();
|
||||
match ctx.try_with_tx(|tx| {
|
||||
crate::editor_project_storage::require_editor_generation_runtime_service_identity(
|
||||
tx, caller,
|
||||
)?;
|
||||
resolve_profile_recharge_refund_manual_review(tx, input.clone())
|
||||
}) {
|
||||
Ok((record, settlement, duplicate, resolution_code)) => {
|
||||
RuntimeProfileRechargeRefundProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
settlement,
|
||||
duplicate,
|
||||
resolution_code,
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
Err(message) => RuntimeProfileRechargeRefundProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
settlement: None,
|
||||
duplicate: false,
|
||||
resolution_code: "manual_review_resolution_failed".to_string(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn list_profile_recharge_refund_holds_for_reconciliation(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -2685,6 +2724,9 @@ mod tests {
|
||||
recovery_status: RuntimeProfileRechargeRefundRecoveryStatus::Pending,
|
||||
last_recovery_ledger_id: None,
|
||||
last_error_code: None,
|
||||
manual_review_resolved_by_admin_user_id: None,
|
||||
manual_review_resolution_reason: None,
|
||||
manual_review_resolved_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2708,6 +2750,27 @@ mod tests {
|
||||
assert!(validate_profile_recharge_order_paid_replay_transaction_id(&order, &None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_manual_review_suppresses_only_the_order_match_conflict() {
|
||||
let order = recharge_order_for_refund(RuntimeProfileRechargeOrderStatus::Paid);
|
||||
let mut refund = recharge_refund_for_order(&order);
|
||||
refund.provider_transaction_id = "wx-transaction-other".to_string();
|
||||
assert_eq!(
|
||||
unresolved_profile_recharge_refund_order_match_error(&order, &refund),
|
||||
Some("provider_transaction_id_mismatch".to_string())
|
||||
);
|
||||
|
||||
refund.manual_review_resolved_at = Some(Timestamp::from_micros_since_unix_epoch(300));
|
||||
assert_eq!(
|
||||
unresolved_profile_recharge_refund_order_match_error(&order, &refund),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
validate_profile_recharge_refund_order_match(&order, &refund),
|
||||
Err("provider_transaction_id_mismatch".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refunded_order_keeps_first_recharge_qualification_consumed() {
|
||||
let refunded = recharge_order_for_refund(RuntimeProfileRechargeOrderStatus::Refunded);
|
||||
@@ -4510,6 +4573,94 @@ fn release_profile_recharge_refund_hold(
|
||||
))
|
||||
}
|
||||
|
||||
fn resolve_profile_recharge_refund_manual_review(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileRechargeRefundManualReviewResolveInput,
|
||||
) -> Result<
|
||||
(
|
||||
RuntimeProfileRechargeRefundSnapshot,
|
||||
Option<RuntimeProfileRechargeOrderRefundSettlementSnapshot>,
|
||||
bool,
|
||||
String,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
let validated = build_runtime_profile_recharge_refund_manual_review_resolve_input(
|
||||
input.out_refund_no,
|
||||
input.admin_user_id,
|
||||
input.reason,
|
||||
)?;
|
||||
let mut refund = ctx
|
||||
.db
|
||||
.profile_recharge_refund()
|
||||
.out_refund_no()
|
||||
.find(&validated.out_refund_no)
|
||||
.ok_or_else(|| "退款记录不存在".to_string())?;
|
||||
if refund.manual_review_resolved_at.is_some() {
|
||||
let settlement = ctx
|
||||
.db
|
||||
.profile_recharge_order_refund_settlement()
|
||||
.order_id()
|
||||
.find(&refund.order_id)
|
||||
.map(|row| build_profile_recharge_order_refund_settlement_snapshot_from_row(&row));
|
||||
return Ok((
|
||||
build_profile_recharge_refund_snapshot_from_row(&refund),
|
||||
settlement,
|
||||
true,
|
||||
"manual_review_already_resolved".to_string(),
|
||||
));
|
||||
}
|
||||
if refund.provider_status != RuntimeProfileRechargeRefundStatus::Success
|
||||
|| refund.recovery_status != RuntimeProfileRechargeRefundRecoveryStatus::ManualReview
|
||||
{
|
||||
return Err("只有已成功且待人工复核的退款可以确认归属".to_string());
|
||||
}
|
||||
let review_code = refund
|
||||
.last_error_code
|
||||
.as_deref()
|
||||
.ok_or_else(|| "退款缺少人工复核原因".to_string())?;
|
||||
if !matches!(
|
||||
review_code,
|
||||
"provider_transaction_id_mismatch" | "order_total_mismatch"
|
||||
) {
|
||||
return Err(format!(
|
||||
"当前人工复核原因不能通过确认订单归属收口: {review_code}"
|
||||
));
|
||||
}
|
||||
let order = ctx
|
||||
.db
|
||||
.profile_recharge_order()
|
||||
.order_id()
|
||||
.find(&refund.order_id)
|
||||
.ok_or_else(|| "退款对应充值订单不存在".to_string())?;
|
||||
if order.kind != RuntimeProfileRechargeProductKind::Points {
|
||||
return Err("会员退款不能使用泥点钱包人工复核入口".to_string());
|
||||
}
|
||||
|
||||
refund.manual_review_resolved_by_admin_user_id = Some(validated.admin_user_id);
|
||||
refund.manual_review_resolution_reason = Some(validated.reason);
|
||||
refund.manual_review_resolved_at = Some(ctx.timestamp);
|
||||
refund.updated_at = ctx.timestamp;
|
||||
let (settlement, resolution_code) = reconcile_profile_recharge_refund_success(ctx, &mut refund);
|
||||
if refund.recovery_status == RuntimeProfileRechargeRefundRecoveryStatus::ManualReview {
|
||||
return Err(format!(
|
||||
"确认订单归属后权益结算仍需人工复核: {}",
|
||||
refund
|
||||
.last_error_code
|
||||
.as_deref()
|
||||
.unwrap_or(resolution_code.as_str())
|
||||
));
|
||||
}
|
||||
upsert_profile_recharge_refund_row(ctx, refund.clone());
|
||||
Ok((
|
||||
build_profile_recharge_refund_snapshot_from_row(&refund),
|
||||
settlement
|
||||
.map(|row| build_profile_recharge_order_refund_settlement_snapshot_from_row(&row)),
|
||||
false,
|
||||
format!("manual_review_resolved:{resolution_code}"),
|
||||
))
|
||||
}
|
||||
|
||||
fn build_profile_admin_wallet_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
@@ -4887,6 +5038,9 @@ fn record_profile_recharge_refund_observation(
|
||||
},
|
||||
last_recovery_ledger_id: None,
|
||||
last_error_code: None,
|
||||
manual_review_resolved_by_admin_user_id: None,
|
||||
manual_review_resolution_reason: None,
|
||||
manual_review_resolved_at: None,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4932,7 +5086,7 @@ fn reconcile_profile_recharge_refund_success(
|
||||
};
|
||||
refund.user_id = Some(order.user_id.clone());
|
||||
|
||||
let mismatch_code = validate_profile_recharge_refund_order_match(&order, refund).err();
|
||||
let mismatch_code = unresolved_profile_recharge_refund_order_match_error(&order, refund);
|
||||
if let Some(code) = mismatch_code {
|
||||
refund.recovery_status = RuntimeProfileRechargeRefundRecoveryStatus::ManualReview;
|
||||
refund.last_error_code = Some(code.clone());
|
||||
@@ -5087,6 +5241,15 @@ fn validate_profile_recharge_refund_order_match(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unresolved_profile_recharge_refund_order_match_error(
|
||||
order: &ProfileRechargeOrder,
|
||||
refund: &ProfileRechargeRefund,
|
||||
) -> Option<String> {
|
||||
validate_profile_recharge_refund_order_match(order, refund)
|
||||
.err()
|
||||
.filter(|_| refund.manual_review_resolved_at.is_none())
|
||||
}
|
||||
|
||||
fn mark_profile_recharge_refund_manual_review(
|
||||
ctx: &ReducerContext,
|
||||
refund: &mut ProfileRechargeRefund,
|
||||
@@ -9653,6 +9816,13 @@ fn build_profile_recharge_refund_snapshot_from_row(
|
||||
recovery_status: row.recovery_status,
|
||||
last_recovery_ledger_id: row.last_recovery_ledger_id.clone(),
|
||||
last_error_code: row.last_error_code.clone(),
|
||||
manual_review_resolved_by_admin_user_id: row
|
||||
.manual_review_resolved_by_admin_user_id
|
||||
.clone(),
|
||||
manual_review_resolution_reason: row.manual_review_resolution_reason.clone(),
|
||||
manual_review_resolved_at_micros: row
|
||||
.manual_review_resolved_at
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user