refactor(after-sale): replace review decisions with direct status panel
售后面板改为状态下拉(待审核/处理中/已完成/已拒绝/已取消)加确认/取消按钮:取消仅收起手风琴,确认为纯状态更新;删除已通过状态、review() 决策机制及其专用退款流水线,无双路无迁移;举证图片并入申请说明列与详情整行。
This commit is contained in:
@@ -1,20 +1,13 @@
|
||||
<?php
|
||||
/** User applications and administrator review for full order refunds. */
|
||||
/** User applications and administrator status management for order after-sales. */
|
||||
|
||||
namespace AKSRT\Payment;
|
||||
|
||||
use AKSRT\Core\DomainModel;
|
||||
use AKSRT\Ledger\AssetAmount;
|
||||
use AKSRT\Ledger\AssetType;
|
||||
use AKSRT\Ledger\LedgerCommand;
|
||||
use AKSRT\Ledger\LedgerService;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
final class AfterSaleService {
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_PROCESSING = 'processing';
|
||||
public const STATUS_APPROVED = 'approved';
|
||||
public const STATUS_REJECTED = 'rejected';
|
||||
public const STATUS_CANCELLED = 'cancelled';
|
||||
public const STATUS_COMPLETED = 'completed';
|
||||
@@ -255,7 +248,7 @@ final class AfterSaleService {
|
||||
$where = [];
|
||||
$params = [];
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
$valid = [self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_APPROVED, self::STATUS_REJECTED, self::STATUS_CANCELLED, self::STATUS_COMPLETED];
|
||||
$valid = [self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_REJECTED, self::STATUS_CANCELLED, self::STATUS_COMPLETED];
|
||||
if ($status !== '' && in_array($status, $valid, true)) { $where[] = 'status = %s'; $params[] = $status; }
|
||||
foreach (['date_from' => '>=', 'date_to' => '<='] as $key => $operator) {
|
||||
$date = sanitize_text_field((string) ($args[$key] ?? ''));
|
||||
@@ -291,7 +284,7 @@ final class AfterSaleService {
|
||||
public function count_user_open(int $user_id): int {
|
||||
if ($user_id <= 0) return 0;
|
||||
global $wpdb;
|
||||
return (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$this->table()} WHERE user_id = %d AND status IN ('pending','processing','approved')", $user_id));
|
||||
return (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$this->table()} WHERE user_id = %d AND status IN ('pending','processing')", $user_id));
|
||||
}
|
||||
|
||||
/** A refund status transition is valid only for its own active application. */
|
||||
@@ -300,371 +293,30 @@ final class AfterSaleService {
|
||||
$row = $this->get($after_sale_id);
|
||||
return is_array($row)
|
||||
&& hash_equals((string) ($row['order_number'] ?? ''), $order_number)
|
||||
&& in_array((string) ($row['status'] ?? ''), [self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_APPROVED], true);
|
||||
&& in_array((string) ($row['status'] ?? ''), [self::STATUS_PENDING, self::STATUS_PROCESSING], true);
|
||||
}
|
||||
|
||||
public function review(int $id, string $decision, string $note = '') {
|
||||
/**
|
||||
* 管理员直接设置售后工单状态:纯状态更新,不触发订单、资产与支付渠道联动。
|
||||
*/
|
||||
public function set_status(int $id, string $status, string $note = '') {
|
||||
if (!current_user_can('manage_options')) return new \WP_Error('aksrt_after_sale_forbidden', __('权限不足', 'aksrt'), ['status' => 403]);
|
||||
$decision = sanitize_key($decision);
|
||||
if (!in_array($decision, ['approve', 'reject', 'complete'], true)) return new \WP_Error('aksrt_after_sale_decision', __('审核动作无效', 'aksrt'), ['status' => 400]);
|
||||
global $wpdb;
|
||||
$initial = $this->get($id);
|
||||
if (!$initial) return new \WP_Error('aksrt_after_sale_state', __('售后申请已处理或不存在', 'aksrt'), ['status' => 409]);
|
||||
$order_number = (string) ($initial['order_number'] ?? '');
|
||||
$lock = 'aksrt_after_sale_' . md5($order_number);
|
||||
if ((int) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s, 5)', $lock)) !== 1) return new \WP_Error('aksrt_after_sale_busy', __('售后申请正在处理中,请稍后重试', 'aksrt'), ['status' => 409]);
|
||||
try {
|
||||
$row = $this->get($id);
|
||||
if (!$row || !hash_equals($order_number, (string) ($row['order_number'] ?? '')) || !in_array((string) $row['status'], [self::STATUS_PENDING, self::STATUS_PROCESSING], true)) return new \WP_Error('aksrt_after_sale_state', __('售后申请已处理或不存在', 'aksrt'), ['status' => 409]);
|
||||
$note = sanitize_textarea_field($note);
|
||||
if ((string) $row['status'] === self::STATUS_PROCESSING) {
|
||||
if ($decision === 'complete') {
|
||||
$gateway_result = json_decode((string) ($row['gateway_result'] ?? ''), true);
|
||||
if (!is_array($gateway_result) || sanitize_key((string) ($gateway_result['status'] ?? '')) !== 'manual') {
|
||||
return new \WP_Error('aksrt_after_sale_manual_only', __('该申请不是待人工确认的退款', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
$order = OrderRepository::get_instance()->get((string) $row['order_number']);
|
||||
if (!$order) return new \WP_Error('aksrt_after_sale_order', __('订单不存在', 'aksrt'), ['status' => 404]);
|
||||
$finished = $this->finish_refund($row, $order, $note !== '' ? $note : (string) ($row['admin_note'] ?? ''));
|
||||
return is_wp_error($finished) ? $finished : $this->get((int) $row['id']);
|
||||
}
|
||||
if ($decision === 'reject') {
|
||||
$gateway_result = json_decode((string) ($row['gateway_result'] ?? ''), true);
|
||||
if (!is_array($gateway_result) || sanitize_key((string) ($gateway_result['status'] ?? '')) !== 'manual') {
|
||||
return new \WP_Error('aksrt_after_sale_processing', __('退款处理中,不能拒绝;请同步退款结果', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
$order = OrderRepository::get_instance()->get((string) $row['order_number']);
|
||||
if (!$order) return new \WP_Error('aksrt_after_sale_order', __('订单不存在', 'aksrt'), ['status' => 404]);
|
||||
$type = DomainModel::resolve_order_product_type((string) ($order['product_type'] ?? ''), (array) ($order['meta_payload'] ?? []));
|
||||
$revoke = $this->assets_to_revoke($order, $type, (array) ($order['meta_payload'] ?? []));
|
||||
if (is_wp_error($revoke)) return $revoke;
|
||||
if ($revoke !== []) {
|
||||
$compensated = $this->compensate_revoke($row, $order, $revoke, $note);
|
||||
if (is_wp_error($compensated)) return $compensated;
|
||||
}
|
||||
if (!$this->update((int) $row['id'], ['status' => self::STATUS_REJECTED, 'admin_note' => $note !== '' ? $note : __('管理员确认未执行渠道退款,已恢复冻结资产', 'aksrt')])) {
|
||||
return new \WP_Error('aksrt_after_sale_db', __('售后审核保存失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
return $this->get((int) $row['id']);
|
||||
}
|
||||
return $this->sync_gateway_refund($row, $note);
|
||||
}
|
||||
if ($decision === 'complete') return new \WP_Error('aksrt_after_sale_manual_only', __('只有待人工确认的退款可以直接完成', 'aksrt'), ['status' => 409]);
|
||||
if ($decision === 'reject') {
|
||||
if (!$this->update($id, ['status' => self::STATUS_REJECTED, 'admin_note' => $note, 'reviewed_at' => current_time('mysql')])) {
|
||||
return new \WP_Error('aksrt_after_sale_db', __('售后审核保存失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
return $this->get($id);
|
||||
}
|
||||
return $this->approve($row, $note);
|
||||
} finally {
|
||||
$wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock));
|
||||
}
|
||||
}
|
||||
|
||||
private function approve(array $row, string $note) {
|
||||
$order_number = (string) $row['order_number'];
|
||||
$order = OrderRepository::get_instance()->get($order_number);
|
||||
if (!$order || (string) ($order['status'] ?? '') !== Order::STATUS_PAID) return new \WP_Error('aksrt_after_sale_order', __('订单已不是可退款状态', 'aksrt'), ['status' => 409]);
|
||||
$type = DomainModel::resolve_order_product_type((string) ($order['product_type'] ?? ''), (array) ($order['meta_payload'] ?? []));
|
||||
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||||
$method = sanitize_key((string) ($order['payment_method'] ?? ''));
|
||||
if ($type === DomainModel::ORDER_PRODUCT_VIP && !$this->vip_reversal_is_safe($order, $meta)) return new \WP_Error('aksrt_after_sale_vip_snapshot', __('会员权益已变化或缺少快照,不能安全撤销', 'aksrt'), ['status' => 409]);
|
||||
$revoke = $this->assets_to_revoke($order, $type, $meta);
|
||||
if (is_wp_error($revoke)) return $revoke;
|
||||
if ($method === 'card_pass') {
|
||||
if (!$this->asset_entries_available($revoke)) return new \WP_Error('aksrt_after_sale_asset', __('当前资产不足,不能撤销卡密权益', 'aksrt'), ['status' => 409]);
|
||||
return $this->commit_status_refund($row, $order, $note, $revoke);
|
||||
}
|
||||
if ($method === 'free') {
|
||||
return $this->commit_status_refund($row, $order, $note, $revoke);
|
||||
}
|
||||
$refund_local = in_array($method, ['balance', 'points'], true);
|
||||
if ($refund_local) {
|
||||
$refund_asset = $method === 'points' ? AssetType::POINTS : AssetType::BALANCE;
|
||||
$refund_minor = $refund_asset === AssetType::POINTS ? max(0, (int) ($order['amount'] ?? 0)) : $this->cash_minor((string) ($row['refund_amount'] ?? '0'));
|
||||
if ($refund_minor <= 0) return new \WP_Error('aksrt_after_sale_amount', __('退款金额无效', 'aksrt'), ['status' => 400]);
|
||||
$entries = $revoke;
|
||||
// A points-purchase reversal already includes the original balance payment
|
||||
// in assets_to_revoke(); do not return that payment twice.
|
||||
if (!($type === DomainModel::ORDER_PRODUCT_POINTS && $method === 'balance')) {
|
||||
$entries[] = ['user_id' => (int) $order['user_id'], 'asset' => $refund_asset->value, 'delta_minor' => $refund_minor, 'entry_type' => 'credit', 'remark' => sprintf(__('订单 %s 售后退款', 'aksrt'), $order_number)];
|
||||
}
|
||||
return $this->commit_local_refund($row, $order, $entries, $note);
|
||||
}
|
||||
$gateway = Gateway::get_instance()->get_gateway($method);
|
||||
if (!$gateway || !method_exists($gateway, 'refund')) return new \WP_Error('aksrt_after_sale_gateway', __('支付渠道不可用,无法退款', 'aksrt'), ['status' => 409]);
|
||||
$request_id = (string) ($row['gateway_request_id'] ?? '') ?: 'AS_' . (int) $row['id'];
|
||||
if (!$this->asset_entries_available($revoke)) {
|
||||
return new \WP_Error('aksrt_after_sale_asset', __('当前资产不足,不能审核通过退款', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
if (!$this->update((int) $row['id'], ['status' => self::STATUS_PROCESSING, 'admin_note' => $note, 'gateway_request_id' => $request_id, 'reviewed_at' => current_time('mysql')])) {
|
||||
return new \WP_Error('aksrt_after_sale_db', __('售后状态保存失败,未发起退款', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
if ($revoke !== []) {
|
||||
$held = $this->post_asset_entries($row, $order, $revoke, 'after-sale-revoke', 'after_sale_revoke', $note);
|
||||
if (is_wp_error($held)) {
|
||||
$this->update((int) $row['id'], ['status' => self::STATUS_PENDING, 'gateway_request_id' => null, 'reviewed_at' => null]);
|
||||
return $held;
|
||||
}
|
||||
}
|
||||
try {
|
||||
$result = $gateway->refund($order_number, (string) $row['refund_amount'], $request_id);
|
||||
} catch (\Throwable $error) {
|
||||
$result = ['success' => false, 'status' => 'unknown', 'message' => $error->getMessage()];
|
||||
}
|
||||
if (!is_array($result)) {
|
||||
$result = ['success' => false, 'status' => 'unknown', 'message' => __('支付渠道未返回有效退款结果', 'aksrt')];
|
||||
}
|
||||
$this->update((int) $row['id'], ['gateway_result' => wp_json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'gateway_refund_id' => sanitize_text_field((string) ($result['refund_id'] ?? ''))]);
|
||||
$status = sanitize_key((string) ($result['status'] ?? 'unknown'));
|
||||
if ($status === 'failed') {
|
||||
if ($revoke !== []) {
|
||||
$compensated = $this->compensate_revoke($row, $order, $revoke, $note);
|
||||
if (is_wp_error($compensated)) {
|
||||
$this->update((int) $row['id'], ['admin_note' => __('渠道明确拒绝退款,但资产补偿失败;申请保持处理中,可再次同步重试补偿。', 'aksrt') . ' ' . $compensated->get_error_message()]);
|
||||
return $this->get((int) $row['id']);
|
||||
}
|
||||
}
|
||||
$this->update((int) $row['id'], ['status' => self::STATUS_REJECTED, 'admin_note' => sanitize_textarea_field((string) ($result['message'] ?? $note))]);
|
||||
return $this->get((int) $row['id']);
|
||||
}
|
||||
// Missing, unknown, and pending provider outcomes are ambiguous. Keep the
|
||||
// application processing and the asset clawback in place until query_refund
|
||||
// explicitly confirms success or failure.
|
||||
if (empty($result['success']) || $status !== 'succeeded') {
|
||||
$this->update((int) $row['id'], ['admin_note' => sanitize_textarea_field((string) ($result['message'] ?? __('退款结果仍待支付渠道确认,请稍后同步或人工核对', 'aksrt')))]);
|
||||
return $this->get((int) $row['id']);
|
||||
}
|
||||
$finished = $this->finish_refund($row, $order, $note);
|
||||
return is_wp_error($finished) ? $finished : $this->get((int) $row['id']);
|
||||
}
|
||||
|
||||
private function commit_local_refund(array $row, array $order, array $entries, string $note) {
|
||||
$number = (string) $order['order_number'];
|
||||
$commission = CommissionLedger::get_instance()->prepare_full_refund($number);
|
||||
if (is_wp_error($commission)) return $commission;
|
||||
$guard = static fn() => CommissionLedger::get_instance()->guard_full_refund($commission);
|
||||
$mutation = function () use ($row, $order, $note, $commission) {
|
||||
$commission_guard = CommissionLedger::get_instance()->guard_full_refund($commission);
|
||||
if (is_wp_error($commission_guard) || $commission_guard !== true) return is_wp_error($commission_guard) ? $commission_guard : new \WP_Error('aksrt_commission_state', __('返佣记录状态已变化', 'aksrt'), ['status' => 409]);
|
||||
$finished = $this->finish_refund($row, $order, $note, false);
|
||||
if ($finished === true) {
|
||||
LedgerService::get_instance()->after_commit(static function (): void {
|
||||
OrderRepository::get_instance()->dispatch_managed_status_events();
|
||||
});
|
||||
}
|
||||
return $finished;
|
||||
};
|
||||
$result = LedgerService::get_instance()->post(new LedgerCommand(
|
||||
\AKSRT\Ledger\OperationKey::deterministic('after-sale-refund', [(int) $row['id']]), 'order_refund', $entries,
|
||||
'order', $number, get_current_user_id(), ['order_number' => $number]
|
||||
), $guard, $mutation);
|
||||
if (is_wp_error($result)) return $result;
|
||||
CommissionLedger::get_instance()->clawback_after_refund($commission);
|
||||
return $this->get((int) $row['id']);
|
||||
}
|
||||
|
||||
private function post_asset_entries(array $row, array $order, array $entries, string $prefix, string $reason, string $note) {
|
||||
return LedgerService::get_instance()->post(new LedgerCommand(
|
||||
\AKSRT\Ledger\OperationKey::deterministic($prefix, [(int) $row['id']]), $reason, $entries,
|
||||
'order', (string) $order['order_number'], get_current_user_id(), ['order_number' => (string) $order['order_number'], 'after_sale_id' => (int) $row['id']]
|
||||
));
|
||||
}
|
||||
|
||||
private function commit_status_refund(array $row, array $order, string $note, array $entries = []) {
|
||||
global $wpdb;
|
||||
$number = (string) $order['order_number'];
|
||||
$commission = CommissionLedger::get_instance()->prepare_full_refund($number);
|
||||
if (is_wp_error($commission)) return $commission;
|
||||
if ($entries !== []) {
|
||||
$mutation = function () use ($row, $order, $note, $commission) {
|
||||
$guard = CommissionLedger::get_instance()->guard_full_refund($commission);
|
||||
if (is_wp_error($guard) || $guard !== true) return is_wp_error($guard) ? $guard : new \WP_Error('aksrt_commission_state', __('返佣记录状态已变化', 'aksrt'), ['status' => 409]);
|
||||
$finished = $this->finish_refund($row, $order, $note, false);
|
||||
if ($finished === true) {
|
||||
LedgerService::get_instance()->after_commit(static function (): void { OrderRepository::get_instance()->dispatch_managed_status_events(); });
|
||||
}
|
||||
return $finished;
|
||||
};
|
||||
$result = LedgerService::get_instance()->post(new LedgerCommand(
|
||||
\AKSRT\Ledger\OperationKey::deterministic('after-sale-card-refund', [(int) $row['id']]), 'order_refund', $entries,
|
||||
'order', $number, get_current_user_id(), ['order_number' => $number]
|
||||
), static fn() => CommissionLedger::get_instance()->guard_full_refund($commission), $mutation);
|
||||
if (is_wp_error($result)) return $result;
|
||||
CommissionLedger::get_instance()->clawback_after_refund($commission);
|
||||
return $this->get((int) $row['id']);
|
||||
}
|
||||
if ($wpdb->query('START TRANSACTION') === false) return new \WP_Error('aksrt_after_sale_finish', __('无法开始退款状态更新', 'aksrt'), ['status' => 500]);
|
||||
$guard = CommissionLedger::get_instance()->guard_full_refund($commission);
|
||||
if (is_wp_error($guard) || $guard !== true) { $wpdb->query('ROLLBACK'); return is_wp_error($guard) ? $guard : new \WP_Error('aksrt_commission_state', __('返佣记录状态已变化', 'aksrt'), ['status' => 409]); }
|
||||
$finished = $this->finish_order_and_application($row, $order, $note);
|
||||
if (is_wp_error($finished) || !$finished) { $wpdb->query('ROLLBACK'); return is_wp_error($finished) ? $finished : new \WP_Error('aksrt_after_sale_finish', __('退款状态保存失败', 'aksrt'), ['status' => 500]); }
|
||||
$applied = CommissionLedger::get_instance()->apply_full_refund($commission);
|
||||
if (is_wp_error($applied) || $wpdb->query('COMMIT') === false) { $wpdb->query('ROLLBACK'); return is_wp_error($applied) ? $applied : new \WP_Error('aksrt_after_sale_finish', __('退款状态保存失败', 'aksrt'), ['status' => 500]); }
|
||||
OrderRepository::get_instance()->dispatch_managed_status_events();
|
||||
CommissionLedger::get_instance()->clawback_after_refund($commission);
|
||||
return $this->get((int) $row['id']);
|
||||
}
|
||||
|
||||
private function sync_gateway_refund(array $row, string $note) {
|
||||
$order = OrderRepository::get_instance()->get((string) $row['order_number']);
|
||||
$method = sanitize_key((string) ($order['payment_method'] ?? ''));
|
||||
$gateway = Gateway::get_instance()->get_gateway($method);
|
||||
$request_id = (string) ($row['gateway_request_id'] ?? '');
|
||||
if (!$order || !$gateway || !method_exists($gateway, 'query_refund') || $request_id === '') return new \WP_Error('aksrt_after_sale_sync_unavailable', __('该支付渠道暂不支持自动查询退款状态', 'aksrt'), ['status' => 409]);
|
||||
try {
|
||||
$result = $gateway->query_refund($request_id, (string) $row['order_number']);
|
||||
} catch (\Throwable $error) {
|
||||
$result = ['success' => false, 'status' => 'unknown', 'message' => $error->getMessage()];
|
||||
}
|
||||
if (!is_array($result)) {
|
||||
$result = ['success' => false, 'status' => 'unknown', 'message' => __('支付渠道未返回有效退款结果', 'aksrt')];
|
||||
}
|
||||
$this->update((int) $row['id'], ['gateway_result' => wp_json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'gateway_refund_id' => sanitize_text_field((string) ($result['refund_id'] ?? $row['gateway_refund_id'] ?? ''))]);
|
||||
$status = sanitize_key((string) ($result['status'] ?? 'unknown'));
|
||||
if ($status === 'succeeded' && !empty($result['success'])) {
|
||||
$finished = $this->finish_refund($row, $order, $note !== '' ? $note : (string) ($row['admin_note'] ?? ''));
|
||||
return is_wp_error($finished) ? $finished : $this->get((int) $row['id']);
|
||||
}
|
||||
if ($status === 'failed' && !empty($result['success'])) {
|
||||
$type = DomainModel::resolve_order_product_type((string) ($order['product_type'] ?? ''), (array) ($order['meta_payload'] ?? []));
|
||||
$revoke = $this->assets_to_revoke($order, $type, (array) ($order['meta_payload'] ?? []));
|
||||
if (is_wp_error($revoke)) {
|
||||
$this->update((int) $row['id'], ['admin_note' => __('渠道明确拒绝退款,但无法确认资产补偿内容;申请保持处理中,请人工核对。', 'aksrt') . ' ' . $revoke->get_error_message()]);
|
||||
return $this->get((int) $row['id']);
|
||||
}
|
||||
if ($revoke !== []) {
|
||||
$compensated = $this->compensate_revoke($row, $order, $revoke, $note);
|
||||
if (is_wp_error($compensated)) {
|
||||
$this->update((int) $row['id'], ['admin_note' => __('渠道明确拒绝退款,但资产补偿失败;申请保持处理中,可再次同步重试补偿。', 'aksrt') . ' ' . $compensated->get_error_message()]);
|
||||
return $this->get((int) $row['id']);
|
||||
}
|
||||
}
|
||||
$this->update((int) $row['id'], ['status' => self::STATUS_REJECTED, 'admin_note' => $note !== '' ? $note : __('支付渠道退款失败,已恢复撤销的资产', 'aksrt')]);
|
||||
} elseif ($status !== 'succeeded') {
|
||||
$this->update((int) $row['id'], ['admin_note' => $note !== '' ? $note : __('退款结果仍待支付渠道确认,请稍后同步或人工核对', 'aksrt')]);
|
||||
}
|
||||
return $this->get((int) $row['id']);
|
||||
}
|
||||
|
||||
private function compensate_revoke(array $row, array $order, array $entries, string $note): bool|\WP_Error {
|
||||
$compensate = array_map(static function (array $entry): array { $entry['delta_minor'] = -(int) $entry['delta_minor']; $entry['entry_type'] = 'credit'; $entry['remark'] = __('退款渠道失败,恢复已扣回资产', 'aksrt'); return $entry; }, $entries);
|
||||
$result = $this->post_asset_entries($row, $order, $compensate, 'after-sale-revoke-compensate', 'after_sale_revoke_compensate', $note);
|
||||
return is_wp_error($result) ? $result : true;
|
||||
}
|
||||
|
||||
private function assets_to_revoke(array $order, string $type, array $meta): array|\WP_Error {
|
||||
$user_id = (int) ($order['user_id'] ?? 0);
|
||||
if ($type === DomainModel::ORDER_PRODUCT_RECHARGE) {
|
||||
$card_meta = is_array($meta['meta'] ?? null) ? $meta['meta'] : [];
|
||||
try { $minor = AssetAmount::balance((string) ($meta['credited_amount'] ?? $meta['credit_amount'] ?? $card_meta['amount'] ?? ''))->minor(); } catch (\InvalidArgumentException $e) { return new \WP_Error('aksrt_after_sale_asset', __('充值到账金额缺失,不能退款', 'aksrt'), ['status' => 409]); }
|
||||
return $this->debit_entry($user_id, AssetType::BALANCE, $minor, __('撤销充值余额', 'aksrt'));
|
||||
}
|
||||
if ($type === DomainModel::ORDER_PRODUCT_POINTS) {
|
||||
$card_meta = is_array($meta['meta'] ?? null) ? $meta['meta'] : [];
|
||||
$points = max(0, (int) ($meta['points'] ?? $card_meta['points'] ?? 0));
|
||||
if ($points <= 0) return new \WP_Error('aksrt_after_sale_asset', __('购买积分数量缺失,不能退款', 'aksrt'), ['status' => 409]);
|
||||
$entries = $this->debit_entry($user_id, AssetType::POINTS, $points, __('撤销订单发放积分', 'aksrt'));
|
||||
if (sanitize_key((string) ($order['payment_method'] ?? '')) === 'balance') {
|
||||
try { $paid = AssetAmount::balance((string) ($order['amount'] ?? ''))->minor(); } catch (\InvalidArgumentException $e) { return new \WP_Error('aksrt_after_sale_amount', __('积分订单支付金额无效', 'aksrt'), ['status' => 409]); }
|
||||
$entries[] = ['user_id' => $user_id, 'asset' => AssetType::BALANCE->value, 'delta_minor' => $paid, 'entry_type' => 'credit', 'remark' => __('退回购买积分使用的余额', 'aksrt')];
|
||||
}
|
||||
return $entries;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private function debit_entry(int $user_id, AssetType $asset, int $minor, string $remark): array|\WP_Error {
|
||||
if ($user_id <= 0 || $minor <= 0) return new \WP_Error('aksrt_after_sale_asset', __('待撤销资产金额无效', 'aksrt'), ['status' => 409]);
|
||||
return [['user_id' => $user_id, 'asset' => $asset->value, 'delta_minor' => -$minor, 'entry_type' => 'debit', 'remark' => $remark]];
|
||||
}
|
||||
|
||||
private function finish_refund(array $row, array $order, string $note, bool $manage_transaction = true) {
|
||||
global $wpdb;
|
||||
$number = (string) $order['order_number'];
|
||||
$commission = CommissionLedger::get_instance()->prepare_full_refund($number);
|
||||
if (is_wp_error($commission)) return $commission;
|
||||
if ($manage_transaction && $wpdb->query('START TRANSACTION') === false) return new \WP_Error('aksrt_after_sale_finish', __('无法开始退款状态更新', 'aksrt'), ['status' => 500]);
|
||||
$guarded = CommissionLedger::get_instance()->guard_full_refund($commission);
|
||||
if (is_wp_error($guarded) || $guarded !== true) { if ($manage_transaction) $wpdb->query('ROLLBACK'); return is_wp_error($guarded) ? $guarded : new \WP_Error('aksrt_commission_state', __('返佣记录状态已变化', 'aksrt'), ['status' => 409]); }
|
||||
$finished = $this->finish_order_and_application($row, $order, $note);
|
||||
if (is_wp_error($finished) || !$finished) { if ($manage_transaction) $wpdb->query('ROLLBACK'); return is_wp_error($finished) ? $finished : new \WP_Error('aksrt_after_sale_finish', __('退款成功但本地订单更新失败,请重试同步', 'aksrt'), ['status' => 500]); }
|
||||
$applied = CommissionLedger::get_instance()->apply_full_refund($commission);
|
||||
if (is_wp_error($applied) || ($manage_transaction && $wpdb->query('COMMIT') === false)) { if ($manage_transaction) $wpdb->query('ROLLBACK'); return is_wp_error($applied) ? $applied : new \WP_Error('aksrt_after_sale_finish', __('退款状态保存失败', 'aksrt'), ['status' => 500]); }
|
||||
if ($manage_transaction) {
|
||||
OrderRepository::get_instance()->dispatch_managed_status_events();
|
||||
CommissionLedger::get_instance()->clawback_after_refund($commission);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function finish_order_and_application(array $row, array $order, string $note): bool|\WP_Error {
|
||||
$number = (string) $order['order_number'];
|
||||
if (!$this->revoke_vip_if_needed($order)) return new \WP_Error('aksrt_after_sale_vip', __('会员权益撤销失败', 'aksrt'), ['status' => 409]);
|
||||
if (!OrderRepository::get_instance()->update_status($number, Order::STATUS_REFUNDED, ['allow_refund' => true, 'after_sale_id' => (int) $row['id'], 'transaction_managed' => true, 'note' => $note !== '' ? $note : __('售后退款完成', 'aksrt')])) return new \WP_Error('aksrt_after_sale_order', __('订单退款状态更新失败', 'aksrt'), ['status' => 409]);
|
||||
return $this->update((int) $row['id'], ['status' => self::STATUS_COMPLETED, 'admin_note' => $note, 'completed_at' => current_time('mysql'), 'reviewed_at' => current_time('mysql')]);
|
||||
}
|
||||
|
||||
private function vip_reversal_is_safe(array $order, array $meta): bool {
|
||||
$before = is_array($meta['vip_before'] ?? null) ? $meta['vip_before'] : null;
|
||||
$after = is_array($meta['vip_after'] ?? null) ? $meta['vip_after'] : null;
|
||||
if (!$before || !$after || !class_exists('\\AKSRT\\Content\\VipService')) return false;
|
||||
$current = \AKSRT\Content\VipService::get_instance()->get_membership_snapshot((int) $order['user_id']);
|
||||
foreach (['level', 'expires', 'base_level', 'base_expires', 'expired_level'] as $key) {
|
||||
if ((int) ($current[$key] ?? 0) !== (int) ($after[$key] ?? 0)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function revoke_vip_if_needed(array $order): bool {
|
||||
$type = DomainModel::resolve_order_product_type((string) ($order['product_type'] ?? ''), (array) ($order['meta_payload'] ?? []));
|
||||
if ($type !== DomainModel::ORDER_PRODUCT_VIP) return true;
|
||||
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||||
if (!$this->vip_reversal_is_safe($order, $meta)) return false;
|
||||
$before = $meta['vip_before'];
|
||||
$service = \AKSRT\Content\VipService::get_instance();
|
||||
if ((int) ($before['level'] ?? 0) <= 0) {
|
||||
$result = $service->mutate_membership((int) $order['user_id'], 'disable', [], ['reason' => 'order_refund', 'source' => 'after_sale']);
|
||||
} elseif ((int) ($before['base_level'] ?? 0) === 0 && (int) ($before['base_expires'] ?? 0) === 0 && (int) ($before['expired_level'] ?? 0) === 0) {
|
||||
$result = $service->mutate_membership((int) $order['user_id'], 'set', ['level' => (int) $before['level'], 'expires' => (int) $before['expires']], ['reason' => 'order_refund', 'source' => 'after_sale']);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return !is_wp_error($result);
|
||||
}
|
||||
|
||||
private function refund_amount(array $order): ?string {
|
||||
if ((string) ($order['currency'] ?? '') === 'points') return (string) max(0, (int) ($order['amount'] ?? 0));
|
||||
try { return AssetAmount::balance((string) ($order['amount'] ?? '0'))->decimal(); } catch (\InvalidArgumentException $e) { return null; }
|
||||
}
|
||||
|
||||
private function cash_minor(string $value): int { try { return AssetAmount::balance($value)->minor(); } catch (\InvalidArgumentException $e) { return 0; } }
|
||||
|
||||
/** @param array<int,array<string,mixed>> $entries */
|
||||
private function asset_entries_available(array $entries): bool {
|
||||
$required = [];
|
||||
foreach ($entries as $entry) {
|
||||
$delta = (int) ($entry['delta_minor'] ?? 0);
|
||||
if ($delta < 0) {
|
||||
$key = (int) ($entry['user_id'] ?? 0) . ':' . (string) ($entry['asset'] ?? '');
|
||||
$required[$key] = ($required[$key] ?? 0) + abs($delta);
|
||||
}
|
||||
}
|
||||
foreach ($required as $key => $amount) {
|
||||
[$user_id, $asset] = explode(':', $key, 2);
|
||||
$balance = (string) $asset === AssetType::POINTS->value
|
||||
? \AKSRT\Credits\MetaAssetService::get_instance()->balance_points((int) $user_id)
|
||||
: \AKSRT\Credits\MetaAssetService::get_instance()->balance_minor((int) $user_id);
|
||||
if ($balance < (int) $amount) return false;
|
||||
}
|
||||
return true;
|
||||
$status = sanitize_key($status);
|
||||
if (!in_array($status, [self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_COMPLETED, self::STATUS_REJECTED, self::STATUS_CANCELLED], true)) return new \WP_Error('aksrt_after_sale_status', __('售后状态无效', 'aksrt'), ['status' => 400]);
|
||||
$row = $this->get($id);
|
||||
if (!$row) return new \WP_Error('aksrt_after_sale_state', __('售后申请不存在', 'aksrt'), ['status' => 409]);
|
||||
$note = sanitize_textarea_field($note);
|
||||
$data = ['status' => $status];
|
||||
if ($note !== '') $data['admin_note'] = $note;
|
||||
if ($status !== self::STATUS_PENDING) $data['reviewed_at'] = current_time('mysql');
|
||||
if (!$this->update($id, $data)) return new \WP_Error('aksrt_after_sale_db', __('售后状态保存失败', 'aksrt'), ['status' => 500]);
|
||||
return $this->get($id);
|
||||
}
|
||||
|
||||
/** @param array<int,array<string,mixed>> $entries */
|
||||
private function find_active(string $number): ?array {
|
||||
global $wpdb;
|
||||
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table()} WHERE order_number = %s AND status IN ('pending','processing','approved') ORDER BY id DESC LIMIT 1", $number), ARRAY_A);
|
||||
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table()} WHERE order_number = %s AND status IN ('pending','processing') ORDER BY id DESC LIMIT 1", $number), ARRAY_A);
|
||||
return is_array($row) ? $this->normalize($row) : null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user