CaiWuXiTong/tests/Unit/EcommerceFinance/DouyinAuthoritativeBillTest.php

189 lines
11 KiB
PHP

<?php
namespace Tests\Unit\EcommerceFinance;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Repositories\StoreReceivingDouyinBillMirrorRepository;
use Modules\EcommerceFinance\Repositories\StoreReceivingAlipayBillMirrorRepository;
use Modules\EcommerceFinance\Services\DouyinAuthoritativeBillImporter;
use Tests\TestCase;
class DouyinAuthoritativeBillTest extends TestCase
{
private array $files = [];
protected function setUp(): void
{
parent::setUp();
config(['database.connections.business_mirror' => ['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '']]);
DB::purge('business_mirror');
(require database_path('migrations/2026_09_14_100000_create_douyin_authoritative_bill_tables.php'))->up();
(require database_path('migrations/2026_09_07_000000_create_original_bill_info_mirror_table.php'))->up();
$schema = Schema::connection('business_mirror');
$schema->create('ops_jst_shop_info', function (Blueprint $t): void {
$t->integer('Id'); $t->integer('shop_id'); $t->string('shop_name');
});
$schema->create('business_mirror_table_states', function (Blueprint $t): void {
$t->string('dataset_key'); $t->dateTime('initial_completed_at')->nullable(); $t->string('status');
});
foreach ([StoreReceivingAlipayBillMirrorRepository::DATASET, StoreReceivingAlipayBillMirrorRepository::SHOP_DATASET] as $key) {
$this->db()->table('business_mirror_table_states')->insert(['dataset_key' => $key, 'initial_completed_at' => now(), 'status' => 'succeeded']);
}
$this->db()->table('ops_jst_shop_info')->insert(['Id' => 1, 'shop_id' => 7, 'shop_name' => '测试店']);
}
protected function tearDown(): void
{
foreach ($this->files as $file) @unlink($file);
parent::tearDown();
}
public function test_month_replaces_split_bills_and_unifies_list_summary_filters_and_export(): void
{
$this->legacy(1, '2026-08-15 12:00:00', 30);
$this->legacy(2, '2026-08-15 12:00:00', 30);
$this->legacy(3, '2026-09-01 00:00:00', 9);
$rows = [$this->row('2000000000000000101', '40.12'), $this->row('2000000000000000102', '0.00', '2.03')];
$this->import($rows, 2, '40.12', '2.03');
$repo = app(StoreReceivingDouyinBillMirrorRepository::class);
$all = $repo->paginate([], 'occurred_at', 'asc', 1, 10);
$this->assertSame(3, $all['total']);
$this->assertSame(49.12, $all['summary']['income']);
$this->assertSame(2.03, $all['summary']['expense']);
$filters = ['start' => '2026-08-01', 'end' => '2026-08-31'];
$first = $repo->paginate($filters, 'record_code', 'asc', 1, 1);
$second = $repo->paginate($filters, 'record_code', 'asc', 2, 1);
$this->assertSame('2000000000000000101', $first['data'][0]['record_code']);
$this->assertSame('2000000000000000102', $second['data'][0]['record_code']);
$this->assertSame('测试主体', $first['data'][0]['payload']['merchant_name']);
$this->assertSame('20260815000100000000001', $first['data'][0]['payload']['move_serial_no']);
$this->assertSame('6912345678901234567', $first['data'][0]['payload']['sub_order_no']);
$this->assertSame('2026-07-31 10:00:00', $first['data'][0]['payload']['order_placed_at']);
$export = iterator_to_array($repo->exportRows($filters, 'record_code', 'asc'));
$this->assertSame([$first['data'][0], $second['data'][0]], $export);
$this->assertSame(2, $repo->paginate($filters + ['store_name' => '测试店', 'bill_item' => '订单实付'], 'income', 'desc', 1, 10)['total']);
$this->assertSame(1, $repo->paginate(['keyword' => '2000000000000000102'], 'occurred_at', 'asc', 1, 10)['total']);
$this->assertContains('测试店', $repo->filterOptions()['shops']);
$this->assertContains('订单实付', $repo->filterOptions()['bill_items']);
// A later source sync must not bring the incorrect month back.
$this->legacy(4, '2026-08-31 23:59:59', 100);
$this->assertSame(2, $repo->paginate($filters, 'occurred_at', 'asc', 1, 10)['total']);
}
public function test_exact_record_filter_preserves_channel_period_and_other_filters(): void
{
$this->legacy(3, '2026-09-01 00:00:00', 9);
$this->legacy(30, '2026-09-01 00:00:00', 10);
$repo = app(StoreReceivingDouyinBillMirrorRepository::class);
$exact = ['keyword' => '3', 'keyword_match' => 'exact-record'];
$this->assertSame(1, $repo->paginate($exact, 'record_code', 'asc', 1, 20)['total']);
$this->assertSame(2, $repo->paginate(['keyword' => '3'], 'record_code', 'asc', 1, 20)['total']);
foreach (['3invalid', '03', '2147483648', '2000000000000000101'] as $code) {
$this->assertSame(0, $repo->paginate(array_replace($exact, ['keyword' => $code]), 'record_code', 'asc', 1, 20)['total']);
}
$this->import([$this->row('2000000000000000101', '10.00')], 1, '10.00', '0.00');
$exact['keyword'] = '2000000000000000101';
$this->assertSame(1, $repo->paginate($exact, 'record_code', 'asc', 1, 20)['total']);
$this->assertCount(1, iterator_to_array($repo->exportRows($exact, 'record_code', 'asc')));
foreach ([['store_name' => '其他店'], ['bill_item' => '其他项目'], ['start' => '2026-09-01'], ['end' => '2026-07-31']] as $extra) {
$this->assertSame(0, $repo->paginate($exact + $extra, 'record_code', 'asc', 1, 20)['total']);
$this->assertCount(0, iterator_to_array($repo->exportRows($exact + $extra, 'record_code', 'asc')));
}
$this->db()->table('ops_jst_original_bill_info')->where('Id', 3)->update(['channel' => 'OtherChannel']);
$this->assertSame(0, $repo->paginate(['keyword' => '3', 'keyword_match' => 'exact-record'], 'record_code', 'asc', 1, 20)['total']);
}
public function test_invalid_counts_totals_duplicate_ids_and_dates_do_not_publish_partial_data(): void
{
$row = $this->row('2000000000000000101', '10.00');
$initial = $this->import([$row], 1, '10.00', '0.00');
$cases = [
[[$row], 2, '10.00', '0.00'],
[[$row], 1, '11.00', '0.00'],
[[$row, $row], 2, '20.00', '0.00'],
[[array_replace($row, ['结算时间' => '2026-09-01 00:00:00'])], 1, '10.00', '0.00'],
[[array_replace($row, ['收入' => '1.005'])], 1, '1.00', '0.00'],
];
foreach ($cases as $case) {
try {
$this->import(...$case);
$this->fail('Invalid import should fail.');
} catch (\InvalidArgumentException) {
$this->assertSame($initial['active_generation'], $this->db()->table('douyin_authoritative_periods')->value('active_generation'));
$this->assertSame(1, $this->db()->table('douyin_authoritative_bills')->count());
}
}
}
public function test_reimport_activates_new_generation_and_keeps_old_rows_for_rollback(): void
{
$first = $this->import([$this->row('2000000000000000101', '10.00')], 1, '10.00', '0.00');
$second = $this->import([$this->row('2000000000000000101', '12.00')], 1, '12.00', '0.00');
$this->assertNotSame($first['active_generation'], $second['active_generation']);
$this->assertSame(2, $this->db()->table('douyin_authoritative_bills')->count());
$this->assertSame(12.0, app(StoreReceivingDouyinBillMirrorRepository::class)->paginate([], 'occurred_at', 'asc', 1, 10)['summary']['income']);
}
public function test_cutoff_preserves_later_bills_and_can_advance_without_duplicates(): void
{
$this->legacy(1, '2026-08-21 23:59:59', 90);
$this->legacy(2, '2026-08-22 00:00:00', 9);
$firstRow = $this->row('2000000000000000101', '10.00');
$first = $this->import([$firstRow], 1, '10.00', '0.00', '2026-08-21');
$this->assertSame('2026-08-22', $first['period_end']);
$repo = app(StoreReceivingDouyinBillMirrorRepository::class);
$this->assertSame(19.0, $repo->paginate([], 'occurred_at', 'asc', 1, 10)['summary']['income']);
$this->assertCount(2, iterator_to_array($repo->exportRows([], 'occurred_at', 'asc')));
$nextRow = array_replace($this->row('2000000000000000102', '12.00'), ['结算时间' => '2026-08-22 00:00:00']);
$second = $this->import([$firstRow, $nextRow], 2, '22.00', '0.00', '2026-08-22');
$this->assertSame('2026-08-23', $second['period_end']);
$result = $repo->paginate([], 'occurred_at', 'asc', 1, 10);
$this->assertSame(2, $result['total']);
$this->assertSame(22.0, $result['summary']['income']);
$this->assertSame(3, $this->db()->table('douyin_authoritative_bills')->count());
}
public function test_invalid_or_regressing_cutoffs_preserve_active_generation(): void
{
$row = $this->row('2000000000000000101', '10.00');
$initial = $this->import([$row], 1, '10.00', '0.00', '2026-08-21');
foreach (['2026-08-20', '2026-08-14', '2026-07-31', '2026-09-01', '2026-08-32', 'invalid', ''] as $through) {
try {
$this->import([$row], 1, '10.00', '0.00', $through);
$this->fail('Invalid cutoff should fail.');
} catch (\InvalidArgumentException) {
$this->assertSame($initial['active_generation'], $this->db()->table('douyin_authoritative_periods')->value('active_generation'));
$this->assertSame(1, $this->db()->table('douyin_authoritative_bills')->count());
}
}
}
private function import(array $rows, int $count, string $income, string $expense, ?string $through = null): array
{
$path = tempnam(sys_get_temp_dir(), 'dy-bills-');
$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(DouyinAuthoritativeBillImporter::class)->import($path, '2026-08', $count, $income, $expense, $through);
}
private function row(string $code, string $income, string $expense = '0.00'): array
{
return ['记录编码' => $code, '店铺' => '测试店', '商户主体名称' => '测试主体', '账单项目' => '订单实付',
'动账流水号' => '20260815000100000000001', '订单号' => '6912345678901234567', '子订单号' => '6912345678901234567',
'结算时间' => '2026-08-15 12:00:00', '下单时间' => '2026-07-31 10:00:00', '收入' => $income, '支出' => $expense];
}
private function legacy(int $id, string $time, float $amount): void
{
$this->db()->table('ops_jst_original_bill_info')->insert(['Id' => $id, 'shop_id' => 7, '_mirror_key' => hash('sha256', (string) $id), '_mirror_synced_at' => now(),
'channel' => 'TouTiaoFXGZJ', 'fee_time' => $time, 'amount' => $amount, 'inout_type' => '1', 'fee_name' => '订单实付']);
}
private function db(): \Illuminate\Database\Connection { return DB::connection('business_mirror'); }
}