chore: init saasshop repo + sql migrations runner + gitee go
This commit is contained in:
55
tests/Feature/AdminAccessTest.php
Normal file
55
tests/Feature/AdminAccessTest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminAccessTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_admin_login_page_displays_new_console_naming(): void
|
||||
{
|
||||
$response = $this->get('/admin/login');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('总台管理登录');
|
||||
$response->assertSee('登录总台管理');
|
||||
}
|
||||
|
||||
public function test_platform_admin_can_login_and_open_dashboard_with_new_naming(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->post('/admin/login', [
|
||||
'email' => 'platform.admin@demo.local',
|
||||
'password' => 'Platform@123456',
|
||||
]);
|
||||
|
||||
$response->assertRedirect('/admin');
|
||||
|
||||
$dashboard = $this->get('/admin');
|
||||
$dashboard->assertOk();
|
||||
$dashboard->assertSee('总台管理');
|
||||
$dashboard->assertSee('总台仪表盘');
|
||||
$dashboard->assertSee('站点管理');
|
||||
$dashboard->assertSee('订单监控');
|
||||
$dashboard->assertSee('商品巡检');
|
||||
$dashboard->assertSee('商品分类');
|
||||
}
|
||||
|
||||
public function test_merchant_admin_cannot_login_from_admin_entry(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->from('/admin/login')->post('/admin/login', [
|
||||
'email' => 'merchant.admin@demo.local',
|
||||
'password' => 'Merchant@123456',
|
||||
]);
|
||||
|
||||
$response->assertRedirect('/admin/login');
|
||||
|
||||
$this->get('/admin/login')->assertSee('当前账号是商家管理员,请从商家后台入口登录');
|
||||
}
|
||||
}
|
||||
349
tests/Feature/AdminBusinessPagesTest.php
Normal file
349
tests/Feature/AdminBusinessPagesTest.php
Normal file
@@ -0,0 +1,349 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Merchant;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductCategory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
use function PHPUnit\Framework\assertStringContainsString;
|
||||
|
||||
class AdminBusinessPagesTest 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_admin_merchants_page_displays_site_management_content(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$this->get('/admin/merchants')
|
||||
->assertOk()
|
||||
->assertSee('站点管理')
|
||||
->assertSee('新增站点')
|
||||
->assertSee('站点列表')
|
||||
->assertSee('创建站点');
|
||||
}
|
||||
|
||||
public function test_admin_orders_page_displays_filters_and_export_entry(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$this->get('/admin/orders?status=shipped&payment_status=paid&platform=wechat_mini&device_type=mini-program&payment_channel=wechat_pay&min_pay_amount=180&max_pay_amount=190&sort=pay_amount_desc')
|
||||
->assertOk()
|
||||
->assertSee('订单监控')
|
||||
->assertSee('筛选条件')
|
||||
->assertSee('导出当前筛选结果 CSV')
|
||||
->assertSee('当前筛选摘要')
|
||||
->assertSee('运营关注项')
|
||||
->assertSee('当前信号')
|
||||
->assertSee('工作台导航')
|
||||
->assertSee('已发货')
|
||||
->assertSee('已支付')
|
||||
->assertSee('微信小程序')
|
||||
->assertSee('小程序环境')
|
||||
->assertSee('微信支付')
|
||||
->assertSee('订单汇总');
|
||||
}
|
||||
|
||||
public function test_admin_order_operations_focus_can_follow_current_filters(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$this->get('/admin/orders?platform=wechat_mini&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦微信小程序订单,建议优先关注下单到支付转化是否顺畅,并同步排查小程序端支付回流体验。')
|
||||
->assertSee('继续查看微信小程序订单');
|
||||
|
||||
$this->get('/admin/orders?payment_channel=wechat_pay&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦微信支付订单,建议优先核对支付成功率、回调稳定性与失败重试转化。')
|
||||
->assertSee('继续查看微信支付订单');
|
||||
|
||||
$this->get('/admin/orders?device_type=mini-program&payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦小程序环境订单,建议优先核对授权链路、支付唤起表现与下单回流是否顺畅。')
|
||||
->assertSee('继续查看小程序环境订单');
|
||||
|
||||
$this->get('/admin/orders?device_type=mobile-webview&payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦微信内网页订单,建议优先关注授权静默登录、页面跳转稳定性与支付拉起后的回流体验。')
|
||||
->assertSee('继续查看微信内网页订单');
|
||||
|
||||
$this->get('/admin/orders?device_type=mobile&payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦移动浏览器订单,建议优先关注 H5 下单链路、页面加载稳定性与支付转化流失点。')
|
||||
->assertSee('继续查看移动浏览器订单');
|
||||
|
||||
$this->get('/admin/orders?device_type=desktop&payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦桌面浏览器订单,建议优先关注 PC 端下单流程、页面首屏稳定性与高客单转化表现。')
|
||||
->assertSee('继续查看桌面浏览器订单');
|
||||
|
||||
$this->get('/admin/orders?payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦支付失败订单,建议优先排查支付渠道、回调结果与用户重试情况。')
|
||||
->assertSee('继续查看支付失败订单');
|
||||
|
||||
$this->get('/admin/orders?status=completed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前正在查看已完成订单,建议复盘高客单成交与复购来源,沉淀更稳定的转化路径。')
|
||||
->assertSee('继续查看已完成订单');
|
||||
}
|
||||
|
||||
public function test_admin_products_page_displays_import_and_history_entries(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$this->get('/admin/products?status=published&min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('商品巡检')
|
||||
->assertSee('批量导入商品')
|
||||
->assertSee('下载导入模板')
|
||||
->assertSee('导出当前筛选结果 CSV')
|
||||
->assertSee('进入完整导入历史页')
|
||||
->assertSee('导入历史摘要')
|
||||
->assertSee('当前筛选摘要')
|
||||
->assertSee('运营关注项')
|
||||
->assertSee('当前信号')
|
||||
->assertSee('工作台导航')
|
||||
->assertSee('已上架')
|
||||
->assertSee('价格从高到低');
|
||||
}
|
||||
|
||||
public function test_admin_product_operations_focus_can_follow_current_filters(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$this->get('/admin/products?status=draft&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前正在查看草稿商品,建议优先补齐标题、分类、价格与库存后再安排上架。')
|
||||
->assertSee('继续查看当前草稿');
|
||||
|
||||
$this->get('/admin/products?status=published&min_stock=0&max_stock=20&sort=stock_asc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架库存视角,建议优先确认低库存补货节奏,并同步观察高库存结构是否健康。')
|
||||
->assertSee('继续查看当前库存视角');
|
||||
|
||||
$this->get('/admin/products?status=published&category_id=1&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架的“演示店铺 / 默认分类”分类下关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对在售商品命名、分类承接、价格梯度与站点承接是否一致,并同步观察库存结构与站点覆盖是否健康。')
|
||||
->assertSee('继续查看当前已上架分类关键词价格带商品')
|
||||
->assertSee('去看当前已上架分类关键词商品');
|
||||
|
||||
$this->get('/admin/products?status=published&category_id=1&keyword=%E6%BC%94%E7%A4%BA&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架的“演示店铺 / 默认分类”分类下关键词“演示”命中的商品,建议优先核对在售商品命名、分类承接与站点承接是否一致,并同步观察价格带、库存结构与站点覆盖是否健康。')
|
||||
->assertSee('继续查看当前已上架分类关键词商品');
|
||||
|
||||
$this->get('/admin/products?status=published&category_id=1&min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架的“演示店铺 / 默认分类”分类下价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对该分类在售商品的价格结构、库存分布与站点覆盖是否协调。')
|
||||
->assertSee('继续查看当前已上架分类价格带商品');
|
||||
|
||||
$this->get('/admin/products?status=published&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架商品中关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 结果,建议优先核对在售商品命名、卖点表达与价格梯度是否一致,并同步观察库存结构、站点承接与站点覆盖是否健康。')
|
||||
->assertSee('继续查看当前已上架关键词价格带商品')
|
||||
->assertSee('去看当前已上架关键词商品');
|
||||
|
||||
$this->get('/admin/products?status=published&keyword=%E6%BC%94%E7%A4%BA&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架商品中关键词“演示”命中的结果,建议优先核对在售商品命名、卖点表达与站点承接是否一致,并同步观察价格带、库存结构与站点覆盖是否健康。')
|
||||
->assertSee('继续查看当前已上架关键词商品');
|
||||
|
||||
$this->get('/admin/products?category_id=1&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦“演示店铺 / 默认分类”分类下关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对分类承接、命名卖点与价格梯度是否一致,并同步观察库存结构、站点承接与站点覆盖是否健康。')
|
||||
->assertSee('继续查看当前分类关键词价格带商品')
|
||||
->assertSee('去看当前分类关键词商品');
|
||||
|
||||
$this->get('/admin/products?category_id=1&keyword=%E6%BC%94%E7%A4%BA&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦“演示店铺 / 默认分类”分类下关键词“演示”命中的商品,建议优先核对分类承接、命名卖点与站点承接是否一致,并同步观察相关商品的价格带、库存结构与站点覆盖。')
|
||||
->assertSee('继续查看当前分类关键词商品');
|
||||
|
||||
$this->get('/admin/products?status=published&category_id=1&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架的“演示店铺 / 默认分类”分类商品,建议优先核对该分类在售商品的价格带、库存结构与站点覆盖是否均衡。')
|
||||
->assertSee('继续查看当前已上架分类商品');
|
||||
|
||||
$this->get('/admin/products?category_id=1&min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦“演示店铺 / 默认分类”分类下价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对该分类价格结构是否连贯,并同步观察库存分布、转化表现与站点覆盖是否健康。')
|
||||
->assertSee('继续查看当前分类价格带商品');
|
||||
|
||||
$this->get('/admin/products?category_id=1&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦“演示店铺 / 默认分类”分类商品,建议优先核对分类承接是否准确,并同步观察价格带、库存结构与站点覆盖是否均衡。')
|
||||
->assertSee('继续查看当前分类商品');
|
||||
|
||||
$this->get('/admin/products?keyword=%E6%BC%94%E7%A4%BA&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦关键词“演示”命中的商品,建议优先核对命名、卖点与站点承接是否一致,并同步观察相关商品的价格带与库存结构。')
|
||||
->assertSee('继续查看当前关键词商品');
|
||||
|
||||
$this->get('/admin/products?min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对定价梯度是否连贯,并同步观察库存结构、转化表现与站点覆盖是否匹配。')
|
||||
->assertSee('继续查看当前价格带商品');
|
||||
}
|
||||
|
||||
public function test_admin_import_histories_page_displays_filter_and_export_entries(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$this->get('/admin/products/import-histories')
|
||||
->assertOk()
|
||||
->assertSee('平台商品导入历史')
|
||||
->assertSee('筛选导入历史')
|
||||
->assertSee('导出当前筛选 CSV')
|
||||
->assertSee('返回商品页');
|
||||
}
|
||||
|
||||
public function test_admin_product_summary_stats_match_export_summary_for_same_filters(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$page = $this->get('/admin/products?merchant_id=1&keyword=%E6%BC%94%E7%A4%BA%E5%95%86%E5%93%81&status=published&min_price=150&max_price=220');
|
||||
$page->assertOk()->assertViewHas('summaryStats', function (array $summaryStats) {
|
||||
return ($summaryStats['total_products'] ?? null) === 1
|
||||
&& ($summaryStats['total_stock'] ?? null) === 100
|
||||
&& (float) ($summaryStats['total_stock_value'] ?? 0) === 19900.0
|
||||
&& (float) ($summaryStats['average_price'] ?? 0) === 199.0;
|
||||
});
|
||||
|
||||
$export = $this->get('/admin/products/export?merchant_id=1&keyword=%E6%BC%94%E7%A4%BA%E5%95%86%E5%93%81&status=published&min_price=150&max_price=220');
|
||||
$export->assertOk();
|
||||
$content = $export->streamedContent();
|
||||
|
||||
assertStringContainsString('导出信息,总台商品导出', $content);
|
||||
assertStringContainsString('站点,"1 / 演示店铺"', $content);
|
||||
assertStringContainsString('状态,已上架', $content);
|
||||
assertStringContainsString('最低价格,¥150.00', $content);
|
||||
assertStringContainsString('最高价格,¥220.00', $content);
|
||||
assertStringContainsString('导出商品数,1', $content);
|
||||
assertStringContainsString('导出总库存,100', $content);
|
||||
assertStringContainsString('导出总货值,19900.00', $content);
|
||||
assertStringContainsString('导出平均售价,199.00', $content);
|
||||
assertStringContainsString('演示商品', $content);
|
||||
}
|
||||
|
||||
public function test_admin_order_summary_stats_match_export_summary_for_same_filters(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$page = $this->get('/admin/orders?status=shipped&payment_status=paid&platform=wechat_mini&device_type=mini-program&payment_channel=wechat_pay&min_pay_amount=180&max_pay_amount=190');
|
||||
$page->assertOk()->assertViewHas('summaryStats', function (array $summaryStats) {
|
||||
return ($summaryStats['total_orders'] ?? null) === 1
|
||||
&& (float) ($summaryStats['total_pay_amount'] ?? 0) === 189.0
|
||||
&& (float) ($summaryStats['average_order_amount'] ?? 0) === 189.0
|
||||
&& ($summaryStats['paid_orders'] ?? null) === 1
|
||||
&& ($summaryStats['failed_payment_orders'] ?? null) === 0;
|
||||
});
|
||||
|
||||
$export = $this->get('/admin/orders/export?status=shipped&payment_status=paid&platform=wechat_mini&device_type=mini-program&payment_channel=wechat_pay&min_pay_amount=180&max_pay_amount=190');
|
||||
$export->assertOk();
|
||||
$content = $export->streamedContent();
|
||||
|
||||
assertStringContainsString('导出信息,总台订单导出', $content);
|
||||
assertStringContainsString('订单状态,已发货', $content);
|
||||
assertStringContainsString('支付状态,已支付', $content);
|
||||
assertStringContainsString('平台,微信小程序', $content);
|
||||
assertStringContainsString('设备类型,小程序环境', $content);
|
||||
assertStringContainsString('支付渠道,微信支付', $content);
|
||||
assertStringContainsString('最低实付金额,¥180.00', $content);
|
||||
assertStringContainsString('最高实付金额,¥190.00', $content);
|
||||
assertStringContainsString('导出订单数,1', $content);
|
||||
assertStringContainsString('导出实付总额,189.00', $content);
|
||||
assertStringContainsString('导出平均客单价,189.00', $content);
|
||||
assertStringContainsString('导出已支付订单数,1', $content);
|
||||
assertStringContainsString('导出支付失败订单,0', $content);
|
||||
assertStringContainsString('ORD202603080003', $content);
|
||||
}
|
||||
|
||||
public function test_admin_products_batch_status_update_changes_selected_products(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$products = Product::query()->orderBy('id')->take(2)->get();
|
||||
|
||||
$this->post('/admin/products/batch', [
|
||||
'product_ids' => $products->pluck('id')->all(),
|
||||
'action' => 'change_status',
|
||||
'status' => 'offline',
|
||||
])->assertRedirect('/admin/products')
|
||||
->assertSessionHas('success', '批量操作已完成,本次更新 2 条商品。');
|
||||
|
||||
foreach ($products as $product) {
|
||||
$this->assertSame('offline', $product->fresh()->status);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_admin_products_batch_change_category_blocks_cross_merchant_operation(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$secondMerchant = Merchant::query()->create([
|
||||
'name' => '第二站点',
|
||||
'slug' => 'merchant-2-demo',
|
||||
'domain' => null,
|
||||
'contact_name' => '测试人',
|
||||
'contact_phone' => '13800000001',
|
||||
'contact_email' => 'merchant2@example.com',
|
||||
'plan' => 'basic',
|
||||
'status' => 'active',
|
||||
'activated_at' => now(),
|
||||
'settings' => ['currency' => 'CNY'],
|
||||
]);
|
||||
|
||||
$secondCategory = ProductCategory::query()->create([
|
||||
'merchant_id' => $secondMerchant->id,
|
||||
'name' => '第二站点分类',
|
||||
'slug' => 'merchant-2-default',
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'description' => '用于跨商家分类拦截测试',
|
||||
]);
|
||||
|
||||
$secondProduct = Product::query()->create([
|
||||
'merchant_id' => $secondMerchant->id,
|
||||
'category_id' => $secondCategory->id,
|
||||
'title' => '第二站点商品',
|
||||
'slug' => 'merchant-2-product',
|
||||
'sku' => 'SKU-MERCHANT-2-001',
|
||||
'summary' => '用于跨商家批量改分类测试',
|
||||
'content' => 'test',
|
||||
'price' => 88,
|
||||
'original_price' => 99,
|
||||
'stock' => 10,
|
||||
'status' => 'published',
|
||||
'images' => [],
|
||||
]);
|
||||
|
||||
$firstProduct = Product::query()->orderBy('id')->firstOrFail();
|
||||
$firstMerchantCategory = ProductCategory::query()->where('merchant_id', $firstProduct->merchant_id)->orderBy('id')->firstOrFail();
|
||||
|
||||
$response = $this->from('/admin/products')->post('/admin/products/batch', [
|
||||
'product_ids' => [$firstProduct->id, $secondProduct->id],
|
||||
'action' => 'change_category',
|
||||
'category_id' => $firstMerchantCategory->id,
|
||||
]);
|
||||
|
||||
$response->assertRedirect('/admin/products');
|
||||
$response->assertSessionHasErrors(['category_id']);
|
||||
$this->assertStringContainsString('所选分类不属于商家 #', session('errors')->first('category_id'));
|
||||
|
||||
$this->assertSame($secondCategory->id, $secondProduct->fresh()->category_id);
|
||||
}
|
||||
}
|
||||
57
tests/Feature/AdminPlanExportTest.php
Normal file
57
tests/Feature/AdminPlanExportTest.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Plan;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminPlanExportTest 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_export_plans_csv(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
Plan::query()->create([
|
||||
'code' => 'plan_export_01',
|
||||
'name' => '套餐导出测试',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 9,
|
||||
'list_price' => 19,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
'description' => '用于导出断言',
|
||||
]);
|
||||
|
||||
$res = $this->get('/admin/plans/export');
|
||||
|
||||
$res->assertOk();
|
||||
$res->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
|
||||
$content = $res->streamedContent();
|
||||
$this->assertStringContainsString('套餐名称', $content);
|
||||
$this->assertStringContainsString('套餐导出测试', $content);
|
||||
$this->assertStringContainsString('plan_export_01', $content);
|
||||
}
|
||||
|
||||
public function test_guest_cannot_export_plans_csv(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->get('/admin/plans/export')
|
||||
->assertRedirect('/admin/login');
|
||||
}
|
||||
}
|
||||
72
tests/Feature/AdminPlanSetStatusTest.php
Normal file
72
tests/Feature/AdminPlanSetStatusTest.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Plan;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminPlanSetStatusTest 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_set_plan_status(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'status_toggle_test',
|
||||
'name' => '状态切换测试',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 9,
|
||||
'list_price' => 9,
|
||||
'status' => 'draft',
|
||||
'sort' => 10,
|
||||
'published_at' => null,
|
||||
]);
|
||||
|
||||
$this->post('/admin/plans/' . $plan->id . '/set-status', [
|
||||
'status' => 'active',
|
||||
])->assertRedirect();
|
||||
|
||||
$plan->refresh();
|
||||
$this->assertSame('active', $plan->status);
|
||||
$this->assertNotNull($plan->published_at);
|
||||
|
||||
$this->post('/admin/plans/' . $plan->id . '/set-status', [
|
||||
'status' => 'inactive',
|
||||
])->assertRedirect();
|
||||
|
||||
$plan->refresh();
|
||||
$this->assertSame('inactive', $plan->status);
|
||||
}
|
||||
|
||||
public function test_guest_cannot_set_plan_status(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'status_toggle_guest_test',
|
||||
'name' => '状态切换测试(guest)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 9,
|
||||
'list_price' => 9,
|
||||
'status' => 'draft',
|
||||
'sort' => 10,
|
||||
]);
|
||||
|
||||
$this->post('/admin/plans/' . $plan->id . '/set-status', [
|
||||
'status' => 'active',
|
||||
])->assertRedirect('/admin/login');
|
||||
}
|
||||
}
|
||||
144
tests/Feature/AdminPlanTest.php
Normal file
144
tests/Feature/AdminPlanTest.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Plan;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminPlanTest 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_open_plans_page(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$this->get('/admin/plans')
|
||||
->assertOk()
|
||||
->assertSee('套餐管理')
|
||||
->assertSee('筛选条件')
|
||||
->assertSee('套餐列表');
|
||||
}
|
||||
|
||||
public function test_guest_cannot_open_plans_page(): void
|
||||
{
|
||||
$this->get('/admin/plans')->assertRedirect('/admin/login');
|
||||
}
|
||||
|
||||
public function test_plans_page_can_filter_by_status_and_billing_cycle(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
Plan::query()->create([
|
||||
'code' => 'basic_monthly',
|
||||
'name' => '基础版(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 99,
|
||||
'list_price' => 99,
|
||||
'status' => 'active',
|
||||
'sort' => 20,
|
||||
'description' => '适合起步商家',
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
Plan::query()->create([
|
||||
'code' => 'enterprise_yearly_draft',
|
||||
'name' => '企业版(年付草稿)',
|
||||
'billing_cycle' => 'yearly',
|
||||
'price' => 2999,
|
||||
'list_price' => 3999,
|
||||
'status' => 'draft',
|
||||
'sort' => 30,
|
||||
'description' => '仅供内部评审',
|
||||
'published_at' => null,
|
||||
]);
|
||||
|
||||
$this->get('/admin/plans?status=active&billing_cycle=monthly')
|
||||
->assertOk()
|
||||
->assertSee('基础版(月付)')
|
||||
->assertDontSee('企业版(年付草稿)')
|
||||
->assertSee('月付');
|
||||
|
||||
// 发布状态筛选(按 published_at):基础版已发布,企业版未发布
|
||||
$this->get('/admin/plans?published=published')
|
||||
->assertOk()
|
||||
->assertSee('基础版(月付)')
|
||||
->assertDontSee('企业版(年付草稿)');
|
||||
|
||||
$this->get('/admin/plans?published=unpublished')
|
||||
->assertOk()
|
||||
->assertSee('企业版(年付草稿)')
|
||||
->assertDontSee('基础版(月付)');
|
||||
}
|
||||
|
||||
public function test_platform_admin_can_create_plan(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$this->get('/admin/plans/create')
|
||||
->assertOk()
|
||||
->assertSee('新建套餐')
|
||||
->assertSee('套餐名称');
|
||||
|
||||
$this->post('/admin/plans', [
|
||||
'code' => 'pro_monthly_form',
|
||||
'name' => '专业版(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 199,
|
||||
'list_price' => 299,
|
||||
'status' => 'active',
|
||||
'sort' => 5,
|
||||
'description' => '主要面向成长型站点',
|
||||
'published_at' => now()->format('Y-m-d H:i:s'),
|
||||
])->assertRedirect('/admin/plans');
|
||||
|
||||
$this->get('/admin/plans')
|
||||
->assertSee('专业版(月付)')
|
||||
->assertSee('主要面向成长型站点');
|
||||
}
|
||||
|
||||
public function test_platform_admin_can_update_plan(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'update_test',
|
||||
'name' => '更新前套餐',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 50,
|
||||
'list_price' => 80,
|
||||
'status' => 'draft',
|
||||
'sort' => 11,
|
||||
]);
|
||||
|
||||
$this->get('/admin/plans/' . $plan->id . '/edit')
|
||||
->assertOk()
|
||||
->assertSee('编辑套餐');
|
||||
|
||||
$this->post('/admin/plans/' . $plan->id, [
|
||||
'code' => 'update_test',
|
||||
'name' => '更新后套餐',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 66,
|
||||
'list_price' => 88,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'description' => '通过编辑表单更新',
|
||||
])->assertRedirect('/admin/plans');
|
||||
|
||||
$this->get('/admin/plans')
|
||||
->assertSee('更新后套餐')
|
||||
->assertSee('¥66.00');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Merchant;
|
||||
use App\Models\Plan;
|
||||
use App\Models\PlatformOrder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminPlatformOrderActivateSubscriptionErrorMetaTest 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_activation_failure_will_write_error_meta(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'activate_error_meta_test',
|
||||
'name' => '同步失败记录测试',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 9,
|
||||
'list_price' => 9,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
// 构造一个“不满足 paid+activated”的订单,触发失败
|
||||
$order = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_ACT_ERR_0001',
|
||||
'order_type' => 'new_purchase',
|
||||
'status' => 'pending',
|
||||
'payment_status' => 'unpaid',
|
||||
'plan_name' => $plan->name,
|
||||
'billing_cycle' => $plan->billing_cycle,
|
||||
'period_months' => 1,
|
||||
'quantity' => 1,
|
||||
'payable_amount' => 9,
|
||||
'paid_amount' => 0,
|
||||
'placed_at' => now(),
|
||||
]);
|
||||
|
||||
$this->post('/admin/platform-orders/' . $order->id . '/activate-subscription')
|
||||
->assertRedirect();
|
||||
|
||||
$order->refresh();
|
||||
$this->assertNotEmpty(data_get($order->meta, 'subscription_activation_error.message'));
|
||||
$this->assertNotEmpty(data_get($order->meta, 'subscription_activation_error.at'));
|
||||
}
|
||||
|
||||
public function test_activation_success_will_clear_error_meta(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'activate_error_meta_test2',
|
||||
'name' => '同步成功清理失败记录测试',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 9,
|
||||
'list_price' => 9,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$order = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_ACT_ERR_0002',
|
||||
'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' => 9,
|
||||
'paid_amount' => 9,
|
||||
'placed_at' => now(),
|
||||
'paid_at' => now(),
|
||||
'activated_at' => now(),
|
||||
'meta' => [
|
||||
'subscription_activation_error' => [
|
||||
'message' => '历史错误',
|
||||
'at' => now()->subDay()->toDateTimeString(),
|
||||
'admin_id' => 1,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->post('/admin/platform-orders/' . $order->id . '/activate-subscription')
|
||||
->assertRedirect();
|
||||
|
||||
$order->refresh();
|
||||
$this->assertEmpty(data_get($order->meta, 'subscription_activation_error'));
|
||||
$this->assertNotNull($order->site_subscription_id);
|
||||
}
|
||||
}
|
||||
104
tests/Feature/AdminPlatformOrderActivateSubscriptionTest.php
Normal file
104
tests/Feature/AdminPlatformOrderActivateSubscriptionTest.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Merchant;
|
||||
use App\Models\Plan;
|
||||
use App\Models\PlatformOrder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminPlatformOrderActivateSubscriptionTest 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_activate_subscription_from_order(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'activate_btn_test',
|
||||
'name' => '按钮激活测试(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 66,
|
||||
'list_price' => 66,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$order = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_BTN_ACT_0001',
|
||||
'order_type' => 'new_purchase',
|
||||
'status' => 'activated',
|
||||
'payment_status' => 'paid',
|
||||
'plan_name' => $plan->name,
|
||||
'billing_cycle' => $plan->billing_cycle,
|
||||
'period_months' => 1,
|
||||
'quantity' => 1,
|
||||
'list_amount' => 66,
|
||||
'discount_amount' => 0,
|
||||
'payable_amount' => 66,
|
||||
'paid_amount' => 66,
|
||||
'placed_at' => now()->subMinutes(10),
|
||||
'paid_at' => now()->subMinutes(5),
|
||||
'activated_at' => now()->subMinutes(1),
|
||||
]);
|
||||
|
||||
$this->post('/admin/platform-orders/' . $order->id . '/activate-subscription')
|
||||
->assertRedirect();
|
||||
|
||||
$order->refresh();
|
||||
$this->assertNotNull($order->site_subscription_id);
|
||||
}
|
||||
|
||||
public function test_guest_cannot_activate_subscription_from_order(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'activate_btn_test_guest',
|
||||
'name' => '按钮激活测试(游客)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 66,
|
||||
'list_price' => 66,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$order = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_BTN_ACT_0002',
|
||||
'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' => 66,
|
||||
'paid_amount' => 66,
|
||||
'placed_at' => now()->subMinutes(10),
|
||||
]);
|
||||
|
||||
$this->post('/admin/platform-orders/' . $order->id . '/activate-subscription')
|
||||
->assertStatus(302)
|
||||
->assertRedirect('/admin/login');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
<?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'));
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
102
tests/Feature/AdminPlatformOrderClearSyncErrorsFilteredTest.php
Normal file
102
tests/Feature/AdminPlatformOrderClearSyncErrorsFilteredTest.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Merchant;
|
||||
use App\Models\Plan;
|
||||
use App\Models\PlatformOrder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminPlatformOrderClearSyncErrorsFilteredTest 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_clear_sync_errors_only_in_filtered_scope(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'clear_sync_err_filtered_test',
|
||||
'name' => '清理失败标记(范围)测试',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 1,
|
||||
'list_price' => 1,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$failedA = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_CLEAR_ERR_F_0001',
|
||||
'order_type' => 'new_purchase',
|
||||
'status' => 'pending',
|
||||
'payment_status' => 'unpaid',
|
||||
'plan_name' => $plan->name,
|
||||
'billing_cycle' => $plan->billing_cycle,
|
||||
'period_months' => 1,
|
||||
'quantity' => 1,
|
||||
'payable_amount' => 1,
|
||||
'paid_amount' => 0,
|
||||
'placed_at' => now(),
|
||||
'meta' => [
|
||||
'subscription_activation_error' => [
|
||||
'message' => '模拟失败A',
|
||||
'at' => now()->toDateTimeString(),
|
||||
'admin_id' => 1,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$failedB = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_CLEAR_ERR_F_0002',
|
||||
'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' => 1,
|
||||
'paid_amount' => 1,
|
||||
'placed_at' => now(),
|
||||
'meta' => [
|
||||
'subscription_activation_error' => [
|
||||
'message' => '模拟失败B',
|
||||
'at' => now()->toDateTimeString(),
|
||||
'admin_id' => 1,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// 仅清理“status=pending”范围内的失败标记:应该只清掉 failedA
|
||||
$this->post('/admin/platform-orders/clear-sync-errors', [
|
||||
'scope' => 'filtered',
|
||||
'status' => 'pending',
|
||||
])->assertRedirect();
|
||||
|
||||
$failedA->refresh();
|
||||
$failedB->refresh();
|
||||
|
||||
$this->assertEmpty(data_get($failedA->meta, 'subscription_activation_error'));
|
||||
$this->assertNotEmpty(data_get($failedA->meta, 'audit.0.action'));
|
||||
$this->assertSame('clear_sync_error', data_get($failedA->meta, 'audit.0.action'));
|
||||
|
||||
$this->assertNotEmpty(data_get($failedB->meta, 'subscription_activation_error.message'));
|
||||
}
|
||||
}
|
||||
78
tests/Feature/AdminPlatformOrderClearSyncErrorsTest.php
Normal file
78
tests/Feature/AdminPlatformOrderClearSyncErrorsTest.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Merchant;
|
||||
use App\Models\Plan;
|
||||
use App\Models\PlatformOrder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminPlatformOrderClearSyncErrorsTest 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_clear_sync_errors(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'clear_sync_err_test',
|
||||
'name' => '清理失败标记测试',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 1,
|
||||
'list_price' => 1,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$order = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_CLEAR_ERR_0001',
|
||||
'order_type' => 'new_purchase',
|
||||
'status' => 'pending',
|
||||
'payment_status' => 'unpaid',
|
||||
'plan_name' => $plan->name,
|
||||
'billing_cycle' => $plan->billing_cycle,
|
||||
'period_months' => 1,
|
||||
'quantity' => 1,
|
||||
'payable_amount' => 1,
|
||||
'paid_amount' => 0,
|
||||
'placed_at' => now(),
|
||||
'meta' => [
|
||||
'subscription_activation_error' => [
|
||||
'message' => '模拟失败',
|
||||
'at' => now()->toDateTimeString(),
|
||||
'admin_id' => 1,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->post('/admin/platform-orders/clear-sync-errors', ['scope' => 'all'])
|
||||
->assertRedirect();
|
||||
|
||||
$order->refresh();
|
||||
$this->assertEmpty(data_get($order->meta, 'subscription_activation_error'));
|
||||
}
|
||||
|
||||
public function test_guest_cannot_clear_sync_errors(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->post('/admin/platform-orders/clear-sync-errors', ['scope' => 'all'])
|
||||
->assertRedirect('/admin/login');
|
||||
}
|
||||
}
|
||||
148
tests/Feature/AdminPlatformOrderExportTest.php
Normal file
148
tests/Feature/AdminPlatformOrderExportTest.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Merchant;
|
||||
use App\Models\Plan;
|
||||
use App\Models\PlatformOrder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminPlatformOrderExportTest 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_export_platform_orders_csv(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'export_order_test',
|
||||
'name' => '导出测试套餐',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 10,
|
||||
'list_price' => 10,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_EXPORT_0001',
|
||||
'order_type' => 'new_purchase',
|
||||
'status' => 'pending',
|
||||
'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(),
|
||||
'meta' => [
|
||||
'subscription_activation_error' => [
|
||||
'message' => '模拟失败',
|
||||
'at' => now()->toDateTimeString(),
|
||||
'admin_id' => 1,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// batch_synced_24h 筛选导出:构造一条 24h 内、一条超 24h 的订单
|
||||
PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_EXPORT_BATCH_RECENT',
|
||||
'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(),
|
||||
'paid_at' => now(),
|
||||
'activated_at' => now(),
|
||||
'meta' => [
|
||||
'batch_activation' => [
|
||||
'at' => now()->toDateTimeString(),
|
||||
'admin_id' => 1,
|
||||
'scope' => 'filtered',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_EXPORT_BATCH_OLD',
|
||||
'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()->subHours(30),
|
||||
'paid_at' => now()->subHours(30),
|
||||
'activated_at' => now()->subHours(30),
|
||||
'meta' => [
|
||||
'batch_activation' => [
|
||||
'at' => now()->subHours(30)->toDateTimeString(),
|
||||
'admin_id' => 1,
|
||||
'scope' => 'filtered',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$res = $this->get('/admin/platform-orders/export');
|
||||
|
||||
$res->assertOk();
|
||||
$res->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
|
||||
// StreamedResponse 在测试环境下需用 streamedContent() 获取内容
|
||||
$content = $res->streamedContent();
|
||||
$this->assertStringContainsString('订单号', $content);
|
||||
$this->assertStringContainsString('PO_EXPORT_0001', $content);
|
||||
$this->assertStringContainsString('同步失败原因', $content);
|
||||
|
||||
// include_meta=1 时应包含 meta(JSON) 列
|
||||
$res2 = $this->get('/admin/platform-orders/export?include_meta=1');
|
||||
$res2->assertOk();
|
||||
$content2 = $res2->streamedContent();
|
||||
$this->assertStringContainsString('原始meta(JSON)', $content2);
|
||||
$this->assertStringContainsString('subscription_activation_error', $content2);
|
||||
|
||||
// batch_synced_24h=1 导出应只包含 24h 内批量同步过的订单
|
||||
$res3 = $this->get('/admin/platform-orders/export?batch_synced_24h=1');
|
||||
$res3->assertOk();
|
||||
$content3 = $res3->streamedContent();
|
||||
$this->assertStringContainsString('PO_EXPORT_BATCH_RECENT', $content3);
|
||||
$this->assertStringNotContainsString('PO_EXPORT_BATCH_OLD', $content3);
|
||||
}
|
||||
|
||||
public function test_guest_cannot_export_platform_orders_csv(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->get('/admin/platform-orders/export')
|
||||
->assertRedirect('/admin/login');
|
||||
}
|
||||
}
|
||||
103
tests/Feature/AdminPlatformOrderMarkPaidAndActivateTest.php
Normal file
103
tests/Feature/AdminPlatformOrderMarkPaidAndActivateTest.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Merchant;
|
||||
use App\Models\Plan;
|
||||
use App\Models\PlatformOrder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminPlatformOrderMarkPaidAndActivateTest 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_mark_order_paid_and_activated_and_sync_subscription(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'mark_paid_test',
|
||||
'name' => '标记支付测试(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 30,
|
||||
'list_price' => 30,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$order = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_MARK_PAID_0001',
|
||||
'order_type' => 'new_purchase',
|
||||
'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(),
|
||||
]);
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
public function test_guest_cannot_mark_order_paid_and_activated(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'mark_paid_test_guest',
|
||||
'name' => '标记支付测试(游客)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 30,
|
||||
'list_price' => 30,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$order = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_MARK_PAID_0002',
|
||||
'order_type' => 'new_purchase',
|
||||
'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(),
|
||||
]);
|
||||
|
||||
$this->post('/admin/platform-orders/' . $order->id . '/mark-paid-and-activate')
|
||||
->assertRedirect('/admin/login');
|
||||
}
|
||||
}
|
||||
123
tests/Feature/AdminPlatformOrderShowTest.php
Normal file
123
tests/Feature/AdminPlatformOrderShowTest.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Merchant;
|
||||
use App\Models\Plan;
|
||||
use App\Models\PlatformOrder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminPlatformOrderShowTest 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_open_platform_order_show_page(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'show_order_test',
|
||||
'name' => '订单详情测试',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 10,
|
||||
'list_price' => 10,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$order = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_SHOW_0001',
|
||||
'order_type' => 'new_purchase',
|
||||
'status' => 'pending',
|
||||
'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(),
|
||||
'meta' => [
|
||||
'subscription_activation_error' => [
|
||||
'message' => 'demo',
|
||||
'at' => now()->toDateTimeString(),
|
||||
],
|
||||
'audit' => [
|
||||
[
|
||||
'action' => 'clear_sync_error',
|
||||
'scope' => 'filtered',
|
||||
'at' => now()->toDateTimeString(),
|
||||
'admin_id' => 1,
|
||||
],
|
||||
[
|
||||
'action' => 'batch_activate_subscription',
|
||||
'scope' => 'filtered',
|
||||
'at' => now()->toDateTimeString(),
|
||||
'admin_id' => 1,
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->get('/admin/platform-orders/' . $order->id)
|
||||
->assertOk()
|
||||
->assertSee('平台订单详情')
|
||||
->assertSee('PO_SHOW_0001')
|
||||
->assertSee('标记支付并生效')
|
||||
->assertSee('同步订阅')
|
||||
->assertSee('订阅同步元数据')
|
||||
->assertSee('审计记录')
|
||||
->assertSee('清除同步失败标记');
|
||||
}
|
||||
|
||||
public function test_guest_cannot_open_platform_order_show_page(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'show_order_test_guest',
|
||||
'name' => '订单详情测试(guest)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 10,
|
||||
'list_price' => 10,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$order = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_SHOW_0002',
|
||||
'order_type' => 'new_purchase',
|
||||
'status' => 'pending',
|
||||
'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(),
|
||||
]);
|
||||
|
||||
$this->get('/admin/platform-orders/' . $order->id)
|
||||
->assertRedirect('/admin/login');
|
||||
}
|
||||
}
|
||||
319
tests/Feature/AdminPlatformOrderTest.php
Normal file
319
tests/Feature/AdminPlatformOrderTest.php
Normal file
@@ -0,0 +1,319 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Merchant;
|
||||
use App\Models\Plan;
|
||||
use App\Models\PlatformOrder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminPlatformOrderTest 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_open_platform_orders_page(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$this->get('/admin/platform-orders')
|
||||
->assertOk()
|
||||
->assertSee('平台订单')
|
||||
->assertSee('筛选条件')
|
||||
->assertSee('平台订单列表')
|
||||
->assertSee('最近批量同步')
|
||||
->assertSee('最近24小时批量同步过');
|
||||
}
|
||||
|
||||
public function test_guest_cannot_open_platform_orders_page(): void
|
||||
{
|
||||
$this->get('/admin/platform-orders')
|
||||
->assertRedirect('/admin/login');
|
||||
}
|
||||
|
||||
public function test_platform_orders_page_can_filter_by_status_and_payment_status(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'pro_monthly_test',
|
||||
'name' => '专业版(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 199,
|
||||
'list_price' => 199,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO202603100101',
|
||||
'order_type' => 'new_purchase',
|
||||
'status' => 'activated',
|
||||
'payment_status' => 'paid',
|
||||
'plan_name' => '专业版(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'period_months' => 1,
|
||||
'quantity' => 1,
|
||||
'list_amount' => 199,
|
||||
'discount_amount' => 0,
|
||||
'payable_amount' => 199,
|
||||
'paid_amount' => 199,
|
||||
'placed_at' => now()->subHour(),
|
||||
'paid_at' => now()->subMinutes(30),
|
||||
'activated_at' => now()->subMinutes(20),
|
||||
]);
|
||||
|
||||
PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO202603100102',
|
||||
'order_type' => 'renewal',
|
||||
'status' => 'pending',
|
||||
'payment_status' => 'unpaid',
|
||||
'plan_name' => '专业版(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'period_months' => 1,
|
||||
'quantity' => 1,
|
||||
'list_amount' => 199,
|
||||
'discount_amount' => 0,
|
||||
'payable_amount' => 199,
|
||||
'paid_amount' => 0,
|
||||
'placed_at' => now()->subMinutes(10),
|
||||
]);
|
||||
|
||||
$this->get('/admin/platform-orders?status=activated&payment_status=paid')
|
||||
->assertOk()
|
||||
->assertSee('PO202603100101')
|
||||
->assertDontSee('PO202603100102')
|
||||
->assertSee('专业版(月付)');
|
||||
|
||||
// 只看同步失败:构造一条带有 subscription_activation_error 的订单
|
||||
PlatformOrder::query()->where('order_no', 'PO202603100102')->update([
|
||||
'meta' => [
|
||||
'subscription_activation_error' => [
|
||||
'message' => '模拟失败',
|
||||
'at' => now()->toDateTimeString(),
|
||||
'admin_id' => 1,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// 额外构造一条同原因失败,用于失败原因聚合统计
|
||||
PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO202603100104',
|
||||
'order_type' => 'renewal',
|
||||
'status' => 'pending',
|
||||
'payment_status' => 'unpaid',
|
||||
'plan_name' => '专业版(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'period_months' => 1,
|
||||
'quantity' => 1,
|
||||
'payable_amount' => 199,
|
||||
'paid_amount' => 0,
|
||||
'placed_at' => now()->subMinutes(9),
|
||||
'meta' => [
|
||||
'subscription_activation_error' => [
|
||||
'message' => '模拟失败',
|
||||
'at' => now()->toDateTimeString(),
|
||||
'admin_id' => 1,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// 再构造一条不同原因失败
|
||||
PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO202603100105',
|
||||
'order_type' => 'renewal',
|
||||
'status' => 'pending',
|
||||
'payment_status' => 'unpaid',
|
||||
'plan_name' => '专业版(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'period_months' => 1,
|
||||
'quantity' => 1,
|
||||
'payable_amount' => 199,
|
||||
'paid_amount' => 0,
|
||||
'placed_at' => now()->subMinutes(8),
|
||||
'meta' => [
|
||||
'subscription_activation_error' => [
|
||||
'message' => '余额不足',
|
||||
'at' => now()->toDateTimeString(),
|
||||
'admin_id' => 1,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->get('/admin/platform-orders?fail_only=1')
|
||||
->assertOk()
|
||||
->assertSee('PO202603100102')
|
||||
->assertDontSee('PO202603100101');
|
||||
|
||||
// 失败原因聚合(Top5)应可见
|
||||
$this->get('/admin/platform-orders')
|
||||
->assertOk()
|
||||
->assertSee('同步失败原因 TOP5')
|
||||
->assertSee('模拟失败')
|
||||
->assertSee('余额不足');
|
||||
|
||||
// 只看已同步:构造一条带有 subscription_activation 的订单 + 批量同步审计(用于列表展示)
|
||||
PlatformOrder::query()->where('order_no', 'PO202603100101')->update([
|
||||
'meta' => [
|
||||
'subscription_activation' => [
|
||||
'subscription_id' => 123,
|
||||
'synced_at' => now()->toDateTimeString(),
|
||||
'admin_id' => 1,
|
||||
],
|
||||
'audit' => [
|
||||
[
|
||||
'action' => 'batch_activate_subscription',
|
||||
'scope' => 'filtered',
|
||||
'at' => '2026-03-10 00:00:00',
|
||||
'admin_id' => 1,
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->get('/admin/platform-orders')
|
||||
->assertOk()
|
||||
->assertSee('2026-03-10 00:00:00')
|
||||
->assertSee('管理员:1');
|
||||
|
||||
// 最近24小时批量同步筛选:构造一条近24h,一条超过24h
|
||||
PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO202603100107',
|
||||
'order_type' => 'renewal',
|
||||
'status' => 'activated',
|
||||
'payment_status' => 'paid',
|
||||
'plan_name' => '专业版(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'period_months' => 1,
|
||||
'quantity' => 1,
|
||||
'payable_amount' => 199,
|
||||
'paid_amount' => 199,
|
||||
'placed_at' => now()->subMinutes(3),
|
||||
'paid_at' => now()->subMinutes(2),
|
||||
'activated_at' => now()->subMinutes(1),
|
||||
'meta' => [
|
||||
'batch_activation' => [
|
||||
'at' => now()->subHours(1)->format('Y-m-d H:i:s'),
|
||||
'admin_id' => 1,
|
||||
'scope' => 'filtered',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO202603100108',
|
||||
'order_type' => 'renewal',
|
||||
'status' => 'activated',
|
||||
'payment_status' => 'paid',
|
||||
'plan_name' => '专业版(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'period_months' => 1,
|
||||
'quantity' => 1,
|
||||
'payable_amount' => 199,
|
||||
'paid_amount' => 199,
|
||||
'placed_at' => now()->subMinutes(13),
|
||||
'paid_at' => now()->subMinutes(12),
|
||||
'activated_at' => now()->subMinutes(11),
|
||||
'meta' => [
|
||||
'batch_activation' => [
|
||||
'at' => now()->subHours(30)->format('Y-m-d H:i:s'),
|
||||
'admin_id' => 1,
|
||||
'scope' => 'filtered',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->get('/admin/platform-orders?batch_synced_24h=1')
|
||||
->assertOk()
|
||||
->assertSee('PO202603100107')
|
||||
->assertDontSee('PO202603100108');
|
||||
|
||||
$this->get('/admin/platform-orders?synced_only=1')
|
||||
->assertOk()
|
||||
->assertSee('PO202603100101')
|
||||
->assertDontSee('PO202603100102');
|
||||
|
||||
// sync_status 下拉筛选:synced / failed / unsynced
|
||||
$this->get('/admin/platform-orders?sync_status=synced')
|
||||
->assertOk()
|
||||
->assertSee('PO202603100101')
|
||||
->assertDontSee('PO202603100102');
|
||||
|
||||
$this->get('/admin/platform-orders?sync_status=failed')
|
||||
->assertOk()
|
||||
->assertSee('PO202603100102')
|
||||
->assertDontSee('PO202603100101');
|
||||
|
||||
// unsynced:构造一条既无 subscription_activation 也无 error 的订单
|
||||
PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO202603100103',
|
||||
'order_type' => 'renewal',
|
||||
'status' => 'pending',
|
||||
'payment_status' => 'unpaid',
|
||||
'plan_name' => '专业版(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'period_months' => 1,
|
||||
'quantity' => 1,
|
||||
'payable_amount' => 199,
|
||||
'paid_amount' => 0,
|
||||
'placed_at' => now()->subMinutes(5),
|
||||
]);
|
||||
|
||||
$this->get('/admin/platform-orders?sync_status=unsynced')
|
||||
->assertOk()
|
||||
->assertSee('PO202603100103')
|
||||
->assertDontSee('PO202603100101')
|
||||
->assertDontSee('PO202603100102');
|
||||
|
||||
// 只看可同步:已支付 + 已生效 + 未同步
|
||||
PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO202603100106',
|
||||
'order_type' => 'renewal',
|
||||
'status' => 'activated',
|
||||
'payment_status' => 'paid',
|
||||
'plan_name' => '专业版(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'period_months' => 1,
|
||||
'quantity' => 1,
|
||||
'payable_amount' => 199,
|
||||
'paid_amount' => 199,
|
||||
'placed_at' => now()->subMinutes(4),
|
||||
'paid_at' => now()->subMinutes(3),
|
||||
'activated_at' => now()->subMinutes(2),
|
||||
]);
|
||||
|
||||
$this->get('/admin/platform-orders?syncable_only=1')
|
||||
->assertOk()
|
||||
->assertSee('PO202603100106')
|
||||
->assertDontSee('PO202603100101')
|
||||
->assertDontSee('PO202603100102');
|
||||
}
|
||||
}
|
||||
51
tests/Feature/AdminProtectedPagesTest.php
Normal file
51
tests/Feature/AdminProtectedPagesTest.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminProtectedPagesTest 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_guest_is_redirected_to_admin_login_for_protected_pages(): void
|
||||
{
|
||||
foreach (['/admin', '/admin/merchants', '/admin/orders', '/admin/products', '/admin/product-categories'] as $path) {
|
||||
$this->get($path)->assertRedirect('/admin/login');
|
||||
}
|
||||
}
|
||||
|
||||
public function test_platform_admin_can_access_key_admin_pages(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$this->get('/admin')->assertOk()->assertSee('总台仪表盘');
|
||||
$this->get('/admin/merchants')->assertOk()->assertSee('站点管理');
|
||||
$this->get('/admin/orders')->assertOk()->assertSee('订单监控');
|
||||
$this->get('/admin/products')->assertOk()->assertSee('商品巡检');
|
||||
$this->get('/admin/product-categories')->assertOk()->assertSee('商品分类');
|
||||
}
|
||||
|
||||
public function test_merchant_admin_is_forbidden_from_admin_pages(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->post('/merchant-admin/login', [
|
||||
'email' => 'merchant.admin@demo.local',
|
||||
'password' => 'Merchant@123456',
|
||||
])->assertRedirect('/merchant-admin');
|
||||
|
||||
$this->get('/admin')->assertForbidden()->assertSee('当前账号没有总台管理访问权限');
|
||||
}
|
||||
}
|
||||
79
tests/Feature/AdminSiteSubscriptionExportTest.php
Normal file
79
tests/Feature/AdminSiteSubscriptionExportTest.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Merchant;
|
||||
use App\Models\Plan;
|
||||
use App\Models\SiteSubscription;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminSiteSubscriptionExportTest 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_export_site_subscriptions_csv(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'export_sub_test',
|
||||
'name' => '导出订阅测试套餐',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 88,
|
||||
'list_price' => 88,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
SiteSubscription::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'status' => 'activated',
|
||||
'source' => 'manual',
|
||||
'subscription_no' => 'SUB_EXPORT_0001',
|
||||
'plan_name' => $plan->name,
|
||||
'billing_cycle' => 'monthly',
|
||||
'period_months' => 1,
|
||||
'amount' => 88,
|
||||
'starts_at' => now()->subDays(1),
|
||||
'ends_at' => now()->addDays(29),
|
||||
'activated_at' => now()->subDays(1),
|
||||
]);
|
||||
|
||||
$res = $this->get('/admin/site-subscriptions/export');
|
||||
|
||||
$res->assertOk();
|
||||
$res->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
|
||||
$content = $res->streamedContent();
|
||||
$this->assertStringContainsString('订阅号', $content);
|
||||
$this->assertStringContainsString('SUB_EXPORT_0001', $content);
|
||||
$this->assertStringContainsString('到期时间', $content);
|
||||
$this->assertStringContainsString('到期状态', $content);
|
||||
$this->assertStringContainsString('未到期', $content);
|
||||
|
||||
// 状态导出应为“中文标签 + 原始值”
|
||||
$this->assertStringContainsString('已生效 (activated)', $content);
|
||||
}
|
||||
|
||||
public function test_guest_cannot_export_site_subscriptions_csv(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->get('/admin/site-subscriptions/export')
|
||||
->assertRedirect('/admin/login');
|
||||
}
|
||||
}
|
||||
143
tests/Feature/AdminSiteSubscriptionTest.php
Normal file
143
tests/Feature/AdminSiteSubscriptionTest.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Merchant;
|
||||
use App\Models\Plan;
|
||||
use App\Models\SiteSubscription;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminSiteSubscriptionTest 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_open_site_subscriptions_page(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$this->get('/admin/site-subscriptions')
|
||||
->assertOk()
|
||||
->assertSee('订阅管理')
|
||||
->assertSee('筛选条件')
|
||||
->assertSee('订阅列表')
|
||||
->assertSee('到期状态');
|
||||
}
|
||||
|
||||
public function test_guest_cannot_open_site_subscriptions_page(): void
|
||||
{
|
||||
$this->get('/admin/site-subscriptions')
|
||||
->assertRedirect('/admin/login');
|
||||
}
|
||||
|
||||
public function test_site_subscriptions_page_can_filter_by_status_and_keyword(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'sub_test_monthly',
|
||||
'name' => '订阅测试套餐(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 88,
|
||||
'list_price' => 88,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
SiteSubscription::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'status' => 'activated',
|
||||
'source' => 'manual',
|
||||
'subscription_no' => 'SUB202603100101',
|
||||
'plan_name' => '订阅测试套餐(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'period_months' => 1,
|
||||
'amount' => 88,
|
||||
'starts_at' => now()->subDays(2),
|
||||
'ends_at' => now()->addDays(28),
|
||||
'activated_at' => now()->subDays(2),
|
||||
]);
|
||||
|
||||
SiteSubscription::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'status' => 'pending',
|
||||
'source' => 'manual',
|
||||
'subscription_no' => 'SUB202603100102',
|
||||
'plan_name' => '订阅测试套餐(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'period_months' => 1,
|
||||
'amount' => 88,
|
||||
'starts_at' => now(),
|
||||
'ends_at' => now()->addMonth(),
|
||||
]);
|
||||
|
||||
$this->get('/admin/site-subscriptions?status=activated&keyword=SUB202603100101')
|
||||
->assertOk()
|
||||
->assertSee('SUB202603100101')
|
||||
->assertDontSee('SUB202603100102');
|
||||
|
||||
// 到期筛选:构造“已过期”和“7天内到期”的订阅
|
||||
SiteSubscription::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'status' => 'activated',
|
||||
'source' => 'manual',
|
||||
'subscription_no' => 'SUB202603100103',
|
||||
'plan_name' => '订阅测试套餐(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'period_months' => 1,
|
||||
'amount' => 88,
|
||||
'starts_at' => now()->subDays(40),
|
||||
'ends_at' => now()->subDays(1),
|
||||
'activated_at' => now()->subDays(40),
|
||||
]);
|
||||
|
||||
SiteSubscription::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'status' => 'activated',
|
||||
'source' => 'manual',
|
||||
'subscription_no' => 'SUB202603100104',
|
||||
'plan_name' => '订阅测试套餐(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'period_months' => 1,
|
||||
'amount' => 88,
|
||||
'starts_at' => now()->subDays(25),
|
||||
'ends_at' => now()->addDays(3),
|
||||
'activated_at' => now()->subDays(25),
|
||||
]);
|
||||
|
||||
$this->get('/admin/site-subscriptions?expiry=expired')
|
||||
->assertOk()
|
||||
->assertSeeInOrder(['SUB202603100103', '已过期'])
|
||||
->assertDontSee('SUB202603100104');
|
||||
|
||||
$this->get('/admin/site-subscriptions?expiry=expiring_7d')
|
||||
->assertOk()
|
||||
->assertSeeInOrder(['SUB202603100104', '7天内到期'])
|
||||
->assertDontSee('SUB202603100103');
|
||||
|
||||
// 新增筛选:按站点/套餐
|
||||
$this->get('/admin/site-subscriptions?merchant_id=' . $merchant->id)
|
||||
->assertOk()
|
||||
->assertSee('订阅列表');
|
||||
|
||||
$this->get('/admin/site-subscriptions?plan_id=' . $plan->id)
|
||||
->assertOk()
|
||||
->assertSee('订阅列表');
|
||||
}
|
||||
}
|
||||
21
tests/Feature/ExampleTest.php
Normal file
21
tests/Feature/ExampleTest.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
177
tests/Feature/ExportEndpointsTest.php
Normal file
177
tests/Feature/ExportEndpointsTest.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
use function PHPUnit\Framework\assertStringContainsString;
|
||||
use function PHPUnit\Framework\assertStringNotContainsString;
|
||||
|
||||
class ExportEndpointsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function loginAsPlatformAdmin(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->post('/admin/login', [
|
||||
'email' => 'platform.admin@demo.local',
|
||||
'password' => 'Platform@123456',
|
||||
])->assertRedirect('/admin');
|
||||
}
|
||||
|
||||
protected function loginAsMerchantAdmin(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->post('/merchant-admin/login', [
|
||||
'email' => 'merchant.admin@demo.local',
|
||||
'password' => 'Merchant@123456',
|
||||
])->assertRedirect('/merchant-admin');
|
||||
}
|
||||
|
||||
protected function loginAsSiteAdmin(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->post('/site-admin/login', [
|
||||
'email' => 'merchant.admin@demo.local',
|
||||
'password' => 'Merchant@123456',
|
||||
])->assertRedirect('/site-admin');
|
||||
}
|
||||
|
||||
public function test_admin_order_and_product_exports_return_csv_response(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$this->get('/admin/orders/export')
|
||||
->assertOk()
|
||||
->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
|
||||
$this->get('/admin/products/export')
|
||||
->assertOk()
|
||||
->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
|
||||
$this->get('/admin/products/import-histories/export')
|
||||
->assertOk()
|
||||
->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
}
|
||||
|
||||
public function test_admin_product_export_uses_chinese_detail_headers(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$response = $this->get('/admin/products/export');
|
||||
$response->assertOk();
|
||||
$content = $response->streamedContent();
|
||||
|
||||
assertStringContainsString('ID,站点ID,站点名称,分类ID,分类名称,分类标识,商品标题,商品Slug,SKU,售价,划线价,库存,状态,商品简介,创建时间,更新时间', $content);
|
||||
}
|
||||
|
||||
public function test_merchant_order_and_product_exports_return_csv_response(): void
|
||||
{
|
||||
$this->loginAsMerchantAdmin();
|
||||
|
||||
$this->get('/merchant-admin/orders/export')
|
||||
->assertOk()
|
||||
->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
|
||||
$this->get('/merchant-admin/products/export')
|
||||
->assertOk()
|
||||
->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
|
||||
$this->get('/merchant-admin/products/import-histories/export')
|
||||
->assertOk()
|
||||
->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
}
|
||||
|
||||
public function test_merchant_product_export_uses_chinese_detail_headers(): void
|
||||
{
|
||||
$this->loginAsMerchantAdmin();
|
||||
|
||||
$response = $this->get('/merchant-admin/products/export');
|
||||
$response->assertOk();
|
||||
$content = $response->streamedContent();
|
||||
|
||||
assertStringContainsString('ID,分类ID,分类名称,分类标识,商品标题,商品Slug,SKU,售价,划线价,库存,状态,商品简介,创建时间,更新时间', $content);
|
||||
}
|
||||
|
||||
public function test_admin_order_export_uses_chinese_detail_headers(): void
|
||||
{
|
||||
$this->loginAsPlatformAdmin();
|
||||
|
||||
$response = $this->get('/admin/orders/export');
|
||||
$response->assertOk();
|
||||
$content = $response->streamedContent();
|
||||
|
||||
assertStringContainsString('ID,商家ID,商家名称,用户ID,订单号,订单状态,支付状态,平台,设备类型,支付渠道,买家姓名,买家手机,买家邮箱,商品金额,优惠金额,运费,实付金额,商品行数,商品件数,商品摘要,创建时间,支付时间,发货时间,完成时间,备注', $content);
|
||||
assertStringContainsString('设备类型,全部', $content);
|
||||
assertStringContainsString('支付渠道,全部', $content);
|
||||
assertStringContainsString('桌面浏览器,微信支付', $content);
|
||||
assertStringContainsString('移动浏览器,支付宝', $content);
|
||||
assertStringContainsString('小程序环境,微信支付', $content);
|
||||
assertStringContainsString('微信内网页,微信支付', $content);
|
||||
assertStringContainsString('"APP 接口预留","APP 接口",微信支付', $content);
|
||||
assertStringNotContainsString('ID,merchant_id,merchant_name', $content);
|
||||
assertStringNotContainsString(',desktop,wechat_pay,', $content);
|
||||
assertStringNotContainsString(',mobile,alipay,', $content);
|
||||
assertStringNotContainsString(',mini-program,wechat_pay,', $content);
|
||||
}
|
||||
|
||||
public function test_merchant_order_export_uses_chinese_detail_headers(): void
|
||||
{
|
||||
$this->loginAsMerchantAdmin();
|
||||
|
||||
$response = $this->get('/merchant-admin/orders/export');
|
||||
$response->assertOk();
|
||||
$content = $response->streamedContent();
|
||||
|
||||
assertStringContainsString('ID,用户ID,订单号,订单状态,支付状态,平台,设备类型,支付渠道,买家姓名,买家手机,买家邮箱,商品金额,优惠金额,运费,实付金额,商品行数,商品件数,商品摘要,创建时间,支付时间,发货时间,完成时间,备注', $content);
|
||||
assertStringContainsString('设备类型,全部', $content);
|
||||
assertStringContainsString('支付渠道,全部', $content);
|
||||
assertStringContainsString('桌面浏览器,微信支付', $content);
|
||||
assertStringContainsString('移动浏览器,支付宝', $content);
|
||||
assertStringContainsString('小程序环境,微信支付', $content);
|
||||
assertStringContainsString('微信内网页,微信支付', $content);
|
||||
assertStringContainsString('"APP 接口预留","APP 接口",微信支付', $content);
|
||||
assertStringNotContainsString('ID,user_id,order_no,status,payment_status,platform', $content);
|
||||
assertStringNotContainsString(',desktop,wechat_pay,', $content);
|
||||
assertStringNotContainsString(',mobile,alipay,', $content);
|
||||
assertStringNotContainsString(',mini-program,wechat_pay,', $content);
|
||||
}
|
||||
|
||||
public function test_site_product_export_uses_chinese_detail_headers(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$response = $this->get('/site-admin/products/export');
|
||||
$response->assertOk();
|
||||
$content = $response->streamedContent();
|
||||
|
||||
assertStringContainsString('ID,分类ID,分类名称,商品标题,商品Slug,SKU,售价,划线价,库存,状态,商品简介,创建时间,更新时间', $content);
|
||||
assertStringNotContainsString('ID,category_id,category_name,title,slug,sku,price,original_price,stock,status,summary,created_at,updated_at', $content);
|
||||
}
|
||||
|
||||
public function test_site_order_export_uses_chinese_detail_headers(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$response = $this->get('/site-admin/orders/export');
|
||||
$response->assertOk();
|
||||
$content = $response->streamedContent();
|
||||
|
||||
assertStringContainsString('ID,订单号,订单状态,支付状态,平台,设备类型,支付渠道,买家姓名,买家手机,买家邮箱,商品金额,优惠金额,运费,实付金额,创建时间,支付时间,完成时间,备注', $content);
|
||||
assertStringContainsString('设备类型,全部', $content);
|
||||
assertStringContainsString('支付渠道,全部', $content);
|
||||
assertStringContainsString('桌面浏览器,微信支付', $content);
|
||||
assertStringContainsString('移动浏览器,支付宝', $content);
|
||||
assertStringContainsString('小程序环境,微信支付', $content);
|
||||
assertStringContainsString('微信内网页,微信支付', $content);
|
||||
assertStringContainsString('"APP 接口预留","APP 接口",微信支付', $content);
|
||||
assertStringNotContainsString('ID,order_no,status,payment_status,platform,device_type,payment_channel', $content);
|
||||
assertStringNotContainsString(',desktop,wechat_pay,', $content);
|
||||
assertStringNotContainsString(',mobile,alipay,', $content);
|
||||
assertStringNotContainsString(',mini-program,wechat_pay,', $content);
|
||||
}
|
||||
}
|
||||
52
tests/Feature/MerchantAdminAccessTest.php
Normal file
52
tests/Feature/MerchantAdminAccessTest.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MerchantAdminAccessTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_merchant_admin_login_page_is_accessible(): void
|
||||
{
|
||||
$response = $this->get('/merchant-admin/login');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('商家后台');
|
||||
}
|
||||
|
||||
public function test_merchant_admin_can_login_and_open_dashboard(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->post('/merchant-admin/login', [
|
||||
'email' => 'merchant.admin@demo.local',
|
||||
'password' => 'Merchant@123456',
|
||||
]);
|
||||
|
||||
$response->assertRedirect('/merchant-admin');
|
||||
|
||||
$dashboard = $this->get('/merchant-admin');
|
||||
$dashboard->assertOk();
|
||||
$dashboard->assertSee('商家仪表盘');
|
||||
$dashboard->assertSee('商品管理');
|
||||
$dashboard->assertSee('订单管理');
|
||||
$dashboard->assertSee('总台管理');
|
||||
}
|
||||
|
||||
public function test_platform_admin_cannot_login_from_merchant_admin_entry(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->from('/merchant-admin/login')->post('/merchant-admin/login', [
|
||||
'email' => 'platform.admin@demo.local',
|
||||
'password' => 'Platform@123456',
|
||||
]);
|
||||
|
||||
$response->assertRedirect('/merchant-admin/login');
|
||||
|
||||
$this->get('/merchant-admin/login')->assertSee('当前账号不是商家管理员,不能登录商家后台');
|
||||
}
|
||||
}
|
||||
377
tests/Feature/MerchantBusinessPagesTest.php
Normal file
377
tests/Feature/MerchantBusinessPagesTest.php
Normal file
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Merchant;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductCategory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
use function PHPUnit\Framework\assertStringContainsString;
|
||||
|
||||
class MerchantBusinessPagesTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function loginAsMerchantAdmin(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->post('/merchant-admin/login', [
|
||||
'email' => 'merchant.admin@demo.local',
|
||||
'password' => 'Merchant@123456',
|
||||
])->assertRedirect('/merchant-admin');
|
||||
}
|
||||
|
||||
public function test_merchant_products_page_displays_import_and_history_entries(): void
|
||||
{
|
||||
$this->loginAsMerchantAdmin();
|
||||
|
||||
$this->get('/merchant-admin/products?status=published&min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('商家商品管理')
|
||||
->assertSee('批量导入商品')
|
||||
->assertSee('下载导入模板')
|
||||
->assertSee('导出当前筛选结果 CSV')
|
||||
->assertSee('进入完整导入历史页')
|
||||
->assertSee('导入历史摘要')
|
||||
->assertSee('当前筛选摘要')
|
||||
->assertSee('运营关注项')
|
||||
->assertSee('当前信号')
|
||||
->assertSee('工作台导航')
|
||||
->assertSee('已上架')
|
||||
->assertSee('价格从高到低');
|
||||
}
|
||||
|
||||
public function test_merchant_product_operations_focus_can_follow_current_filters(): void
|
||||
{
|
||||
$this->loginAsMerchantAdmin();
|
||||
|
||||
$this->get('/merchant-admin/products?status=draft&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前正在查看草稿商品,建议优先补齐标题、分类、价格与库存后再安排上架。')
|
||||
->assertSee('继续查看当前草稿');
|
||||
|
||||
$this->get('/merchant-admin/products?status=published&min_stock=0&max_stock=20&sort=stock_asc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架库存视角,建议优先确认低库存补货节奏,并同步观察高库存结构是否健康。')
|
||||
->assertSee('继续查看当前库存视角');
|
||||
|
||||
$this->get('/merchant-admin/products?status=published&category_id=1&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架的“默认分类”分类下关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对在售商品命名、分类承接、价格梯度与搜索结果是否一致,并同步观察库存结构与转化表现是否健康。')
|
||||
->assertSee('继续查看当前已上架分类关键词价格带商品')
|
||||
->assertSee('去看当前已上架分类关键词商品');
|
||||
|
||||
$this->get('/merchant-admin/products?status=published&category_id=1&keyword=%E6%BC%94%E7%A4%BA&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架的“默认分类”分类下关键词“演示”命中的商品,建议优先核对在售商品命名、分类承接与搜索结果是否一致,并同步观察价格带与库存结构是否健康。')
|
||||
->assertSee('继续查看当前已上架分类关键词商品');
|
||||
|
||||
$this->get('/merchant-admin/products?status=published&category_id=1&min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架的“默认分类”分类下价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对该分类在售商品的价格结构、库存分布与转化表现是否协调。')
|
||||
->assertSee('继续查看当前已上架分类价格带商品');
|
||||
|
||||
$this->get('/merchant-admin/products?status=published&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架商品中关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 结果,建议优先核对在售商品命名、卖点表达与价格梯度是否一致,并同步观察库存结构、转化表现与搜索承接是否健康。')
|
||||
->assertSee('继续查看当前已上架关键词价格带商品')
|
||||
->assertSee('去看当前已上架关键词商品');
|
||||
|
||||
$this->get('/merchant-admin/products?status=published&keyword=%E6%BC%94%E7%A4%BA&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架商品中关键词“演示”命中的结果,建议优先核对在售商品命名、卖点表达与搜索承接是否一致,并同步观察价格带与库存结构是否健康。')
|
||||
->assertSee('继续查看当前已上架关键词商品');
|
||||
|
||||
$this->get('/merchant-admin/products?category_id=1&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦“默认分类”分类下关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对分类承接、命名卖点与价格梯度是否一致,并同步观察库存结构、转化表现与搜索承接是否健康。')
|
||||
->assertSee('继续查看当前分类关键词价格带商品')
|
||||
->assertSee('去看当前分类关键词商品');
|
||||
|
||||
$this->get('/merchant-admin/products?category_id=1&keyword=%E6%BC%94%E7%A4%BA&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦“默认分类”分类下关键词“演示”命中的商品,建议优先核对分类承接、命名卖点与搜索结果是否一致,并同步观察相关商品的价格带与库存结构。')
|
||||
->assertSee('继续查看当前分类关键词商品');
|
||||
|
||||
$this->get('/merchant-admin/products?status=published&category_id=1&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架的“默认分类”分类商品,建议优先核对该分类在售商品的价格带、库存结构与转化表现是否均衡。')
|
||||
->assertSee('继续查看当前已上架分类商品');
|
||||
|
||||
$this->get('/merchant-admin/products?category_id=1&min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦“默认分类”分类下价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对该分类价格结构是否连贯,并同步观察库存分布与转化表现是否健康。')
|
||||
->assertSee('继续查看当前分类价格带商品');
|
||||
|
||||
$this->get('/merchant-admin/products?category_id=1&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦“默认分类”分类商品,建议优先核对分类承接是否准确,并同步观察价格带与库存结构是否均衡。')
|
||||
->assertSee('继续查看当前分类商品');
|
||||
|
||||
$this->get('/merchant-admin/products?keyword=%E6%BC%94%E7%A4%BA&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦关键词“演示”命中的商品,建议优先核对命名、卖点与搜索承接是否一致,并同步观察相关商品的价格带与库存结构。')
|
||||
->assertSee('继续查看当前关键词商品');
|
||||
|
||||
$this->get('/merchant-admin/products?min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对定价梯度是否连贯,并同步观察库存结构与转化表现是否匹配。')
|
||||
->assertSee('继续查看当前价格带商品');
|
||||
}
|
||||
|
||||
public function test_merchant_orders_page_displays_filters_and_export_entry(): void
|
||||
{
|
||||
$this->loginAsMerchantAdmin();
|
||||
|
||||
$this->get('/merchant-admin/orders?status=shipped&payment_status=paid&platform=wechat_mini&device_type=mini-program&payment_channel=wechat_pay&min_pay_amount=180&max_pay_amount=190&sort=pay_amount_desc')
|
||||
->assertOk()
|
||||
->assertSee('商家订单管理')
|
||||
->assertSee('筛选条件')
|
||||
->assertSee('导出当前筛选结果 CSV')
|
||||
->assertSee('当前筛选摘要')
|
||||
->assertSee('运营关注项')
|
||||
->assertSee('当前信号')
|
||||
->assertSee('工作台导航')
|
||||
->assertSee('已发货')
|
||||
->assertSee('已支付')
|
||||
->assertSee('微信小程序')
|
||||
->assertSee('小程序环境')
|
||||
->assertSee('微信支付')
|
||||
->assertSee('订单汇总');
|
||||
}
|
||||
|
||||
public function test_merchant_order_operations_focus_can_follow_current_filters(): void
|
||||
{
|
||||
$this->loginAsMerchantAdmin();
|
||||
|
||||
$this->get('/merchant-admin/orders?platform=wechat_mini&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦微信小程序订单,建议优先关注下单到支付转化是否顺畅,并同步排查小程序端支付回流体验。')
|
||||
->assertSee('继续查看微信小程序订单');
|
||||
|
||||
$this->get('/merchant-admin/orders?payment_channel=wechat_pay&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦微信支付订单,建议优先核对支付成功率、回调稳定性与失败重试转化。')
|
||||
->assertSee('继续查看微信支付订单');
|
||||
|
||||
$this->get('/merchant-admin/orders?device_type=mini-program&payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦小程序环境订单,建议优先核对授权链路、支付唤起表现与下单回流是否顺畅。')
|
||||
->assertSee('继续查看小程序环境订单');
|
||||
|
||||
$this->get('/merchant-admin/orders?device_type=mobile-webview&payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦微信内网页订单,建议优先关注授权静默登录、页面跳转稳定性与支付拉起后的回流体验。')
|
||||
->assertSee('继续查看微信内网页订单');
|
||||
|
||||
$this->get('/merchant-admin/orders?device_type=mobile&payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦移动浏览器订单,建议优先关注 H5 下单链路、页面加载稳定性与支付转化流失点。')
|
||||
->assertSee('继续查看移动浏览器订单');
|
||||
|
||||
$this->get('/merchant-admin/orders?device_type=desktop&payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦桌面浏览器订单,建议优先关注 PC 端下单流程、页面首屏稳定性与高客单转化表现。')
|
||||
->assertSee('继续查看桌面浏览器订单');
|
||||
|
||||
$this->get('/merchant-admin/orders?payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦支付失败订单,建议优先排查支付渠道、回调结果与用户重试情况。')
|
||||
->assertSee('继续查看支付失败订单');
|
||||
|
||||
$this->get('/merchant-admin/orders?status=completed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前正在查看已完成订单,建议复盘高客单成交与复购来源,沉淀更稳定的转化路径。')
|
||||
->assertSee('继续查看已完成订单');
|
||||
}
|
||||
|
||||
public function test_merchant_categories_and_users_pages_display_expected_content(): void
|
||||
{
|
||||
$this->loginAsMerchantAdmin();
|
||||
|
||||
$this->get('/merchant-admin/product-categories')
|
||||
->assertOk()
|
||||
->assertSee('商家商品分类')
|
||||
->assertSee('新增分类')
|
||||
->assertSee('分类列表');
|
||||
|
||||
$this->get('/merchant-admin/users')
|
||||
->assertOk()
|
||||
->assertSee('商家用户管理')
|
||||
->assertSee('用户列表');
|
||||
}
|
||||
|
||||
public function test_merchant_import_histories_page_displays_filter_and_export_entries(): void
|
||||
{
|
||||
$this->loginAsMerchantAdmin();
|
||||
|
||||
$this->get('/merchant-admin/products/import-histories')
|
||||
->assertOk()
|
||||
->assertSee('商家商品导入历史')
|
||||
->assertSee('筛选导入历史')
|
||||
->assertSee('导出当前筛选 CSV')
|
||||
->assertSee('返回商品页');
|
||||
}
|
||||
|
||||
public function test_merchant_product_summary_stats_match_export_summary_for_same_filters(): void
|
||||
{
|
||||
$this->loginAsMerchantAdmin();
|
||||
|
||||
$page = $this->get('/merchant-admin/products?keyword=%E6%BC%94%E7%A4%BA%E5%95%86%E5%93%81&status=published&min_price=150&max_price=220');
|
||||
$page->assertOk()->assertViewHas('summaryStats', function (array $summaryStats) {
|
||||
return ($summaryStats['total_products'] ?? null) === 1
|
||||
&& ($summaryStats['total_stock'] ?? null) === 100
|
||||
&& (float) ($summaryStats['total_stock_value'] ?? 0) === 19900.0
|
||||
&& (float) ($summaryStats['average_price'] ?? 0) === 199.0;
|
||||
});
|
||||
|
||||
$export = $this->get('/merchant-admin/products/export?keyword=%E6%BC%94%E7%A4%BA%E5%95%86%E5%93%81&status=published&min_price=150&max_price=220');
|
||||
$export->assertOk();
|
||||
$content = $export->streamedContent();
|
||||
|
||||
assertStringContainsString('导出信息,商家商品导出', $content);
|
||||
assertStringContainsString('状态,已上架', $content);
|
||||
assertStringContainsString('最低价格,¥150.00', $content);
|
||||
assertStringContainsString('最高价格,¥220.00', $content);
|
||||
assertStringContainsString('导出商品数,1', $content);
|
||||
assertStringContainsString('导出总库存,100', $content);
|
||||
assertStringContainsString('导出总货值,19900.00', $content);
|
||||
assertStringContainsString('导出平均售价,199.00', $content);
|
||||
assertStringContainsString('演示商品', $content);
|
||||
}
|
||||
|
||||
public function test_merchant_order_summary_stats_match_export_summary_for_same_filters(): void
|
||||
{
|
||||
$this->loginAsMerchantAdmin();
|
||||
|
||||
$page = $this->get('/merchant-admin/orders?status=shipped&payment_status=paid&platform=wechat_mini&device_type=mini-program&payment_channel=wechat_pay&min_pay_amount=180&max_pay_amount=190');
|
||||
$page->assertOk()->assertViewHas('summaryStats', function (array $summaryStats) {
|
||||
return ($summaryStats['total_orders'] ?? null) === 1
|
||||
&& (float) ($summaryStats['total_pay_amount'] ?? 0) === 189.0
|
||||
&& (float) ($summaryStats['average_order_amount'] ?? 0) === 189.0
|
||||
&& ($summaryStats['paid_orders'] ?? null) === 1
|
||||
&& ($summaryStats['failed_payment_orders'] ?? null) === 0;
|
||||
});
|
||||
|
||||
$export = $this->get('/merchant-admin/orders/export?status=shipped&payment_status=paid&platform=wechat_mini&device_type=mini-program&payment_channel=wechat_pay&min_pay_amount=180&max_pay_amount=190');
|
||||
$export->assertOk();
|
||||
$content = $export->streamedContent();
|
||||
|
||||
assertStringContainsString('导出信息,商家订单导出', $content);
|
||||
assertStringContainsString('订单状态,已发货', $content);
|
||||
assertStringContainsString('支付状态,已支付', $content);
|
||||
assertStringContainsString('平台,微信小程序', $content);
|
||||
assertStringContainsString('设备类型,小程序环境', $content);
|
||||
assertStringContainsString('支付渠道,微信支付', $content);
|
||||
assertStringContainsString('最低实付金额,¥180.00', $content);
|
||||
assertStringContainsString('最高实付金额,¥190.00', $content);
|
||||
assertStringContainsString('导出订单数,1', $content);
|
||||
assertStringContainsString('导出实付总额,189.00', $content);
|
||||
assertStringContainsString('导出平均客单价,189.00', $content);
|
||||
assertStringContainsString('导出已支付订单数,1', $content);
|
||||
assertStringContainsString('导出支付失败订单,0', $content);
|
||||
assertStringContainsString('ORD202603080003', $content);
|
||||
}
|
||||
|
||||
public function test_merchant_products_batch_update_blocks_out_of_scope_products(): void
|
||||
{
|
||||
$this->loginAsMerchantAdmin();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$foreignMerchant = Merchant::query()->create([
|
||||
'name' => '外部站点',
|
||||
'slug' => 'foreign-merchant-demo',
|
||||
'domain' => null,
|
||||
'contact_name' => '测试人',
|
||||
'contact_phone' => '13800000002',
|
||||
'contact_email' => 'foreign@example.com',
|
||||
'plan' => 'basic',
|
||||
'status' => 'active',
|
||||
'activated_at' => now(),
|
||||
'settings' => ['currency' => 'CNY'],
|
||||
]);
|
||||
|
||||
$foreignCategory = ProductCategory::query()->create([
|
||||
'merchant_id' => $foreignMerchant->id,
|
||||
'name' => '外部分类',
|
||||
'slug' => 'foreign-default',
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'description' => '用于越权测试',
|
||||
]);
|
||||
|
||||
$foreignProduct = Product::query()->create([
|
||||
'merchant_id' => $foreignMerchant->id,
|
||||
'category_id' => $foreignCategory->id,
|
||||
'title' => '外部商品',
|
||||
'slug' => 'foreign-product',
|
||||
'sku' => 'SKU-FOREIGN-001',
|
||||
'summary' => '用于越权测试',
|
||||
'content' => 'test',
|
||||
'price' => 66,
|
||||
'original_price' => 88,
|
||||
'stock' => 12,
|
||||
'status' => 'published',
|
||||
'images' => [],
|
||||
]);
|
||||
|
||||
$localProduct = Product::query()->forMerchant($merchant->id)->orderBy('id')->firstOrFail();
|
||||
|
||||
$response = $this->from('/merchant-admin/products')->post('/merchant-admin/products/batch', [
|
||||
'product_ids' => [$localProduct->id, $foreignProduct->id],
|
||||
'action' => 'change_status',
|
||||
'status' => 'offline',
|
||||
]);
|
||||
|
||||
$response->assertRedirect('/merchant-admin/products');
|
||||
$response->assertSessionHasErrors(['product_ids']);
|
||||
$response->assertSessionHasErrors(['product_ids' => '勾选商品中存在越权或已删除的数据,请刷新后重试。']);
|
||||
|
||||
$this->assertSame('published', $localProduct->fresh()->status);
|
||||
$this->assertSame('published', $foreignProduct->fresh()->status);
|
||||
}
|
||||
|
||||
public function test_merchant_products_batch_change_category_blocks_invalid_category(): void
|
||||
{
|
||||
$this->loginAsMerchantAdmin();
|
||||
|
||||
$foreignMerchant = Merchant::query()->create([
|
||||
'name' => '外部分站',
|
||||
'slug' => 'foreign-category-merchant-demo',
|
||||
'domain' => null,
|
||||
'contact_name' => '测试人',
|
||||
'contact_phone' => '13800000003',
|
||||
'contact_email' => 'foreign-category@example.com',
|
||||
'plan' => 'basic',
|
||||
'status' => 'active',
|
||||
'activated_at' => now(),
|
||||
'settings' => ['currency' => 'CNY'],
|
||||
]);
|
||||
|
||||
$foreignCategory = ProductCategory::query()->create([
|
||||
'merchant_id' => $foreignMerchant->id,
|
||||
'name' => '外部分站分类',
|
||||
'slug' => 'foreign-category',
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'description' => '用于非法分类测试',
|
||||
]);
|
||||
|
||||
$product = Product::query()->where('merchant_id', Merchant::query()->firstOrFail()->id)->orderBy('id')->firstOrFail();
|
||||
$originalCategoryId = $product->category_id;
|
||||
|
||||
$response = $this->from('/merchant-admin/products')->post('/merchant-admin/products/batch', [
|
||||
'product_ids' => [$product->id],
|
||||
'action' => 'change_category',
|
||||
'category_id' => $foreignCategory->id,
|
||||
]);
|
||||
|
||||
$response->assertRedirect('/merchant-admin/products');
|
||||
$response->assertSessionHasErrors(['category_id']);
|
||||
$response->assertSessionHasErrors(['category_id' => '所选分类不存在或不属于当前商家。']);
|
||||
|
||||
$this->assertSame($originalCategoryId, $product->fresh()->category_id);
|
||||
}
|
||||
}
|
||||
51
tests/Feature/MerchantProtectedPagesTest.php
Normal file
51
tests/Feature/MerchantProtectedPagesTest.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MerchantProtectedPagesTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function loginAsMerchantAdmin(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->post('/merchant-admin/login', [
|
||||
'email' => 'merchant.admin@demo.local',
|
||||
'password' => 'Merchant@123456',
|
||||
])->assertRedirect('/merchant-admin');
|
||||
}
|
||||
|
||||
public function test_guest_is_redirected_to_merchant_admin_login_for_protected_pages(): void
|
||||
{
|
||||
foreach (['/merchant-admin', '/merchant-admin/products', '/merchant-admin/orders', '/merchant-admin/product-categories', '/merchant-admin/users'] as $path) {
|
||||
$this->get($path)->assertRedirect('/merchant-admin/login');
|
||||
}
|
||||
}
|
||||
|
||||
public function test_merchant_admin_can_access_key_merchant_pages(): void
|
||||
{
|
||||
$this->loginAsMerchantAdmin();
|
||||
|
||||
$this->get('/merchant-admin')->assertOk()->assertSee('商家后台仪表盘');
|
||||
$this->get('/merchant-admin/products')->assertOk()->assertSee('商家商品管理');
|
||||
$this->get('/merchant-admin/orders')->assertOk()->assertSee('商家订单管理');
|
||||
$this->get('/merchant-admin/product-categories')->assertOk()->assertSee('商家商品分类');
|
||||
$this->get('/merchant-admin/users')->assertOk()->assertSee('商家用户管理');
|
||||
}
|
||||
|
||||
public function test_platform_admin_is_forbidden_from_merchant_admin_pages(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->post('/admin/login', [
|
||||
'email' => 'platform.admin@demo.local',
|
||||
'password' => 'Platform@123456',
|
||||
])->assertRedirect('/admin');
|
||||
|
||||
$this->get('/merchant-admin')->assertForbidden()->assertSee('当前账号未绑定商家后台访问权限');
|
||||
}
|
||||
}
|
||||
54
tests/Feature/SiteAdminAccessTest.php
Normal file
54
tests/Feature/SiteAdminAccessTest.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SiteAdminAccessTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_site_admin_login_page_is_accessible(): void
|
||||
{
|
||||
$response = $this->get('/site-admin/login');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('站点后台登录');
|
||||
$response->assertSee('登录站点后台');
|
||||
}
|
||||
|
||||
public function test_site_admin_can_login_and_open_dashboard(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->post('/site-admin/login', [
|
||||
'email' => 'merchant.admin@demo.local',
|
||||
'password' => 'Merchant@123456',
|
||||
]);
|
||||
|
||||
$response->assertRedirect('/site-admin');
|
||||
|
||||
$dashboard = $this->get('/site-admin');
|
||||
$dashboard->assertOk();
|
||||
$dashboard->assertSee('站点后台仪表盘');
|
||||
$dashboard->assertSee('站点仪表盘');
|
||||
$dashboard->assertSee('站点商品');
|
||||
$dashboard->assertSee('站点订单');
|
||||
$dashboard->assertSee('站点商家');
|
||||
}
|
||||
|
||||
public function test_platform_admin_cannot_login_from_site_admin_entry(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->from('/site-admin/login')->post('/site-admin/login', [
|
||||
'email' => 'platform.admin@demo.local',
|
||||
'password' => 'Platform@123456',
|
||||
]);
|
||||
|
||||
$response->assertRedirect('/site-admin/login');
|
||||
|
||||
$this->get('/site-admin/login')->assertSee('当前账号不是站点管理员,不能登录站点后台');
|
||||
}
|
||||
}
|
||||
733
tests/Feature/SiteAdminBusinessPagesTest.php
Normal file
733
tests/Feature/SiteAdminBusinessPagesTest.php
Normal file
@@ -0,0 +1,733 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SiteAdminBusinessPagesTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function loginAsSiteAdmin(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->post('/site-admin/login', [
|
||||
'email' => 'merchant.admin@demo.local',
|
||||
'password' => 'Merchant@123456',
|
||||
])->assertRedirect('/site-admin');
|
||||
}
|
||||
|
||||
public function test_site_dashboard_displays_site_scope_content(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin')
|
||||
->assertOk()
|
||||
->assertSee('站点后台仪表盘')
|
||||
->assertSee('站点管理员')
|
||||
->assertSee('站点用户')
|
||||
->assertSee('站点商品')
|
||||
->assertSee('站点订单');
|
||||
}
|
||||
|
||||
public function test_site_business_pages_display_expected_entries(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/merchants')
|
||||
->assertOk()
|
||||
->assertSee('站点商家')
|
||||
->assertSee('筛选条件')
|
||||
->assertSee('导出当前筛选结果 CSV')
|
||||
->assertSee('当前筛选摘要')
|
||||
->assertSee('站点运营摘要')
|
||||
->assertSee('运营关注项')
|
||||
->assertSee('工作台导航')
|
||||
->assertSee('商品工作台')
|
||||
->assertSee('订单工作台')
|
||||
->assertSee('站点信息')
|
||||
->assertSee('当前站点资料')
|
||||
->assertSee('站点承接说明')
|
||||
->assertSee('运营建议')
|
||||
->assertSee('查看站点商品')
|
||||
->assertSee('查看站点订单')
|
||||
->assertSee('查看低库存商品')
|
||||
->assertSee('查看待支付订单')
|
||||
->assertSee('高库存已上架商品')
|
||||
->assertSee('低库存补货视角')
|
||||
->assertSee('最近新增商品')
|
||||
->assertSee('高金额已支付订单')
|
||||
->assertSee('待支付订单跟进')
|
||||
->assertSee('支付失败订单排查')
|
||||
->assertSee('查看当前站点资料')
|
||||
->assertSee('查看承接记录')
|
||||
->assertSee('返回当前筛选视图')
|
||||
->assertSee('建议动作')
|
||||
->assertSee('去看最近新增商品')
|
||||
->assertSee('去看已上架商品')
|
||||
->assertSee('去看待支付订单')
|
||||
->assertSee('去看支付失败订单')
|
||||
->assertSee('去看站点资料')
|
||||
->assertSee('去看承接记录')
|
||||
->assertSee('当前站点承接记录')
|
||||
->assertSee('查看商品')
|
||||
->assertSee('查看订单')
|
||||
->assertSee('低库存')
|
||||
->assertSee('支付失败')
|
||||
->assertSee('快捷入口默认带入更适合运营查看的排序与状态条件')
|
||||
->assertSee('/site-admin/products?sort=stock_desc&status=published', false)
|
||||
->assertSee('/site-admin/products?sort=stock_asc&status=published', false)
|
||||
->assertSee('/site-admin/products?sort=latest', false)
|
||||
->assertSee('/site-admin/orders?sort=pay_amount_desc&payment_status=paid', false)
|
||||
->assertSee('/site-admin/orders?sort=latest&payment_status=unpaid', false)
|
||||
->assertSee('/site-admin/orders?sort=latest&payment_status=failed', false)
|
||||
->assertSee('#site-profile', false)
|
||||
->assertSee('#site-records', false);
|
||||
|
||||
$this->get('/site-admin/products')
|
||||
->assertOk()
|
||||
->assertSee('站点商品')
|
||||
->assertSee('筛选条件')
|
||||
->assertSee('商品运营汇总')
|
||||
->assertSee('当前筛选摘要')
|
||||
->assertSee('运营关注项')
|
||||
->assertSee('工作台导航')
|
||||
->assertSee('当前信号')
|
||||
->assertSee('建议动作')
|
||||
->assertSee('商品状态统计')
|
||||
->assertSee('最低价格')
|
||||
->assertSee('最高库存')
|
||||
->assertSee('导出当前筛选结果 CSV')
|
||||
->assertSee('商品列表');
|
||||
|
||||
$this->get('/site-admin/orders')
|
||||
->assertOk()
|
||||
->assertSee('站点订单')
|
||||
->assertSee('筛选条件')
|
||||
->assertSee('订单汇总')
|
||||
->assertSee('当前筛选摘要')
|
||||
->assertSee('运营关注项')
|
||||
->assertSee('工作台导航')
|
||||
->assertSee('当前信号')
|
||||
->assertSee('建议动作')
|
||||
->assertSee('订单状态统计')
|
||||
->assertSee('最低实付金额')
|
||||
->assertSee('最高实付金额')
|
||||
->assertSee('导出当前筛选结果 CSV')
|
||||
->assertSee('订单列表');
|
||||
}
|
||||
|
||||
public function test_site_merchant_page_can_reflect_current_filters_and_stats(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/merchants?keyword=%E6%BC%94%E7%A4%BA&status=active&plan=pro&sort=name_asc')
|
||||
->assertOk()
|
||||
->assertSee('演示店铺')
|
||||
->assertSee('启用中')
|
||||
->assertSee('专业版')
|
||||
->assertSee('名称 A-Z')
|
||||
->assertSee('站点管理员数')
|
||||
->assertSee('站点商品数')
|
||||
->assertSee('站点订单数')
|
||||
->assertSee('商品分类数')
|
||||
->assertSee('运营关注项')
|
||||
->assertSee('工作台导航')
|
||||
->assertSee('站点承接说明')
|
||||
->assertSee('运营建议')
|
||||
->assertSee('当前站点商品仍较少,建议优先查看最近新增与基础信息是否完整。')
|
||||
->assertSee('当前站点订单已形成基础规模,建议优先关注待支付、支付失败与高金额订单。')
|
||||
->assertSee('联系人信息已具备,可继续作为站点日常跟进入口。')
|
||||
->assertSee('建议动作')
|
||||
->assertSee('去看最近新增商品')
|
||||
->assertSee('去看已上架商品')
|
||||
->assertSee('去看待支付订单')
|
||||
->assertSee('去看支付失败订单')
|
||||
->assertSee('去看站点资料')
|
||||
->assertSee('去看承接记录')
|
||||
->assertSee('/site-admin/products?sort=stock_desc&status=published', false)
|
||||
->assertSee('/site-admin/products?sort=stock_asc&status=published', false)
|
||||
->assertSee('/site-admin/orders?sort=pay_amount_desc&payment_status=paid', false)
|
||||
->assertSee('/site-admin/orders?sort=latest&payment_status=failed', false)
|
||||
->assertSee('#site-profile', false)
|
||||
->assertSee('#site-records', false);
|
||||
}
|
||||
|
||||
public function test_site_merchant_export_contains_scope_summary_and_filtered_content(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$export = $this->get('/site-admin/merchants/export?keyword=%E6%BC%94%E7%A4%BA&status=active&plan=pro&sort=name_asc');
|
||||
$export->assertOk();
|
||||
$content = $export->streamedContent();
|
||||
|
||||
$this->assertStringContainsString('导出信息,站点商家导出', $content);
|
||||
$this->assertStringContainsString('站点ID,1', $content);
|
||||
$this->assertStringContainsString('站点名称,演示店铺', $content);
|
||||
$this->assertStringContainsString('关键词,演示', $content);
|
||||
$this->assertStringContainsString('状态,启用中', $content);
|
||||
$this->assertStringContainsString('套餐,专业版', $content);
|
||||
$this->assertStringContainsString('排序', $content);
|
||||
$this->assertStringContainsString('名称 A-Z', $content);
|
||||
$this->assertStringContainsString('站点管理员数,1', $content);
|
||||
$this->assertStringContainsString('站点商品数,2', $content);
|
||||
$this->assertStringContainsString('站点订单数,5', $content);
|
||||
$this->assertStringContainsString('当前站点资料,', $content);
|
||||
$this->assertStringContainsString('当前套餐,专业版', $content);
|
||||
$this->assertStringContainsString('ID,名称,Slug,状态,套餐,联系人,联系电话,联系邮箱,管理员数,用户数,商品数,订单数,商品分类数,激活时间', $content);
|
||||
$this->assertStringContainsString('演示店铺,demo-shop,启用中,专业版', $content);
|
||||
}
|
||||
|
||||
public function test_site_product_filters_can_narrow_results(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/products?keyword=%E6%BC%94%E7%A4%BA%E5%95%86%E5%93%81&status=published')
|
||||
->assertOk()
|
||||
->assertSee('演示商品')
|
||||
->assertDontSee('精选礼包');
|
||||
}
|
||||
|
||||
public function test_site_product_range_filters_can_narrow_results(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/products?min_price=150&max_price=220')
|
||||
->assertOk()
|
||||
->assertSee('演示商品')
|
||||
->assertDontSee('精选礼包');
|
||||
|
||||
$this->get('/site-admin/products?min_stock=60')
|
||||
->assertOk()
|
||||
->assertSee('演示商品')
|
||||
->assertDontSee('精选礼包');
|
||||
}
|
||||
|
||||
public function test_site_product_invalid_ranges_show_chinese_errors_and_empty_state(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/products?min_price=300&max_price=100&min_stock=abc')
|
||||
->assertOk()
|
||||
->assertSee('最低价格不能大于最高价格。')
|
||||
->assertSee('最低库存必须为整数。')
|
||||
->assertSee('暂无商品');
|
||||
}
|
||||
|
||||
public function test_site_product_filter_summary_can_reflect_current_filters(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/products?keyword=%E6%BC%94%E7%A4%BA%E5%95%86%E5%93%81&status=published&category_id=1&min_price=150&max_price=220&min_stock=50&max_stock=120&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选摘要')
|
||||
->assertSee('演示商品')
|
||||
->assertSee('已上架')
|
||||
->assertSee('默认分类')
|
||||
->assertSee('¥150.00 ~ ¥220.00')
|
||||
->assertSee('50 ~ 120 件')
|
||||
->assertSee('价格从高到低');
|
||||
}
|
||||
|
||||
public function test_site_product_status_cards_can_drive_filtering(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/products?status=published')
|
||||
->assertOk()
|
||||
->assertSee('已上架')
|
||||
->assertSee('演示商品')
|
||||
->assertSee('精选礼包');
|
||||
|
||||
$this->get('/site-admin/products?status=draft')
|
||||
->assertOk()
|
||||
->assertSee('暂无商品');
|
||||
}
|
||||
|
||||
public function test_site_order_filters_can_narrow_results(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/orders?status=shipped&platform=wechat_mini')
|
||||
->assertOk()
|
||||
->assertSee('ORD202603080003')
|
||||
->assertDontSee('ORD202603080001');
|
||||
}
|
||||
|
||||
public function test_site_order_pay_amount_range_filters_can_narrow_results(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/orders?min_pay_amount=180&max_pay_amount=190')
|
||||
->assertOk()
|
||||
->assertSee('ORD202603080003')
|
||||
->assertDontSee('ORD202603080002')
|
||||
->assertDontSee('ORD202603080004');
|
||||
}
|
||||
|
||||
public function test_site_order_invalid_ranges_show_chinese_errors_and_empty_state(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/orders?min_pay_amount=300&max_pay_amount=100')
|
||||
->assertOk()
|
||||
->assertSee('最低实付金额不能大于最高实付金额。')
|
||||
->assertSee('暂无订单');
|
||||
}
|
||||
|
||||
public function test_site_order_filter_summary_can_reflect_current_filters(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/orders?keyword=ORD202603080003&status=shipped&payment_status=paid&platform=wechat_mini&device_type=mini-program&payment_channel=wechat_pay&min_pay_amount=180&max_pay_amount=190&sort=pay_amount_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选摘要')
|
||||
->assertSee('ORD202603080003')
|
||||
->assertSee('已发货')
|
||||
->assertSee('已支付')
|
||||
->assertSee('微信小程序')
|
||||
->assertSee('小程序环境')
|
||||
->assertSee('微信支付')
|
||||
->assertSee('¥180.00 ~ ¥190.00')
|
||||
->assertSee('实付金额从高到低');
|
||||
}
|
||||
|
||||
public function test_site_product_operations_focus_and_workbench_are_displayed(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/products')
|
||||
->assertOk()
|
||||
->assertSee('当前站点商品仍较少,建议优先查看最近新增与已上架商品,确认基础信息是否完整。')
|
||||
->assertSee('去看最近新增商品')
|
||||
->assertSee('去看已上架商品')
|
||||
->assertSee('已上架商品')
|
||||
->assertSee('低库存商品')
|
||||
->assertSee('分类覆盖数')
|
||||
->assertSee('高库存已上架')
|
||||
->assertSee('低库存补货')
|
||||
->assertSee('最近新增')
|
||||
->assertSee('草稿待整理')
|
||||
->assertSee('返回当前筛选视图');
|
||||
}
|
||||
|
||||
public function test_site_order_operations_focus_and_workbench_are_displayed(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/orders')
|
||||
->assertOk()
|
||||
->assertSee('当前站点订单已形成基础规模,建议优先关注待支付、支付失败与高金额已支付订单。')
|
||||
->assertSee('去看待支付订单')
|
||||
->assertSee('去看支付失败订单')
|
||||
->assertSee('待支付订单')
|
||||
->assertSee('支付失败订单')
|
||||
->assertSee('已完成订单')
|
||||
->assertSee('高金额已支付')
|
||||
->assertSee('待支付跟进')
|
||||
->assertSee('支付失败排查')
|
||||
->assertSee('最近完成订单')
|
||||
->assertSee('返回当前筛选视图');
|
||||
}
|
||||
|
||||
public function test_site_product_operations_focus_can_follow_current_filters(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/products?status=published&min_stock=0&max_stock=20&sort=stock_asc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架库存视角,建议优先确认低库存补货节奏,并同步观察高库存结构是否健康。')
|
||||
->assertSee('继续查看当前库存视角')
|
||||
->assertSee('去看高库存商品')
|
||||
->assertSee('/site-admin/products?status=published&min_stock=0&max_stock=20&sort=stock_asc', false);
|
||||
|
||||
$this->get('/site-admin/products?status=draft')
|
||||
->assertOk()
|
||||
->assertSee('当前正在查看草稿商品,建议优先补齐标题、分类、价格与库存后再安排上架。')
|
||||
->assertSee('继续查看当前草稿')
|
||||
->assertSee('去看已上架商品')
|
||||
->assertSee('/site-admin/products?status=draft&sort=latest', false);
|
||||
|
||||
$response = $this->get('/site-admin/products?status=published&category_id=1&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc');
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架的“默认分类”分类下关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对在售商品命名、分类承接、价格梯度与搜索结果是否一致,并同步观察库存结构是否健康。')
|
||||
->assertSee('继续查看当前已上架分类关键词价格带商品')
|
||||
->assertSee('去看当前已上架分类关键词商品')
|
||||
->assertSee('status=published', false)
|
||||
->assertSee('category_id=1', false)
|
||||
->assertSee('keyword=%E6%BC%94%E7%A4%BA', false)
|
||||
->assertSee('min_price=150', false)
|
||||
->assertSee('max_price=220', false)
|
||||
->assertSee('sort=price_desc', false);
|
||||
|
||||
$response = $this->get('/site-admin/products?status=published&category_id=1&keyword=%E6%BC%94%E7%A4%BA&sort=latest');
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架的“默认分类”分类下关键词“演示”命中的商品,建议优先核对在售商品命名、分类承接与搜索结果是否一致,并同步观察价格带与库存结构是否健康。')
|
||||
->assertSee('继续查看当前已上架分类关键词商品')
|
||||
->assertSee('去看当前已上架分类商品')
|
||||
->assertSee('status=published', false)
|
||||
->assertSee('category_id=1', false)
|
||||
->assertSee('keyword=%E6%BC%94%E7%A4%BA', false)
|
||||
->assertSee('sort=latest', false);
|
||||
|
||||
$response = $this->get('/site-admin/products?status=published&category_id=1&min_price=150&max_price=220&sort=price_desc');
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架的“默认分类”分类下价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对该分类在售商品的价格结构、库存分布与承接效率是否协调。')
|
||||
->assertSee('继续查看当前已上架分类价格带商品')
|
||||
->assertSee('去看当前已上架分类商品')
|
||||
->assertSee('status=published', false)
|
||||
->assertSee('category_id=1', false)
|
||||
->assertSee('min_price=150', false)
|
||||
->assertSee('max_price=220', false)
|
||||
->assertSee('sort=price_desc', false);
|
||||
|
||||
$response = $this->get('/site-admin/products?status=published&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc');
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架商品中关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 结果,建议优先核对在售商品命名、卖点表达与价格梯度是否一致,并同步观察库存结构与搜索承接是否健康。')
|
||||
->assertSee('继续查看当前已上架关键词价格带商品')
|
||||
->assertSee('去看当前已上架关键词商品')
|
||||
->assertSee('status=published', false)
|
||||
->assertSee('keyword=%E6%BC%94%E7%A4%BA', false)
|
||||
->assertSee('min_price=150', false)
|
||||
->assertSee('max_price=220', false)
|
||||
->assertSee('sort=price_desc', false);
|
||||
|
||||
$response = $this->get('/site-admin/products?status=published&keyword=%E6%BC%94%E7%A4%BA&sort=latest');
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架商品中关键词“演示”命中的结果,建议优先核对在售商品命名、卖点表达与搜索承接是否一致,并同步观察价格带与库存结构是否健康。')
|
||||
->assertSee('继续查看当前已上架关键词商品')
|
||||
->assertSee('去看全部已上架商品')
|
||||
->assertSee('status=published', false)
|
||||
->assertSee('keyword=%E6%BC%94%E7%A4%BA', false)
|
||||
->assertSee('sort=latest', false);
|
||||
|
||||
$response = $this->get('/site-admin/products?category_id=1&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc');
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦“默认分类”分类下关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对分类承接、命名卖点与价格梯度是否一致,并同步观察库存结构与搜索承接是否健康。')
|
||||
->assertSee('继续查看当前分类关键词价格带商品')
|
||||
->assertSee('去看当前分类关键词商品')
|
||||
->assertSee('category_id=1', false)
|
||||
->assertSee('keyword=%E6%BC%94%E7%A4%BA', false)
|
||||
->assertSee('min_price=150', false)
|
||||
->assertSee('max_price=220', false)
|
||||
->assertSee('sort=price_desc', false);
|
||||
|
||||
$response = $this->get('/site-admin/products?category_id=1&keyword=%E6%BC%94%E7%A4%BA&sort=latest');
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦“默认分类”分类下关键词“演示”命中的商品,建议优先核对分类承接、命名卖点与搜索结果是否一致,并同步观察相关商品的价格带与库存结构。')
|
||||
->assertSee('继续查看当前分类关键词商品')
|
||||
->assertSee('去看当前分类商品')
|
||||
->assertSee('keyword=%E6%BC%94%E7%A4%BA', false)
|
||||
->assertSee('category_id=1', false)
|
||||
->assertSee('sort=latest', false);
|
||||
|
||||
$response = $this->get('/site-admin/products?status=published&category_id=1&sort=latest');
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦已上架的“默认分类”分类商品,建议优先核对该分类在售商品的价格带、库存结构与承接质量是否均衡。')
|
||||
->assertSee('继续查看当前已上架分类商品')
|
||||
->assertSee('去看当前分类商品')
|
||||
->assertSee('status=published', false)
|
||||
->assertSee('category_id=1', false)
|
||||
->assertSee('sort=latest', false);
|
||||
|
||||
$response = $this->get('/site-admin/products?category_id=1&min_price=150&max_price=220&sort=price_desc');
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦“默认分类”分类下价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对该分类价格结构是否连贯,并同步观察库存分布与承接效率是否健康。')
|
||||
->assertSee('继续查看当前分类价格带商品')
|
||||
->assertSee('去看当前分类商品')
|
||||
->assertSee('category_id=1', false)
|
||||
->assertSee('min_price=150', false)
|
||||
->assertSee('max_price=220', false)
|
||||
->assertSee('sort=price_desc', false);
|
||||
|
||||
$this->get('/site-admin/products?category_id=1&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦“默认分类”分类商品,建议优先核对分类承接是否准确,并同步观察价格带与库存结构是否均衡。')
|
||||
->assertSee('继续查看当前分类商品')
|
||||
->assertSee('去看低库存商品')
|
||||
->assertSee('/site-admin/products?category_id=1&sort=latest', false);
|
||||
|
||||
$this->get('/site-admin/products?keyword=%E6%BC%94%E7%A4%BA&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦关键词“演示”命中的商品,建议优先核对命名、卖点与搜索承接是否一致,并同步观察相关商品的价格带与库存结构。')
|
||||
->assertSee('继续查看当前关键词商品')
|
||||
->assertSee('去看最近新增商品')
|
||||
->assertSee('/site-admin/products?keyword=%E6%BC%94%E7%A4%BA&sort=latest', false);
|
||||
|
||||
$this->get('/site-admin/products?min_price=150&max_price=220&sort=price_desc')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对定价梯度是否连贯,并同步观察库存结构与转化表现是否匹配。')
|
||||
->assertSee('继续查看当前价格带商品')
|
||||
->assertSee('去看最近新增商品')
|
||||
->assertSee('/site-admin/products?min_price=150&max_price=220&sort=price_desc', false);
|
||||
}
|
||||
|
||||
public function test_site_order_operations_focus_can_follow_current_filters(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/orders?platform=wechat_mini&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦微信小程序订单,建议优先关注下单到支付转化是否顺畅,并同步排查小程序端支付回流体验。')
|
||||
->assertSee('继续查看微信小程序订单')
|
||||
->assertSee('去看待支付订单')
|
||||
->assertSee('/site-admin/orders?platform=wechat_mini&sort=latest', false);
|
||||
|
||||
$this->get('/site-admin/orders?payment_channel=wechat_pay&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦微信支付订单,建议优先核对支付成功率、回调稳定性与失败重试转化。')
|
||||
->assertSee('继续查看微信支付订单')
|
||||
->assertSee('去看支付失败订单')
|
||||
->assertSee('/site-admin/orders?payment_channel=wechat_pay&sort=latest', false);
|
||||
|
||||
$this->get('/site-admin/orders?device_type=mini-program&payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦小程序环境订单,建议优先核对授权链路、支付唤起表现与下单回流是否顺畅。')
|
||||
->assertSee('继续查看小程序环境订单')
|
||||
->assertSee('去看微信支付订单')
|
||||
->assertSee('/site-admin/orders?payment_status=failed&device_type=mini-program&sort=latest', false);
|
||||
|
||||
$this->get('/site-admin/orders?device_type=mobile-webview&payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦微信内网页订单,建议优先关注授权静默登录、页面跳转稳定性与支付拉起后的回流体验。')
|
||||
->assertSee('继续查看微信内网页订单')
|
||||
->assertSee('去看待支付订单')
|
||||
->assertSee('/site-admin/orders?payment_status=failed&device_type=mobile-webview&sort=latest', false);
|
||||
|
||||
$this->get('/site-admin/orders?device_type=mobile&payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦移动浏览器订单,建议优先关注 H5 下单链路、页面加载稳定性与支付转化流失点。')
|
||||
->assertSee('继续查看移动浏览器订单')
|
||||
->assertSee('去看待支付订单')
|
||||
->assertSee('/site-admin/orders?payment_status=failed&device_type=mobile&sort=latest', false);
|
||||
|
||||
$this->get('/site-admin/orders?device_type=desktop&payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦桌面浏览器订单,建议优先关注 PC 端下单流程、页面首屏稳定性与高客单转化表现。')
|
||||
->assertSee('继续查看桌面浏览器订单')
|
||||
->assertSee('去看高金额已支付订单')
|
||||
->assertSee('/site-admin/orders?payment_status=failed&device_type=desktop&sort=latest', false);
|
||||
|
||||
$this->get('/site-admin/orders?payment_status=failed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦支付失败订单,建议优先排查支付渠道、回调结果与用户重试情况。')
|
||||
->assertSee('继续查看支付失败订单')
|
||||
->assertSee('去看高金额已支付订单')
|
||||
->assertSee('/site-admin/orders?payment_status=failed&sort=latest', false);
|
||||
|
||||
$this->get('/site-admin/orders?payment_status=unpaid&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前筛选已聚焦待支付订单,建议优先跟进下单未支付用户,并观察支付转化时效。')
|
||||
->assertSee('继续查看待支付订单')
|
||||
->assertSee('去看高金额已支付订单')
|
||||
->assertSee('/site-admin/orders?payment_status=unpaid&sort=latest', false);
|
||||
|
||||
$this->get('/site-admin/orders?status=completed&sort=latest')
|
||||
->assertOk()
|
||||
->assertSee('当前正在查看已完成订单,建议复盘高客单成交与复购来源,沉淀更稳定的转化路径。')
|
||||
->assertSee('继续查看已完成订单')
|
||||
->assertSee('去看高金额已支付订单')
|
||||
->assertSee('/site-admin/orders?status=completed&sort=latest', false);
|
||||
}
|
||||
|
||||
public function test_site_order_status_cards_can_drive_filtering(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/orders?status=completed')
|
||||
->assertOk()
|
||||
->assertSee('已完成')
|
||||
->assertSee('ORD202603080004')
|
||||
->assertDontSee('ORD202603080001');
|
||||
|
||||
$this->get('/site-admin/orders?status=refunded')
|
||||
->assertOk()
|
||||
->assertSee('暂无订单');
|
||||
}
|
||||
|
||||
public function test_site_business_pages_display_empty_state_when_filters_miss(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/products?keyword=no-match-keyword')
|
||||
->assertOk()
|
||||
->assertSee('暂无商品');
|
||||
|
||||
$this->get('/site-admin/orders?keyword=no-match-keyword')
|
||||
->assertOk()
|
||||
->assertSee('暂无订单');
|
||||
}
|
||||
|
||||
public function test_site_export_endpoints_return_csv_response(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin/products/export')
|
||||
->assertOk()
|
||||
->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
|
||||
$this->get('/site-admin/orders/export')
|
||||
->assertOk()
|
||||
->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
}
|
||||
|
||||
public function test_site_invalid_product_export_is_blocked_with_redirect_and_error(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$response = $this->get('/site-admin/products/export?min_price=300&max_price=100');
|
||||
$response->assertStatus(302);
|
||||
$this->assertStringContainsString('/site-admin/products?', $response->headers->get('Location'));
|
||||
$this->assertStringContainsString('min_price=300', $response->headers->get('Location'));
|
||||
$this->assertStringContainsString('max_price=100', $response->headers->get('Location'));
|
||||
$this->assertStringContainsString('sort=latest', $response->headers->get('Location'));
|
||||
|
||||
$this->followRedirects($this->get('/site-admin/products/export?min_price=300&max_price=100'))
|
||||
->assertSee('最低价格不能大于最高价格。')
|
||||
->assertSee('暂无商品');
|
||||
}
|
||||
|
||||
public function test_site_invalid_order_export_is_blocked_with_redirect_and_error(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$response = $this->get('/site-admin/orders/export?min_pay_amount=300&max_pay_amount=100');
|
||||
$response->assertStatus(302);
|
||||
$this->assertStringContainsString('/site-admin/orders?', $response->headers->get('Location'));
|
||||
$this->assertStringContainsString('min_pay_amount=300', $response->headers->get('Location'));
|
||||
$this->assertStringContainsString('max_pay_amount=100', $response->headers->get('Location'));
|
||||
$this->assertStringContainsString('sort=latest', $response->headers->get('Location'));
|
||||
|
||||
$this->followRedirects($this->get('/site-admin/orders/export?min_pay_amount=300&max_pay_amount=100'))
|
||||
->assertSee('最低实付金额不能大于最高实付金额。')
|
||||
->assertSee('暂无订单');
|
||||
}
|
||||
|
||||
public function test_site_export_contains_scope_summary_and_filtered_content(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$productExport = $this->get('/site-admin/products/export?keyword=%E6%BC%94%E7%A4%BA%E5%95%86%E5%93%81&status=published&min_price=150&max_price=220');
|
||||
$productExport->assertOk();
|
||||
$productContent = $productExport->streamedContent();
|
||||
$this->assertStringContainsString('导出信息,站点商品导出', $productContent);
|
||||
$this->assertStringContainsString('站点ID,1', $productContent);
|
||||
$this->assertStringContainsString('状态,已上架', $productContent);
|
||||
$this->assertStringContainsString('分类,全部', $productContent);
|
||||
$this->assertStringContainsString('最低价格,¥150.00', $productContent);
|
||||
$this->assertStringContainsString('最高价格,¥220.00', $productContent);
|
||||
$this->assertStringContainsString('排序,最新创建', $productContent);
|
||||
$this->assertStringContainsString('导出商品数,1', $productContent);
|
||||
$this->assertStringContainsString('导出总库存,100', $productContent);
|
||||
$this->assertStringContainsString('演示商品', $productContent);
|
||||
$this->assertStringNotContainsString('精选礼包', $productContent);
|
||||
|
||||
$orderExport = $this->get('/site-admin/orders/export?status=shipped&payment_status=paid&platform=wechat_mini&device_type=mini-program&payment_channel=wechat_pay&min_pay_amount=180&max_pay_amount=190');
|
||||
$orderExport->assertOk();
|
||||
$orderContent = $orderExport->streamedContent();
|
||||
$this->assertStringContainsString('导出信息,站点订单导出', $orderContent);
|
||||
$this->assertStringContainsString('站点ID,1', $orderContent);
|
||||
$this->assertStringContainsString('订单状态,已发货', $orderContent);
|
||||
$this->assertStringContainsString('支付状态,已支付', $orderContent);
|
||||
$this->assertStringContainsString('平台,微信小程序', $orderContent);
|
||||
$this->assertStringContainsString('设备类型,小程序环境', $orderContent);
|
||||
$this->assertStringContainsString('支付渠道,微信支付', $orderContent);
|
||||
$this->assertStringContainsString('最低实付金额,¥180.00', $orderContent);
|
||||
$this->assertStringContainsString('最高实付金额,¥190.00', $orderContent);
|
||||
$this->assertStringContainsString('排序,创建时间倒序', $orderContent);
|
||||
$this->assertStringContainsString('导出订单数,1', $orderContent);
|
||||
$this->assertStringContainsString('导出已支付订单数,1', $orderContent);
|
||||
$this->assertStringContainsString('ORD202603080003', $orderContent);
|
||||
$this->assertStringNotContainsString('ORD202603080001', $orderContent);
|
||||
}
|
||||
|
||||
public function test_site_product_summary_stats_match_export_summary_for_same_filters(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$page = $this->get('/site-admin/products?keyword=%E6%BC%94%E7%A4%BA%E5%95%86%E5%93%81&status=published&min_price=150&max_price=220');
|
||||
$page->assertOk()->assertViewHas('summaryStats', function (array $summaryStats) {
|
||||
return ($summaryStats['total_products'] ?? null) === 1
|
||||
&& ($summaryStats['total_stock'] ?? null) === 100
|
||||
&& (float) ($summaryStats['total_stock_value'] ?? 0) === 19900.0
|
||||
&& (float) ($summaryStats['average_price'] ?? 0) === 199.0;
|
||||
});
|
||||
|
||||
$page->assertSee('商品总数')
|
||||
->assertSee('总库存')
|
||||
->assertSee('总货值')
|
||||
->assertSee('平均售价')
|
||||
->assertSee('19,900.00')
|
||||
->assertSee('199.00');
|
||||
|
||||
$export = $this->get('/site-admin/products/export?keyword=%E6%BC%94%E7%A4%BA%E5%95%86%E5%93%81&status=published&min_price=150&max_price=220');
|
||||
$export->assertOk();
|
||||
$content = $export->streamedContent();
|
||||
|
||||
$this->assertStringContainsString('导出商品数,1', $content);
|
||||
$this->assertStringContainsString('导出总库存,100', $content);
|
||||
$this->assertStringContainsString('导出总货值,19900.00', $content);
|
||||
$this->assertStringContainsString('导出平均售价,199.00', $content);
|
||||
}
|
||||
|
||||
public function test_site_order_summary_stats_match_export_summary_for_same_filters(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$page = $this->get('/site-admin/orders?status=shipped&payment_status=paid&platform=wechat_mini&device_type=mini-program&payment_channel=wechat_pay&min_pay_amount=180&max_pay_amount=190');
|
||||
$page->assertOk()->assertViewHas('summaryStats', function (array $summaryStats) {
|
||||
return ($summaryStats['total_orders'] ?? null) === 1
|
||||
&& (float) ($summaryStats['total_pay_amount'] ?? 0) === 189.0
|
||||
&& (float) ($summaryStats['average_order_amount'] ?? 0) === 189.0
|
||||
&& ($summaryStats['paid_orders'] ?? null) === 1
|
||||
&& ($summaryStats['failed_payment_orders'] ?? null) === 0;
|
||||
});
|
||||
|
||||
$page->assertSee('订单总数')
|
||||
->assertSee('实付总额')
|
||||
->assertSee('平均客单价')
|
||||
->assertSee('已支付订单数')
|
||||
->assertSee('支付失败订单')
|
||||
->assertSee('小程序环境')
|
||||
->assertSee('微信支付')
|
||||
->assertSee('189.00');
|
||||
|
||||
$export = $this->get('/site-admin/orders/export?status=shipped&payment_status=paid&platform=wechat_mini&device_type=mini-program&payment_channel=wechat_pay&min_pay_amount=180&max_pay_amount=190');
|
||||
$export->assertOk();
|
||||
$content = $export->streamedContent();
|
||||
|
||||
$this->assertStringContainsString('设备类型,小程序环境', $content);
|
||||
$this->assertStringContainsString('支付渠道,微信支付', $content);
|
||||
$this->assertStringContainsString('导出订单数,1', $content);
|
||||
$this->assertStringContainsString('导出实付总额,189.00', $content);
|
||||
$this->assertStringContainsString('导出平均客单价,189.00', $content);
|
||||
$this->assertStringContainsString('导出已支付订单数,1', $content);
|
||||
$this->assertStringContainsString('导出支付失败订单,0', $content);
|
||||
}
|
||||
|
||||
public function test_platform_site_list_displays_site_admin_entry_link(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->post('/admin/login', [
|
||||
'email' => 'platform.admin@demo.local',
|
||||
'password' => 'Platform@123456',
|
||||
])->assertRedirect('/admin');
|
||||
|
||||
$this->get('/admin/merchants')
|
||||
->assertOk()
|
||||
->assertSee('进入站点后台')
|
||||
->assertSee('当前阶段请使用该站点管理员账号登录');
|
||||
}
|
||||
}
|
||||
53
tests/Feature/SiteAdminProtectedPagesTest.php
Normal file
53
tests/Feature/SiteAdminProtectedPagesTest.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SiteAdminProtectedPagesTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function loginAsSiteAdmin(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->post('/site-admin/login', [
|
||||
'email' => 'merchant.admin@demo.local',
|
||||
'password' => 'Merchant@123456',
|
||||
])->assertRedirect('/site-admin');
|
||||
}
|
||||
|
||||
public function test_guest_is_redirected_to_site_admin_login_for_protected_pages(): void
|
||||
{
|
||||
foreach (['/site-admin', '/site-admin/merchants', '/site-admin/merchants/export', '/site-admin/products', '/site-admin/products/export', '/site-admin/orders', '/site-admin/orders/export'] as $path) {
|
||||
$this->get($path)->assertRedirect('/site-admin/login');
|
||||
}
|
||||
}
|
||||
|
||||
public function test_site_admin_can_access_key_site_pages(): void
|
||||
{
|
||||
$this->loginAsSiteAdmin();
|
||||
|
||||
$this->get('/site-admin')->assertOk()->assertSee('站点后台仪表盘');
|
||||
$this->get('/site-admin/merchants')->assertOk()->assertSee('站点商家');
|
||||
$this->get('/site-admin/merchants/export')->assertOk()->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
$this->get('/site-admin/products')->assertOk()->assertSee('站点商品');
|
||||
$this->get('/site-admin/products/export')->assertOk()->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
$this->get('/site-admin/orders')->assertOk()->assertSee('站点订单');
|
||||
$this->get('/site-admin/orders/export')->assertOk()->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
}
|
||||
|
||||
public function test_platform_admin_is_forbidden_from_site_admin_pages(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->post('/admin/login', [
|
||||
'email' => 'platform.admin@demo.local',
|
||||
'password' => 'Platform@123456',
|
||||
])->assertRedirect('/admin');
|
||||
|
||||
$this->get('/site-admin')->assertForbidden()->assertSee('当前账号未绑定站点后台访问权限');
|
||||
}
|
||||
}
|
||||
177
tests/Feature/SubscriptionActivationServiceTest.php
Normal file
177
tests/Feature/SubscriptionActivationServiceTest.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Merchant;
|
||||
use App\Models\Plan;
|
||||
use App\Models\PlatformOrder;
|
||||
use App\Models\SiteSubscription;
|
||||
use App\Support\SubscriptionActivationService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SubscriptionActivationServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_activate_order_creates_subscription_and_links_order(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'activate_test_monthly',
|
||||
'name' => '激活测试(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 99,
|
||||
'list_price' => 99,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$order = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_ACTIVATE_0001',
|
||||
'order_type' => 'new_purchase',
|
||||
'status' => 'activated',
|
||||
'payment_status' => 'paid',
|
||||
'plan_name' => $plan->name,
|
||||
'billing_cycle' => $plan->billing_cycle,
|
||||
'period_months' => 1,
|
||||
'quantity' => 1,
|
||||
'list_amount' => 99,
|
||||
'discount_amount' => 0,
|
||||
'payable_amount' => 99,
|
||||
'paid_amount' => 99,
|
||||
'placed_at' => now()->subMinutes(10),
|
||||
'paid_at' => now()->subMinutes(5),
|
||||
'activated_at' => now()->subMinutes(1),
|
||||
]);
|
||||
|
||||
$service = new SubscriptionActivationService();
|
||||
$subscription = $service->activateOrder($order->id, 1);
|
||||
|
||||
// 二次执行应为幂等:不重复续期/不重复创建,直接返回同一个订阅
|
||||
$again = $service->activateOrder($order->id, 1);
|
||||
|
||||
$this->assertInstanceOf(SiteSubscription::class, $subscription);
|
||||
$this->assertSame($subscription->id, $again->id);
|
||||
$this->assertSame('activated', $subscription->status);
|
||||
$this->assertSame($merchant->id, $subscription->merchant_id);
|
||||
$this->assertSame($plan->id, $subscription->plan_id);
|
||||
$this->assertNotNull($subscription->starts_at);
|
||||
$this->assertNotNull($subscription->ends_at);
|
||||
$this->assertTrue($subscription->ends_at->greaterThan($subscription->starts_at));
|
||||
|
||||
$order->refresh();
|
||||
$this->assertSame($subscription->id, $order->site_subscription_id);
|
||||
}
|
||||
|
||||
public function test_activate_order_extends_existing_subscription(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'renew_test_monthly',
|
||||
'name' => '续费测试(月付)',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 120,
|
||||
'list_price' => 120,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$subscription = SiteSubscription::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'status' => 'activated',
|
||||
'source' => 'manual',
|
||||
'subscription_no' => 'SUB_EXIST_0001',
|
||||
'plan_name' => $plan->name,
|
||||
'billing_cycle' => $plan->billing_cycle,
|
||||
'period_months' => 1,
|
||||
'amount' => 120,
|
||||
'starts_at' => now()->subDays(10),
|
||||
'ends_at' => now()->addDays(20),
|
||||
'activated_at' => now()->subDays(10),
|
||||
]);
|
||||
|
||||
$oldEndsAt = $subscription->ends_at->copy();
|
||||
|
||||
$order = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'site_subscription_id' => $subscription->id,
|
||||
'order_no' => 'PO_RENEW_0001',
|
||||
'order_type' => 'renewal',
|
||||
'status' => 'activated',
|
||||
'payment_status' => 'paid',
|
||||
'plan_name' => $plan->name,
|
||||
'billing_cycle' => $plan->billing_cycle,
|
||||
'period_months' => 1,
|
||||
'quantity' => 1,
|
||||
'list_amount' => 120,
|
||||
'discount_amount' => 0,
|
||||
'payable_amount' => 120,
|
||||
'paid_amount' => 120,
|
||||
'placed_at' => now()->subMinutes(10),
|
||||
'paid_at' => now()->subMinutes(5),
|
||||
'activated_at' => now()->subMinutes(1),
|
||||
]);
|
||||
|
||||
$service = new SubscriptionActivationService();
|
||||
$updated = $service->activateOrder($order->id, 1);
|
||||
|
||||
$updated->refresh();
|
||||
$this->assertTrue($updated->ends_at->greaterThan($oldEndsAt));
|
||||
$this->assertSame('activated', $updated->status);
|
||||
|
||||
// 再次执行应为幂等:ends_at 不应继续增长
|
||||
$endsAtAfterFirst = $updated->ends_at->copy();
|
||||
$again = $service->activateOrder($order->id, 1);
|
||||
$again->refresh();
|
||||
$this->assertTrue($again->ends_at->equalTo($endsAtAfterFirst));
|
||||
}
|
||||
|
||||
public function test_activate_order_requires_paid_and_activated(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$merchant = Merchant::query()->firstOrFail();
|
||||
$plan = Plan::query()->create([
|
||||
'code' => 'invalid_test',
|
||||
'name' => '无效测试',
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 10,
|
||||
'list_price' => 10,
|
||||
'status' => 'active',
|
||||
'sort' => 10,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$order = PlatformOrder::query()->create([
|
||||
'merchant_id' => $merchant->id,
|
||||
'plan_id' => $plan->id,
|
||||
'order_no' => 'PO_INVALID_0001',
|
||||
'order_type' => 'new_purchase',
|
||||
'status' => 'pending',
|
||||
'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(),
|
||||
]);
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
$service = new SubscriptionActivationService();
|
||||
$service->activateOrder($order->id, 1);
|
||||
}
|
||||
}
|
||||
10
tests/TestCase.php
Normal file
10
tests/TestCase.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
16
tests/Unit/ExampleTest.php
Normal file
16
tests/Unit/ExampleTest.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user