377 lines
15 KiB
PHP
377 lines
15 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\EcommerceFinance;
|
|
|
|
use Modules\EcommerceFinance\Services\StoreReceivingBalanceOverviewService;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class StoreReceivingBalanceOverviewServiceTest extends TestCase
|
|
{
|
|
public function test_overview_sums_all_eight_platforms_for_the_selected_month(): void
|
|
{
|
|
$seenPeriods = [];
|
|
$service = $this->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_overview_uses_an_explicit_date_range_for_all_platforms(): void
|
|
{
|
|
$seenPeriods = [];
|
|
$service = $this->service(function (string $platform, string $start, string $end) use (&$seenPeriods): array {
|
|
$seenPeriods[] = [$platform, $start, $end];
|
|
|
|
return [
|
|
'opening_balance' => 10.0,
|
|
'income' => 2.0,
|
|
'expense' => 1.0,
|
|
];
|
|
});
|
|
|
|
$result = $service->overview('2026-10-05', '2026-10-20');
|
|
|
|
$this->assertSame(['start' => '2026-10-05', 'end' => '2026-10-20'], $result['period']);
|
|
$this->assertCount(8, $seenPeriods);
|
|
$this->assertSame(['taobao-alipay', '2026-10-05', '2026-10-20'], $seenPeriods[0]);
|
|
$this->assertSame(['xiaohongshu', '2026-10-05', '2026-10-20'], $seenPeriods[7]);
|
|
$this->assertTrue($result['summary']['complete']);
|
|
}
|
|
|
|
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']);
|
|
}
|
|
|
|
public function test_balance_detail_uses_each_platform_closing_balance(): void
|
|
{
|
|
$service = $this->service(function (string $platform): array {
|
|
$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->balanceDetail('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' => '',
|
|
'bill_item' => '',
|
|
'amount' => 109.05,
|
|
], $result['details'][0]);
|
|
$this->assertSame('xiaohongshu', $result['details'][7]['platform_slug']);
|
|
$this->assertSame(872.40, $result['details'][7]['amount']);
|
|
$this->assertSame(3925.80, $result['summary']['balance']);
|
|
$this->assertTrue($result['summary']['complete']);
|
|
$this->assertSame(8, $result['summary']['available_platform_count']);
|
|
}
|
|
|
|
public function test_balance_store_detail_calculates_each_store_closing_balance(): void
|
|
{
|
|
$service = new class extends StoreReceivingBalanceOverviewService {
|
|
public function __construct()
|
|
{
|
|
}
|
|
|
|
protected function fetchPlatformStoreBalances(string $platform, string $start, string $end): array
|
|
{
|
|
$index = array_search($platform, array_column(StoreReceivingBalanceOverviewService::PLATFORMS, 'slug'), true) + 1;
|
|
|
|
return [[
|
|
'store_name' => '店铺-' . $index,
|
|
'opening_balance' => 100.10 * $index,
|
|
'income' => 10.20 * $index,
|
|
'expense' => 1.25 * $index,
|
|
]];
|
|
}
|
|
};
|
|
|
|
$result = $service->balanceStoreDetail('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' => '',
|
|
'opening_balance' => 100.10,
|
|
'income' => 10.20,
|
|
'expense' => 1.25,
|
|
'amount' => 109.05,
|
|
'available' => true,
|
|
], $result['details'][0]);
|
|
$this->assertSame(3925.80, $result['summary']['balance']);
|
|
$this->assertSame(8, $result['summary']['store_count']);
|
|
$this->assertSame(8, $result['summary']['ready_store_count']);
|
|
$this->assertTrue($result['summary']['complete']);
|
|
}
|
|
|
|
public function test_balance_store_detail_marks_missing_opening_instead_of_using_zero(): void
|
|
{
|
|
$service = new class extends StoreReceivingBalanceOverviewService {
|
|
public function __construct()
|
|
{
|
|
}
|
|
|
|
protected function fetchPlatformStoreBalances(string $platform, string $start, string $end): array
|
|
{
|
|
return [[
|
|
'store_name' => '待补期初店铺',
|
|
'opening_balance' => $platform === 'vipshop-jx' ? null : 25.0,
|
|
'income' => 5.0,
|
|
'expense' => 2.0,
|
|
]];
|
|
}
|
|
};
|
|
|
|
$result = $service->balanceStoreDetail('2026-10');
|
|
|
|
$this->assertFalse($result['summary']['complete']);
|
|
$this->assertNull($result['summary']['balance']);
|
|
$this->assertSame(7, $result['summary']['ready_store_count']);
|
|
$unavailable = array_values(array_filter(
|
|
$result['details'],
|
|
static fn (array $row): bool => $row['platform_slug'] === 'vipshop-jx',
|
|
))[0];
|
|
$this->assertNull($unavailable['opening_balance']);
|
|
$this->assertNull($unavailable['amount']);
|
|
$this->assertFalse($unavailable['available']);
|
|
}
|
|
|
|
public function test_balance_store_detail_merges_duplicate_store_rows_without_creating_zero_opening(): void
|
|
{
|
|
$service = new class extends StoreReceivingBalanceOverviewService {
|
|
public function __construct()
|
|
{
|
|
}
|
|
|
|
protected function fetchPlatformStoreBalances(string $platform, string $start, string $end): array
|
|
{
|
|
return [[
|
|
'store_name' => '重复店铺',
|
|
'opening_balance' => null,
|
|
'income' => 5.0,
|
|
'expense' => 0.0,
|
|
], [
|
|
'store_name' => '重复店铺',
|
|
'opening_balance' => null,
|
|
'income' => 0.0,
|
|
'expense' => 2.0,
|
|
]];
|
|
}
|
|
};
|
|
|
|
$result = $service->balanceStoreDetail('2026-10');
|
|
$rows = array_values(array_filter(
|
|
$result['details'],
|
|
static fn (array $row): bool => $row['store_name'] === '重复店铺',
|
|
));
|
|
|
|
$this->assertCount(1, $rows);
|
|
$this->assertNull($rows[0]['opening_balance']);
|
|
$this->assertSame(5.0, $rows[0]['income']);
|
|
$this->assertSame(2.0, $rows[0]['expense']);
|
|
$this->assertNull($rows[0]['amount']);
|
|
}
|
|
|
|
public function test_monthly_closing_returns_july_to_october_for_all_platforms(): void
|
|
{
|
|
$monthOrdinal = ['2026-07' => 1, '2026-08' => 2, '2026-09' => 3, '2026-10' => 4];
|
|
$seenPeriods = [];
|
|
$service = $this->service(function (string $platform, string $start, string $end) use (&$seenPeriods, $monthOrdinal): array {
|
|
$seenPeriods[] = [$platform, $start, $end];
|
|
$index = array_search($platform, array_column(StoreReceivingBalanceOverviewService::PLATFORMS, 'slug'), true) + 1;
|
|
|
|
return [
|
|
'opening_balance' => 1000.00 * $index,
|
|
'income' => 10.00 * $monthOrdinal[substr($start, 0, 7)],
|
|
'expense' => 0.0,
|
|
];
|
|
});
|
|
|
|
$result = $service->monthlyClosing();
|
|
|
|
$this->assertCount(32, $seenPeriods);
|
|
$this->assertSame(['2026-07', '2026-08', '2026-09', '2026-10'], array_column($result['months'], 'key'));
|
|
$this->assertCount(8, $result['platforms']);
|
|
$this->assertSame([
|
|
'2026-07' => 1010.00,
|
|
'2026-08' => 1020.00,
|
|
'2026-09' => 1030.00,
|
|
'2026-10' => 1040.00,
|
|
], $result['platforms'][0]['balances']);
|
|
$this->assertSame([
|
|
'amount' => 10.0,
|
|
'rate' => 0.97,
|
|
], $result['platforms'][0]['change']);
|
|
$this->assertSame('合计', $result['total']['name']);
|
|
$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);
|
|
}
|
|
|
|
protected function fetchPlatformStoreBalances(string $platform, string $start, string $end): array
|
|
{
|
|
return [];
|
|
}
|
|
};
|
|
}
|
|
}
|