service( snapshotRows: [ $this->snapshot('taobao', '淘系店铺', 9950), $this->snapshot('pinduoduo', '拼多多店铺', 9000), $this->snapshot('pinduoduo', '仅聚水潭店铺', 800), ], snapshotDates: [ 'taobao' => '2026-10-31', 'pinduoduo' => '2026-10-31', 'douyin' => '2026-10-31', ], )->build('2026-10-01', '2026-10-31'); $rows = collect($result['stores'])->keyBy('key'); $this->assertSame('normal', $rows['taobao-alipay|淘系店铺']['status']); $this->assertSame(50.0, $rows['taobao-alipay|淘系店铺']['difference']); $this->assertSame('abnormal', $rows['pinduoduo|拼多多店铺']['status']); $this->assertSame(1000.0, $rows['pinduoduo|拼多多店铺']['difference']); $this->assertSame('jst_missing', $rows['douyin|抖音店铺']['status']); $this->assertSame('unavailable', $rows['taobao-wechat|微信店铺']['status']); $this->assertSame('financial_missing', $rows['pinduoduo|仅聚水潭店铺']['status']); $this->assertSame(8, count($result['sources'])); $this->assertSame(5, $result['summary']['unconnected_platform_count']); $this->assertSame(3, count($result['dual_source']['connected_platforms'])); $this->assertSame(200.0, $result['tolerance_yuan']); } public function test_it_marks_a_lagging_snapshot_as_stale_instead_of_matching_old_balances(): void { $result = $this->service( snapshotRows: [$this->snapshot('taobao', '淘系店铺', 9950)], snapshotDates: [ 'taobao' => '2026-10-25', 'pinduoduo' => '2026-10-24', 'douyin' => '2026-10-23', ], )->build('2026-11-01', '2026-11-30'); $taobao = collect($result['stores']) ->firstWhere('platform_slug', 'taobao-alipay'); $this->assertSame('stale', $taobao['status']); $this->assertSame('2026-10-25', $taobao['snapshot_date']); $this->assertSame('取数时间点差', $taobao['reasons'][0]['kind']); $this->assertSame(1, $result['summary']['stale_count']); $this->assertSame('stale', collect($result['sources'])->firstWhere('slug', 'taobao-alipay')['status']); } public function test_it_limits_trend_queries_to_five_representative_dates(): void { $financialRows = [ $this->financial('taobao-alipay', '淘系支付宝账户', '淘系店铺', 10000), $this->financial('pinduoduo', '拼多多账户', '拼多多店铺', 10000), $this->financial('douyin', '抖音账户', '抖音店铺', 3000), ]; $snapshotRows = []; for ($day = 7; $day <= 26; $day++) { $snapshotRows[] = (object) [ 'source_scope' => 'taobao', 'normalized_store_name' => '淘系店铺', 'store_name' => '淘系店铺', 'balance' => 9950, 'balance_date' => '2026-10-' . str_pad((string) $day, 2, '0', STR_PAD_LEFT), ]; } $overview = Mockery::mock(StoreReceivingBalanceOverviewService::class); $overview->shouldReceive('storeBalancesForPlatforms') ->times(5) ->andReturn(['rows' => $financialRows, 'errors' => []]); $repository = Mockery::mock(JushuitanShopBalanceRepository::class); $repository->shouldReceive('comparisonSnapshotsBetween') ->once() ->andReturn($snapshotRows); $result = (new PeriodBalanceReconciliationService($overview, $repository)) ->buildTrend('2026-10-01', '2026-10-31'); $this->assertCount(5, $result['points']); $this->assertSame('2026-10-07', $result['points'][0]['date']); $this->assertSame('2026-10-31', $result['points'][4]['date']); } private function service(array $snapshotRows, array $snapshotDates): PeriodBalanceReconciliationService { $financialRows = [ $this->financial('taobao-alipay', '淘系支付宝账户', '淘系店铺', 10000), $this->financial('pinduoduo', '拼多多账户', '拼多多店铺', 10000), $this->financial('douyin', '抖音账户', '抖音店铺', 3000), $this->financial('taobao-wechat', '淘系微信账户', '微信店铺', 2000), ]; $overview = Mockery::mock(StoreReceivingBalanceOverviewService::class); $overview->shouldReceive('overview') ->once() ->andReturn([ 'summary' => [ 'opening_balance' => 20000, 'income' => 5000, 'expense' => 4000, 'balance' => 21000, 'platform_count' => 8, 'available_platform_count' => 8, 'complete' => true, ], 'platforms' => [], ]); $overview->shouldReceive('storeBalancesForPlatforms') ->andReturn(['rows' => $financialRows, 'errors' => []]); $repository = Mockery::mock(JushuitanShopBalanceRepository::class); $repository->shouldReceive('latestComparisonSnapshots') ->andReturn(['rows' => $snapshotRows, 'dates' => $snapshotDates]); $repository->shouldReceive('earliestBalanceDate')->once()->andReturn('2026-09-01'); $repository->shouldReceive('latestBalanceDate')->once()->andReturn('2026-10-31'); return new PeriodBalanceReconciliationService($overview, $repository); } private function financial(string $platformSlug, string $platform, string $storeName, float $amount): array { return [ 'platform_slug' => $platformSlug, 'platform' => $platform, 'store_name' => $storeName, 'opening_balance' => $amount, 'income' => 0.0, 'expense' => 0.0, 'amount' => $amount, 'available' => true, ]; } private function snapshot(string $scope, string $storeName, float $balance): object { return (object) [ 'source_scope' => $scope, 'normalized_store_name' => JushuitanShopBalanceRepository::normalizeStoreName($storeName), 'store_name' => $storeName, 'balance' => $balance, 'balance_date' => '2026-10-31', ]; } }