user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $quote = $this->createQuote(); $this->createSettlement($quote, []); $this->loginAs()->get(route('business.pages.show', ['section' => 'courier-payables'])) ->assertOk() ->assertSee('data-courier-tab="warehouse-freight"', false); $this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-quote'])) ->assertOk() ->assertDontSee('data-courier-tab="warehouse-freight"', false); $this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'warehouse-freight'])) ->assertOk() ->assertSeeText('仓储费') ->assertSeeText('2026-08') ->assertSeeText('查看明细') ->assertSeeText('导出核算') ->assertSee('data-warehouse-freight-page', false) ->assertDontSeeText('录入新报价'); } public function test_restricted_quote_permission_does_not_grant_warehouse_freight_access(): void { $this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]); $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $this->setPagePermissions(['courier-payables/quote-management']); $this->withExceptionHandling()->loginAs(); $this->getJson(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'warehouse-freight']), [ 'X-Requested-With' => 'XMLHttpRequest', ])->assertNotFound(); Livewire::test(WarehouseFreightComponent::class)->assertForbidden(); } public function test_component_actions_recheck_warehouse_freight_permission(): void { $this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]); $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $this->setPagePermissions(['courier-payables/warehouse-freight']); $this->withExceptionHandling()->loginAs(); $component = Livewire::test(WarehouseFreightComponent::class); $this->setPagePermissions(['courier-payables/quote-management']); $component->call('calculateStorageFee')->assertForbidden(); $this->assertDatabaseCount('business_third_party_warehouse_storage_settlements', 0); } public function test_calculation_saves_a_month_without_overwriting_other_history(): void { $this->loginAs(); $quote = $this->createQuote(); $this->createSettlement($quote, []); $summary = ThirdPartyWarehouseStorageSettlement::query()->firstOrFail()->attributesToArray(); $summary['period'] = '2026-09'; $summary['days_in_month'] = 30; $summary['inbound_fee'] = 46.0; $summary['outbound_fee'] = 8.5; $summary['storage_fee'] = 10.25; $summary['total_fee'] = 64.75; $calculator = \Mockery::mock(\App\Business\ThirdPartyWarehouseStorageFeeCalculator::class); $calculator->shouldReceive('calculate')->once() ->with(WarehouseFreightComponent::WAREHOUSE_ID, '南昌樵舍仓', '2026-09') ->andReturn(['summary' => $summary, 'daily' => [], 'per_sku' => [], 'daily_sku' => []]); $this->instance(\App\Business\ThirdPartyWarehouseStorageFeeCalculator::class, $calculator); Livewire::test(WarehouseFreightComponent::class) ->set('settlementPeriod', '2026-09') ->call('calculateStorageFee') ->assertHasNoErrors() ->assertSet('settlementSummary.total_fee', 64.75) ->assertSee('64.75'); $this->assertDatabaseCount('business_third_party_warehouse_storage_settlements', 2); $this->assertDatabaseHas('business_third_party_warehouse_storage_settlements', [ 'company_id' => company_id(), 'period' => '2026-09', 'total_fee' => 64.75, ]); } public function test_invalid_month_and_missing_quote_show_errors_without_saving(): void { $this->loginAs(); Livewire::test(WarehouseFreightComponent::class) ->set('settlementPeriod', '2026-13') ->call('calculateStorageFee') ->assertHasErrors(['settlementPeriod' => 'regex']) ->assertSee('结算月份格式应为 YYYY-MM。'); Livewire::test(WarehouseFreightComponent::class) ->set('settlementPeriod', '2026-08') ->call('calculateStorageFee') ->assertSet('settlementSummary', null) ->assertSee('role="alert"', false); $this->assertDatabaseCount('business_third_party_warehouse_storage_settlements', 0); } public function test_saved_monthly_calculation_can_be_exported(): void { $this->loginAs(); $quote = $this->createQuote(); $this->createSettlement($quote, []); Excel::fake(); Livewire::test(WarehouseFreightComponent::class)->call('exportSettlement', '2026-08'); Excel::assertDownloaded('南昌樵舍仓仓储费用_2026-08.xlsx'); } public function test_daily_sku_storage_detail_can_be_opened_and_switched_by_date(): void { $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $this->loginAs(); $quote = $this->createQuote(); $this->createSettlement($quote, [ ['date' => '2026-08-01', 'owner' => '甲', 'sku_code' => 'SKU-A', 'product_name' => '商品A', 'supplier' => '供应商甲', 'category' => '桌类', 'unit' => '件', 'opening' => 100, 'net_movement' => 0, 'closing' => 100, 'storage_fee' => 4.417952], ['date' => '2026-08-01', 'owner' => '甲', 'sku_code' => 'SKU-B', 'product_name' => '商品B', 'supplier' => '供应商乙', 'category' => '柜类', 'unit' => '件', 'opening' => 50, 'net_movement' => 10, 'closing' => 60, 'storage_fee' => 2.650771], ['date' => '2026-08-02', 'owner' => '甲', 'sku_code' => 'SKU-A', 'product_name' => '商品A', 'supplier' => '供应商甲', 'category' => '桌类', 'unit' => '件', 'opening' => 100, 'net_movement' => -10, 'closing' => 90, 'storage_fee' => 3.976157], ]); Livewire::test(WarehouseFreightComponent::class) ->call('viewSettlementDetails', '2026-08') ->assertSet('settlementDetailDate', '2026-08-01') ->assertDispatched('warehouse-settlement-details-opened') ->assertSee('每日商品仓储费明细') ->assertSee('导出整月商品明细') ->assertSee('160.00') ->assertSee('SKU-A') ->assertSee('SKU-B') ->set('settlementDetailDate', '2026-08-02') ->assertSee('90.00') ->assertSee('3.976157') ->assertDontSee('SKU-B'); } public function test_legacy_settlement_explains_that_recalculation_is_required(): void { $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $this->loginAs(); $quote = $this->createQuote(); $this->createSettlement($quote, []); Livewire::test(WarehouseFreightComponent::class) ->call('viewSettlementDetails', '2026-08') ->assertDispatched('warehouse-settlement-details-opened') ->assertSee('原存档没有每日商品明细,请重新核算该月份后查看和导出'); } public function test_daily_sku_storage_details_can_be_exported_for_the_whole_month(): void { $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $this->loginAs(); $quote = $this->createQuote(); $dailySku = [ ['date' => '2026-08-01', 'owner' => '甲', 'sku_code' => 'SKU-A', 'product_name' => '商品A', 'supplier' => '供应商甲', 'category' => '桌类', 'unit' => '件', 'opening' => 100, 'net_movement' => 0, 'closing' => 100, 'storage_fee' => 4.417952], ['date' => '2026-08-02', 'owner' => '甲', 'sku_code' => 'SKU-A', 'product_name' => '商品A', 'supplier' => '供应商甲', 'category' => '桌类', 'unit' => '件', 'opening' => 100, 'net_movement' => -10, 'closing' => 90, 'storage_fee' => 3.976157], ]; $this->createSettlement($quote, $dailySku); Excel::fake(); Livewire::test(WarehouseFreightComponent::class) ->call('exportSettlementDetails', '2026-08'); Excel::assertDownloaded('南昌樵舍仓每日商品仓储费明细_2026-08.xlsx', function ($export) use ($dailySku): bool { return $export->title() === '每日商品仓储费' && count($export->array()) === count($dailySku) && $export->array()[1][0] === '2026-08-02'; }); } public function test_legacy_settlement_does_not_export_an_empty_detail_file(): void { $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $this->loginAs(); $quote = $this->createQuote(); $this->createSettlement($quote, []); Excel::fake(); Livewire::test(WarehouseFreightComponent::class) ->call('exportSettlementDetails', '2026-08') ->assertSet('settlementDetailPeriod', '2026-08') ->assertDispatched('warehouse-settlement-details-opened') ->assertSee('原存档没有每日商品明细,请重新核算该月份后查看和导出'); $this->assertSame([], $this->readExcelFakeDownloads()); } public function test_settlement_history_detail_and_export_are_company_scoped(): void { $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $this->loginAs(); $quote = $this->createQuote(); $this->createSettlement($quote, [], company_id() + 999, '2026-07'); Livewire::test(WarehouseFreightComponent::class) ->assertDontSee('2026-07') ->call('viewSettlementDetails', '2026-07') ->assertSet('settlementDetailMessage', '未找到 2026-07 的核算存档。') ->call('exportSettlement', '2026-07') ->assertSet('settlementError', '未找到 2026-07 的核算存档,请先核算。') ->call('exportSettlementDetails', '2026-07') ->assertSet('settlementError', '未找到 2026-07 的核算存档,请先核算。'); } private function createQuote(): ThirdPartyWarehouseQuote { return ThirdPartyWarehouseQuote::query()->create([ 'company_id' => company_id(), 'warehouse_name' => '南昌樵舍仓', 'inbound_rate_per_ton' => 20, 'outbound_rate_per_order' => 0.85, 'pieces_per_pallet' => 23, 'pallet_area_sqm' => 3, 'storage_rate_per_sqm_month' => 10.5, 'effective_at' => '2026-08-01', 'status' => ThirdPartyWarehouseQuote::STATUS_ACTIVE, 'remark' => '八月合同报价', 'created_by' => $this->user->id, 'updated_by' => $this->user->id, ]); } private function setPagePermissions(array $pages): void { $subject = 'user_' . $this->user->id; setting()->set('system.sidebar_permissions', [ 'configured' => [$subject], 'subjects' => [$subject => $pages], ]); setting()->save(); } /** @return array */ private function readExcelFakeDownloads(): array { $fake = Excel::getFacadeRoot(); $property = new \ReflectionProperty($fake, 'downloads'); $property->setAccessible(true); return $property->getValue($fake); } /** @param list> $dailySku */ private function createSettlement( ThirdPartyWarehouseQuote $quote, array $dailySku, ?int $companyId = null, string $period = '2026-08', ): void { ThirdPartyWarehouseStorageSettlement::query()->create([ 'company_id' => $companyId ?? company_id(), 'warehouse_id' => WarehouseFreightComponent::WAREHOUSE_ID, 'warehouse_name' => '南昌樵舍仓', 'period' => $period, 'quote_id' => $quote->id, 'inbound_rate_per_ton' => 20, 'outbound_rate_per_order' => 0.85, 'pieces_per_pallet' => 23, 'pallet_area_sqm' => 3, 'storage_rate_per_sqm_month' => 10.5, 'days_in_month' => 31, 'inbound_tons' => 0, 'outbound_qty' => 0, 'inbound_fee' => 0, 'outbound_fee' => 0, 'storage_fee' => array_sum(array_column($dailySku, 'storage_fee')), 'memo_fee' => 0, 'loss_fee' => 0, 'compensation_fee' => 0, 'total_fee' => array_sum(array_column($dailySku, 'storage_fee')), 'detail_json' => ['daily' => [], 'per_sku' => [], 'daily_sku' => $dailySku], 'calculated_by' => user_id(), ]); } }