diff --git a/src/Admin/Native/PayOrders.php b/src/Admin/Native/PayOrders.php index da06daa9..ddbf83d2 100644 --- a/src/Admin/Native/PayOrders.php +++ b/src/Admin/Native/PayOrders.php @@ -30,8 +30,7 @@ class PayOrders { add_action('admin_post_aksrt_pay_update_order_note', [$this, 'handle_update_order_note']); add_action('admin_post_aksrt_pay_clear_closed_orders', [$this, 'handle_clear_closed_orders']); add_action('admin_post_aksrt_pay_delete_order', [$this, 'handle_delete_order']); - add_action('admin_post_aksrt_after_sale_review', [$this, 'handle_after_sale_review']); - add_action('admin_post_aksrt_after_sale_bulk', [$this, 'handle_after_sale_bulk']); + add_action('admin_post_aksrt_after_sale_update', [$this, 'handle_after_sale_status']); } public function render_orders_page(): void { @@ -359,7 +358,7 @@ class PayOrders { $after_sales = \AKSRT\Payment\AfterSaleService::get_instance()->list_for_order($order_number); if ($after_sales) { echo '' . esc_html__('售后记录', 'aksrt') . ''; - $status_labels = ['pending' => '待审核', 'processing' => '处理中', 'approved' => '已通过', 'rejected' => '已拒绝', 'cancelled' => '已取消', 'completed' => '已完成']; + $status_labels = ['pending' => '待审核', 'processing' => '处理中', 'completed' => '已完成', 'rejected' => '已拒绝', 'cancelled' => '已取消']; foreach ($after_sales as $application) { echo '

' . esc_html($status_labels[(string) ($application['status'] ?? '')] ?? (string) ($application['status'] ?? '')) . ' · ' . esc_html((string) ($application['refund_amount'] ?? '')) . '
' . esc_html((string) ($application['reason'] ?? '')); if (!empty($application['admin_note'])) echo '
' . esc_html__('处理备注:', 'aksrt') . esc_html((string) $application['admin_note']); @@ -399,7 +398,7 @@ class PayOrders { $items = $service->admin_list($filter_args + ['limit' => 100]); echo '

'; echo ' '; submit_button(__('筛选', 'aksrt'), 'secondary', '', false); echo '
'; @@ -414,18 +413,12 @@ class PayOrders { . '.aksrt-as-detail .aksrt-as-thumb{width:80px;height:80px;}' . '.aksrt-as-review{margin-top:12px;padding-top:10px;border-top:1px solid #dcdcde;max-width:720px;}' . '.aksrt-as-review textarea{width:100%;box-sizing:border-box;margin:4px 0 8px;}' - . '.aksrt-as-review-actions{display:flex;gap:8px;flex-wrap:wrap;}' - . '.aksrt-qa-danger{color:#b32d2e;border-color:#b32d2e;}' + . '.aksrt-as-review-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;}' + . '.aksrt-as-field--wide{grid-column:1/-1;}' . ''; - echo '
'; - echo ''; wp_nonce_field('aksrt_after_sale_bulk'); - echo ''; - echo '
'; - echo ' '; - echo '
'; - echo ''; - if (!$items) echo ''; - $status_labels = [\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_APPROVED => '已通过', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成']; + echo '
ID订单号用户原因申请资料申请说明举证图片退款金额状态操作 / 处理记录
' . esc_html__('暂无售后申请', 'aksrt') . '
'; + if (!$items) echo ''; + $status_labels = [\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消']; foreach ($items as $item) { $id = (int) $item['id']; $number = (string) $item['order_number']; $uid = (int) $item['user_id']; $user = $uid > 0 ? get_userdata($uid) : null; @@ -446,19 +439,20 @@ class PayOrders { } $materials_cell = $material_lines ? implode('
', $material_lines) : '—'; $desc_text = trim((string) ($apply_form['description'] ?? '')); - $desc_cell = $desc_text !== '' ? nl2br(esc_html($desc_text)) : '—'; } else { // 老数据:融合串原样展示在原因列 $reason_cell = nl2br(esc_html((string) $item['reason'])); $materials_cell = '—'; - $desc_cell = '—'; + $desc_text = ''; } $image_urls = array_values(array_filter((array) ($item['images'] ?? []), 'is_string')); $thumbs = []; foreach ($image_urls as $url) { $thumbs[] = ''; } - $images_cell = $thumbs ? implode('', $thumbs) : '—'; + // 举证图片并入申请说明:缩略图展示在描述文字下方,不再单独成列。 + $desc_cell = ($desc_text !== '' ? nl2br(esc_html($desc_text)) : '') . ($thumbs ? '
' . implode('', $thumbs) . '
' : ''); + if ($desc_cell === '') $desc_cell = '—'; $status_label = $status_labels[(string) $item['status']] ?? (string) $item['status']; $admin_note = (string) ($item['admin_note'] ?? ''); $gateway_text = ''; @@ -466,32 +460,19 @@ class PayOrders { $result_data = json_decode((string) $item['gateway_result'], true); $gateway_text = is_array($result_data) ? implode(' · ', array_filter([(string) ($result_data['status'] ?? ''), (string) ($result_data['message'] ?? ''), !empty($result_data['refund_id']) ? '退款单号 ' . (string) $result_data['refund_id'] : ''])) : (string) $item['gateway_result']; } - // 按状态决定该记录可执行的审核动作(经 JS 以 POST 提交) - $quick_actions = []; - if ((string) $item['status'] === 'pending') { - $quick_actions = ['approve' => __('通过', 'aksrt'), 'reject' => __('拒绝', 'aksrt')]; - } elseif ((string) $item['status'] === 'processing') { - $gateway_result = json_decode((string) ($item['gateway_result'] ?? ''), true); - $manual_refund = is_array($gateway_result) && sanitize_key((string) ($gateway_result['status'] ?? '')) === 'manual'; - $quick_actions = $manual_refund - ? ['complete' => __('确认线下退款完成', 'aksrt'), 'reject' => __('确认未退款并拒绝', 'aksrt')] - : ['approve' => __('同步退款状态', 'aksrt')]; - } - // 摘要行 + 手风琴详情行:点击「操作」展开整条记录的完整信息与审核表单 + // 摘要行 + 手风琴详情行:点击「操作」展开整条记录的完整信息与状态面板 echo ''; - echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; - echo ''; echo ''; echo ''; echo ''; echo ''; - echo ''; } echo '
ID订单号用户原因申请资料申请说明退款金额状态操作 / 处理记录
' . esc_html__('暂无售后申请', 'aksrt') . '
' . esc_html((string) $id) . '
' . esc_html((string) ($item['created_at'] ?? '')) . '
' . esc_html($number) . '
' . esc_html($type_label) . ($product_name !== '' ? ' · ' . esc_html($product_name) : '') . '
' . esc_html($user ? $user->display_name . ' (#' . $uid . ')' : '#' . $uid) . '' . $reason_cell . '' . $materials_cell . '' . $desc_cell . '' . $images_cell . '' . esc_html($refund_amount) . '' . esc_html($status_label) . '
'; - echo '
'; - // 事件委托:点击「操作」切换手风琴详情;点击审核按钮经 JS 以 POST 提交(详情行在批量表单内,禁止嵌套 form) + // 事件委托:点击「操作」切换手风琴详情;「取消」仅收起面板;「确认」以 POST 提交选中的售后状态 echo ''; } - public function handle_after_sale_review(): void { + public function handle_after_sale_status(): void { if (!current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'aksrt')); $id = isset($_POST['after_sale_id']) ? absint($_POST['after_sale_id']) : 0; - check_admin_referer('aksrt_after_sale_review_' . $id); - $decision = isset($_POST['decision']) ? sanitize_key((string) wp_unslash($_POST['decision'])) : ''; + check_admin_referer('aksrt_after_sale_status_' . $id); + $status = isset($_POST['after_sale_status']) ? sanitize_key((string) wp_unslash($_POST['after_sale_status'])) : ''; $note = isset($_POST['admin_note']) ? sanitize_textarea_field((string) wp_unslash($_POST['admin_note'])) : ''; - $result = \AKSRT\Payment\AfterSaleService::get_instance()->review($id, $decision, $note); - $args = ['aksrt_pay_msg' => is_wp_error($result) ? 'after_sale_review_failed' : 'after_sale_reviewed']; + $result = \AKSRT\Payment\AfterSaleService::get_instance()->set_status($id, $status, $note); + $args = ['aksrt_pay_msg' => is_wp_error($result) ? 'after_sale_update_failed' : 'after_sale_updated']; if (is_wp_error($result)) $args['after_sale_error'] = substr($result->get_error_message(), 0, 240); wp_safe_redirect($this->page_url(self::PAGE_AFTER_SALES_SLUG, $args)); exit; } - /** - * 批量审核售后申请(仅对待审核/处理中有效,其余由 review() 校验拒绝并计入失败)。 - */ - public function handle_after_sale_bulk(): void { - if (!current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'aksrt')); - check_admin_referer('aksrt_after_sale_bulk'); - $decision = isset($_POST['bulk_decision']) ? sanitize_key((string) wp_unslash($_POST['bulk_decision'])) : ''; - if (!in_array($decision, ['approve', 'reject'], true)) { - wp_safe_redirect($this->page_url(self::PAGE_AFTER_SALES_SLUG, ['aksrt_pay_msg' => 'after_sale_review_failed'])); exit; - } - $note = isset($_POST['bulk_note']) ? sanitize_textarea_field((string) wp_unslash($_POST['bulk_note'])) : ''; - $ids = array_values(array_unique(array_map('absint', (array) ($_POST['after_sale_ids'] ?? [])))); - $service = \AKSRT\Payment\AfterSaleService::get_instance(); - $done = 0; $failed = 0; - foreach ($ids as $id) { - if ($id <= 0) continue; - $result = $service->review($id, $decision, $note); - if (is_wp_error($result)) $failed++; else $done++; - } - $args = ['aksrt_pay_msg' => $failed > 0 ? 'after_sale_review_failed' : 'after_sale_reviewed']; - if ($done > 0) $args['aksrt_bulk_done'] = $done; - if ($failed > 0) $args['aksrt_bulk_failed'] = $failed; - wp_safe_redirect($this->page_url(self::PAGE_AFTER_SALES_SLUG, $args)); exit; - } - public function handle_update_order_status(): void { if (!current_user_can(self::CAPABILITY)) { wp_die(esc_html__('权限不足', 'aksrt')); @@ -793,15 +741,15 @@ class PayOrders { 'shipping_create_failed' => __('发货记录创建失败。', 'aksrt'), 'after_sale_created' => __('售后工单已创建。', 'aksrt'), 'after_sale_create_failed' => __('售后工单创建失败。', 'aksrt'), - 'after_sale_reviewed' => __('售后审核已处理。', 'aksrt'), - 'after_sale_review_failed' => __('售后审核失败,请查看申请状态和错误原因。', 'aksrt'), + 'after_sale_updated' => __('售后状态已更新。', 'aksrt'), + 'after_sale_update_failed' => __('售后状态更新失败,请查看申请状态和错误原因。', 'aksrt'), ]; if (!isset($map[$code])) { return; } $class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success'; echo '

' . esc_html($map[$code]) . '

'; - if ($code === 'after_sale_review_failed' && isset($_GET['after_sale_error'])) { + if ($code === 'after_sale_update_failed' && isset($_GET['after_sale_error'])) { echo '

' . esc_html(sanitize_text_field((string) wp_unslash($_GET['after_sale_error']))) . '

'; } } diff --git a/src/Payment/AfterSaleService.php b/src/Payment/AfterSaleService.php index b0bb0542..06214d95 100644 --- a/src/Payment/AfterSaleService.php +++ b/src/Payment/AfterSaleService.php @@ -1,20 +1,13 @@ '>=', '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> $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> $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; } diff --git a/src/Payment/UserOrders.php b/src/Payment/UserOrders.php index 52d7806d..26f398b5 100644 --- a/src/Payment/UserOrders.php +++ b/src/Payment/UserOrders.php @@ -261,7 +261,7 @@ class UserOrders { : []; $settings = is_array($settings) ? $settings : []; - $open_statuses = ['pending', 'processing', 'approved']; + $open_statuses = ['pending', 'processing']; $latest = null; $open = false; if ($service !== null) { @@ -287,7 +287,7 @@ class UserOrders { $has_application = $latest !== null; // 可再次申请售后的状态判定(按工单状态推理,不再以「不能再申请」为常态): - // - pending / processing / approved:工单仍在流转(待审核 / 处理中 / 已通过待退款),不允许重复申请; + // - pending / processing:工单仍在流转(待审核 / 处理中),不允许重复申请; // - rejected:审核未通过,允许补充资料后再次申请; // - cancelled:用户自行取消,允许重新发起; // - completed:退款已完成,工单终结,不允许再次申请。 diff --git a/tests/dev/render-after-sales-harness.php b/tests/dev/render-after-sales-harness.php index fc68fef0..27b746a1 100644 --- a/tests/dev/render-after-sales-harness.php +++ b/tests/dev/render-after-sales-harness.php @@ -37,7 +37,6 @@ namespace AKSRT\Payment { 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'; @@ -47,7 +46,7 @@ namespace AKSRT\Payment { return [ ['id' => 5, 'order_number' => 'ORD100', 'user_id' => 3, 'reason' => 'legacy-reason', 'apply_form' => ['reason_label' => '商品与描述不符', 'materials' => [['label' => '联系方式', 'value' => 'wx-abc']], 'description' => '说明文字'], - 'images' => [], 'refund_amount' => '12.50', 'status' => 'pending', 'admin_note' => '', 'gateway_result' => '', 'created_at' => '2026-10-01 10:00:00'], + 'images' => ['https://example.test/img1.png', 'https://example.test/img2.png'], 'refund_amount' => '12.50', 'status' => 'pending', 'admin_note' => '', 'gateway_result' => '', 'created_at' => '2026-10-01 10:00:00'], ['id' => 6, 'order_number' => 'ORD101', 'user_id' => 4, 'reason' => 'r2', 'apply_form' => null, 'images' => [], 'refund_amount' => '8.00', 'status' => 'processing', 'admin_note' => '已有备注', 'gateway_result' => json_encode(['status' => 'manual', 'message' => '线下退款']), 'created_at' => '2026-10-02 11:00:00'], diff --git a/tests/widget-system/financial-flow-contract.test.cjs b/tests/widget-system/financial-flow-contract.test.cjs index aa570fec..86f7aabc 100644 --- a/tests/widget-system/financial-flow-contract.test.cjs +++ b/tests/widget-system/financial-flow-contract.test.cjs @@ -140,23 +140,30 @@ test('order detail page drops tabs; after-sales becomes a standalone submenu', ( assert.ok(page.includes('render_after_sales_admin_page'), 'after-sales admin page entry') assert.ok(page.includes("PAGE_AFTER_SALES_SLUG = 'aksrt_after_sales'"), 'after-sales slug constant') assert.ok(!page.includes('after-sales'), 'old tab slug must not remain') - // 售后表格列对齐售后策略的申请表单配置项:原因 / 申请资料 / 申请说明 + // 售后表格列对齐售后策略的申请表单配置项:原因 / 申请资料 / 申请说明 assert.ok(page.includes('申请资料'), 'materials column') assert.ok(page.includes('申请说明'), 'description column') - assert.ok(page.includes('举证图片'), 'evidence images column') - assert.ok(page.includes('$images_cell'), 'evidence images cell rendered from item images') - // 操作列:点击「操作」以手风琴展开整条记录详情(含完整字段与内联审核表单),经 JS 以 POST 提交 + assert.ok(!page.includes('举证图片'), 'evidence images must not be a standalone column') + assert.ok(page.includes('aksrt-as-desc-thumbs'), 'evidence thumbnails render inside the description cell') + // 操作列:点击「操作」以手风琴展开整条记录详情(含完整字段与状态面板),经 JS 以 POST 提交 assert.ok(page.includes('aksrt-toggle-row-actions'), 'toggle to expand record accordion') assert.ok(page.includes('aksrt-as-detail-row'), 'accordion detail row per after-sale record') assert.ok(page.includes('aksrt-as-detail-grid'), 'accordion panel renders full record fields') assert.ok(page.includes('aksrt-review-note'), 'review note textarea inside expanded panel') - assert.ok(page.includes('aksrt-as-review-actions'), 'inline approve/reject buttons inside expanded panel') + assert.ok(page.includes('aksrt-as-review-actions'), 'review actions row inside expanded panel') + assert.ok(page.includes('aksrt-status-select'), 'status select inside expanded panel') + assert.ok(page.includes('aksrt-after-sale-confirm'), 'confirm button posts the selected status') + assert.ok(page.includes('aksrt-after-sale-cancel'), 'cancel button only collapses the panel') + assert.ok(page.includes('aksrt_after_sale_update'), 'status update handler registered') + assert.ok(page.includes('aksrt_after_sale_status_'), 'per-row nonce covers the status update') + assert.ok(page.includes('aksrt-as-field--wide'), 'description renders as a full-width row') + assert.ok(page.includes('justify-content:flex-end'), 'confirm/cancel buttons right-aligned') assert.ok(page.includes('.aksrt-as-detail-row[hidden]'), 'hidden attribute guard keeps accordion closed') assert.ok(page.includes('document.addEventListener("click"'), 'delegated click handler survives row re-render') assert.ok(!page.includes('class="row-actions"'), 'no hover-only row-actions') - assert.ok(page.includes('aksrt_after_sale_bulk'), 'bulk review handler registered') - assert.ok(page.includes('批量操作'), 'bulk actions dropdown') - assert.ok(page.includes('after_sale_ids[]'), 'row checkboxes for bulk') + assert.ok(!page.includes('aksrt_after_sale_bulk'), 'bulk review handler removed') + assert.ok(!page.includes('批量操作'), 'bulk actions dropdown removed') + assert.ok(!page.includes('after_sale_ids[]'), 'row checkboxes removed') assert.ok(page.includes('$materials_cell'), 'materials cell rendered from apply_form') assert.ok(page.includes('$desc_cell'), 'description cell rendered from apply_form') // 页头展示售后策略摘要(功能开关 / 时效 / 预置原因 / 申请材料) @@ -165,7 +172,8 @@ test('order detail page drops tabs; after-sales becomes a standalone submenu', ( assert.ok(page.includes('申请所需材料'), 'policy summary shows materials') const domain = read('src/Core/DomainModel.php') for (const status of ['pending', 'paid', 'refunded', 'closed']) assert.ok(domain.includes(`ORDER_STATUS_${status.toUpperCase()}`)) - assert.match(page, /STATUS_PENDING[\s\S]*?STATUS_PROCESSING[\s\S]*?STATUS_APPROVED[\s\S]*?STATUS_REJECTED[\s\S]*?STATUS_CANCELLED[\s\S]*?STATUS_COMPLETED/) + assert.match(page, /STATUS_PENDING[\s\S]*?STATUS_PROCESSING[\s\S]*?STATUS_COMPLETED[\s\S]*?STATUS_REJECTED[\s\S]*?STATUS_CANCELLED/) + assert.ok(!page.includes('STATUS_APPROVED'), 'approved status removed') assert.match(page, /ORDER_PRODUCT_READING[\s\S]*?付费阅读[\s\S]*?ORDER_PRODUCT_DOWNLOAD[\s\S]*?付费下载[\s\S]*?ORDER_PRODUCT_IMAGE[\s\S]*?付费图片[\s\S]*?ORDER_PRODUCT_VIDEO[\s\S]*?付费视频[\s\S]*?ORDER_PRODUCT_VIP[\s\S]*?购买会员[\s\S]*?ORDER_PRODUCT_RECHARGE[\s\S]*?余额充值[\s\S]*?ORDER_PRODUCT_POINTS[\s\S]*?购买积分/) }) diff --git a/tests/widget-system/order-after-sale-assets-contract.test.cjs b/tests/widget-system/order-after-sale-assets-contract.test.cjs index 641ef2d0..ae1a7b58 100644 --- a/tests/widget-system/order-after-sale-assets-contract.test.cjs +++ b/tests/widget-system/order-after-sale-assets-contract.test.cjs @@ -49,7 +49,7 @@ test('free points keep a bounded daily summary alongside the recent points histo assert.match(assets, /'points_record'/) }) -test('after-sale submissions are order-locked and only completed by their own refund application', () => { +test('after-sale submissions are order-locked and admins set status directly', () => { const afterSale = read('src/Payment/AfterSaleService.php') const schema = read('src/Core/DBMigratorPart2.php') const repository = read('src/Payment/OrderRepositoryCore.php') @@ -57,14 +57,12 @@ test('after-sale submissions are order-locked and only completed by their own re assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_order_aftersale[\s\S]*?gateway_request_id[\s\S]*?gateway_result[\s\S]*?completed_at/) assert.match(afterSale, /GET_LOCK\(%s, 5\)[\s\S]*?get_user_order\(\$user_id, \$order_number\)[\s\S]*?find_active\(\$order_number\)/) assert.match(afterSale, /Order::STATUS_PAID/) - assert.match(afterSale, /\$gateway->refund\(\$order_number, \(string\) \$row\['refund_amount'\], \$request_id\)/) - assert.match(afterSale, /query_refund\(\$request_id, \(string\) \$row\['order_number'\]\)/) - assert.match(afterSale, /sanitize_key\(\(string\) \(\$gateway_result\['status'\] \?\? ''\)\) !== 'manual'/) - assert.match(afterSale, /\$decision === 'reject'[\s\S]*?compensate_revoke\(\$row, \$order, \$revoke, \$note\)/) - assert.match(afterSale, /Order::STATUS_REFUNDED/) + assert.match(afterSale, /public function set_status\(int \$id, string \$status, string \$note = ''\)/) + assert.match(afterSale, /in_array\(\$status, \[self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_COMPLETED, self::STATUS_REJECTED, self::STATUS_CANCELLED\], true\)/) + assert.ok(!afterSale.includes('STATUS_APPROVED'), 'approved status removed') + assert.ok(!afterSale.includes('function review('), 'decision-based review removed') + assert.ok(!afterSale.includes('approve('), 'refund pipeline removed') assert.match(repository, /can_finalize_order_refund\(\(int\) \(\$extra\['after_sale_id'\] \?\? 0\), \$order_number\)/) - assert.match(read('src/Payment/Gateways/PayPalGateway.php'), /'status' => 'manual'/) - assert.match(read('src/Payment/Gateways/EpayGateway.php'), /'status'\s*=>\s*'manual'/) }) test('withdrawal holds and returns use the selected user-meta or commission pool', () => {