CaiWuXiTong/tests/Feature/Business/SmallPaymentWorkOrderPageTest.php
yueyang 296e528482 小额打款工单:筛选下拉可滚动 + 完成日期改为账单页同款日历 (#276)
- 店铺/打款原因筛选项改为自绘可滚动面板(原生 datalist 高度与滚动不可控)
- 完成日期改为与账单页一致的日历控件(文本输入 + 日历按钮 + 中文月历弹层)
  值只承载日期,服务端补成整天半开区间,避免分钟级边界丢单
- 测试:php artisan test --filter=SmallPayment 16 passed (126 assertions)
2026-09-27 13:45:18 +08:00

275 lines
14 KiB
PHP

<?php
namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use Modules\EcommerceFinance\Repositories\SmallPaymentWorkOrderMirrorRepository;
use Tests\Feature\FeatureTestCase;
class SmallPaymentWorkOrderPageTest extends FeatureTestCase
{
private function grantAdminPermission(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
}
public function test_page_renders_filtered_mirror_rows_summary_and_pagination(): void
{
$this->grantAdminPermission();
setting()->set('default.list_limit', 25);
$states = [
SmallPaymentWorkOrderMirrorRepository::WORK_ORDER_DATASET => [
'status' => 'running', 'initial_completed_at' => '2026-09-01 00:00:00',
],
SmallPaymentWorkOrderMirrorRepository::PAYMENT_MAP_DATASET => [
'status' => 'running', 'initial_completed_at' => '2026-09-01 00:00:00',
],
];
$repository = \Mockery::mock(SmallPaymentWorkOrderMirrorRepository::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-09-03 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-09-01T08:30:00', 'to' => '2026-09-03T17:45:59',
'work_order_no' => 'GD-001', 'shop_name' => '京东旗舰店',
'transaction_no' => 'BATCH-001', 'online_order_no' => 'ORDER-001',
'status' => '已完成', 'payment_reason' => '售后补偿',
] && $page === 2 && $limit === 25
)->andReturn([
'data' => [[
'work_order_no' => 'GD-001', 'alipay_batch_no' => 'BATCH-001',
'work_order_status' => '已完成', 'payment_status' => '支付宝已打款',
'work_order_title' => '小额打款', 'work_order_content' => '售后补偿',
'work_order_type' => '小额打款', 'priority' => '普通',
'payment_amount' => '50.00', 'urging_count' => 0,
'shop_name' => '京东旗舰店', 'online_order_no' => 'ORDER-001',
'buyer_account' => 'buyer-one', 'alipay_account' => 'account-one',
'alipay_name' => '测试买家', 'product_code' => 'ITEM-001',
'sku_code' => 'SKU-001', 'platform' => '京东',
'actual_paid_amount' => '199.90', 'payment_time' => '2026-09-02 10:30:00',
'payment_reason' => '售后补偿', 'creator' => '客服甲',
'work_order_created_at' => '2026-09-02 09:00:00',
'completed_at' => '2026-09-02 11:00:00',
]],
'total' => 26,
'page_payment_amount' => '50.00',
'filtered_payment_amount' => '50.00',
]);
$repository->shouldReceive('filterOptions')->once()->andReturn([
'shops' => ['京东旗舰店'],
'payment_reasons' => ['售后补偿'],
]);
$this->app->instance(SmallPaymentWorkOrderMirrorRepository::class, $repository);
$url = route('business.pages.show', [
'company_id' => $this->company->id,
'section' => 'financial-operations', 'page' => 'small-payment-work-orders',
]) . '?' . http_build_query([
'from' => '2026-09-01T08:30', 'to' => '2026-09-03T17:45', 'work_order_no' => 'GD-001',
'shop_name' => '京东旗舰店', 'transaction_no' => 'BATCH-001',
'online_order_no' => 'ORDER-001', 'status' => '已完成',
'payment_reason' => '售后补偿', 'page' => 2,
]);
$this->loginAs()->get($url)
->assertOk()
->assertSee('data-small-payment-work-orders="true"', false)
->assertSee('data-filter-contract="small-payment-work-orders.filters"', false)
->assertSee('data-sync-state="syncing"', false)
->assertSeeText('业务镜像同步中,当前显示最近成功数据')
->assertSeeText('完成日期')
->assertSee('value="2026-09-01"', false)
->assertSee('value="2026-09-03"', false)
->assertSee('data-date-picker', false)
->assertSee('data-date-toggle', false)
->assertSee('aria-label="聚水潭小额打款工单"', false)
->assertSeeText('GD-001')
->assertSeeText('支付宝已打款')
->assertSeeText('¥ 50.00')
->assertSeeText('共 26 条记录,本页 1 条')
->assertSeeText('第 2 / 2 页')
->assertSee('workspace/financial-operations/small-payment-work-orders/export', false)
->assertSee('data-finance-export-base=', false)
->assertSee('data-finance-export-query="{&quot;from&quot;:&quot;2026-09-01T08:30:00&quot;,&quot;to&quot;:&quot;2026-09-03T17:45:59&quot;', false)
->assertDontSee('data-finance-export-query="{&amp;quot;', false)
->assertSee('data-export-field="work_order_no"', false)
->assertSee('value="ORDER-001"', false);
}
public function test_export_downloads_filtered_csv(): void
{
$this->grantAdminPermission();
$repository = \Mockery::mock(SmallPaymentWorkOrderMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('queryReady')->once()->andReturnTrue();
$repository->shouldReceive('exportRows')->once()->andReturn([[
'work_order_no' => 'GD-001', 'alipay_batch_no' => 'BATCH-001',
'work_order_status' => '已完成', 'payment_status' => '支付宝已打款',
'work_order_title' => '小额打款', 'work_order_content' => '售后补偿',
'work_order_type' => '小额打款', 'priority' => '普通',
'payment_amount' => '50.00', 'urging_count' => 0,
'shop_name' => '京东旗舰店', 'online_order_no' => 'ORDER-001',
'buyer_account' => 'buyer-one', 'alipay_account' => 'account-one',
'alipay_name' => '测试买家', 'product_code' => 'ITEM-001',
'sku_code' => 'SKU-001', 'platform' => '京东',
'actual_paid_amount' => '199.90', 'payment_time' => '2026-09-02 10:30:00',
'payment_reason' => '售后补偿', 'creator' => '客服甲',
'work_order_created_at' => '2026-09-02 09:00:00',
'completed_at' => '2026-09-02 11:00:00',
]]);
$this->app->instance(SmallPaymentWorkOrderMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route('business.financial-operations.small-payment-work-orders.export', [
'from' => '2026-09-01', 'to' => '2026-09-03',
]));
$response->assertOk()->assertDownload('small_payment_work_orders_2026-09-01_00-00-00_2026-09-03_23-59-59.csv');
$content = $response->streamedContent();
$this->assertStringStartsWith("\xEF\xBB\xBF", $content);
$this->assertStringContainsString('工单编号', $content);
$this->assertStringContainsString('GD-001', $content);
$this->assertStringContainsString('50.00', $content);
}
/**
* 筛选下拉必须是自绘的可滚动面板:原生 <datalist> 的面板由浏览器绘制,
* 高度与滚动条无法控制,店铺有 200+ 个选项时看不全也滑不动。
*/
public function test_filter_dropdowns_are_scrollable_option_panels(): void
{
$this->grantAdminPermission();
setting()->set('default.list_limit', 25);
$states = [
SmallPaymentWorkOrderMirrorRepository::WORK_ORDER_DATASET => [
'status' => 'succeeded', 'initial_completed_at' => '2026-09-01 00:00:00',
],
SmallPaymentWorkOrderMirrorRepository::PAYMENT_MAP_DATASET => [
'status' => 'succeeded', 'initial_completed_at' => '2026-09-01 00:00:00',
],
];
$repository = \Mockery::mock(SmallPaymentWorkOrderMirrorRepository::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-09-03 12:00:00',
]);
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('queryReady')->once()->andReturnTrue();
$repository->shouldReceive('paginate')->once()->andReturn([
'data' => [], 'total' => 0,
'page_payment_amount' => '0.00', 'filtered_payment_amount' => '0.00',
]);
$repository->shouldReceive('filterOptions')->once()->andReturn([
'shops' => ['京东-HX旗舰店101', '京东-HX旗舰店102'],
'payment_reasons' => ['买家原因/买家-不想要'],
]);
$this->app->instance(SmallPaymentWorkOrderMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'financial-operations', 'page' => 'small-payment-work-orders',
]))
->assertOk()
->assertSee('data-filter-combobox', false)
->assertSee('class="upstream-data-combobox__menu"', false)
// 唯一能锁住"可滚动"的常量:面板有最大高度且内部滚动
->assertSee('max-height: min(18rem, 50vh)', false)
->assertSee('data-combobox-option', false)
->assertSee('data-value="京东-HX旗舰店101"', false)
->assertSee('data-combobox-empty', false)
->assertDontSee('list="small-payment', false)
->assertDontSee('<datalist id=', false);
}
public function test_page_does_not_report_empty_before_both_mirrors_finish(): void
{
$this->grantAdminPermission();
$states = [
SmallPaymentWorkOrderMirrorRepository::WORK_ORDER_DATASET => ['status' => 'running'],
SmallPaymentWorkOrderMirrorRepository::PAYMENT_MAP_DATASET => ['status' => 'running'],
];
$repository = \Mockery::mock(SmallPaymentWorkOrderMirrorRepository::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(SmallPaymentWorkOrderMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'financial-operations', 'page' => 'small-payment-work-orders',
]))
->assertOk()
->assertSeeText('业务镜像正在执行首次同步')
->assertSeeText('首次同步尚未完成,完成后将自动显示小额打款工单。')
->assertDontSeeText('当前筛选范围没有小额打款工单');
}
public function test_export_accepts_only_declared_columns_in_requested_order(): void
{
$this->grantAdminPermission();
$repository = \Mockery::mock(SmallPaymentWorkOrderMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('queryReady')->once()->andReturnTrue();
$repository->shouldReceive('exportRows')->once()->andReturn([[
'work_order_no' => '12345678901234567890',
'payment_amount' => '50.00',
'shop_name' => '京东旗舰店',
]]);
$this->app->instance(SmallPaymentWorkOrderMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route('business.financial-operations.small-payment-work-orders.export', [
'columns' => 'shop_name,work_order_no,not_a_real_field,shop_name',
]));
$response->assertOk();
$content = $response->streamedContent();
$header = strtok($content, "\r\n");
$this->assertSame("\xEF\xBB\xBF店铺名称,工单编号", $header);
$this->assertStringContainsString("京东旗舰店,'12345678901234567890", $content);
$this->assertStringNotContainsString('打款金额', $content);
}
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' => 'financial-operations', 'page' => 'small-payment-work-orders',
]))->assertForbidden();
}
public function test_legacy_refund_url_redirects_to_financial_operations_and_preserves_filters(): void
{
$this->grantAdminPermission();
$this->loginAs()->get(route('business.pages.show', [
'section' => 'refunds',
'page' => 'small-payment-work-orders',
'from' => '2026-09-01',
'shop_name' => '测试店铺',
]))->assertRedirect(route('business.pages.show', [
'section' => 'financial-operations',
'page' => 'small-payment-work-orders',
'from' => '2026-09-01',
'shop_name' => '测试店铺',
]));
}
}