在京东账单右侧新增京喜账单页签,复用京东日账单的取数、列定义、筛选、排序、分页与导出,只增加平台维度过滤:京东账单排除京喜店铺,京喜账单只取京喜店铺。平台口径集中在 config/jd_bill_platform.php 与 JdBillPlatform(含口径切换起始日期参数,默认全量历史),仓库通过 forPlatform() 收窄,两个口径互补、总额守恒,底层镜像数据不修改不删除。真实镜像实测:京喜 488,464 笔 + 京东 210,983 笔 = 拆分前 699,447 笔,收入/支出/差额逐项误差为 0。
171 lines
7.7 KiB
PHP
171 lines
7.7 KiB
PHP
<?php
|
||
|
||
namespace Tests\Unit\EcommerceFinance;
|
||
|
||
use Illuminate\Support\Facades\DB;
|
||
use Modules\EcommerceFinance\AccountBillColumns;
|
||
use Modules\EcommerceFinance\JdBillPlatform;
|
||
use Modules\EcommerceFinance\Repositories\JdDailyBillRepository;
|
||
use Modules\EcommerceFinance\Services\JdDailyBillImporter;
|
||
use PHPUnit\Framework\TestCase;
|
||
|
||
/**
|
||
* 京东账单 › 京喜账单的剪切口径:
|
||
* 拆分后两个页签的笔数与金额之和恒等于拆分前,且不修改底层数据。
|
||
*/
|
||
class JdJingxiBillSplitTest extends TestCase
|
||
{
|
||
private array $files = [];
|
||
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
$app = require dirname(__DIR__, 3) . '/bootstrap/app.php';
|
||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||
config(['database.connections.business_mirror' => ['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '']]);
|
||
DB::purge('business_mirror');
|
||
(require database_path('migrations/2026_09_18_140000_create_jd_daily_bill_mirror_tables.php'))->up();
|
||
}
|
||
|
||
protected function tearDown(): void
|
||
{
|
||
foreach ($this->files as $file) unlink($file);
|
||
parent::tearDown();
|
||
}
|
||
|
||
public function test_split_keeps_京东_and_京喜_totals_equal_to_the_unsplit_bill(): void
|
||
{
|
||
$this->importFixture();
|
||
|
||
$all = $this->repository(null)->paginate([], 'occurred_at', 'desc', 1, 10);
|
||
$jd = $this->repository(JdBillPlatform::BILL_TYPE_JD)->paginate([], 'occurred_at', 'desc', 1, 10);
|
||
$jingxi = $this->repository(JdBillPlatform::BILL_TYPE_JINGXI)->paginate([], 'occurred_at', 'desc', 1, 10);
|
||
|
||
// 全量共 6 笔;京喜 4 笔(含京喜小店),京东账单 2 笔。
|
||
$this->assertSame(6, $all['total']);
|
||
$this->assertSame(4, $jingxi['total']);
|
||
$this->assertSame(2, $jd['total']);
|
||
|
||
foreach (['count', 'income', 'expense', 'balance'] as $field) {
|
||
$this->assertSame(
|
||
round($all['summary'][$field], 2),
|
||
round($jd['summary'][$field] + $jingxi['summary'][$field], 2),
|
||
"拆分后 {$field} 不守恒",
|
||
);
|
||
}
|
||
}
|
||
|
||
public function test_same_day_rows_are_only_visible_in_the_jingxi_tab(): void
|
||
{
|
||
$this->importFixture();
|
||
|
||
$filters = ['start' => '2026-09-23', 'end' => '2026-09-23', 'store_name' => '京喜-安隅京喜自营工厂店'];
|
||
$jingxi = $this->repository(JdBillPlatform::BILL_TYPE_JINGXI)->paginate($filters, 'occurred_at', 'desc', 1, 10);
|
||
$jd = $this->repository(JdBillPlatform::BILL_TYPE_JD)->paginate($filters, 'occurred_at', 'desc', 1, 10);
|
||
|
||
$this->assertSame(2, $jingxi['total']);
|
||
$this->assertSame(['京喜-安隅京喜自营工厂店'], array_unique(array_column($jingxi['data'], 'store_name')));
|
||
$this->assertSame(0, $jd['total']);
|
||
$this->assertSame([], $jd['data']);
|
||
}
|
||
|
||
public function test_cutoff_date_keeps_earlier_jingxi_rows_in_the_jd_bill(): void
|
||
{
|
||
$this->importFixture();
|
||
config(['jd_bill_platform.scope_start_date' => '2026-09-23']);
|
||
|
||
$all = $this->repository(null)->paginate([], 'occurred_at', 'desc', 1, 10);
|
||
$jd = $this->repository(JdBillPlatform::BILL_TYPE_JD)->paginate([], 'occurred_at', 'desc', 1, 10);
|
||
$jingxi = $this->repository(JdBillPlatform::BILL_TYPE_JINGXI)->paginate([], 'occurred_at', 'desc', 1, 10);
|
||
|
||
// 只有 09-23 起的京喜行被切走,09-10 的京喜行仍留在京东账单,总数守恒。
|
||
$this->assertSame(3, $jingxi['total']);
|
||
$this->assertSame(3, $jd['total']);
|
||
$this->assertSame($all['total'], $jd['total'] + $jingxi['total']);
|
||
$this->assertSame(
|
||
round($all['summary']['income'], 2),
|
||
round($jd['summary']['income'] + $jingxi['summary']['income'], 2),
|
||
);
|
||
}
|
||
|
||
public function test_filter_options_follow_the_tab_scope(): void
|
||
{
|
||
$this->importFixture();
|
||
|
||
$jingxi = $this->repository(JdBillPlatform::BILL_TYPE_JINGXI)->filterOptions();
|
||
$jd = $this->repository(JdBillPlatform::BILL_TYPE_JD)->filterOptions();
|
||
|
||
$this->assertSame(['京喜-安隅京喜自营工厂店', '京喜小店-简尚家居家具制造店'], $jingxi['shops']);
|
||
$this->assertSame(['京东-慕加旗舰店'], $jd['shops']);
|
||
}
|
||
|
||
public function test_export_follows_the_tab_scope(): void
|
||
{
|
||
$this->importFixture();
|
||
|
||
$jingxi = array_column(iterator_to_array($this->repository(JdBillPlatform::BILL_TYPE_JINGXI)->exportRows([], 'occurred_at', 'asc')), 'store_name');
|
||
$jd = array_column(iterator_to_array($this->repository(JdBillPlatform::BILL_TYPE_JD)->exportRows([], 'occurred_at', 'asc')), 'store_name');
|
||
|
||
$this->assertCount(4, $jingxi);
|
||
$this->assertCount(2, $jd);
|
||
$this->assertSame([], array_intersect($jingxi, $jd));
|
||
}
|
||
|
||
public function test_jingxi_tab_reuses_the_jd_bill_columns_and_sits_next_to_it(): void
|
||
{
|
||
$jd = AccountBillColumns::for(JdBillPlatform::BILL_TYPE_JD);
|
||
$jingxi = AccountBillColumns::for(JdBillPlatform::BILL_TYPE_JINGXI);
|
||
|
||
$this->assertSame('京喜账单', $jingxi['title']);
|
||
$this->assertSame($jd['columns'], $jingxi['columns']);
|
||
$this->assertSame([], $jingxi['groups']);
|
||
|
||
$tabs = collect(collect(config('business_navigation'))->firstWhere('slug', 'billing-transactions')['pages'])
|
||
->firstWhere('slug', 'store-receiving-bills')['tabs'];
|
||
$slugs = array_column($tabs, 'slug');
|
||
$this->assertSame('jingxi', $slugs[array_search('jd-transactions', $slugs, true) + 1]);
|
||
}
|
||
|
||
private function repository(?string $platform): JdDailyBillRepository
|
||
{
|
||
return app(JdDailyBillRepository::class)->forPlatform(
|
||
$platform === null ? null : JdBillPlatform::platformForBillType($platform),
|
||
);
|
||
}
|
||
|
||
/** 6 笔:京喜 4 笔(含「京喜小店」店铺)、京东 2 笔,横跨 2026-09-10 与 2026-09-23 两个记账日期。 */
|
||
private function importFixture(): void
|
||
{
|
||
$rows = [
|
||
$this->row('1', '京喜-安隅京喜自营工厂店', '2026-09-10 09:00:00', '10.00', '0.00'),
|
||
$this->row('2', '京喜-安隅京喜自营工厂店', '2026-09-23 09:00:00', '20.00', '0.00'),
|
||
$this->row('3', '京喜-安隅京喜自营工厂店', '2026-09-23 10:00:00', '0.00', '5.00'),
|
||
$this->row('4', '京喜小店-简尚家居家具制造店', '2026-09-23 11:00:00', '7.00', '0.00'),
|
||
$this->row('5', '京东-慕加旗舰店', '2026-09-23 12:00:00', '3.00', '0.00'),
|
||
$this->row('6', '京东-慕加旗舰店', '2026-09-10 12:00:00', '0.00', '2.00'),
|
||
];
|
||
$this->import($rows, 6, '40.00', '7.00');
|
||
}
|
||
|
||
private function import(array $rows, int $count, string $income, string $expense): array
|
||
{
|
||
$path = tempnam(sys_get_temp_dir(), 'jd-jingxi-');
|
||
$this->files[] = $path;
|
||
$file = fopen($path, 'wb');
|
||
fputcsv($file, array_keys($rows[0]));
|
||
foreach ($rows as $row) fputcsv($file, array_values($row));
|
||
fclose($file);
|
||
|
||
return app(JdDailyBillImporter::class)->import($path, '2026-09', $count, $income, $expense);
|
||
}
|
||
|
||
private function row(string $code, string $store, string $occurredAt, string $income, string $expense): array
|
||
{
|
||
return array_replace(array_fill_keys(array_values(JdDailyBillImporter::PAYLOAD_FIELDS), ''), [
|
||
'记录编码' => $code, '店铺' => $store, '账单项目' => '交易服务费', '订单编号' => '360640400000000' . $code,
|
||
'单据编号' => '1010012608312386678178000' . $code, '费用结算时间' => $occurredAt,
|
||
'费用发生时间' => $occurredAt, '收入金额' => $income, '支出金额' => $expense, '币种' => 'CNY',
|
||
]);
|
||
}
|
||
}
|