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="{"from":"2026-09-01T08:30:00","to":"2026-09-03T17:45:59"', false) ->assertDontSee('data-finance-export-query="{"', 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); } /** * 筛选下拉必须是自绘的可滚动面板:原生 的面板由浏览器绘制, * 高度与滚动条无法控制,店铺有 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(' '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' => '测试店铺', ])); } }