CaiWuXiTong/tests/Feature/Business/ProductCatalogPageTest.php
WuYongJie 342cdd1b99 整合报价计费规则与独立成本价观测,保留现有发布保护
Integrate feature/limeng-stock-quote-billing-rules at bca3c074 and port the snapshot purpose of feature/jushuitan-cost-price-snapshot at 4c2c422c onto current main. Keep authoritative operation history and accounting readers separate from observations; retain deployment guards and existing source data.
2026-10-20 19:07:57 +08:00

560 lines
24 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',
'price_change_history' => [
[
'id' => 12,
'record_type' => '1',
'record_type_label' => '变价',
'before_price' => 490,
'after_price' => 475,
'diff_amount' => -15,
'effect_start_time' => '2026-08-11 00:00:00',
'effect_end_time' => null,
'created_at' => '2026-08-10 09:30:00',
],
[
'id' => 3,
'record_type' => '0',
'record_type_label' => '定价',
'before_price' => null,
'after_price' => 490,
'diff_amount' => null,
'effect_start_time' => '2026-01-01 00:00:00',
'effect_end_time' => '2026-08-10 00:00:00',
'created_at' => '2025-12-31 08:00:00',
],
],
'price_change_history_count' => 2,
'work_platform_price_history_count' => 2,
'jushuitan_cost_price_history' => [[
'source' => '聚水潭商品及库存管理',
'record_type_label' => '成本价变更',
'cost_price' => 500,
'before_price' => 480,
'after_price' => 500,
'diff_amount' => 20,
'effect_start_time' => '2026-08-12',
'effect_end_time' => null,
'remark' => '采购价:480->500',
'operator' => '简尚',
'operation' => '商品与库存管理导入',
]],
'jushuitan_cost_price_history_count' => 1,
'jushuitan_cost_price_observations' => [[
'record_type_label' => '观测到成本价变化', 'before_price' => 499, 'after_price' => 500,
'effect_start_time' => '2026-10-20 12:00:00', 'remark' => '发现时间,不是真实改价时间',
]],
'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',
],
'jushuitan_cost_price_history_sync_status' => [
'code' => 'failed',
'initialized' => true,
'last_succeeded_at' => '2026-09-15 00:05: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('普通商品资料')
->assertSeeText('聚水潭成本价历史最近同步失败')
->assertSeeText('观测 1 条(最近20条)')
->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('变价前价格')
->assertSeeText('变价后价格')
->assertSeeText('2 条')
->assertSeeText('1 条')
->assertSee('data-price-history-trigger', false)
->assertSeeText('价格记录')
->assertSeeText('当前成本价')
->assertSeeText('当前工作平台价格')
->assertSeeText('导出价格记录')
->assertSeeText('生效结束日期')
->assertSeeText('记录时间')
->assertSeeText('操作方式')
->assertSeeText('操作人')
->assertSee('"remark":"采购价:480-\\u003E500"', false)
->assertSee('简尚', false)
->assertSee('商品与库存管理导入', false)
->assertSee('"record_type_label":"变价"', false)
->assertSee('"record_type_label":"定价"', false)
->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_page_does_not_report_zero_jushuitan_history_before_initial_sync(): void
{
$this->grantAdminPermission();
$state = [
'status' => 'succeeded',
'initial_completed_at' => '2026-08-14 21:15:36',
'last_succeeded_at' => '2026-09-15 10:00:00',
'mirror_row_count' => 558035,
];
$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' => 2,
]);
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('paginate')->once()->andReturn([
'data' => [(object) [
'autoid' => 15961,
'sku_id' => '15961G',
'name' => '测试商品',
'cost_price' => 65,
'jushuitan_cost_price_history' => [],
'jushuitan_cost_price_history_count' => 0,
]],
'total' => 1,
'jushuitan_cost_price_history_sync_status' => [
'code' => 'initial_failed',
'initialized' => false,
'last_succeeded_at' => null,
],
]);
$this->app->instance(ProductCatalogMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'product-inventory',
'page' => 'normal-products',
'sku_id' => '15961G',
]))
->assertOk()
->assertSee('data-jushuitan-cost-price-history-sync-state="initial_failed"', false)
->assertSeeText('聚水潭成本价记录首次同步失败')
->assertSeeText('当前记录数量不代表聚水潭真实历史')
->assertSeeText('待同步')
->assertSee('title="聚水潭成本价记录尚未完成首次同步"', false);
}
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();
}
}