388 lines
18 KiB
PHP
388 lines
18 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(70.66, $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(0.45, $byCompany['标签二甲有限公司']['invoicing_ratio'], 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(0.0, $xinxuan['invoiced_to_buyer'], 0.000001);
|
|
$this->assertEqualsWithDelta(1000000.0, $xinxuan['internal_invoiced'], 0.000001);
|
|
$this->assertEqualsWithDelta(1.0, $xinxuan['invoicing_ratio'], 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;
|
|
}
|
|
}
|