CaiWuXiTong/tests/Feature/Console/InternalFlowReportImportTest.php
DoDo 6334fa0adf feat: 心选按索菲亚收票倒推收入和全额开票
心选人工填写索菲亚收票,按成本/90%倒推收入,内部开票按全额收入并继续向标签2顺延。
2026-11-09 15:01:50 +08:00

386 lines
17 KiB
PHP

<?php
namespace Tests\Feature\Console;
use Illuminate\Support\Facades\DB;
use Modules\EcommerceFinance\Repositories\InternalFlowReportRepository;
use Modules\EcommerceFinance\Services\InternalFlowWorkbookImporter;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Tests\TestCase;
class InternalFlowReportImportTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config([
'database.connections.business_mirror' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
],
'upstream_mirrors.connection' => 'business_mirror',
]);
DB::purge('business_mirror');
$migration = require base_path(
'database/migrations/2026_10_05_000000_create_business_internal_flow_records_table.php'
);
$migration->up();
$overridesMigration = require base_path(
'database/migrations/2026_11_09_200000_create_internal_flow_supplier_receipt_overrides_table.php'
);
$overridesMigration->up();
$suofeiyaMigration = require base_path(
'database/migrations/2026_11_09_210000_add_suofeiya_receipt_to_internal_flow_overrides.php'
);
$suofeiyaMigration->up();
}
public function testImportKeepsWorkbookValuesAndSkipsTotalRows(): void
{
$path = $this->writeWorkbook($this->dataRows());
try {
$result = app(InternalFlowWorkbookImporter::class)->import($path);
} finally {
@unlink($path);
}
$this->assertSame(3, $result['imported']);
$this->assertSame(1, $result['skipped']);
$rows = DB::connection('business_mirror')
->table(InternalFlowWorkbookImporter::TABLE)
->orderBy('sort_order')
->get();
$this->assertCount(3, $rows);
$this->assertSame('示例甲供应链有限公司', $rows[0]->company_name);
$this->assertSame('1', (string) $rows[0]->tag);
$this->assertSame('1', $rows[0]->invoicing_ratio);
$this->assertEqualsWithDelta(1200000.0, (float) $rows[0]->projected_sales, 0.000001);
$this->assertSame(1, (int) $rows[0]->sort_order);
$this->assertSame('示例乙电子商务有限公司', $rows[1]->company_name);
$this->assertEqualsWithDelta(512345.6789, (float) $rows[1]->internal_invoiced, 0.000001);
$this->assertEqualsWithDelta(1833433.2211, (float) $rows[1]->required_input_cost, 0.000001);
$this->assertEqualsWithDelta(300000.5, (float) $rows[1]->internal_transfer_1, 0.000001);
$this->assertEqualsWithDelta(1200000.5, (float) $rows[1]->receipt_total, 0.000001);
$this->assertSame('0.42', $rows[1]->invoicing_ratio);
$this->assertSame('示例丙家居有限公司', $rows[2]->company_name);
$this->assertNull($rows[2]->invoiced_to_buyer);
$this->assertEqualsWithDelta(220000.11, (float) $rows[2]->receipt_total, 0.000001);
$this->assertSame('/', $rows[2]->invoicing_ratio);
$this->assertSame(basename($path), $rows[0]->source_file);
$this->assertSame(
0,
DB::connection('business_mirror')
->table(InternalFlowWorkbookImporter::TABLE)
->where('company_name', '合计')
->count(),
);
}
public function testImportReplacesPreviousDatasetAndCommandReportsCounts(): void
{
$first = $this->writeWorkbook($this->dataRows());
app(InternalFlowWorkbookImporter::class)->import($first);
@unlink($first);
$second = $this->writeWorkbook([
['示例乙电子商务有限公司', 2, 2345678.9, 456789.12, 512345.6789, 1833433.2211, 900000, null, null, 300000.5, 0, 0, 1200000.5, 633432.7211, 0.42],
]);
try {
$this->artisan('internal-flow:import', ['path' => $second])
->expectsOutputToContain('明细 1 行')
->assertSuccessful();
} finally {
@unlink($second);
}
$rows = DB::connection('business_mirror')
->table(InternalFlowWorkbookImporter::TABLE)
->get();
$this->assertCount(1, $rows);
$this->assertSame('示例乙电子商务有限公司', $rows[0]->company_name);
}
public function testRepositoryFiltersSortsAndPaginates(): void
{
$path = $this->writeWorkbook($this->dataRows());
app(InternalFlowWorkbookImporter::class)->import($path);
@unlink($path);
$repository = app(InternalFlowReportRepository::class);
$this->assertTrue($repository->ready());
$all = $repository->paginate(1, 50);
$this->assertSame(3, $all['total']);
$this->assertSame('示例甲供应链有限公司', $all['data'][0]['company_name']);
$this->assertSame(3, $all['summary']['companies']);
$this->assertSame(3669135.68, $all['summary']['projected_sales']);
$this->assertSame(1420000.61, $all['summary']['receipt_total']);
$this->assertEqualsWithDelta(1401086.9311, $all['summary']['remaining_gap'], 0.005);
$this->assertEqualsWithDelta(71.0, $all['summary']['average_invoicing_ratio'], 0.000001);
$filtered = $repository->paginate(1, 50, ['company_name' => '示例乙']);
$this->assertSame(1, $filtered['total']);
$this->assertSame('示例乙电子商务有限公司', $filtered['data'][0]['company_name']);
$tagged = $repository->paginate(1, 50, ['tag' => '4']);
$this->assertSame(1, $tagged['total']);
$this->assertSame('示例丙家居有限公司', $tagged['data'][0]['company_name']);
$sorted = $repository->paginate(1, 50, [], 'projected_sales', 'desc');
$this->assertSame('示例乙电子商务有限公司', $sorted['data'][0]['company_name']);
$unknownSort = $repository->paginate(1, 50, [], 'company_name; drop table users', 'desc');
$this->assertSame(3, $unknownSort['total']);
$this->assertSame('示例甲供应链有限公司', $unknownSort['data'][0]['company_name']);
$paged = $repository->paginate(2, 2);
$this->assertSame(3, $paged['total']);
$this->assertCount(1, $paged['data']);
$this->assertSame('示例丙家居有限公司', $paged['data'][0]['company_name']);
}
public function testProjectionRecalculatesAmountsAndSupplierReceiptOverrideSurvivesReimport(): void
{
$path = $this->writeWorkbook($this->dataRows());
app(InternalFlowWorkbookImporter::class)->import($path);
@unlink($path);
$projection = [
'forecast_month' => '2026-11',
'forecast_month_label' => '11月',
'source_month' => '2026-10',
'source_month_label' => '10月',
'sales_by_company' => [
'示例甲供应链有限公司' => 1000000.0,
'示例乙电子商务有限公司' => 2000000.0,
'示例丙家居有限公司' => 123456.78,
],
];
$repository = app(InternalFlowReportRepository::class);
$all = $repository->paginate(1, 50, [], null, 'asc', $projection);
$second = $all['data'][1];
$this->assertSame('示例乙电子商务有限公司', $second['company_name']);
$this->assertEqualsWithDelta(2000000.0, $second['projected_sales'], 0.000001);
$this->assertEqualsWithDelta(400000.0, $second['invoiced_to_buyer'], 0.000001);
$this->assertEqualsWithDelta(500000.0, $second['internal_invoiced'], 0.000001);
$this->assertEqualsWithDelta(1600000.0, $second['required_input_cost'], 0.000001);
$this->assertEqualsWithDelta(250000.0, $second['internal_transfer_1'], 0.000001);
$this->assertEqualsWithDelta(1150000.0, $second['receipt_total'], 0.000001);
$this->assertEqualsWithDelta(450000.0, $second['remaining_gap'], 0.000001);
$this->assertEqualsWithDelta(3123456.78, $all['summary']['projected_sales'], 0.000001);
$recordId = (int) DB::connection('business_mirror')
->table(InternalFlowWorkbookImporter::TABLE)
->where('company_name', '示例乙电子商务有限公司')
->value('id');
$this->assertSame(
'示例乙电子商务有限公司',
$repository->updateSupplierReceipt($recordId, 1800000.0, 99),
);
$updated = $repository->find($recordId, $projection);
$this->assertEqualsWithDelta(1800000.0, $updated['receipt_from_supplier'], 0.000001);
$this->assertEqualsWithDelta(0.0, $updated['internal_transfer_1'], 0.000001);
$this->assertEqualsWithDelta(1800000.0, $updated['receipt_total'], 0.000001);
$this->assertEqualsWithDelta(-200000.0, $updated['remaining_gap'], 0.000001);
$replacement = $this->writeWorkbook([
['示例乙电子商务有限公司', 2, 2500000, 500000, 625000, 2000000, 100000, null, null, 0, 0, 0, 100000, 1900000, 0.5],
]);
try {
app(InternalFlowWorkbookImporter::class)->import($replacement);
} finally {
@unlink($replacement);
}
$afterReimport = $repository->paginate(
1,
50,
['company_name' => '示例乙'],
null,
'asc',
$projection,
);
$this->assertCount(1, $afterReimport['data']);
$this->assertEqualsWithDelta(
1800000.0,
$afterReimport['data'][0]['receipt_from_supplier'],
0.000001,
);
$this->assertEqualsWithDelta(1800000.0, $afterReimport['data'][0]['receipt_total'], 0.000001);
}
public function testInternalInvoiceCapacityFlowsOnlyToTheNextTagAndAllocatesByRemainingDemand(): void
{
$rows = [
['标签一有限公司', 1, 1000000, 0, 250000, 800000, 0, null, null, 0, 0, 0, 0, 800000, 1],
['标签二甲有限公司', 2, 2000000, 0, 500000, 1600000, 0, null, null, 0, 0, 0, 0, 1600000, 1],
['标签二乙有限公司', 2, 2000000, 0, 500000, 1600000, 0, null, null, 0, 0, 0, 0, 1600000, 1],
['标签三有限公司', 3, 1000000, 0, 250000, 800000, 0, null, null, 0, 0, 0, 0, 800000, 1],
['标签四有限公司', 4, 1000000, 0, 250000, 800000, 0, null, null, 0, 0, 0, 0, 800000, 1],
];
$path = $this->writeWorkbook($rows);
app(InternalFlowWorkbookImporter::class)->import($path);
@unlink($path);
$projection = [
'forecast_month' => '2026-11',
'forecast_month_label' => '11月',
'source_month' => '2026-10',
'source_month_label' => '10月',
'sales_by_company' => [
'标签一有限公司' => 1000000.0,
'标签二甲有限公司' => 2000000.0,
'标签二乙有限公司' => 2000000.0,
'标签三有限公司' => 1000000.0,
'标签四有限公司' => 1000000.0,
],
];
$result = app(InternalFlowReportRepository::class)->paginate(
1,
50,
[],
null,
'asc',
$projection,
);
$byCompany = collect($result['data'])->keyBy('company_name');
$this->assertEqualsWithDelta(125000.0, $byCompany['标签二甲有限公司']['internal_transfer_1'], 0.000001);
$this->assertEqualsWithDelta(125000.0, $byCompany['标签二乙有限公司']['internal_transfer_1'], 0.000001);
$this->assertEqualsWithDelta(250000.0, $byCompany['标签一有限公司']['internal_invoiced'], 0.000001);
$this->assertEqualsWithDelta(500000.0, $byCompany['标签二甲有限公司']['internal_invoiced'], 0.000001);
$this->assertEqualsWithDelta(800000.0, $byCompany['标签三有限公司']['internal_transfer_2'], 0.000001);
$this->assertEqualsWithDelta(250000.0, $byCompany['标签四有限公司']['internal_transfer_3'], 0.000001);
$this->assertEqualsWithDelta(550000.0, $byCompany['标签四有限公司']['remaining_gap'], 0.000001);
$this->assertEqualsWithDelta(0.0, $byCompany['标签一有限公司']['internal_transfer_2'], 0.000001);
$this->assertEqualsWithDelta(0.0, $byCompany['标签二甲有限公司']['internal_transfer_3'], 0.000001);
$tagTwoAId = (int) DB::connection('business_mirror')
->table(InternalFlowWorkbookImporter::TABLE)
->where('company_name', '标签二甲有限公司')
->value('id');
app(InternalFlowReportRepository::class)->updateSupplierReceipt($tagTwoAId, 800000.0, 99);
$reallocated = app(InternalFlowReportRepository::class)->paginate(
1,
50,
[],
null,
'asc',
$projection,
);
$reallocatedByCompany = collect($reallocated['data'])->keyBy('company_name');
$this->assertEqualsWithDelta(
83333.333333,
$reallocatedByCompany['标签二甲有限公司']['internal_transfer_1'],
0.000001,
);
$this->assertEqualsWithDelta(
166666.666667,
$reallocatedByCompany['标签二乙有限公司']['internal_transfer_1'],
0.000001,
);
}
public function testXinxuanSuofeiyaCostReversesIncomeAndUsesFullInternalInvoiceCapacity(): void
{
$rows = [
['常州心选供应链有限公司', 1, 0, 0, 0, 0, null, null, null, 0, 0, 0, 0, null, '/'],
['标签二有限公司', 2, 2000000, 0, 500000, 1600000, 0, null, null, 0, 0, 0, 0, 1600000, 1],
];
$path = $this->writeWorkbook($rows);
app(InternalFlowWorkbookImporter::class)->import($path);
@unlink($path);
$xinxuanId = (int) DB::connection('business_mirror')
->table(InternalFlowWorkbookImporter::TABLE)
->where('company_name', '常州心选供应链有限公司')
->value('id');
$repository = app(InternalFlowReportRepository::class);
$repository->updateSuofeiyaReceipt($xinxuanId, 900000.0, 99);
$projection = [
'forecast_month' => '2026-11',
'forecast_month_label' => '11月',
'source_month' => '2026-10',
'source_month_label' => '10月',
'sales_by_company' => [
'标签二有限公司' => 2000000.0,
],
];
$result = $repository->paginate(1, 50, [], null, 'asc', $projection);
$byCompany = collect($result['data'])->keyBy('company_name');
$xinxuan = $byCompany['常州心选供应链有限公司'];
$tagTwo = $byCompany['标签二有限公司'];
$this->assertEqualsWithDelta(900000.0, $xinxuan['required_input_cost'], 0.000001);
$this->assertEqualsWithDelta(1000000.0, $xinxuan['projected_sales'], 0.000001);
$this->assertEqualsWithDelta(200000.0, $xinxuan['invoiced_to_buyer'], 0.000001);
$this->assertEqualsWithDelta(1000000.0, $xinxuan['internal_invoiced'], 0.000001);
$this->assertEqualsWithDelta(900000.0, $xinxuan['receipt_total'], 0.000001);
$this->assertEqualsWithDelta(0.0, $xinxuan['remaining_gap'], 0.000001);
$this->assertEqualsWithDelta(1000000.0, $tagTwo['internal_transfer_1'], 0.000001);
$this->assertEqualsWithDelta(600000.0, $tagTwo['remaining_gap'], 0.000001);
$this->assertEqualsWithDelta(3000000.0, $result['summary']['projected_sales'], 0.000001);
}
/**
* @return list<list<mixed>>
*/
private function dataRows(): array
{
return [
['示例甲供应链有限公司', 1, 1200000, 0, 1200000, 0, null, null, null, 0, 0, 0, 0, 0, 1],
['示例乙电子商务有限公司', 2, 2345678.9, 456789.12, 512345.6789, 1833433.2211, 900000, null, null, 300000.5, 0, 0, 1200000.5, 633432.7211, 0.42],
['示例丙家居有限公司', 4, 123456.78, null, null, 987654.32, null, null, null, 0, 0, 220000.11, 220000.11, 767654.21, '/'],
['合计', null, 3679135.68, 456789.12, 1712345.6789, 2821087.5411, 900000, 0, null, 300000.5, 0, 0, 1420000.61, 1401086.9311, '/'],
[null, null, null, null, null, null, null, null, null, null, null, null, null, null, null],
];
}
/**
* @param list<list<mixed>> $dataRows
*/
private function writeWorkbook(array $dataRows): string
{
$temporary = tempnam(sys_get_temp_dir(), 'internal-flow-');
$path = dirname($temporary) . DIRECTORY_SEPARATOR . 'internal-flow-' . basename($temporary) . '.xlsx';
rename($temporary, $path);
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle(InternalFlowWorkbookImporter::SHEET_TITLE);
$sheet->fromArray([
['抬头', '标签', '预计9月销量', '开票', null, '所需进项成本', '收票', null, null, null, null, null, null, '剩余缺口', '开票比例'],
[null, null, null, null, null, null, '供应商', '索菲亚', '顾家', '内部流转(来源)', null, null, '合计', null, null],
[null, null, null, '给买家开票', '内部开票', null, null, null, null, '1', '2', '3', null, null, null],
...$dataRows,
], null, 'A1');
foreach (['A1:A3', 'B1:B3', 'C1:C3', 'D1:E2', 'F1:F3', 'G1:M1', 'H2:H3', 'I2:I3', 'J2:L2', 'M2:M3', 'N1:N3', 'O1:O3'] as $range) {
$sheet->mergeCells($range);
}
(new Xlsx($spreadsheet))->save($path);
$spreadsheet->disconnectWorksheets();
return $path;
}
}