loginAs() ->get(route('business.pages.show', ['section' => 'basic-data'])) ->assertOk() ->assertSeeText('基础数据') ->assertSeeText('商品价格管理') ->assertSee('/workspace/order-data/raw-data', false) ->assertSee('/workspace/refunds/non-jingxi-refund-data', false) ->assertSee('/workspace/product-inventory/normal-products', false) ->assertSee('/workspace/product-inventory/monthly-weighted-average-price', false) ->assertSee('/workspace/product-inventory/inventory-report', false); } public function testInventoryReportMenuPageIsAvailable(): void { $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'product-inventory', 'page' => 'inventory-report', ])) ->assertOk() ->assertSeeText('进销存报表') ->assertSeeTextInOrder([ '货主', '月份', '仓储方', '仓库', '商品编码', '分类', '期初数量', '期末数量', '加工进仓', '加工出仓', '采购进仓', '采购退货', '其它进仓', '其它退货', '销售出仓', '销售退货', '盘点', '调拨出', '调拨入', '期初', '其它出库', ]) ->assertSeeText('进销存总表') ->assertSeeText('仓库名称映射表') ->assertSee('data-inventory-report-tabs', false) ->assertSee('data-inventory-report-tab="mapping"', false) ->assertSee('id="inventory-report-mapping-panel"', false) ->assertDontSeeText('复用“应付款项 → 快递应付 → 三方仓进销存”') ->assertSeeText('进销存共享镜像尚未建立') ->assertDontSeeText('已按附件模板导入 20 条演示数据') ->assertDontSeeText('进销存报表字段和数据来源尚未定义。'); } public function testMonthlyWeightedAveragePricePageIsAvailable(): void { $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'product-inventory', 'page' => 'monthly-weighted-average-price', ])) ->assertOk() ->assertSeeText('商品月加权平均单价') ->assertSeeText('确认取数规则后再接入经营分析副本库数据') // 第一层:未分组的列标题跨两行,期初数据与入库数据各自合并子列。 ->assertSeeTextInOrder([ '月份', '条码+仓库', '仓库简称', '供应商', '商品编码', '商品名称', '期初数据', '入库数据', '加权平均单价', '定价状态', // 第二层:只有分组列出现在这一行,顺序固定。 '原始数量', '原始价格', '原始期初金额', '更改价格', '期初金额', '采购数量', '采购单价', '入库金额', ]) ->assertSee('data-grouped-header="true"', false) ->assertSee('data-finance-table-native', false) ->assertSee('rowspan="2"', false) ->assertSee('colspan="5"', false) ->assertSee('colspan="3"', false) // 两层表头里第二行的最后一个单元格不在表格最右侧,需要保留右边框, // 否则右侧跨行一级标题列会和左侧分组列断格。 ->assertSee('data-directory-last-column', false) // 工作表没有序号列,页面也不再渲染固定序号列。 ->assertDontSee('assertDontSee('assertSeeText('确认取数规则后再接入经营分析副本库数据'); } public function testPerformanceDetailsPageIsAvailable(): void { $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'financial-operations', 'page' => 'performance-details', ])) ->assertOk() ->assertSeeText('业绩明细') // 列顺序:发生时间 → 店铺名称 → 线上单号 → 收入金额 → 商品金额 → 差额 → 匹配状态 → 异常标注 ->assertSee('data-column-key="platform"', false) ->assertSee('data-column-key="occurred_date"', false) ->assertSee('data-column-key="store_name"', false) ->assertSee('data-column-key="online_order_no"', false) ->assertSee('data-column-key="income_amount"', false) ->assertSee('data-column-key="discount_amount"', false) ->assertSee('data-column-key="offset_amount"', false) ->assertSee('data-column-key="planting_amount"', false) ->assertSee('data-column-key="product_amount"', false) ->assertSee('data-column-key="difference"', false) ->assertSee('data-column-key="match_status"', false) ->assertSee('data-column-key="remark"', false) // 子订单号已按业务要求移除。 ->assertDontSeeText('子订单号') // 展示粒度=一笔账单收款,不再展开发货明细列。 ->assertDontSee('data-column-key="warehouse"', false) ->assertDontSee('data-column-key="product_code"', false) ->assertDontSee('data-column-key="quantity"', false) // 筛选栏:月份选择器 + 店铺名称 + 线上单号 + 匹配状态。 ->assertSee('data-inventory-month-picker', false) ->assertSee('name="store_name"', false) ->assertSee('name="online_order_no"', false) ->assertSee('name="match_status"', false) ->assertSee('/workspace/financial-operations/performance-details/export', false) ->assertSeeText('店铺收款匹配订单明细尚未同步,请先执行 store-receipt:order-match:sync。'); } public function testFinancialRiskControlPageIsAvailableBetweenFinancialReports(): void { $navigation = BusinessNavigation::all(); $slugs = array_column($navigation, 'slug'); $riskControlIndex = array_search('financial-risk-control', $slugs, true); $this->assertNotFalse($riskControlIndex); $this->assertSame('financial-operations', $slugs[$riskControlIndex - 1]); $this->assertSame('financial-sw', $slugs[$riskControlIndex + 1]); $this->assertSame('财务经营风险管控', $navigation[$riskControlIndex]['title']); $this->assertSame('security', $navigation[$riskControlIndex]['icon']); $this->assertSame([ ['slug' => 'store-violation-alert', 'title' => '店铺违规信息预警'], ['slug' => 'preview-statistics', 'title' => '预览统计'], [ 'slug' => 'process-statistics', 'title' => '流程统计', 'directory' => [ 'icon' => 'timeline', 'status' => '共享镜像', 'description' => '店铺申请流程(开店 / 变更 / 关店)明细,数据来自共享业务镜像,只读展示。', 'empty' => '共享镜像中暂未读取到店铺申请流程数据,请检查镜像同步状态。', 'fields' => [ ['name' => 'flow_id', 'label' => '单据编号'], ['name' => 'flow_type', 'label' => '流程类型'], ['name' => 'approval_status', 'label' => '审批状态'], ['name' => 'current_approver', 'label' => '当前审批人'], ['name' => 'shop_name', 'label' => '店铺名称'], ['name' => 'platform', 'label' => '平台'], ['name' => 'shop_head', 'label' => '店铺运营负责人'], ['name' => 'customer_manager', 'label' => '店铺客服负责人'], ['name' => 'responsible_department', 'label' => '利润中心'], ['name' => 'erp_shop_name', 'label' => 'ERP店铺名称'], ['name' => 'apply_user', 'label' => '申请人'], ['name' => 'apply_time', 'label' => '申请日期', 'kind' => 'date'], ['name' => 'trademark_number', 'label' => '商标号'], ['name' => 'change_reason', 'label' => '变更原因'], ['name' => 'close_reason', 'label' => '关店原因'], ['name' => 'expected_closing_time', 'label' => '预计关店时间', 'kind' => 'date'], ['name' => 'refund_type', 'label' => '退回类型'], ['name' => 'refund_amount', 'label' => '退回金额', 'kind' => 'number'], ['name' => 'change_successful', 'label' => '是否完成', 'kind' => 'boolean'], ], ], ], ], $navigation[$riskControlIndex]['pages']); $this->loginAs() ->get(route('business.pages.show', ['section' => 'financial-risk-control'])) ->assertRedirect(route('business.pages.show', [ 'section' => 'financial-risk-control', 'page' => 'store-violation-alert', ])); $this->get(route('business.pages.show', [ 'section' => 'financial-risk-control', 'page' => 'store-violation-alert', ]) . '?platform=jingxi') ->assertOk() ->assertSeeText('财务经营风险管控') ->assertSeeText('店铺违规信息预警') ->assertSeeTextInOrder(['淘系', '阿里巴巴', '京东', '京喜', '拼多多', '抖音']) ->assertSeeText('京喜店铺违规信息预警') ->assertSeeText('统一表头:') ->assertSeeText('处理状态') ->assertSee('data-active-platform="jingxi"', false) ->assertSee('id="financial-risk-control-panel-jingxi"', false) ->assertSee('data-platform-tab="jingxi"', false) ->assertSee('/workspace/financial-risk-control/store-violation-alert', false); } public function test_business_pages_render_menu_filter_and_open_page_tabs(): void { $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'financial-risk-control', 'page' => 'store-violation-alert', ])) ->assertOk() // 侧边栏菜单搜索框(方便按名称找页面)。 ->assertSee('id="prototype-menu-filter"', false) ->assertSee('prototype-menu-filter', false) // 已打开页面标签栏(固定在页面上方,点开的页面并排显示)。 ->assertSee('data-open-tabs', false) ->assertSee('data-open-tabs-list', false) ->assertSee('caiwu.open-pages.v1.', false) // 目录 iframe 里的子页面不再重复渲染这条标签栏。 ->assertSee('window.self !== window.top', false); } public function testProcessStatisticsPageIsAvailableUnderRiskControl(): void { $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'financial-risk-control', 'page' => 'process-statistics', ])) ->assertOk() ->assertSeeText('流程统计') // 店铺申请流程镜像:列头与查询条件都按外部店铺申请页对齐。 ->assertSeeText('单据编号') ->assertSeeText('审批状态') ->assertSeeText('ERP店铺名称') ->assertSee('data-directory-multi-select="flow_type"', false) ->assertSee('data-directory-multi-select="approval_status"', false) // 测试环境没有业务镜像,页面给出同步提示而不是报错。 ->assertSeeText('店铺申请流程共享镜像尚未完成首次同步。') // 目录内页签:两个页面可以互相切换。 ->assertSee('/workspace/financial-risk-control/store-violation-alert', false) ->assertSee('/workspace/financial-risk-control/process-statistics', false); } public function testGroupedNavigationKeepsRestrictedPagePermissions(): void { $this->user->roles()->sync([Role::where('name', 'manager')->firstOrFail()->id]); $subject = 'user_' . $this->user->id; setting()->set('system.sidebar_permissions', [ 'configured' => [$subject], 'subjects' => [$subject => ['basic-data', 'refunds/non-jingxi-refund-data']], ]); setting()->save(); $this->loginAs() ->get(route('business.pages.show', ['section' => 'basic-data'])) ->assertOk() ->assertSee('/workspace/refunds/non-jingxi-refund-data', false) ->assertDontSee('/workspace/order-data/raw-data', false) ->assertDontSee('/workspace/product-inventory/normal-products', false); $this->withExceptionHandling() ->get(route('business.pages.show', ['section' => 'order-data'])) ->assertForbidden(); } public function testAdminCanUpdateSidebarLabels() { $this->user->roles()->sync([ Role::where('name', 'admin')->firstOrFail()->id, ]); $this->user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail() ); $this->loginAs() ->get(route('settings.navigation.manage')) ->assertOk() ->assertSeeText('侧边栏功能名称') ->assertSee('name="page_titles[courier-payables][quote-management]"', false) ->assertSee('name="page_titles[courier-payables][courier-surcharge]"', false); $this->patch(route('settings.navigation.save'), [ 'section_titles' => [ 'company-management' => '企业档案', ], 'page_titles' => [ 'company-management' => [ 'subject-operating-report' => '主体经营', ], 'courier-payables' => [ 'quote-management' => '快递报价单', 'unknown-page' => '不应保存', ], ], ])->assertOk(); $navigation = BusinessNavigation::all(); $companyRegistration = collect($navigation[0]['pages'])->firstWhere('slug', 'subject-operating-report'); $this->assertSame('企业档案', $navigation[0]['title']); $this->assertSame('主体经营', $companyRegistration['title']); $courier = collect($navigation)->firstWhere('slug', 'courier-payables'); $quotes = collect($courier['pages'])->firstWhere('slug', 'courier-quote'); $this->assertSame('快递报价单', collect($quotes['children'])->firstWhere('slug', 'quote-management')['title']); $this->assertArrayNotHasKey('unknown-page', setting('navigation.titles')['pages']['courier-payables']); } public function testOrderDataAndRefundAreConfiguredBetweenInvoiceAndReceivablesMenus() { $navigation = BusinessNavigation::all(); $slugs = array_column($navigation, 'slug'); $invoiceIndex = array_search('invoice-management', $slugs, true); $fundIndex = array_search('fund-management', $slugs, true); $orderIndex = array_search('order-data', $slugs, true); $refundIndex = array_search('refunds', $slugs, true); $billingIndex = array_search('billing-transactions', $slugs, true); $balanceIndex = array_search('account-balance-data', $slugs, true); $productIndex = array_search('product-inventory', $slugs, true); $receivablesIndex = array_search('store-receivables', $slugs, true); $orderData = $navigation[$orderIndex] ?? null; $refund = $navigation[$refundIndex] ?? null; $productInventory = $navigation[$productIndex] ?? null; $financialOperations = collect($navigation)->firstWhere('slug', 'financial-operations'); $this->assertNotFalse($invoiceIndex); $this->assertNotFalse($fundIndex); $this->assertNotFalse($orderIndex); $this->assertNotFalse($refundIndex); $this->assertNotFalse($billingIndex); $this->assertNotFalse($balanceIndex); $this->assertNotFalse($productIndex); $this->assertNotFalse($receivablesIndex); $this->assertSame( [ 'invoice-management', 'fund-management', 'order-data', 'refunds', 'billing-transactions', 'planting-data', 'account-balance-data', 'product-inventory', ], array_slice($slugs, $invoiceIndex, 8) ); $this->assertSame( [ '票务管理', '资金管理', '订单数据', '退款数据', '账单流水数据', '种菜数据汇总', '账单余额数据', '商品价格管理', ], array_column(array_slice($navigation, $invoiceIndex, 8), 'title') ); $this->assertGreaterThan($invoiceIndex, $orderIndex); $this->assertGreaterThan($orderIndex, $refundIndex); $this->assertGreaterThan($refundIndex, $billingIndex); $this->assertGreaterThan($billingIndex, $balanceIndex); $this->assertGreaterThan($balanceIndex, $productIndex); $this->assertLessThan($receivablesIndex, $productIndex); $this->assertSame('订单数据', $orderData['title']); $this->assertSame( ['raw-data', 'bill-offset'], array_column($orderData['pages'], 'slug') ); $this->assertSame( ['原始数据', '账单冲减'], array_column($orderData['pages'], 'title') ); $this->assertSame( ['after-sales-expenses', 'promotion-expenses'], array_column($orderData['pages'][0]['tabs'], 'slug') ); $this->assertSame( ['订单售后费用明细', '商品推广费用明细'], array_column($orderData['pages'][0]['tabs'], 'title') ); $this->assertSame( ['normal-products', 'online-daifa-price', 'monthly-weighted-average-price', 'inventory-report'], array_column($productInventory['pages'], 'slug') ); $this->assertSame( ['普通商品资料', '线上代发价格', '商品月加权平均单价', '进销存报表'], array_column($productInventory['pages'], 'title') ); $this->assertArrayNotHasKey('children', $orderData['pages'][0]); $this->assertSame( ['bill-offset'], array_column(array_slice($orderData['pages'], 1), 'slug') ); $this->assertSame( ['账单冲减'], array_column(array_slice($orderData['pages'], 1), 'title') ); $this->assertSame('退款数据', $refund['title']); $this->assertSame('assignment_return', $refund['icon']); $this->assertSame( ['jingxi-refund-data', 'non-jingxi-refund-data'], array_column($refund['pages'], 'slug') ); $this->assertSame( ['京喜退款数据', '售后(退货退款)'], array_column($refund['pages'], 'title') ); $this->assertNotNull($financialOperations); $smallPayment = collect($financialOperations['pages'])->firstWhere('slug', 'small-payment-work-orders'); $this->assertSame('小额打款', $smallPayment['title'] ?? null); $this->assertGreaterThan( array_search('after-sales-expenses', array_column($financialOperations['pages'], 'slug'), true), array_search('small-payment-work-orders', array_column($financialOperations['pages'], 'slug'), true) ); } }