353 lines
16 KiB
PHP
353 lines
16 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Business;
|
|
|
|
use App\Models\Auth\Permission;
|
|
use Modules\EcommerceFinance\Repositories\NonJingxiRefundMirrorRepository;
|
|
use Tests\Feature\FeatureTestCase;
|
|
|
|
class NonJingxiRefundPageTest extends FeatureTestCase
|
|
{
|
|
protected function grantAdminPermission(): void
|
|
{
|
|
$this->user->attachPermission(
|
|
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
|
);
|
|
}
|
|
|
|
public function test_page_renders_mirror_rows_second_page_and_filters(): void
|
|
{
|
|
$this->grantAdminPermission();
|
|
setting()->set('default.list_limit', 25);
|
|
|
|
$states = [
|
|
NonJingxiRefundMirrorRepository::INFO_DATASET => ['status' => 'running', 'initial_completed_at' => '2026-08-15 00:00:00'],
|
|
NonJingxiRefundMirrorRepository::ITEM_DATASET => ['status' => 'running', 'initial_completed_at' => '2026-08-15 00:00:00'],
|
|
];
|
|
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
|
|
$repository->shouldReceive('available')->once()->andReturnTrue();
|
|
$repository->shouldReceive('states')->once()->andReturn($states);
|
|
$repository->shouldReceive('syncStatus')->once()->with($states)->andReturn([
|
|
'code' => 'syncing',
|
|
'last_succeeded_at' => '2026-08-21 12:00:00',
|
|
]);
|
|
$repository->shouldReceive('ready')->once()->andReturnTrue();
|
|
$repository->shouldReceive('queryReady')->once()->andReturnTrue();
|
|
$repository->shouldReceive('paginate')->once()->withArgs(
|
|
static fn (array $filters, int $page, int $limit): bool =>
|
|
$filters === [
|
|
'from' => '2026-08-01',
|
|
'to' => '2026-08-21',
|
|
'order_no' => 'ORDER-1',
|
|
'shop_name' => '测试店',
|
|
'sku_id' => 'SKU-1',
|
|
] && $page === 2 && $limit === 25
|
|
)->andReturn([
|
|
'data' => [(object) [
|
|
'Id' => 102,
|
|
'refundDate' => '2026-08-20 10:00:00',
|
|
'as_id' => 2002,
|
|
'outer_as_id' => 'AS-2002',
|
|
'o_id' => 3002,
|
|
'so_id' => 'ORDER-1',
|
|
'type' => '退款',
|
|
'node' => '完成',
|
|
'isZC' => '是',
|
|
'shop_name' => '测试店',
|
|
'platform' => '天猫',
|
|
'responsibleDepartment' => '电商部',
|
|
'totalRefundMoney' => 99.5,
|
|
'originPostcode' => 'ZIP',
|
|
'shop_i_id' => 'ITEM-1',
|
|
'shop_sku_id' => 'SHOP-SKU-1',
|
|
'i_id' => 'STYLE-1',
|
|
'sku_id' => 'SKU-1',
|
|
'buyer_paid_amount' => 120,
|
|
'qty' => 1,
|
|
'unitCostGoods' => 50,
|
|
'detailRefundMoney' => 99.5,
|
|
'refundType' => '退货退款',
|
|
'refundQty' => 1,
|
|
'refundCostGoods' => 50,
|
|
'RefundMoneyIncome' => 99.5,
|
|
'AfSalesMoney' => 99.5,
|
|
'createTime' => '2026-08-20 09:00:00',
|
|
'status' => '完成',
|
|
'shop_status' => '完成',
|
|
'created' => '2026-08-20 09:00:00',
|
|
'confirm_date' => '2026-08-20 10:00:00',
|
|
]],
|
|
'total' => 26,
|
|
]);
|
|
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
|
|
|
|
$url = route('business.pages.show', [
|
|
'company_id' => $this->company->id,
|
|
'section' => 'refunds',
|
|
'page' => 'non-jingxi-refund-data',
|
|
]) . '?' . http_build_query([
|
|
'from' => '2026-08-01', 'to' => '2026-08-21', 'order_no' => 'ORDER-1',
|
|
'shop_name' => '测试店', 'sku_id' => 'SKU-1', 'page' => 2,
|
|
]);
|
|
|
|
$this->loginAs()->get($url)
|
|
->assertOk()
|
|
->assertSee('data-filter-contract="non-jingxi-refund-data.filters"', false)
|
|
->assertSee('data-sync-state="syncing"', false)
|
|
->assertSeeText('业务镜像同步中,当前显示最近成功数据')
|
|
->assertSeeText('AS-2002')
|
|
->assertSeeText('SKU-1')
|
|
->assertSeeText('99.50')
|
|
->assertSeeText('共 26 条记录')
|
|
->assertSeeText('第 2 / 2 页')
|
|
->assertSeeText('导出筛选结果')
|
|
->assertSee('workspace/refunds/non-jingxi-refund-data/export', false)
|
|
->assertSee('data-finance-export-base=', false)
|
|
->assertSee('data-export-field="refundDate"', false)
|
|
->assertSee('value="2026-08-01"', false)
|
|
->assertSee('value="ORDER-1"', false);
|
|
}
|
|
|
|
public function test_refund_export_downloads_filtered_csv(): void
|
|
{
|
|
$this->grantAdminPermission();
|
|
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
|
|
$repository->shouldReceive('available')->once()->andReturnTrue();
|
|
$repository->shouldReceive('ready')->once()->andReturnTrue();
|
|
$repository->shouldReceive('queryReady')->once()->andReturnTrue();
|
|
$repository->shouldReceive('exportRows')->once()->with([
|
|
'from' => '2026-08-01',
|
|
'to' => '2026-08-21',
|
|
'order_no' => 'ORDER-1',
|
|
'shop_name' => '测试店',
|
|
'sku_id' => 'SKU-1',
|
|
])->andReturn([(object) [
|
|
'Id' => 102,
|
|
'refundDate' => '2026-08-20 10:00:00',
|
|
'as_id' => 2002,
|
|
'outer_as_id' => 'AS-2002',
|
|
'o_id' => 3002,
|
|
'so_id' => 'ORDER-1',
|
|
'type' => '退款',
|
|
'node' => '完成',
|
|
'isZC' => '是',
|
|
'shop_name' => '测试店',
|
|
'platform' => '天猫',
|
|
'responsibleDepartment' => '电商部',
|
|
'totalRefundMoney' => 99.5,
|
|
'originPostcode' => 'ZIP',
|
|
'shop_i_id' => 'ITEM-1',
|
|
'shop_sku_id' => 'SHOP-SKU-1',
|
|
'i_id' => 'STYLE-1',
|
|
'sku_id' => 'SKU-1',
|
|
'buyer_paid_amount' => 120,
|
|
'qty' => 1,
|
|
'unitCostGoods' => 50,
|
|
'detailRefundMoney' => 99.5,
|
|
'refundType' => '退货退款',
|
|
'refundQty' => 1,
|
|
'refundCostGoods' => 50,
|
|
'RefundMoneyIncome' => 99.5,
|
|
'AfSalesMoney' => 99.5,
|
|
'createTime' => '2026-08-20 09:00:00',
|
|
'status' => '完成',
|
|
'shop_status' => '完成',
|
|
'created' => '2026-08-20 09:00:00',
|
|
'confirm_date' => '2026-08-20 10:00:00',
|
|
]]);
|
|
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
|
|
|
|
$response = $this->loginAs()->get(route('business.refunds.non-jingxi-refund-data.export', [
|
|
'from' => '2026-08-01',
|
|
'to' => '2026-08-21',
|
|
'order_no' => 'ORDER-1',
|
|
'shop_name' => '测试店',
|
|
'sku_id' => 'SKU-1',
|
|
]));
|
|
|
|
$response->assertOk()->assertDownload('after_sales_refunds_2026-08-01_2026-08-21.csv');
|
|
$content = $response->streamedContent();
|
|
$this->assertStringStartsWith("\xEF\xBB\xBF", $content);
|
|
$this->assertStringContainsString('序号/主键ID', $content);
|
|
$this->assertStringContainsString('AS-2002', $content);
|
|
$this->assertStringContainsString('99.5', $content);
|
|
}
|
|
|
|
public function test_refund_export_accepts_only_declared_columns_in_requested_order(): void
|
|
{
|
|
$this->grantAdminPermission();
|
|
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
|
|
$repository->shouldReceive('available')->once()->andReturnTrue();
|
|
$repository->shouldReceive('ready')->once()->andReturnTrue();
|
|
$repository->shouldReceive('queryReady')->once()->andReturnTrue();
|
|
$repository->shouldReceive('exportRows')->once()->andReturn([(object) [
|
|
'shop_name' => '测试店',
|
|
'refundDate' => '2026-08-20',
|
|
'sku_id' => 'SKU-1',
|
|
]]);
|
|
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
|
|
|
|
$response = $this->loginAs()->get(route('business.refunds.non-jingxi-refund-data.export', [
|
|
'columns' => 'shop_name,refundDate,not_a_real_field',
|
|
]));
|
|
|
|
$response->assertOk();
|
|
$content = $response->streamedContent();
|
|
$header = strtok($content, "\r\n");
|
|
$this->assertSame("\xEF\xBB\xBF店铺名称,退款日期", $header);
|
|
$this->assertStringContainsString('测试店,2026-08-20', $content);
|
|
$this->assertStringNotContainsString('SKU编码', $content);
|
|
}
|
|
|
|
public function test_page_does_not_report_empty_data_before_initial_sync_finishes(): void
|
|
{
|
|
$this->grantAdminPermission();
|
|
|
|
$states = [
|
|
NonJingxiRefundMirrorRepository::INFO_DATASET => ['status' => 'running'],
|
|
NonJingxiRefundMirrorRepository::ITEM_DATASET => ['status' => 'running'],
|
|
];
|
|
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
|
|
$repository->shouldReceive('available')->once()->andReturnTrue();
|
|
$repository->shouldReceive('states')->once()->andReturn($states);
|
|
$repository->shouldReceive('syncStatus')->once()->with($states)->andReturn([
|
|
'code' => 'initializing',
|
|
'last_succeeded_at' => null,
|
|
]);
|
|
$repository->shouldReceive('ready')->once()->andReturnFalse();
|
|
$repository->shouldNotReceive('queryReady');
|
|
$repository->shouldNotReceive('paginate');
|
|
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
|
|
|
|
$this->loginAs()->get(route('business.pages.show', [
|
|
'section' => 'refunds',
|
|
'page' => 'non-jingxi-refund-data',
|
|
]))
|
|
->assertOk()
|
|
->assertSeeText('业务镜像正在执行首次同步')
|
|
->assertSeeText('首次同步尚未完成,完成后将自动显示数据。')
|
|
->assertDontSeeText('当前筛选范围暂无数据。');
|
|
}
|
|
|
|
public function test_page_shows_a_real_empty_filter_result(): void
|
|
{
|
|
$this->grantAdminPermission();
|
|
|
|
$states = [
|
|
NonJingxiRefundMirrorRepository::INFO_DATASET => ['status' => 'succeeded', 'initial_completed_at' => '2026-08-15 00:00:00'],
|
|
NonJingxiRefundMirrorRepository::ITEM_DATASET => ['status' => 'succeeded', 'initial_completed_at' => '2026-08-15 00:00:00'],
|
|
];
|
|
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
|
|
$repository->shouldReceive('available')->once()->andReturnTrue();
|
|
$repository->shouldReceive('states')->once()->andReturn($states);
|
|
$repository->shouldReceive('syncStatus')->once()->andReturn(['code' => 'healthy', 'last_succeeded_at' => '2026-08-21 12:00:00']);
|
|
$repository->shouldReceive('ready')->once()->andReturnTrue();
|
|
$repository->shouldReceive('queryReady')->once()->andReturnTrue();
|
|
$repository->shouldReceive('paginate')->once()->andReturn(['data' => [], 'total' => 0]);
|
|
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
|
|
|
|
$this->loginAs()->get(route('business.pages.show', [
|
|
'section' => 'refunds',
|
|
'page' => 'non-jingxi-refund-data',
|
|
'sku_id' => 'NOT-FOUND',
|
|
]))
|
|
->assertOk()
|
|
->assertSeeText('当前筛选范围暂无数据。')
|
|
->assertSeeText('共 0 条记录')
|
|
->assertSee('is-disabled', false)
|
|
->assertSeeText('导出筛选结果');
|
|
}
|
|
|
|
public function test_page_does_not_run_the_query_before_required_indexes_exist(): void
|
|
{
|
|
$this->grantAdminPermission();
|
|
|
|
$states = [
|
|
NonJingxiRefundMirrorRepository::INFO_DATASET => ['status' => 'running', 'initial_completed_at' => '2026-08-15 00:00:00'],
|
|
NonJingxiRefundMirrorRepository::ITEM_DATASET => ['status' => 'running', 'initial_completed_at' => '2026-08-15 00:00:00'],
|
|
];
|
|
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
|
|
$repository->shouldReceive('available')->once()->andReturnTrue();
|
|
$repository->shouldReceive('states')->once()->andReturn($states);
|
|
$repository->shouldReceive('syncStatus')->once()->with($states)->andReturn([
|
|
'code' => 'syncing',
|
|
'last_succeeded_at' => '2026-08-17 10:05:07',
|
|
]);
|
|
$repository->shouldReceive('ready')->once()->andReturnTrue();
|
|
$repository->shouldReceive('queryReady')->once()->andReturnFalse();
|
|
$repository->shouldNotReceive('paginate');
|
|
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
|
|
|
|
$this->loginAs();
|
|
$url = route('business.pages.show', [
|
|
'section' => 'refunds',
|
|
'page' => 'non-jingxi-refund-data',
|
|
]).'?page=2';
|
|
|
|
$this->get($url)
|
|
->assertOk()
|
|
->assertSee('upstream-data-alert--warning', false)
|
|
->assertSeeText('页面查询索引正在准备,生产同步轮转完成后将自动显示数据。')
|
|
->assertSeeText('页面查询索引尚未就绪,当前不会执行高压力查询。')
|
|
->assertSeeText('第 1 / 1 页')
|
|
->assertDontSeeText('第 2 / 1 页')
|
|
->assertDontSeeText('当前筛选范围暂无数据。');
|
|
}
|
|
|
|
public function test_page_replaces_invalid_dates_with_the_default_range(): void
|
|
{
|
|
$this->grantAdminPermission();
|
|
$this->travelTo('2026-08-21 12:00:00');
|
|
|
|
$states = [
|
|
NonJingxiRefundMirrorRepository::INFO_DATASET => ['status' => 'succeeded', 'initial_completed_at' => '2026-08-15 00:00:00'],
|
|
NonJingxiRefundMirrorRepository::ITEM_DATASET => ['status' => 'succeeded', 'initial_completed_at' => '2026-08-15 00:00:00'],
|
|
];
|
|
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
|
|
$repository->shouldReceive('available')->once()->andReturnTrue();
|
|
$repository->shouldReceive('states')->once()->andReturn($states);
|
|
$repository->shouldReceive('syncStatus')->once()->with($states)->andReturn([
|
|
'code' => 'healthy',
|
|
'last_succeeded_at' => '2026-08-21 12:00:00',
|
|
]);
|
|
$repository->shouldReceive('ready')->once()->andReturnTrue();
|
|
$repository->shouldReceive('queryReady')->once()->andReturnTrue();
|
|
$repository->shouldReceive('paginate')->once()->withArgs(
|
|
static fn (array $filters, int $page, int $limit): bool =>
|
|
$filters['from'] === '2026-08-15'
|
|
&& $filters['to'] === '2026-08-21'
|
|
&& $page === 1
|
|
&& $limit === 25
|
|
)->andReturn(['data' => [], 'total' => 0]);
|
|
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
|
|
|
|
$this->loginAs()->get(route('business.pages.show', [
|
|
'section' => 'refunds',
|
|
'page' => 'non-jingxi-refund-data',
|
|
'from' => '2026-02-31',
|
|
'to' => 'not-a-date',
|
|
]))
|
|
->assertOk()
|
|
->assertSee('value="2026-08-15"', false)
|
|
->assertSee('value="2026-08-21"', false)
|
|
->assertDontSee('非京喜退款数据暂时无法读取,请稍后重试。');
|
|
}
|
|
|
|
public function test_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' => 'refunds',
|
|
'page' => 'non-jingxi-refund-data',
|
|
]))->assertForbidden();
|
|
}
|
|
}
|