Files
saasshop/tests/Feature/AdminBillingClosedLoopNewPurchaseSopTest.php

137 lines
5.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use App\Models\SiteSubscription;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* 收费闭环(最短链路)可验收用例:新购
*
* 目标:用 Feature 测试把一条“无需开发兜底”的 SOP 锁住:
* 套餐 -> 创建平台订单 -> 补回执 -> 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);
}
}