CaiWuXiTong/tests/Feature/Business/ProductCatalogPageTest.php

440 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use Modules\EcommerceFinance\Repositories\ProductCatalogMirrorRepository;
use Tests\Feature\FeatureTestCase;
class ProductCatalogPageTest extends FeatureTestCase
{
protected function grantAdminPermission(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
}
public function test_normal_products_page_renders_mirror_rows_and_filters(): void
{
$this->grantAdminPermission();
$state = [
'status' => 'succeeded',
'last_succeeded_at' => '2026-08-15 17:51:03',
'last_full_succeeded_at' => '2026-08-10 03:15:00',
'mirror_row_count' => 547217,
];
$repository = \Mockery::mock(ProductCatalogMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('state')->once()->andReturn($state);
$repository->shouldReceive('syncStatus')->once()->with($state)->andReturn([
'code' => 'healthy',
'lag_minutes' => 4,
]);
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('paginate')
->once()
->withArgs(static function (array $filters, int $page, int $limit): bool {
return $filters['i_id'] === 'STYLE-001'
&& $filters['sku_id'] === 'SKU-001'
&& $filters['keyword'] === '胡桃'
&& $filters['enabled'] === '1'
&& $filters['stock_disabled'] === '0'
&& $filters['sale_price_min'] === '800'
&& $filters['sale_price_max'] === '900'
&& $page === 1
&& $limit <= 50;
})
->andReturn([
'data' => [(object) [
'autoid' => 1001,
'i_id' => 'STYLE-001',
'sku_id' => 'SKU-001',
'name' => '岩板餐桌',
'short_name' => '餐桌',
'properties_value' => '1.4米',
'color' => '胡桃色',
'sale_price' => 899,
'cost_price' => 500,
'purchase_price' => 480,
'work_platform_price' => 475,
'work_platform_price_effect_time' => '2026-08-11 00:00:00',
'pricing_status' => '已定价',
'price_change_before_price' => 490,
'price_change_after_price' => 475,
'price_change_diff_amount' => -15,
'price_change_effect_time' => '2026-08-11 00:00:00',
'purchase_order_unit_price' => 65,
'purchase_order_finished_at' => '2026-08-20 11:03:05',
'purchase_order_id' => 827380,
'data_sources' => ['商品及库存管理(普通商品资料)', '工作平台价格', '1688代发单价'],
'market_price' => 1099,
'brand' => '北木',
'category' => '住宅家具',
'supplier_name' => '新北供应商',
'weight' => 4.32,
'l' => 49,
'w' => 18.5,
'h' => 10.5,
'volume' => 9518.25,
'unit' => '个',
'board_material' => '正常板材',
'product_type' => '成品',
'creator_id' => 19203883,
'enabled' => 1,
'stock_disabled' => 0,
'modified' => '2026-08-15 10:00:00',
]],
'total' => 1,
'purchase_price_sync_status' => [
'code' => 'ready',
'initialized' => true,
'last_succeeded_at' => '2026-08-21 12:00:00',
],
'work_platform_price_sync_status' => [
'code' => 'ready',
'initialized' => true,
'last_succeeded_at' => '2026-09-01 03:02:00',
],
'price_change_sync_status' => [
'code' => 'ready',
'initialized' => true,
'last_succeeded_at' => '2026-09-01 03:02:00',
],
]);
$this->app->instance(ProductCatalogMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'product-inventory',
'page' => 'normal-products',
'i_id' => 'STYLE-001',
'sku_id' => 'SKU-001',
'keyword' => '胡桃',
'enabled' => '1',
'stock_disabled' => '0',
'sale_price_min' => '900',
'sale_price_max' => '800',
]));
$response
->assertOk()
->assertSee('data-product-catalog-page', false)
->assertSeeText('普通商品资料')
->assertSee('data-sync-state="healthy"', false)
->assertSeeText('同步正常')
->assertSeeText('最近同步 2026-08-15 17:51:03')
->assertSeeText('数据延迟 4 分钟')
->assertSeeText('最近校准 2026-08-10 03:15:00')
->assertSeeText('STYLE-001')
->assertSeeText('SKU-001')
->assertSeeText('岩板餐桌')
->assertSeeText('899.00')
->assertSeeText('普通商品采购价')
->assertSeeText('工作平台价格')
->assertSeeText('475.00')
->assertSeeText('2026-08-11')
->assertSeeText('已定价')
->assertSeeText('变价前价格')
->assertSeeText('490.00')
->assertSeeText('变价后价格')
->assertSeeText('差额')
->assertSeeText('-15.00')
->assertSeeText('数据来源')
->assertSeeText('商品及库存管理(普通商品资料)')
->assertSeeText('1688代发单价')
->assertDontSee('<th class="is-numeric">1688代发单价</th>', false)
->assertDontSeeText('65.00')
->assertSeeText('导出')
->assertDontSeeText('工作平台价格尚未完成首次同步')
->assertDontSeeText('商品价格变价记录尚未完成首次同步')
->assertSeeText('正常同步')
->assertSeeText('重量(kg)')
->assertSeeText('4.32')
->assertSeeText('长(cm)')
->assertSeeText('49.00')
->assertSeeText('体积(cm³)')
->assertSeeText('9,518.25')
->assertSeeText('正常板材')
->assertSeeText('成品')
->assertSeeText('19203883')
->assertDontSee('pic_url');
}
public function test_normal_products_export_uses_all_filters_and_includes_data_source_column(): void
{
$this->grantAdminPermission();
$repository = \Mockery::mock(ProductCatalogMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('exportRows')
->once()
->withArgs(static function (array $filters): bool {
return $filters['sku_id'] === 'SKU-001'
&& $filters['supplier_name'] === '新北供应商'
&& $filters['sale_price_min'] === '800'
&& $filters['sale_price_max'] === '900';
})
->andReturn((static function (): \Generator {
yield (object) [
'autoid' => 1001,
'i_id' => 'STYLE-001',
'sku_id' => 'SKU-001',
'name' => '岩板餐桌',
'short_name' => '餐桌',
'properties_value' => '1.4米',
'color' => '胡桃色',
'brand' => '北木',
'category' => '住宅家具',
'supplier_name' => '新北供应商',
'sale_price' => 899,
'cost_price' => 500,
'purchase_price' => 480,
'work_platform_price' => 475,
'work_platform_price_effect_time' => '2026-08-11 00:00:00',
'pricing_status' => '已定价',
'price_change_before_price' => 490,
'price_change_after_price' => 475,
'price_change_diff_amount' => -15,
'price_change_effect_time' => '2026-08-11 00:00:00',
'purchase_order_unit_price' => 65,
'data_sources' => ['商品及库存管理(普通商品资料)', '工作平台价格', '1688代发单价'],
'market_price' => 1099,
'enabled' => 1,
'stock_disabled' => 0,
'modified' => '2026-08-15 10:00:00',
];
})());
$this->app->instance(ProductCatalogMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route('business.product-inventory.normal-products.export', [
'sku_id' => 'SKU-001',
'supplier_name' => '新北供应商',
'sale_price_min' => '900',
'sale_price_max' => '800',
]));
$response->assertOk();
$this->assertStringContainsString('attachment;', (string) $response->headers->get('content-disposition'));
$content = $response->streamedContent();
$this->assertStringStartsWith(chr(0xEF) . chr(0xBB) . chr(0xBF), $content);
$this->assertStringContainsString('数据来源', $content);
$this->assertStringContainsString('工作平台价格', $content);
$this->assertStringContainsString('475', $content);
$this->assertStringContainsString('定价状态', $content);
$this->assertStringContainsString('已定价', $content);
$this->assertStringContainsString('变价前价格', $content);
$this->assertStringContainsString('490', $content);
$this->assertStringContainsString('变价后价格', $content);
$stream = fopen('php://memory', 'r+');
fwrite($stream, $content);
rewind($stream);
$header = fgetcsv($stream);
fclose($stream);
$this->assertIsArray($header);
$this->assertNotContains('1688代发单价', $header);
$this->assertStringContainsString('商品及库存管理(普通商品资料);工作平台价格;1688代发单价', $content);
$this->assertStringContainsString('SKU-001', $content);
}
public function test_normal_products_page_marks_work_platform_prices_as_not_yet_synchronized(): void
{
$this->grantAdminPermission();
$state = [
'status' => 'succeeded',
'initial_completed_at' => '2026-08-14 21:15:36',
'last_succeeded_at' => '2026-08-21 12:00:00',
'mirror_row_count' => 547217,
];
$repository = \Mockery::mock(ProductCatalogMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('state')->once()->andReturn($state);
$repository->shouldReceive('syncStatus')->once()->with($state)->andReturn([
'code' => 'healthy',
'lag_minutes' => 1,
]);
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('paginate')->once()->andReturn([
'data' => [],
'total' => 0,
'work_platform_price_sync_status' => [
'code' => 'pending',
'initialized' => false,
'last_succeeded_at' => null,
],
'price_change_sync_status' => [
'code' => 'ready',
'initialized' => true,
'last_succeeded_at' => '2026-09-01 03:02:00',
],
]);
$this->app->instance(ProductCatalogMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'product-inventory',
'page' => 'normal-products',
]))
->assertOk()
->assertSee('data-work-platform-price-sync-state="pending"', false)
->assertSeeText('工作平台价格尚未完成首次同步')
->assertSeeText('当前仅显示普通商品采购价');
}
public function test_normal_products_page_does_not_treat_initial_sync_as_empty_data(): void
{
$this->grantAdminPermission();
$state = ['status' => 'running'];
$repository = \Mockery::mock(ProductCatalogMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('state')->once()->andReturn($state);
$repository->shouldReceive('syncStatus')->once()->with($state)->andReturn([
'code' => 'initializing',
'lag_minutes' => null,
]);
$repository->shouldReceive('ready')->once()->andReturnFalse();
$repository->shouldNotReceive('paginate');
$this->app->instance(ProductCatalogMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'product-inventory',
'page' => 'normal-products',
]))
->assertOk()
->assertSeeText('普通商品资料正在执行首次全量同步')
->assertSeeText('首次同步中')
->assertDontSeeText('当前镜像中没有普通商品资料。');
}
public function test_normal_products_page_reports_an_unavailable_mirror(): void
{
$this->grantAdminPermission();
$repository = \Mockery::mock(ProductCatalogMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnFalse();
$repository->shouldNotReceive('state');
$repository->shouldNotReceive('syncStatus');
$repository->shouldNotReceive('ready');
$repository->shouldNotReceive('paginate');
$this->app->instance(ProductCatalogMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'product-inventory',
'page' => 'normal-products',
]))
->assertOk()
->assertSeeText('共享业务镜像尚未建立')
->assertDontSeeText('当前镜像中没有普通商品资料。');
}
public function test_normal_products_page_keeps_old_rows_visible_after_a_sync_failure(): void
{
$this->grantAdminPermission();
$state = [
'status' => 'failed',
'run_mode' => 'incremental',
'initial_completed_at' => '2026-08-14 21:15:36',
'last_succeeded_at' => '2026-08-15 17:51:03',
'mirror_row_count' => 547217,
];
$repository = \Mockery::mock(ProductCatalogMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('state')->once()->andReturn($state);
$repository->shouldReceive('syncStatus')->once()->with($state)->andReturn([
'code' => 'failed',
'lag_minutes' => 18,
]);
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('paginate')->once()->andReturn([
'data' => [(object) [
'autoid' => 1001,
'i_id' => 'STYLE-OLD',
'sku_id' => 'SKU-OLD',
'name' => '最近成功同步的商品',
'short_name' => null,
'properties_value' => null,
'color' => null,
'sale_price' => 100,
'cost_price' => null,
'purchase_price' => null,
'market_price' => null,
'brand' => null,
'category' => null,
'supplier_name' => null,
'enabled' => 1,
'stock_disabled' => 0,
'modified' => '2026-08-15 17:45:00',
]],
'total' => 1,
]);
$this->app->instance(ProductCatalogMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'product-inventory',
'page' => 'normal-products',
]))
->assertOk()
->assertSee('data-sync-state="failed"', false)
->assertSeeText('同步失败,显示最近数据')
->assertSeeText('最近成功同步的商品');
}
public function test_normal_products_export_reports_an_unavailable_mirror(): void
{
$this->grantAdminPermission();
$repository = \Mockery::mock(ProductCatalogMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnFalse();
$repository->shouldNotReceive('ready');
$repository->shouldNotReceive('exportRows');
$this->app->instance(ProductCatalogMirrorRepository::class, $repository);
$this->withExceptionHandling()
->loginAs()
->get(route('business.product-inventory.normal-products.export'))
->assertServiceUnavailable();
}
public function test_normal_products_export_rejects_users_without_sidebar_permission(): void
{
$subject = 'user_' . $this->user->id;
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
setting()->set('system.sidebar_permissions', json_encode([
'configured' => [$subject],
'subjects' => [
$subject => ['company-management/basic-information'],
],
], JSON_UNESCAPED_UNICODE));
$this->withExceptionHandling()
->loginAs()
->get(route('business.product-inventory.normal-products.export'))
->assertForbidden();
}
public function test_normal_products_page_rejects_users_without_sidebar_permission(): void
{
$subject = 'user_' . $this->user->id;
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
setting()->set('system.sidebar_permissions', json_encode([
'configured' => [$subject],
'subjects' => [
$subject => ['company-management/basic-information'],
],
], JSON_UNESCAPED_UNICODE));
$this->withExceptionHandling()
->loginAs()
->get(route('business.pages.show', [
'section' => 'product-inventory',
'page' => 'normal-products',
]))
->assertForbidden();
}
}