service(function (string $platform, string $start, string $end) use (&$seenPeriods): array { $seenPeriods[] = [$platform, $start, $end]; $index = array_search($platform, array_column(StoreReceivingBalanceOverviewService::PLATFORMS, 'slug'), true) + 1; return [ 'opening_balance' => 100.10 * $index, 'income' => 10.20 * $index, 'expense' => 1.25 * $index, ]; }); $result = $service->overview('2026-10'); $this->assertSame('2026-10', $result['month']); $this->assertSame(['start' => '2026-10-01', 'end' => '2026-10-31'], $result['period']); $this->assertCount(8, $result['platforms']); $this->assertCount(8, $seenPeriods); $this->assertSame('taobao-alipay', $seenPeriods[0][0]); $this->assertSame('xiaohongshu', $seenPeriods[7][0]); $this->assertSame(3603.60, $result['summary']['opening_balance']); $this->assertSame(367.20, $result['summary']['income']); $this->assertSame(45.00, $result['summary']['expense']); $this->assertSame(3925.80, $result['summary']['balance']); $this->assertTrue($result['summary']['complete']); $this->assertSame(8, $result['summary']['available_platform_count']); } public function test_overview_does_not_report_a_total_when_an_opening_balance_is_missing(): void { $service = $this->service(function (string $platform): array { return [ 'opening_balance' => $platform === 'vipshop-jx' ? null : 25.0, 'income' => 5.0, 'expense' => 2.0, ]; }); $result = $service->overview('2026-09'); $this->assertFalse($result['summary']['complete']); $this->assertNull($result['summary']['opening_balance']); $this->assertNull($result['summary']['income']); $this->assertNull($result['summary']['expense']); $this->assertNull($result['summary']['balance']); $this->assertSame(7, $result['summary']['available_platform_count']); $vipshopJx = array_values(array_filter( $result['platforms'], static fn (array $platform): bool => $platform['slug'] === 'vipshop-jx', ))[0]; $this->assertFalse($vipshopJx['available']); } public function test_income_detail_groups_platform_store_and_bill_item_rows(): void { $service = new class extends StoreReceivingBalanceOverviewService { public function __construct() { } protected function fetchPlatformIncomeRows(string $platform, string $start, string $end): iterable { $index = array_search($platform, array_column(StoreReceivingBalanceOverviewService::PLATFORMS, 'slug'), true) + 1; yield [ 'store_name' => '店铺-' . $index, 'bill_item' => '货款收入', 'income' => 100.10 * $index, ]; yield [ 'store_name' => '店铺-' . $index, 'bill_item' => '货款收入', 'income' => 0.20 * $index, ]; yield [ 'store_name' => '店铺-' . $index, 'bill_item' => '退款', 'income' => 0, ]; } }; $result = $service->incomeDetail('2026-10'); $this->assertSame('2026-10', $result['month']); $this->assertSame(['start' => '2026-10-01', 'end' => '2026-10-31'], $result['period']); $this->assertCount(8, $result['details']); $this->assertSame([ 'platform_slug' => 'taobao-alipay', 'platform' => '淘系支付宝账户', 'store_name' => '店铺-1', 'bill_item' => '货款收入', 'amount' => 100.30, ], $result['details'][0]); $this->assertSame('xiaohongshu', $result['details'][7]['platform_slug']); $this->assertSame(3610.80, $result['summary']['income']); $this->assertTrue($result['summary']['complete']); $this->assertSame(8, $result['summary']['available_platform_count']); } public function test_expense_detail_groups_platform_store_and_bill_item_rows(): void { $service = new class extends StoreReceivingBalanceOverviewService { public function __construct() { } protected function fetchPlatformExpenseRows(string $platform, string $start, string $end): iterable { $index = array_search($platform, array_column(StoreReceivingBalanceOverviewService::PLATFORMS, 'slug'), true) + 1; yield [ 'store_name' => '店铺-' . $index, 'bill_item' => '平台扣费', 'expense' => 20.10 * $index, ]; yield [ 'store_name' => '店铺-' . $index, 'bill_item' => '平台扣费', 'expense' => 0.20 * $index, ]; yield [ 'store_name' => '店铺-' . $index, 'bill_item' => '退款', 'expense' => 0, ]; } }; $result = $service->expenseDetail('2026-10'); $this->assertSame('2026-10', $result['month']); $this->assertSame(['start' => '2026-10-01', 'end' => '2026-10-31'], $result['period']); $this->assertCount(8, $result['details']); $this->assertSame([ 'platform_slug' => 'taobao-alipay', 'platform' => '淘系支付宝账户', 'store_name' => '店铺-1', 'bill_item' => '平台扣费', 'amount' => 20.30, ], $result['details'][0]); $this->assertSame('xiaohongshu', $result['details'][7]['platform_slug']); $this->assertSame(730.80, $result['summary']['expense']); $this->assertArrayNotHasKey('income', $result['summary']); $this->assertTrue($result['summary']['complete']); $this->assertSame(8, $result['summary']['available_platform_count']); } private function service(callable $summary): StoreReceivingBalanceOverviewService { return new class($summary) extends StoreReceivingBalanceOverviewService { public function __construct(private $summaryCallback) { } protected function fetchPlatformSummary(string $platform, string $start, string $end): array { return ($this->summaryCallback)($platform, $start, $end); } }; } }