442 lines
21 KiB
PHP
442 lines
21 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,
|
|
]);
|
|
|
|
$response = $this->loginAs()->get($url);
|
|
$response
|
|
->assertOk()
|
|
->assertSee('data-filter-contract="non-jingxi-refund-data.filters"', false)
|
|
->assertSee('data-sync-state="syncing"', false)
|
|
->assertSeeText('业务镜像同步中,当前显示最近成功数据')
|
|
->assertSeeText('AS-2002')
|
|
->assertSeeText('业务来源')
|
|
->assertSeeText('商品明细')
|
|
->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);
|
|
|
|
$this->assertSame(1, preg_match('/data-finance-export-query="([^"]+)"/', $response->getContent(), $exportQuery));
|
|
$this->assertSame([
|
|
'from' => '2026-08-01', 'to' => '2026-08-21', 'order_no' => 'ORDER-1',
|
|
'shop_name' => '测试店', 'sku_id' => 'SKU-1',
|
|
], json_decode(html_entity_decode($exportQuery[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'), true, 512, JSON_THROW_ON_ERROR));
|
|
}
|
|
|
|
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_removes_dates_before_2026_without_changing_other_filters(): void
|
|
{
|
|
$this->grantAdminPermission();
|
|
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
|
|
$repository->shouldReceive('available', 'ready', 'queryReady')->andReturnTrue();
|
|
$repository->shouldReceive('states')->andReturn([]);
|
|
$repository->shouldReceive('syncStatus')->andReturn(['code' => 'healthy', 'last_succeeded_at' => null]);
|
|
$repository->shouldReceive('paginate')->once()->withArgs(
|
|
static fn (array $filters): bool => $filters['from'] === '2026-01-01'
|
|
&& $filters['to'] === '2026-08-21' && $filters['shop_name'] === '测试店'
|
|
)->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' => '2025-01-01', 'to' => '2026-08-21', 'shop_name' => '测试店',
|
|
]))->assertOk()->assertSee('min="2026-01-01"', false)
|
|
->assertSee('value="2026-01-01"', false);
|
|
}
|
|
|
|
public function test_export_clamps_the_date_and_sends_headers_before_consuming_all_rows(): void
|
|
{
|
|
$this->grantAdminPermission();
|
|
$finished = false;
|
|
$rows = (static function () use (&$finished): \Generator {
|
|
yield (object) ['refundDate' => '2026-01-01 00:00:00'];
|
|
yield (object) ['refundDate' => '2026-01-02 00:00:00'];
|
|
$finished = true;
|
|
})();
|
|
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
|
|
$repository->shouldReceive('available', 'ready', 'queryReady')->andReturnTrue();
|
|
$repository->shouldReceive('exportRows')->once()->withArgs(
|
|
static fn (array $filters): bool => $filters['from'] === '2026-01-01' && $filters['to'] === '2026-01-02'
|
|
)->andReturn($rows);
|
|
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
|
|
|
|
$response = $this->loginAs()->get(route('business.refunds.non-jingxi-refund-data.export', [
|
|
'from' => '2025-12-01', 'to' => '2026-01-02', 'columns' => 'refundDate',
|
|
]));
|
|
$response->assertOk()->assertDownload('after_sales_refunds_2026-01-01_2026-01-02.csv')
|
|
->assertHeader('X-Accel-Buffering', 'no');
|
|
$this->assertFalse($finished);
|
|
$content = $response->streamedContent();
|
|
$this->assertTrue($finished);
|
|
$this->assertStringContainsString('2026-01-01 00:00:00', $content);
|
|
$this->assertStringContainsString('2026-01-02 00:00:00', $content);
|
|
}
|
|
|
|
public function test_export_with_only_old_dates_does_not_expand_into_the_retained_range(): void
|
|
{
|
|
$this->grantAdminPermission();
|
|
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
|
|
$repository->shouldReceive('available', 'ready', 'queryReady')->andReturnTrue();
|
|
$repository->shouldReceive('exportRows')->once()->withArgs(
|
|
static fn (array $filters): bool => $filters['from'] === '2026-01-01' && $filters['to'] === '2025-12-31'
|
|
)->andReturn([]);
|
|
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
|
|
|
|
$response = $this->loginAs()->get(route('business.refunds.non-jingxi-refund-data.export', [
|
|
'from' => '2025-12-31', 'to' => '2025-01-01', 'columns' => 'refundDate',
|
|
]));
|
|
$response->assertOk();
|
|
$this->assertSame("\xEF\xBB\xBF退款日期\n", $response->streamedContent());
|
|
}
|
|
|
|
public function test_export_reports_first_batch_failure_before_starting_download(): void
|
|
{
|
|
$this->grantAdminPermission();
|
|
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
|
|
$repository->shouldReceive('available', 'ready', 'queryReady')->andReturnTrue();
|
|
$repository->shouldReceive('exportRows')->andReturn((static function (): \Generator {
|
|
throw new \RuntimeException('refund_export_test_failure');
|
|
yield;
|
|
})());
|
|
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
|
|
|
|
$this->loginAs()->get(route('business.refunds.non-jingxi-refund-data.export'))
|
|
->assertRedirect()->assertSessionHas('error', '售后(退货退款)导出暂时无法完成,请稍后重试。');
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|