loginAs(); ThirdPartyWarehouseQuote::query()->create([ 'company_id' => (int) 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, ]); $rows = [ ['date' => '2026-08-01', 'sku_code' => 'A', 'owner' => '甲', 'product_name' => '商品A', 'supplier' => '', 'category' => '', 'unit' => '个', 'opening_qty' => 100, 'closing_qty' => 100, 'purchase_in' => 0, 'sales_out' => 0, 'other_out' => 0], ['date' => '2026-08-01', 'sku_code' => 'B', 'owner' => '甲', 'product_name' => '商品B', 'supplier' => '', 'category' => '', 'unit' => '个', 'opening_qty' => 0, 'closing_qty' => 50, 'purchase_in' => 50, 'sales_out' => 0, 'other_out' => 0], ['date' => '2026-08-02', 'sku_code' => 'A', 'owner' => '甲', 'product_name' => '商品A', 'supplier' => '', 'category' => '', 'unit' => '个', 'opening_qty' => 100, 'closing_qty' => 90, 'purchase_in' => 0, 'sales_out' => -10, 'other_out' => 0], ]; $mirror = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class); $mirror->shouldReceive('exportRows')->andReturnUsing(static function () use ($rows): \Generator { foreach ($rows as $row) { yield $row; } }); $this->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $mirror); $weights = Mockery::mock(ThirdPartyWarehouseProductWeightRepository::class); $weights->shouldReceive('weightsBySkuCodes')->andReturn(['A' => 23.0, 'B' => 46.0]); $this->instance(ThirdPartyWarehouseProductWeightRepository::class, $weights); $result = app(ThirdPartyWarehouseStorageFeeCalculator::class) ->calculate(15045364, '南昌樵舍仓', '2026-08'); $summary = $result['summary']; // 入库:B 采购进仓 50 × 46kg/1000 = 2.3 吨 × 20 = 46.00 $this->assertSame(46.0, (float) $summary['inbound_fee']); // 出库:|−10| = 10 件 × 0.85 = 8.50 $this->assertSame(10.0, (float) $summary['outbound_qty']); $this->assertSame(8.5, (float) $summary['outbound_fee']); // 仓储:日结余 08-01=150、08-02=90;当月 31 天 $expectedStorage = round(150 / 23 * 3 * 10.5 / 31 + 90 / 23 * 3 * 10.5 / 31, 2); $this->assertSame($expectedStorage, (float) $summary['storage_fee']); $this->assertSame(round(46.0 + 8.5 + $expectedStorage, 2), (float) $summary['total_fee']); $this->assertCount(2, $result['daily']); $this->assertCount(2, $result['per_sku']); $this->assertCount(3, $result['daily_sku']); $firstDay = collect($result['daily_sku'])->where('date', '2026-08-01'); $carriedStock = $firstDay->firstWhere('sku_code', 'A'); $this->assertSame(100.0, (float) $carriedStock['opening']); $this->assertSame(0.0, (float) $carriedStock['net_movement']); $this->assertSame(100.0, (float) $carriedStock['closing']); $this->assertEqualsWithDelta(100 / 23 * 3 * 10.5 / 31, $carriedStock['storage_fee'], 0.000001); $this->assertEqualsWithDelta( $result['daily'][0]['storage_fee'], $firstDay->sum('storage_fee'), 0.0001, ); $sheets = (new ThirdPartyWarehouseStorageSettlementExport($result))->sheets(); $this->assertCount(4, $sheets); $this->assertSame('每日商品仓储费', $sheets[3]->title()); $this->assertSame('A', $sheets[3]->array()[0][2]); } public function test_requires_effective_quote(): void { $this->loginAs(); $mirror = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class); $this->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $mirror); $weights = Mockery::mock(ThirdPartyWarehouseProductWeightRepository::class); $this->instance(ThirdPartyWarehouseProductWeightRepository::class, $weights); $this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class); app(ThirdPartyWarehouseStorageFeeCalculator::class) ->calculate(15045364, '南昌樵舍仓', '2026-08'); } }