From e24a3b031ca15a95e5a59173eb68ed1c0ee0b282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=90=9D=E5=8D=9C?= Date: Tue, 17 Mar 2026 10:32:16 +0800 Subject: [PATCH] fix(platform-orders): refresh order after activation to keep subscription_activation meta --- .../Admin/PlatformOrderController.php | 8 ++ ...minBillingClosedLoopNewPurchaseSopTest.php | 136 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 tests/Feature/AdminBillingClosedLoopNewPurchaseSopTest.php diff --git a/app/Http/Controllers/Admin/PlatformOrderController.php b/app/Http/Controllers/Admin/PlatformOrderController.php index 1cf4f26..30d9a88 100644 --- a/app/Http/Controllers/Admin/PlatformOrderController.php +++ b/app/Http/Controllers/Admin/PlatformOrderController.php @@ -763,6 +763,10 @@ class PlatformOrderController extends Controller try { $subscription = $service->activateOrder($order->id, $admin->id); + // 重要:activateOrder() 内会更新订单 meta(subscription_activation 等)。 + // 这里必须 refresh,避免用旧 $order->meta 覆盖掉刚写入的 subscription_activation。 + $order->refresh(); + // 同步成功:清理失败记录(若存在)+ 写入审计记录 $meta = (array) ($order->meta ?? []); data_forget($meta, 'subscription_activation_error'); @@ -872,6 +876,10 @@ class PlatformOrderController extends Controller try { $subscription = $service->activateOrder($order->id, $admin->id); + // 重要:activateOrder() 内会更新订单 meta(subscription_activation 等)。 + // 这里必须 refresh,避免用旧 $order->meta 覆盖掉刚写入的 subscription_activation。 + $order->refresh(); + $meta = (array) ($order->meta ?? []); data_forget($meta, 'subscription_activation_error'); diff --git a/tests/Feature/AdminBillingClosedLoopNewPurchaseSopTest.php b/tests/Feature/AdminBillingClosedLoopNewPurchaseSopTest.php new file mode 100644 index 0000000..46b435d --- /dev/null +++ b/tests/Feature/AdminBillingClosedLoopNewPurchaseSopTest.php @@ -0,0 +1,136 @@ + 创建平台订单 -> 补回执 -> BMPA -> 订阅生效 -> 退款轨迹 -> 支付状态自动推进 + */ +class AdminBillingClosedLoopNewPurchaseSopTest extends TestCase +{ + use RefreshDatabase; + + protected function loginAsPlatformAdmin(): void + { + $this->seed(); + + $this->post('/admin/login', [ + 'email' => 'platform.admin@demo.local', + 'password' => 'Platform@123456', + ])->assertRedirect('/admin'); + } + + public function test_sop_new_purchase_create_order_add_receipt_bmpa_sync_subscription_and_refund_flow(): void + { + $this->loginAsPlatformAdmin(); + + $merchant = Merchant::query()->firstOrFail(); + + $plan = Plan::query()->create([ + 'code' => 'sop_new_purchase_monthly', + 'name' => 'SOP新购(月付)', + 'billing_cycle' => 'monthly', + 'price' => 30, + 'list_price' => 30, + 'status' => 'active', + 'sort' => 10, + 'published_at' => now(), + ]); + + // 1) 创建平台订单(新购) + $res = $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' => 'SOP 新购演练', + ]); + + $res->assertRedirect(); + + /** @var PlatformOrder $order */ + $order = PlatformOrder::query()->latest('id')->firstOrFail(); + $this->assertSame('unpaid', $order->payment_status); + $this->assertSame('pending', $order->status); + $this->assertSame(30.0, (float) $order->payable_amount); + + // 2) 补回执(仅留痕,不自动改状态) + $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', $order->payment_status); + $this->assertSame('pending', $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')); + + // 3) BMPA:标记支付并生效(并同步订阅) + $this->post('/admin/platform-orders/' . $order->id . '/mark-paid-and-activate') + ->assertRedirect(); + + $order->refresh(); + $this->assertSame('paid', $order->payment_status); + $this->assertSame('activated', $order->status); + $this->assertNotNull($order->paid_at); + $this->assertNotNull($order->activated_at); + $this->assertNotNull($order->site_subscription_id); + + $subId = (int) $order->site_subscription_id; + $this->assertGreaterThan(0, $subId); + + /** @var SiteSubscription $sub */ + $sub = SiteSubscription::query()->findOrFail($subId); + $this->assertSame('activated', (string) $sub->status); + $this->assertSame($merchant->id, (int) $sub->merchant_id); + $this->assertSame($plan->id, (int) $sub->plan_id); + $this->assertNotNull($sub->starts_at); + $this->assertNotNull($sub->ends_at); + $this->assertTrue($sub->ends_at->greaterThan($sub->starts_at)); + + $this->assertSame($sub->id, (int) data_get($order->meta, 'subscription_activation.subscription_id')); + $this->assertNotEmpty((string) data_get($order->meta, 'subscription_activation.synced_at')); + + // 4) 退款轨迹:先部分退款 -> 自动推进支付状态为 partially_refunded + $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(10.0, (float) data_get($order->meta, 'refund_summary.total_amount')); + + // 5) 再追加一笔退款 -> 退款总额达到已付 -> 自动推进为 refunded + $this->post('/admin/platform-orders/' . $order->id . '/add-refund-receipt', [ + 'type' => 'refund', + 'channel' => 'icbc', + 'amount' => 20, + 'refunded_at' => now()->format('Y-m-d H:i:s'), + 'note' => '补齐退款(测试)', + ])->assertRedirect(); + + $order->refresh(); + $this->assertSame('refunded', (string) $order->payment_status); + $this->assertSame(30.0, (float) data_get($order->meta, 'refund_summary.total_amount')); + $this->assertNotNull($order->refunded_at); + } +}