user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail() ); } public function test_negative_alerts_is_a_payables_section_with_two_children(): void { $navigation = collect(BusinessNavigation::all())->keyBy('slug'); $alertSection = $navigation->get('negative-alerts'); $this->assertNotNull($alertSection); $this->assertSame('负数预警', $alertSection['title']); $this->assertSame( ['supplier-negative-alert', 'courier-negative-alert'], array_column($alertSection['pages'], 'slug'), ); $this->assertNotContains( 'supplier-negative-alert', array_column($navigation->get('supplier-reconciliation')['pages'], 'slug'), ); $grouped = ShowInAdmin::groupPayables(BusinessNavigation::all()); $payables = collect($grouped)->firstWhere('slug', 'payable-items'); $this->assertSame('negative-alerts', $payables['sections'][0]['slug']); } public function test_supplier_and_courier_negative_alert_pages_render_as_tabs(): void { $this->saveSupplierPayable('negative:1', '负余额供应商', '-128.50', '2026-09', [ 'company' => '测试供应商有限公司', 'negative_reason' => '历史多付款', ]); $this->saveSupplierPayable('positive:1', '正余额供应商', '88.00', '2026-09'); $this->saveSupplierPayable('formula:1', '公式命中供应商', '100.00', '2026-09', [ 'should_pay' => '180.00', 'estimated_price' => '50.00', ]); $this->saveSupplierPayable('formula:exclude', '负余额但公式为正供应商', '-100.00', '2026-09', [ 'should_pay' => '-500.00', 'estimated_price' => '50.00', ]); $supplierManagement = \Mockery::mock(SupplierManagementMirrorRepository::class); $supplierManagement->shouldReceive('suppliersByNames') ->once() ->withArgs(static fn (array $names): bool => count($names) === 2 && in_array('负余额供应商', $names, true) && in_array('公式命中供应商', $names, true)) ->andReturn([ (object) ['scmname' => '负余额供应商', 'buyer' => '采购甲'], (object) ['scmname' => '公式命中供应商', 'buyer' => '采购乙'], ]); $this->app->instance(SupplierManagementMirrorRepository::class, $supplierManagement); $response = $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'negative-alerts', 'page' => 'supplier-negative-alert', ])) ->assertOk() ->assertSeeText('供应商负数预警') ->assertSeeText('快递负数预警') ->assertSeeText('按负数预警公式筛出的终版数据') ->assertSeeText('供应商余额 - 应付余额 + 预估金额 < 0') ->assertSeeText('负余额供应商') ->assertSeeText('公式命中供应商') ->assertSeeText('-128.50') ->assertSeeText('历史多付款') ->assertSeeText('采购甲') ->assertSeeText('采购乙') ->assertDontSeeText('正余额供应商') ->assertDontSeeText('负余额但公式为正供应商'); $fields = app(PayablesFinalRegistry::class)->fields( PayablesFinalRegistry::SECTION_SUPPLIER, 'supplier-payables-report', ); $html = (string) $response->getContent(); preg_match_all('/)/', $html, $headers); // 序号 + 隐藏主体名称后的原字段 + 动态匹配的采购 + 处理结果。 $this->assertCount(count($fields) + 2, $headers[0]); preg_match_all('/]*>(.*?)<\/th>/s', $html, $headerMatches); $labels = array_map( static fn (string $label): string => trim(html_entity_decode(strip_tags($label), ENT_QUOTES | ENT_HTML5, 'UTF-8')), $headerMatches[1], ); $negativeReasonIndex = array_search('负数原因', $labels, true); $this->assertNotFalse($negativeReasonIndex); $this->assertSame('采购', $labels[$negativeReasonIndex + 1]); $this->assertSame('处理结果', $labels[$negativeReasonIndex + 2]); $this->assertStringContainsString('待采购处理', $html); $this->assertStringNotContainsString('供应商公司主体名称', $html); $this->assertStringNotContainsString('测试供应商有限公司', $html); preg_match_all('/]*data-fixed-column="([^"]+)"/', $html, $fixedHeaders); $this->assertSame( ['index', 'summary_month', 'checker', 'settlement_method', 'supplier'], $fixedHeaders[1], ); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'negative-alerts', 'page' => 'courier-negative-alert', ])) ->assertOk() ->assertSeeText('供应商负数预警') ->assertSeeText('快递负数预警') ->assertSeeText('快递负数判定口径待确认') ->assertSeeText('待接入'); } public function test_supplier_negative_alert_filters_by_month_and_supplier(): void { $this->saveSupplierPayable('negative:sep', '九月负余额供应商', '-99.00', '2026-09'); $this->saveSupplierPayable('negative:aug', '八月负余额供应商', '-66.00', '2026-08'); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'negative-alerts', 'page' => 'supplier-negative-alert', 'month' => '2026-08', 'supplier' => '八月负余额', ])) ->assertOk() ->assertSeeText('八月负余额供应商') ->assertSeeText('-66.00') ->assertDontSeeText('九月负余额供应商'); } public function test_supplier_negative_alert_filters_by_checker_settlement_and_purchaser(): void { $this->saveSupplierPayable('filter:target', '目标供应商', '-50.00', '2026-09', [ 'checker' => '李萌', 'settlement_method' => '订单合同', ]); $this->saveSupplierPayable('filter:other', '其他供应商', '-60.00', '2026-09', [ 'checker' => '陈亲园', 'settlement_method' => '终止合作', ]); $supplierManagement = \Mockery::mock(SupplierManagementMirrorRepository::class); $supplierManagement->shouldReceive('suppliersByNames') ->andReturn([ (object) ['scmname' => '目标供应商', 'buyer' => '采购甲'], (object) ['scmname' => '其他供应商', 'buyer' => '采购乙'], ]); $this->app->instance(SupplierManagementMirrorRepository::class, $supplierManagement); app(SupplierNegativeAlertDingTalkWorkflow::class)->saveConfiguration( 'https://oapi.dingtalk.com/robot/send?access_token=test-token', 'SEC-test', '财务&采购沟通群', ); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'negative-alerts', 'page' => 'supplier-negative-alert', ])) ->assertOk() ->assertSeeText('一键发送全部采购(2 人)'); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'negative-alerts', 'page' => 'supplier-negative-alert', 'checker' => '李萌', 'settlement_method' => '订单合同', 'purchaser' => '采购甲', ])) ->assertOk() ->assertSeeText('目标供应商') ->assertSeeText('采购甲') ->assertSeeText('发送给 采购甲(1 家)') ->assertDontSeeText('其他供应商'); } public function test_supplier_negative_alert_route_no_longer_renders_under_supplier_reconciliation(): void { $this->withExceptionHandling() ->loginAs() ->get(route('business.pages.show', [ 'section' => 'supplier-reconciliation', 'page' => 'supplier-negative-alert', ])) ->assertRedirect(); } private function saveSupplierPayable( string $key, string $supplier, string $supplierBalance, string $month, array $extra = [], ): void { app(SupplierPayableOverrideStore::class)->save( PayablesFinalActions::DATASET_PREFIX . 'supplier:supplier-payables-report', $key, $extra + [ 'summary_month' => $month, 'supplier' => $supplier, 'supplier_balance' => $supplierBalance, ], (int) $this->company->id, (int) $this->user->id, ); } }