diff --git a/app/Http/Controllers/Admin/PlatformOrderController.php b/app/Http/Controllers/Admin/PlatformOrderController.php index 30d9a88..b48d023 100644 --- a/app/Http/Controllers/Admin/PlatformOrderController.php +++ b/app/Http/Controllers/Admin/PlatformOrderController.php @@ -958,6 +958,27 @@ class PlatformOrderController extends Controller 'last_channel' => (string) (data_get($latest, 'channel') ?? ''), ]); + // 审计:追加支付回执(用于对账留痕,不自动改状态) + $audit = (array) (data_get($meta, 'audit', []) ?? []); + $audit[] = [ + 'action' => 'add_payment_receipt', + 'scope' => 'single', + 'at' => $now->toDateTimeString(), + 'admin_id' => $admin->id, + 'note' => '追加支付回执(对账留痕,不自动改状态)', + 'snapshot' => [ + 'type' => (string) $data['type'], + 'channel' => (string) ($data['channel'] ?? ''), + 'amount' => (float) $data['amount'], + 'paid_at' => $data['paid_at'] ? (string) $data['paid_at'] : null, + 'summary' => [ + 'count' => (int) data_get($meta, 'payment_summary.count'), + 'total_amount' => (float) data_get($meta, 'payment_summary.total_amount'), + ], + ], + ]; + data_set($meta, 'audit', $audit); + $order->meta = $meta; $order->save(); @@ -1007,6 +1028,27 @@ class PlatformOrderController extends Controller 'last_channel' => (string) (data_get($latestRefund, 'channel') ?? ''), ]); + // 审计:追加退款回执(用于退款轨迹留痕) + $audit = (array) (data_get($meta, 'audit', []) ?? []); + $audit[] = [ + 'action' => 'add_refund_receipt', + 'scope' => 'single', + 'at' => $now->toDateTimeString(), + 'admin_id' => $admin->id, + 'note' => '追加退款回执(用于退款轨迹留痕)', + 'snapshot' => [ + 'type' => (string) $data['type'], + 'channel' => (string) ($data['channel'] ?? ''), + 'amount' => (float) $data['amount'], + 'refunded_at' => $data['refunded_at'] ? (string) $data['refunded_at'] : null, + 'summary' => [ + 'count' => (int) data_get($meta, 'refund_summary.count'), + 'total_amount' => (float) data_get($meta, 'refund_summary.total_amount'), + ], + ], + ]; + data_set($meta, 'audit', $audit); + // 可治理辅助:自动推进退款标记(仅当退款金额>0 时) // 注意:允许从 paid / partially_refunded 推进到 partially_refunded / refunded // 且不会把已 refunded 的订单降级。 diff --git a/tests/Feature/AdminPlatformOrderAddPaymentReceiptShouldWriteAuditTest.php b/tests/Feature/AdminPlatformOrderAddPaymentReceiptShouldWriteAuditTest.php new file mode 100644 index 0000000..be2e5d2 --- /dev/null +++ b/tests/Feature/AdminPlatformOrderAddPaymentReceiptShouldWriteAuditTest.php @@ -0,0 +1,84 @@ +seed(); + + $this->post('/admin/login', [ + 'email' => 'platform.admin@demo.local', + 'password' => 'Platform@123456', + ])->assertRedirect('/admin'); + } + + public function test_add_payment_receipt_will_write_audit_entry_and_keep_status_unchanged(): void + { + $this->loginAsPlatformAdmin(); + + $merchant = Merchant::query()->firstOrFail(); + $plan = Plan::query()->create([ + 'code' => 'audit_payment_receipt_plan', + 'name' => '审计回执用套餐', + 'billing_cycle' => 'monthly', + 'price' => 30, + 'list_price' => 30, + 'status' => 'active', + 'sort' => 10, + 'published_at' => now(), + ]); + + $this->post('/admin/platform-orders', [ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_type' => 'new_purchase', + 'quantity' => 1, + 'discount_amount' => 0, + 'payment_channel' => 'bank_transfer', + 'remark' => '审计回执测试', + ])->assertRedirect(); + + /** @var PlatformOrder $order */ + $order = PlatformOrder::query()->latest('id')->firstOrFail(); + $this->assertSame('unpaid', (string) $order->payment_status); + $this->assertSame('pending', (string) $order->status); + + $this->post('/admin/platform-orders/' . $order->id . '/add-payment-receipt', [ + 'type' => 'bank_transfer', + 'channel' => 'icbc', + 'amount' => 30, + 'paid_at' => now()->format('Y-m-d H:i:s'), + 'note' => '财务确认', + ])->assertRedirect(); + + $order->refresh(); + + $this->assertSame('unpaid', (string) $order->payment_status); + $this->assertSame('pending', (string) $order->status); + $this->assertSame(1, (int) data_get($order->meta, 'payment_summary.count')); + $this->assertSame(30.0, (float) data_get($order->meta, 'payment_summary.total_amount')); + + $audit = (array) (data_get($order->meta, 'audit', []) ?? []); + $this->assertNotEmpty($audit); + + $last = end($audit); + $this->assertSame('add_payment_receipt', (string) data_get($last, 'action')); + $this->assertSame('single', (string) data_get($last, 'scope')); + $this->assertNotEmpty((string) data_get($last, 'at')); + $this->assertGreaterThan(0, (int) data_get($last, 'admin_id')); + $this->assertSame(30.0, (float) data_get($last, 'snapshot.amount')); + $this->assertSame('icbc', (string) data_get($last, 'snapshot.channel')); + $this->assertSame(1, (int) data_get($last, 'snapshot.summary.count')); + $this->assertSame(30.0, (float) data_get($last, 'snapshot.summary.total_amount')); + } +} diff --git a/tests/Feature/AdminPlatformOrderAddRefundReceiptShouldWriteAuditTest.php b/tests/Feature/AdminPlatformOrderAddRefundReceiptShouldWriteAuditTest.php new file mode 100644 index 0000000..04a775e --- /dev/null +++ b/tests/Feature/AdminPlatformOrderAddRefundReceiptShouldWriteAuditTest.php @@ -0,0 +1,97 @@ +seed(); + + $this->post('/admin/login', [ + 'email' => 'platform.admin@demo.local', + 'password' => 'Platform@123456', + ])->assertRedirect('/admin'); + } + + public function test_add_refund_receipt_will_write_audit_entry_and_auto_advance_payment_status(): void + { + $this->loginAsPlatformAdmin(); + + $merchant = Merchant::query()->firstOrFail(); + $plan = Plan::query()->create([ + 'code' => 'audit_refund_receipt_plan', + 'name' => '审计退款用套餐', + 'billing_cycle' => 'monthly', + 'price' => 30, + 'list_price' => 30, + 'status' => 'active', + 'sort' => 10, + 'published_at' => now(), + ]); + + $this->post('/admin/platform-orders', [ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_type' => 'new_purchase', + 'quantity' => 1, + 'discount_amount' => 0, + 'payment_channel' => 'bank_transfer', + 'remark' => '审计退款测试', + ])->assertRedirect(); + + /** @var PlatformOrder $order */ + $order = PlatformOrder::query()->latest('id')->firstOrFail(); + + // 先让订单进入 paid,便于验证 addRefundReceipt 的“自动推进支付状态”口径 + $this->post('/admin/platform-orders/' . $order->id . '/add-payment-receipt', [ + 'type' => 'bank_transfer', + 'channel' => 'icbc', + 'amount' => 30, + 'paid_at' => now()->format('Y-m-d H:i:s'), + 'note' => '财务确认', + ])->assertRedirect(); + + $this->post('/admin/platform-orders/' . $order->id . '/mark-paid-and-activate') + ->assertRedirect(); + + $order->refresh(); + $this->assertSame('paid', (string) $order->payment_status); + $this->assertSame('activated', (string) $order->status); + + // 追加一笔部分退款 + $this->post('/admin/platform-orders/' . $order->id . '/add-refund-receipt', [ + 'type' => 'refund', + 'channel' => 'icbc', + 'amount' => 10, + 'refunded_at' => now()->format('Y-m-d H:i:s'), + 'note' => '部分退款', + ])->assertRedirect(); + + $order->refresh(); + $this->assertSame('partially_refunded', (string) $order->payment_status); + $this->assertSame(1, (int) data_get($order->meta, 'refund_summary.count')); + $this->assertSame(10.0, (float) data_get($order->meta, 'refund_summary.total_amount')); + + $audit = (array) (data_get($order->meta, 'audit', []) ?? []); + $this->assertNotEmpty($audit); + + $last = end($audit); + $this->assertSame('add_refund_receipt', (string) data_get($last, 'action')); + $this->assertSame('single', (string) data_get($last, 'scope')); + $this->assertNotEmpty((string) data_get($last, 'at')); + $this->assertGreaterThan(0, (int) data_get($last, 'admin_id')); + $this->assertSame(10.0, (float) data_get($last, 'snapshot.amount')); + $this->assertSame('icbc', (string) data_get($last, 'snapshot.channel')); + $this->assertSame(1, (int) data_get($last, 'snapshot.summary.count')); + $this->assertSame(10.0, (float) data_get($last, 'snapshot.summary.total_amount')); + } +}