378 lines
14 KiB
PHP
378 lines
14 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature;
|
||
|
||
use App\Models\Merchant;
|
||
use App\Models\Plan;
|
||
use App\Models\PlatformOrder;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use App\Models\SiteSubscription;
|
||
use App\Support\SubscriptionActivationService;
|
||
use Tests\TestCase;
|
||
|
||
class AdminPlatformOrderBatchActivateSubscriptionsTest 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_platform_admin_can_batch_activate_subscriptions_in_filtered_scope(): void
|
||
{
|
||
$this->loginAsPlatformAdmin();
|
||
|
||
$merchant = Merchant::query()->firstOrFail();
|
||
$plan = Plan::query()->create([
|
||
'code' => 'batch_activate_plan',
|
||
'name' => '批量同步测试套餐',
|
||
'billing_cycle' => 'monthly',
|
||
'price' => 10,
|
||
'list_price' => 10,
|
||
'status' => 'active',
|
||
'sort' => 10,
|
||
'published_at' => now(),
|
||
]);
|
||
|
||
// 可同步(命中):已支付 + 已生效 + 未同步
|
||
$syncable = PlatformOrder::query()->create([
|
||
'merchant_id' => $merchant->id,
|
||
'plan_id' => $plan->id,
|
||
'order_no' => 'PO_BATCH_SYNC_0001',
|
||
'order_type' => 'new_purchase',
|
||
'status' => 'activated',
|
||
'payment_status' => 'paid',
|
||
'plan_name' => $plan->name,
|
||
'billing_cycle' => $plan->billing_cycle,
|
||
'period_months' => 1,
|
||
'quantity' => 1,
|
||
'payable_amount' => 10,
|
||
'paid_amount' => 10,
|
||
'placed_at' => now()->subMinutes(10),
|
||
'paid_at' => now()->subMinutes(9),
|
||
'activated_at' => now()->subMinutes(8),
|
||
]);
|
||
|
||
// 不可同步(不应命中):未支付
|
||
PlatformOrder::query()->create([
|
||
'merchant_id' => $merchant->id,
|
||
'plan_id' => $plan->id,
|
||
'order_no' => 'PO_BATCH_SYNC_0002',
|
||
'order_type' => 'renewal',
|
||
'status' => 'activated',
|
||
'payment_status' => 'unpaid',
|
||
'plan_name' => $plan->name,
|
||
'billing_cycle' => $plan->billing_cycle,
|
||
'period_months' => 1,
|
||
'quantity' => 1,
|
||
'payable_amount' => 10,
|
||
'paid_amount' => 0,
|
||
'placed_at' => now()->subMinutes(5),
|
||
]);
|
||
|
||
$this->post('/admin/platform-orders/batch-activate-subscriptions', [
|
||
'scope' => 'filtered',
|
||
'syncable_only' => '1',
|
||
])->assertRedirect();
|
||
|
||
$syncable->refresh();
|
||
$this->assertNotNull($syncable->site_subscription_id);
|
||
$this->assertNotEmpty(data_get($syncable->meta, 'subscription_activation.subscription_id'));
|
||
$this->assertSame('batch_activate_subscription', data_get($syncable->meta, 'audit.0.action'));
|
||
$this->assertNotEmpty(data_get($syncable->meta, 'audit.0.subscription_id'));
|
||
$this->assertNotEmpty(data_get($syncable->meta, 'audit.0.filters'));
|
||
$this->assertNotEmpty(data_get($syncable->meta, 'audit.0.note'));
|
||
}
|
||
|
||
public function test_platform_admin_batch_activate_records_failure_reason_and_summary(): void
|
||
{
|
||
$this->loginAsPlatformAdmin();
|
||
|
||
$merchant = Merchant::query()->firstOrFail();
|
||
$plan = Plan::query()->create([
|
||
'code' => 'batch_activate_fail_plan',
|
||
'name' => '批量同步失败治理测试套餐',
|
||
'billing_cycle' => 'monthly',
|
||
'price' => 10,
|
||
'list_price' => 10,
|
||
'status' => 'active',
|
||
'sort' => 10,
|
||
'published_at' => now(),
|
||
]);
|
||
|
||
$ok = PlatformOrder::query()->create([
|
||
'merchant_id' => $merchant->id,
|
||
'plan_id' => $plan->id,
|
||
'order_no' => 'PO_BATCH_FAIL_0001',
|
||
'order_type' => 'renewal',
|
||
'status' => 'activated',
|
||
'payment_status' => 'paid',
|
||
'plan_name' => $plan->name,
|
||
'billing_cycle' => $plan->billing_cycle,
|
||
'period_months' => 1,
|
||
'quantity' => 1,
|
||
'payable_amount' => 10,
|
||
'paid_amount' => 10,
|
||
'placed_at' => now()->subMinutes(10),
|
||
'paid_at' => now()->subMinutes(9),
|
||
'activated_at' => now()->subMinutes(8),
|
||
]);
|
||
|
||
// 强制失败:构造一条同样“可同步”的订单,然后在测试中用 DI 绑定一个假的 SubscriptionActivationService,
|
||
// 让它在处理该订单时抛异常,从而验证失败原因落库与 Top 汇总。
|
||
$bad = PlatformOrder::query()->create([
|
||
'merchant_id' => $merchant->id,
|
||
'plan_id' => $plan->id,
|
||
'order_no' => 'PO_BATCH_FAIL_0002',
|
||
'order_type' => 'renewal',
|
||
'status' => 'activated',
|
||
'payment_status' => 'paid',
|
||
'plan_name' => $plan->name,
|
||
'billing_cycle' => $plan->billing_cycle,
|
||
'period_months' => 1,
|
||
'quantity' => 1,
|
||
'payable_amount' => 10,
|
||
'paid_amount' => 10,
|
||
'placed_at' => now()->subMinutes(7),
|
||
'paid_at' => now()->subMinutes(6),
|
||
'activated_at' => now()->subMinutes(5),
|
||
]);
|
||
|
||
$badId = $bad->id;
|
||
$this->app->bind(SubscriptionActivationService::class, function () use ($badId) {
|
||
return new class($badId) extends SubscriptionActivationService {
|
||
public function __construct(private int $badId) {}
|
||
|
||
public function activateOrder(int $orderId, ?int $adminId = null): SiteSubscription
|
||
{
|
||
if ($orderId === $this->badId) {
|
||
throw new \RuntimeException('模拟失败:订阅同步异常');
|
||
}
|
||
|
||
return parent::activateOrder($orderId, $adminId);
|
||
}
|
||
};
|
||
});
|
||
|
||
$res = $this->post('/admin/platform-orders/batch-activate-subscriptions', [
|
||
'scope' => 'filtered',
|
||
'syncable_only' => '1',
|
||
'limit' => 50,
|
||
]);
|
||
|
||
$res->assertRedirect()->assertSessionHas('success');
|
||
|
||
$ok->refresh();
|
||
$bad->refresh();
|
||
|
||
$this->assertNotNull($ok->site_subscription_id);
|
||
$this->assertNotEmpty(data_get($ok->meta, 'subscription_activation.subscription_id'));
|
||
|
||
$this->assertNotEmpty(data_get($bad->meta, 'subscription_activation_error.message'));
|
||
$this->assertNotEmpty(data_get($bad->meta, 'subscription_activation_error.at'));
|
||
|
||
// 批量结果摘要应包含失败原因Top
|
||
$msg = (string) $res->getSession()->get('success');
|
||
$this->assertStringContainsString('失败原因Top', $msg);
|
||
$this->assertStringContainsString('模拟失败:订阅同步异常', $msg);
|
||
}
|
||
|
||
public function test_platform_admin_batch_activate_respects_limit(): void
|
||
{
|
||
$this->loginAsPlatformAdmin();
|
||
|
||
$merchant = Merchant::query()->firstOrFail();
|
||
$plan = Plan::query()->create([
|
||
'code' => 'batch_activate_limit_plan',
|
||
'name' => '批量同步限额测试套餐',
|
||
'billing_cycle' => 'monthly',
|
||
'price' => 10,
|
||
'list_price' => 10,
|
||
'status' => 'active',
|
||
'sort' => 10,
|
||
'published_at' => now(),
|
||
]);
|
||
|
||
$o1 = PlatformOrder::query()->create([
|
||
'merchant_id' => $merchant->id,
|
||
'plan_id' => $plan->id,
|
||
'order_no' => 'PO_BATCH_LIMIT_0001',
|
||
'order_type' => 'renewal',
|
||
'status' => 'activated',
|
||
'payment_status' => 'paid',
|
||
'plan_name' => $plan->name,
|
||
'billing_cycle' => $plan->billing_cycle,
|
||
'period_months' => 1,
|
||
'quantity' => 1,
|
||
'payable_amount' => 10,
|
||
'paid_amount' => 10,
|
||
'placed_at' => now()->subMinutes(10),
|
||
'paid_at' => now()->subMinutes(9),
|
||
'activated_at' => now()->subMinutes(8),
|
||
]);
|
||
|
||
$o2 = PlatformOrder::query()->create([
|
||
'merchant_id' => $merchant->id,
|
||
'plan_id' => $plan->id,
|
||
'order_no' => 'PO_BATCH_LIMIT_0002',
|
||
'order_type' => 'renewal',
|
||
'status' => 'activated',
|
||
'payment_status' => 'paid',
|
||
'plan_name' => $plan->name,
|
||
'billing_cycle' => $plan->billing_cycle,
|
||
'period_months' => 1,
|
||
'quantity' => 1,
|
||
'payable_amount' => 10,
|
||
'paid_amount' => 10,
|
||
'placed_at' => now()->subMinutes(7),
|
||
'paid_at' => now()->subMinutes(6),
|
||
'activated_at' => now()->subMinutes(5),
|
||
]);
|
||
|
||
$o3 = PlatformOrder::query()->create([
|
||
'merchant_id' => $merchant->id,
|
||
'plan_id' => $plan->id,
|
||
'order_no' => 'PO_BATCH_LIMIT_0003',
|
||
'order_type' => 'renewal',
|
||
'status' => 'activated',
|
||
'payment_status' => 'paid',
|
||
'plan_name' => $plan->name,
|
||
'billing_cycle' => $plan->billing_cycle,
|
||
'period_months' => 1,
|
||
'quantity' => 1,
|
||
'payable_amount' => 10,
|
||
'paid_amount' => 10,
|
||
'placed_at' => now()->subMinutes(4),
|
||
'paid_at' => now()->subMinutes(3),
|
||
'activated_at' => now()->subMinutes(2),
|
||
]);
|
||
|
||
$this->post('/admin/platform-orders/batch-activate-subscriptions', [
|
||
'scope' => 'filtered',
|
||
'syncable_only' => '1',
|
||
'limit' => 1,
|
||
])->assertRedirect();
|
||
|
||
$o1->refresh();
|
||
$o2->refresh();
|
||
$o3->refresh();
|
||
|
||
// 当前实现按 id 倒序优先处理最新订单,因此 limit=1 时应只处理 o3
|
||
$this->assertNull($o1->site_subscription_id);
|
||
$this->assertNull($o2->site_subscription_id);
|
||
$this->assertNotNull($o3->site_subscription_id);
|
||
}
|
||
|
||
public function test_platform_admin_batch_activate_requires_syncable_only_filter(): void
|
||
{
|
||
$this->loginAsPlatformAdmin();
|
||
|
||
$merchant = Merchant::query()->firstOrFail();
|
||
$plan = Plan::query()->create([
|
||
'code' => 'batch_activate_guard_plan',
|
||
'name' => '批量同步防误操作测试套餐',
|
||
'billing_cycle' => 'monthly',
|
||
'price' => 10,
|
||
'list_price' => 10,
|
||
'status' => 'active',
|
||
'sort' => 10,
|
||
'published_at' => now(),
|
||
]);
|
||
|
||
$syncable = PlatformOrder::query()->create([
|
||
'merchant_id' => $merchant->id,
|
||
'plan_id' => $plan->id,
|
||
'order_no' => 'PO_BATCH_GUARD_0001',
|
||
'order_type' => 'new_purchase',
|
||
'status' => 'activated',
|
||
'payment_status' => 'paid',
|
||
'plan_name' => $plan->name,
|
||
'billing_cycle' => $plan->billing_cycle,
|
||
'period_months' => 1,
|
||
'quantity' => 1,
|
||
'payable_amount' => 10,
|
||
'paid_amount' => 10,
|
||
'placed_at' => now()->subMinutes(10),
|
||
'paid_at' => now()->subMinutes(9),
|
||
'activated_at' => now()->subMinutes(8),
|
||
]);
|
||
|
||
$this->post('/admin/platform-orders/batch-activate-subscriptions', [
|
||
'scope' => 'filtered',
|
||
// 故意不传 syncable_only
|
||
])->assertRedirect()->assertSessionHas('warning');
|
||
|
||
$syncable->refresh();
|
||
$this->assertNull($syncable->site_subscription_id);
|
||
}
|
||
|
||
public function test_platform_admin_batch_activate_scope_all_requires_confirm_yes(): void
|
||
{
|
||
$this->loginAsPlatformAdmin();
|
||
|
||
$merchant = Merchant::query()->firstOrFail();
|
||
$plan = Plan::query()->create([
|
||
'code' => 'batch_activate_all_guard_plan',
|
||
'name' => '批量同步全量二次确认测试套餐',
|
||
'billing_cycle' => 'monthly',
|
||
'price' => 10,
|
||
'list_price' => 10,
|
||
'status' => 'active',
|
||
'sort' => 10,
|
||
'published_at' => now(),
|
||
]);
|
||
|
||
$syncable = PlatformOrder::query()->create([
|
||
'merchant_id' => $merchant->id,
|
||
'plan_id' => $plan->id,
|
||
'order_no' => 'PO_BATCH_ALL_GUARD_0001',
|
||
'order_type' => 'new_purchase',
|
||
'status' => 'activated',
|
||
'payment_status' => 'paid',
|
||
'plan_name' => $plan->name,
|
||
'billing_cycle' => $plan->billing_cycle,
|
||
'period_months' => 1,
|
||
'quantity' => 1,
|
||
'payable_amount' => 10,
|
||
'paid_amount' => 10,
|
||
'placed_at' => now()->subMinutes(10),
|
||
'paid_at' => now()->subMinutes(9),
|
||
'activated_at' => now()->subMinutes(8),
|
||
]);
|
||
|
||
// 未确认:不应执行
|
||
$this->post('/admin/platform-orders/batch-activate-subscriptions', [
|
||
'scope' => 'all',
|
||
'limit' => 50,
|
||
])->assertRedirect()->assertSessionHas('warning');
|
||
|
||
$syncable->refresh();
|
||
$this->assertNull($syncable->site_subscription_id);
|
||
|
||
// 确认 YES:应执行
|
||
$this->post('/admin/platform-orders/batch-activate-subscriptions', [
|
||
'scope' => 'all',
|
||
'confirm' => 'YES',
|
||
'limit' => 50,
|
||
])->assertRedirect()->assertSessionHas('success');
|
||
|
||
$syncable->refresh();
|
||
$this->assertNotNull($syncable->site_subscription_id);
|
||
}
|
||
|
||
public function test_guest_cannot_batch_activate_subscriptions(): void
|
||
{
|
||
$this->seed();
|
||
|
||
$this->post('/admin/platform-orders/batch-activate-subscriptions', [
|
||
'scope' => 'filtered',
|
||
])->assertRedirect('/admin/login');
|
||
}
|
||
}
|