star-cloud/tests/Feature/Admin/AnalysisReportsTest.php
sky121113 8f2dd1375e [FEAT] 實作機台報表分析功能與商品報表篩選列優化
1. 實作 AnalysisController@machineReports 後端邏輯,支援租戶隔離與帳號機台授權過濾。
2. 建立 machine-reports.blade.php 視圖,加入極簡奢華風統計卡片、ApexCharts 趨勢圖與自訂多選支付工具篩選器,並停用圖表縮放優化滾輪滾動體驗。
3. 優化 product-reports.blade.php,新增機台下拉選單過濾,並啟用寬度優化與響應式自動換行,且同步停用圖表縮放。
4. 在 Machine model 全域 Scope 中排除單元測試阻擋,並更新 RoleSeeder 播種機台報表權限。
5. 新增 AnalysisReportsTest 測試,並更新 AnalysisPermissionTest 權限測試,且在 Sail 中驗證為 100% 通過。
6. 同步更新繁中、英文、日文翻譯 JSON 檔案,以 zh_TW 為基準完美對齊、字母排序且還原斜線。
2026-06-05 16:21:05 +08:00

275 lines
9.4 KiB
PHP

<?php
namespace Tests\Feature\Admin;
use App\Models\System\Company;
use App\Models\System\User;
use App\Models\Machine\Machine;
use App\Models\Product\Product;
use App\Models\Transaction\Order;
use App\Models\Transaction\OrderItem;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AnalysisReportsTest extends TestCase
{
use RefreshDatabase;
protected $company;
protected $superAdmin;
protected $tenantUser;
protected $machineA;
protected $machineB;
protected $productX;
protected $productY;
protected function setUp(): void
{
parent::setUp();
$this->withoutExceptionHandling();
// 1. Seed Roles & Permissions
$this->artisan('db:seed', ['--class' => 'RoleSeeder']);
// 2. Create Company
$this->company = Company::create([
'name' => 'Report Test Company',
'code' => 'REPTEST',
'status' => 1,
]);
// Register SQLite compatible DATE_FORMAT function
if (\Illuminate\Support\Facades\Schema::getConnection()->getDriverName() === 'sqlite') {
$pdo = \Illuminate\Support\Facades\Schema::getConnection()->getPdo();
$pdo->sqliteCreateFunction('DATE_FORMAT', function ($date, $format) {
if (empty($date)) return null;
$phpFormat = str_replace(
['%Y', '%m', '%d', '%H', '%i', '%s'],
['Y', 'm', 'd', 'H', 'i', 's'],
$format
);
return date($phpFormat, strtotime($date));
}, 2);
}
// 3. Create Super Admin
$this->superAdmin = User::create([
'name' => 'Super Admin',
'username' => 'super_reports_test',
'email' => 'superadmin@example.com',
'password' => bcrypt('password'),
'company_id' => null,
]);
$this->superAdmin->assignRole('super-admin');
// 4. Create Tenant User
$this->tenantUser = User::create([
'name' => 'Tenant User',
'username' => 'tenant_reports_test',
'email' => 'tenantuser@example.com',
'password' => bcrypt('password'),
'company_id' => $this->company->id,
]);
// Give them a role that has analysis permissions
$role = \App\Models\System\Role::create([
'name' => '測試角色',
'company_id' => $this->company->id,
'guard_name' => 'web',
'is_system' => false,
]);
$role->syncPermissions([
'menu.analysis',
'menu.analysis.machine-reports',
'menu.analysis.product-reports'
]);
$this->tenantUser->assignRole($role);
// 5. Create Machines
$this->machineA = Machine::create([
'company_id' => $this->company->id,
'name' => 'Machine Alpha',
'serial_no' => 'MC0000A',
'status' => 'online',
]);
$this->machineB = Machine::create([
'company_id' => $this->company->id,
'name' => 'Machine Beta',
'serial_no' => 'MC0000B',
'status' => 'online',
]);
// Authorize Tenant User to ONLY see Machine A
$this->machineA->users()->attach($this->tenantUser->id);
// 6. Create Products
$this->productX = Product::create([
'company_id' => $this->company->id,
'name' => 'Cola',
'barcode' => 'BAR0001',
'cost' => 10.00,
'price' => 15.00,
'is_active' => true,
]);
$this->productY = Product::create([
'company_id' => $this->company->id,
'name' => 'Soda',
'barcode' => 'BAR0002',
'cost' => 20.00,
'price' => 25.00,
'is_active' => true,
]);
// 7. Seed Orders
// Order 1: Machine A, Product X, payment type 1 (Credit Card), price 15, quantity 2. (Sales: 30, Cost: 20, Profit: 10)
$order1 = Order::create([
'company_id' => $this->company->id,
'order_no' => 'ORD0001',
'machine_id' => $this->machineA->id,
'payment_type' => 1,
'total_amount' => 30.00,
'pay_amount' => 30.00,
'payment_status' => Order::PAYMENT_STATUS_SUCCESS,
'payment_at' => now(),
]);
OrderItem::create([
'order_id' => $order1->id,
'product_id' => $this->productX->id,
'product_name' => $this->productX->name,
'barcode' => $this->productX->barcode,
'quantity' => 2,
'price' => 15.00,
'subtotal' => 30.00,
]);
// Order 2: Machine B, Product Y, payment type 2 (E-ticket), price 25, quantity 1. (Sales: 25, Cost: 20, Profit: 5)
$order2 = Order::create([
'company_id' => $this->company->id,
'order_no' => 'ORD0002',
'machine_id' => $this->machineB->id,
'payment_type' => 2,
'total_amount' => 25.00,
'pay_amount' => 25.00,
'payment_status' => Order::PAYMENT_STATUS_SUCCESS,
'payment_at' => now(),
]);
OrderItem::create([
'order_id' => $order2->id,
'product_id' => $this->productY->id,
'product_name' => $this->productY->name,
'barcode' => $this->productY->barcode,
'quantity' => 1,
'price' => 25.00,
'subtotal' => 25.00,
]);
}
/**
* 測試超級管理員可以撈出所有機台的銷售統計
*/
public function test_super_admin_gets_all_machines_statistics()
{
$this->actingAs($this->superAdmin);
$response = $this->getJson(route('admin.analysis.machine-reports', [
'company_id' => $this->company->id
]));
$response->assertStatus(200)
->assertJsonPath('success', true);
$data = $response->json();
// 總和驗證
$this->assertEquals(3, $data['summary']['total_quantity']);
$this->assertEquals(55.0, $data['summary']['total_sales']);
$this->assertEquals(40.0, $data['summary']['total_cost']);
$this->assertEquals(15.0, $data['summary']['total_profit']);
// 機台明細驗證
$this->assertCount(2, $data['table_data']);
$machineAData = collect($data['table_data'])->firstWhere('machine_id', $this->machineA->id);
$this->assertNotNull($machineAData);
$this->assertEquals(2, $machineAData['total_quantity']);
$this->assertEquals(30.0, $machineAData['total_sales']);
$machineBData = collect($data['table_data'])->firstWhere('machine_id', $this->machineB->id);
$this->assertNotNull($machineBData);
$this->assertEquals(1, $machineBData['total_quantity']);
$this->assertEquals(25.0, $machineBData['total_sales']);
}
/**
* 測試一般租戶使用者只能撈出有被授權機台的數據 (Machine A),無法看到 Machine B
*/
public function test_tenant_user_only_sees_authorized_machine_statistics()
{
$this->actingAs($this->tenantUser);
$response = $this->getJson(route('admin.analysis.machine-reports'));
$response->assertStatus(200)
->assertJsonPath('success', true);
$data = $response->json();
// 總和應只包含 Machine A
$this->assertEquals(2, $data['summary']['total_quantity']);
$this->assertEquals(30.0, $data['summary']['total_sales']);
$this->assertEquals(20.0, $data['summary']['total_cost']);
$this->assertEquals(10.0, $data['summary']['total_profit']);
// 機台明細應只有 1 筆 (Machine A)
$this->assertCount(1, $data['table_data']);
$this->assertEquals($this->machineA->id, $data['table_data'][0]['machine_id']);
}
/**
* 測試支付工具篩選器能正確過濾資料
*/
public function test_payment_types_filter_works()
{
$this->actingAs($this->superAdmin);
// 僅篩選支付類型 1 (信用卡)
$response = $this->getJson(route('admin.analysis.machine-reports', [
'company_id' => $this->company->id,
'payment_types' => '1'
]));
$response->assertStatus(200);
$data = $response->json();
// 應只有 Order 1 的資料 (Machine A)
$this->assertEquals(2, $data['summary']['total_quantity']);
$this->assertEquals(30.0, $data['summary']['total_sales']);
$this->assertCount(1, $data['table_data']);
$this->assertEquals($this->machineA->id, $data['table_data'][0]['machine_id']);
}
/**
* 測試商品報表中的機台過濾功能
*/
public function test_product_reports_machine_filter_works()
{
$this->actingAs($this->superAdmin);
// 篩選 Machine B
$response = $this->getJson(route('admin.analysis.product-reports', [
'company_id' => $this->company->id,
'machine_id' => $this->machineB->id
]));
$response->assertStatus(200);
$data = $response->json();
// Machine B 上面只有 Product Y (Soda)
$this->assertEquals(1, $data['summary']['total_quantity']);
$this->assertEquals(25.0, $data['summary']['total_sales']);
$this->assertCount(1, $data['table_data']);
$this->assertEquals($this->productY->id, $data['table_data'][0]['product_id']);
}
}