Files
saasshop/tests/Feature/AdminBillingClosedLoopRenewalMissingSubscriptionSopTest.php

136 lines
5.0 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;
/**
* 收费闭环(可验收 SOP续费缺订阅治理
*
* 目标:把一条“无需开发兜底”的续费治理 SOP 锁住:
* - 存量续费单缺订阅site_subscription_id 为空BMPA 必须阻断
* - 运营先绑定正确订阅attach-subscription
* - 然后补回执并执行 BMPA应完成订阅同步/续期,订单进入 activated
*/
class AdminBillingClosedLoopRenewalMissingSubscriptionSopTest 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_renewal_missing_subscription_bind_then_receipt_then_bmpa_should_activate_and_keep_subscription(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'sop_renewal_missing_sub_monthly',
'name' => 'SOP续费缺订阅月付',
'billing_cycle' => 'monthly',
'price' => 30,
'list_price' => 30,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
// 准备一条“正确订阅”(模拟真实续费对应的订阅)
$sub = SiteSubscription::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'status' => 'activated',
'source' => 'manual',
'subscription_no' => 'SS_SOP_RENEWAL_0001',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'amount' => 30,
'starts_at' => now()->subDays(10),
'ends_at' => now()->addDays(10),
'snapshot' => [],
'meta' => [],
]);
// 准备一条“续费单但缺订阅”订单(存量脏数据)
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_SOP_RENEWAL_MISS_SUB_0001',
'order_type' => 'renewal',
'site_subscription_id' => null,
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 30,
'paid_amount' => 0,
'placed_at' => now()->subMinutes(10),
'meta' => [],
]);
// 1) 缺订阅时 BMPA 必须阻断
$res = $this->from('/admin/platform-orders/' . $order->id)
->post('/admin/platform-orders/' . $order->id . '/mark-paid-and-activate');
$res->assertRedirect('/admin/platform-orders/' . $order->id);
$res->assertSessionHas('warning');
$order->refresh();
$this->assertNull($order->site_subscription_id);
$this->assertSame('pending', (string) $order->status);
$this->assertSame('unpaid', (string) $order->payment_status);
// 2) 运营先绑定正确订阅
$this->post('/admin/platform-orders/' . $order->id . '/attach-subscription', [
'site_subscription_id' => $sub->id,
])->assertRedirect();
$order->refresh();
$this->assertSame($sub->id, (int) $order->site_subscription_id);
// 3) 补回执(用于对账留痕)
$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();
// 4) 再执行 BMPA应推进订单为 activated且仍绑定原订阅不应生成新订阅
// 同时订阅 ends_at 应被正确延长(续期真正发生)。
$oldEndsAt = $sub->ends_at->copy();
$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->assertSame($sub->id, (int) $order->site_subscription_id);
$sub->refresh();
$this->assertSame($merchant->id, (int) $sub->merchant_id);
$this->assertSame($plan->id, (int) $sub->plan_id);
// 续期断言:新 ends_at 必须大于原 ends_at按 months=period_months*quantity 续期)
$this->assertNotNull($sub->ends_at);
$this->assertTrue($sub->ends_at->greaterThan($oldEndsAt));
}
}