369 lines
15 KiB
PHP
369 lines
15 KiB
PHP
<?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,
|
||
'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' => '新北供应商',
|
||
'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',
|
||
],
|
||
]);
|
||
$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('1688代发单价')
|
||
->assertSeeText('数据来源')
|
||
->assertSeeText('商品及库存管理(普通商品资料)')
|
||
->assertSeeText('1688代发单价')
|
||
->assertSeeText('65.00')
|
||
->assertSeeText('导出')
|
||
->assertDontSeeText('1688代发单价尚未完成首次同步')
|
||
->assertSeeText('正常同步')
|
||
->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,
|
||
'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('商品及库存管理(普通商品资料);1688代发单价', $content);
|
||
$this->assertStringContainsString('SKU-001', $content);
|
||
}
|
||
|
||
public function test_normal_products_page_marks_purchase_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,
|
||
'purchase_price_sync_status' => [
|
||
'code' => 'pending',
|
||
'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',
|
||
]))
|
||
->assertOk()
|
||
->assertSee('data-purchase-price-sync-state="pending"', false)
|
||
->assertSeeText('1688代发单价尚未完成首次同步')
|
||
->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();
|
||
}
|
||
}
|