CaiWuXiTong/tests/Feature/Business/ProcessDetailsPageTest.php
yueyang fbf2c634b7 fix: OA审批自动刷新限制为每小时一次 (#794)
同一账号同一页面每小时最多定向刷新一次;失败状态改为中性提示。
2026-10-30 17:06:18 +08:00

345 lines
15 KiB
PHP

<?php
namespace Tests\Feature\Business;
use App\Http\Controllers\Business\Pages;
use App\Models\Auth\Permission;
use Illuminate\Http\Request;
use Mockery;
use Modules\EcommerceFinance\Repositories\DingProcessDetailMirrorRepository;
use Modules\EcommerceFinance\Services\DingProcessRefreshQueue;
use RuntimeException;
use Tests\Feature\FeatureTestCase;
class ProcessDetailsPageTest extends FeatureTestCase
{
public function test_page_renders_process_detail_rows_and_filters(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$repository = Mockery::mock(DingProcessDetailMirrorRepository::class);
$repository->shouldReceive('paginate')->once()
->with(Mockery::type('array'), 1, 100)
->andReturn([
'data' => [
(object) [
'business_id' => '202601011200000000001',
'line_no' => 1,
'originator_name' => '张三',
'status' => '已完成',
'payment_state' => '已付款',
'cashier_status' => 'completed',
'current_approver_name' => '王五',
'current_task_name' => '财务用途审批',
'current_task_started_at' => \Modules\EcommerceFinance\Support\OaProcessTime::now()
->subMinutes(90)
->startOfMinute()
->format('Y-m-d H:i:s'),
'cashier_task_name' => '出纳打款',
'cashier_task_started_at' => now()->subHours(3)->startOfMinute()->format('Y-m-d H:i:s'),
'payment_fact_amount' => 0,
'payment_difference' => 0.20,
'document_type' => '公摊日常报销',
'purpose' => '办公用品采购',
'account_code' => '5602.04',
'account_name' => '管理费用_办公费',
'amount' => 100.00,
'bank_account_code' => '1002.01.001',
'bank_account_name' => '华夏银行-测试',
'bank_amount' => 100.00,
'payment_method' => '银行卡',
'paid_at' => '2026-01-02 00:00',
],
],
'total' => 1,
'summary' => [
'rows' => 1,
'approvals' => 1,
'amount_total' => 100.0,
'bank_amount_total' => 100.0,
],
'status' => 'succeeded',
'last_synced_at' => '2026-10-26 15:00:00',
'sync_stale' => true,
'sync_stale_after_minutes' => 15,
'scope' => 'personal_permission',
]);
$repository->shouldReceive('filterOptions')->once()->andReturn([
'document_types' => ['公摊日常报销'],
'statuses' => ['已完成'],
'payment_states' => [
'待付款',
'已付款',
'不涉及付款',
],
'current_approvers' => ['王五', '赵六'],
]);
$this->app->instance(DingProcessDetailMirrorRepository::class, $repository);
$processSection = collect(\App\Utilities\BusinessNavigation::all())
->firstWhere('slug', 'process-data');
$this->assertContains(
'oa-process',
array_column($processSection['pages'] ?? [], 'slug'),
);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'process-data',
'page' => 'oa-process',
'per_page' => 100,
'payment_state' => '待付款',
'current_approver_name' => '王五',
'waiting_bucket' => '1_2d',
]))->assertOk()
->assertSee('流程数据')
->assertSee('OA流程')
->assertSee('审批编号')
->assertSee('发起人')
->assertSee('单据类型')
->assertSee('打款时间')
->assertSee('name="paid_start"', false)
->assertSee('name="paid_end"', false)
->assertSee('name="current_approver_name"', false)
->assertSee('data-process-approver-combobox', false)
->assertSee('data-process-approver-option', false)
->assertSee('role="combobox"', false)
->assertSee('placeholder="输入姓名或展开选择"', false)
->assertSee('data-value="赵六"', false)
->assertSee('name="waiting_bucket"', false)
->assertSee('value="1_2d" selected', false)
->assertSee('2天以上', false)
->assertDontSee('name="entry_start"', false)
->assertDontSee('name="entry_end"', false)
->assertSee('placeholder="开始日期"', false)
->assertSee('placeholder="结算日期"', false)
->assertSee('aria-label="选择打款开始日期"', false)
->assertSee('aria-label="选择打款结算日期"', false)
->assertSee('CaiWuDatePicker.setup', false)
->assertSee('class="process-details-filter-grid"', false)
->assertSee('class="process-details-note-disclosure"', false)
->assertSee('name="payment_state"', false)
->assertSee('审批状态')
->assertSee('付款状态')
->assertSee('当前审批人')
->assertSee('进入时间')
->assertSee('已等待')
->assertSee('data-oa-refresh-url', false)
->assertSee('data-oa-refresh-status-url', false)
->assertSee('data-oa-refresh-user-id', false)
->assertSee('页面每小时最多定向刷新当前页审批中的流程一次', false)
->assertDontSee('当前节点')
->assertDontSee('出纳节点')
->assertDontSee('财务用途审批')
->assertDontSee('出纳打款 · 已完成')
->assertSee('王五')
->assertSee('1小时')
->assertSee('付款明细合计')
->assertSee('已付款')
->assertDontSee('付款待核实')
->assertDontSee('未核验')
->assertSee('付款明细金额')
->assertSee('付款差额')
->assertSee('0.20')
->assertDontSee('<span>付款合计</span>', false)
->assertDontSee('银行付款合计')
->assertSee('流程数据已超过 15 分钟未成功同步')
->assertSee('202601011200000000001')
->assertSee('张三')
->assertSee('管理费用_办公费')
->assertSee('100.00')
->assertSee('当前账号权限范围')
->assertSee('同一笔付款不会因关联多条费用而重复计算')
->assertSee('name="per_page"', false)
->assertSee('value="50"', false)
->assertSee('value="100" selected', false)
->assertSee('value="200"', false)
->assertSee('导出');
}
public function test_refresh_endpoint_only_queues_active_workflows(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$repository = Mockery::mock(DingProcessDetailMirrorRepository::class);
$repository->shouldReceive('activeBusinessIds')
->once()
->with(['202601011200000000001', '202602031200000000002'])
->andReturn(['202602031200000000002']);
$queue = Mockery::mock(DingProcessRefreshQueue::class);
$queue->shouldReceive('enqueue')
->once()
->with(
['202602031200000000002'],
Mockery::type('int'),
Mockery::type('int'),
)
->andReturn([
'queued' => true,
'token' => '00000000-0000-4000-8000-000000000001',
'count' => 1,
]);
$queue->shouldReceive('status')
->once()
->with(
'00000000-0000-4000-8000-000000000001',
Mockery::type('int'),
Mockery::type('int'),
)
->andReturn([
'status' => 'completed',
'count' => 1,
'created_at' => '2026-10-30T14:00:00+08:00',
'started_at' => '2026-10-30T14:00:05+08:00',
'completed_at' => '2026-10-30T14:00:12+08:00',
'error' => null,
]);
$this->app->instance(DingProcessDetailMirrorRepository::class, $repository);
$this->app->instance(DingProcessRefreshQueue::class, $queue);
$this->loginAs();
$baseUrl = route('business.pages.show', [
'section' => 'process-data',
'page' => 'oa-process',
]);
$this->getJson($baseUrl . '?' . http_build_query([
'refresh_current' => 1,
'business_ids' => '202601011200000000001,202602031200000000002',
]))->assertOk()
->assertJson([
'success' => true,
'queued' => true,
'token' => '00000000-0000-4000-8000-000000000001',
'count' => 1,
]);
$this->getJson($baseUrl . '?' . http_build_query([
'refresh_status' => '00000000-0000-4000-8000-000000000001',
]))->assertOk()
->assertJsonPath('refresh.status', 'completed')
->assertJsonPath('refresh.count', 1);
}
public function test_generic_business_page_controller_handles_refresh_requests(): void
{
$moduleController = Mockery::mock(\Modules\EcommerceFinance\Http\Controllers\ProcessDetails::class);
$moduleController->shouldReceive('requestRefresh')
->once()
->andReturn(response()->json(['success' => true, 'queued' => true]));
$moduleController->shouldReceive('refreshStatus')
->once()
->with('00000000-0000-4000-8000-000000000001')
->andReturn(response()->json(['success' => true, 'refresh' => ['status' => 'completed']]));
$this->app->instance(
\Modules\EcommerceFinance\Http\Controllers\ProcessDetails::class,
$moduleController,
);
$genericPage = (new \ReflectionClass(Pages::class))->newInstanceWithoutConstructor();
$this->app->instance('request', Request::create(
'/1/workspace/process-data/oa-process',
'GET',
['refresh_current' => '1'],
));
$this->assertSame(
200,
$genericPage->show('process-data', 'oa-process')->getStatusCode(),
);
$this->app->instance('request', Request::create(
'/1/workspace/process-data/oa-process',
'GET',
['refresh_status' => '00000000-0000-4000-8000-000000000001'],
));
$this->assertSame(
200,
$genericPage->show('process-data', 'oa-process')->getStatusCode(),
);
}
public function test_page_stays_readable_when_mirror_is_not_ready(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$repository = Mockery::mock(DingProcessDetailMirrorRepository::class);
$repository->shouldReceive('filterOptions')->once()->andThrow(new RuntimeException('not ready'));
$repository->shouldReceive('scope')->once()->andReturn('personal_permission');
$this->app->instance(DingProcessDetailMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'process-data',
'page' => 'oa-process',
]))->assertOk()
->assertSee('OA流程镜像尚未完成导入')
->assertSee('当前账号权限范围');
}
public function test_legacy_process_details_url_redirects_to_oa_process(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs()->get(
'/1/workspace/financial-operations/process-details?status=' . urlencode('已完成'),
)->assertRedirect(route('business.pages.show', [
'section' => 'process-data',
'page' => 'oa-process',
'status' => '已完成',
]));
}
public function test_legacy_process_details_export_url_redirects_to_oa_process(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs()->get(
'/1/workspace/financial-operations/process-details/export?status=' . urlencode('已完成'),
)->assertRedirect(route('business.process-details.export', [
'status' => '已完成',
]));
}
public function test_export_streams_filtered_csv(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$repository = Mockery::mock(DingProcessDetailMirrorRepository::class);
$repository->shouldReceive('ready')->once()->andReturn(true);
$repository->shouldReceive('exportRows')->once()->andReturnUsing(function (): \Generator {
yield [
'business_id' => '202601011200000000001',
'originator_name' => '张三',
'status' => '已完成',
'payment_state' => '已付款',
'payment_difference' => 0.20,
'document_type' => '公摊日常报销',
'purpose' => '办公用品采购',
'line_no' => 1,
'account_code' => '5602.04',
'account_name' => '管理费用_办公费',
'amount' => '100.00',
'bank_account_code' => '1002.01.001',
'bank_account_name' => '华夏银行-测试',
'bank_amount' => '100.00',
'payment_method' => '银行卡',
'paid_at' => '2026-01-02 00:00',
];
});
$this->app->instance(DingProcessDetailMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route('business.process-details.export', [
'status' => '已完成',
'payment_state' => '已付款',
]));
$response->assertOk();
$content = $response->streamedContent();
$this->assertStringContainsString('审批编号', $content);
$this->assertStringContainsString('202601011200000000001', $content);
$this->assertStringContainsString('张三', $content);
$this->assertStringContainsString('管理费用_办公费', $content);
$this->assertStringContainsString('付款状态', $content);
$this->assertStringContainsString('已付款', $content);
$this->assertStringContainsString('付款差额', $content);
$this->assertStringContainsString('0.20', $content);
}
}