CaiWuXiTong/tests/Feature/Business/PlantingDetailsPageTest.php
qiuxiang 8261b17328 种菜明细表同步:利润中心优先用历史值,缺失的用店铺维度报表的 ERP 店铺资料补齐
9 月有 19 家新店铺(1245 行)在历史映射里查不到,导致利润中心为空。现在按「ops_fys_shop_info.erpShopName → responsibleDepartment」补(就是店铺维度报表里 ERP系统店铺名称 → 店铺所属利润中心 那一列),两边都查不到才留空并在命令输出里列出。

历史值仍然优先,避免把人工维护过的取值改掉:实测 371 家可在 ERP 命中的店铺里 365 家两边一致,6 家不一致(朴美家居、朴美书房、拼多多-HX103、索菲亚办公、顾升、乐之益)已单独报给用户确认。
2026-10-08 09:59:11 +08:00

360 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Feature\Business;
use App\Business\PlantingDetailImporter;
use App\Business\PlantingWorkOrderSynchronizer;
use App\Models\Auth\Permission;
use App\Models\Business\PlantingDetail;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\Feature\FeatureTestCase;
/**
* 账单流水数据 › 种菜明细表(导入 + 页面 + 筛选 + 导出)。
*/
class PlantingDetailsPageTest extends FeatureTestCase
{
private array $files = [];
protected function tearDown(): void
{
foreach ($this->files as $file) {
if (is_file($file)) {
unlink($file);
}
}
parent::tearDown();
}
public function test_import_then_page_filter_and_export(): void
{
$this->allowPage();
$result = app(PlantingDetailImporter::class)->import($this->csvFixture(), $this->company->id, true, null, 'test-import');
$this->assertSame(2, $result['inserted']);
$this->assertSame('2665.3600', $result['selling_price']);
$this->assertSame('2665.8600', $result['payment_amount']);
$this->assertSame('2026-06-01', $result['date_min']);
$this->assertSame('2026-07-15', $result['date_max']);
$page = $this->loginAs()->get(route('business.pages.show', [
'section' => 'planting-data',
'page' => 'planting-details',
]));
$page->assertOk()
->assertSee('种菜明细表')
->assertSee('3306215028042824171')
->assertSee('260529-659019537933390')
->assertSee('2,665.35')
->assertSee('阿里巴巴-常州乐畅家居有限公司');
// 时间筛选是可用的日期区间控件:点整块开日历、单个清除、快捷区间按钮
$page->assertSee('id="planting-date-from"', false)
->assertSee('id="planting-date-to"', false)
->assertSee('data-planting-clear="planting-date-from"', false)
->assertSee('data-planting-range="this-month"', false)
->assertSee('showPicker', false)
->assertSee('addEventListener(\'change\'', false);
// 按店铺 + 日期筛选
$filtered = $this->loginAs()->get(route('business.pages.show', [
'section' => 'planting-data',
'page' => 'planting-details',
'store' => '拼多多-亿家达实木家具官方旗舰店',
'date_from' => '2026-07-01',
]));
$filtered->assertOk()
->assertSee('260529-659019537933390')
->assertDontSee('3306215028042824171');
// 导出:表头 + 行数与页面筛选一致
$export = $this->loginAs()->get(route('business.planting-data.planting-details.export', [
'store' => '拼多多-亿家达实木家具官方旗舰店',
]));
$export->assertOk()->assertDownload();
$csv = $export->streamedContent();
$this->assertStringContainsString('日期,订单编号,财务售价', explode("\n", $csv)[0]);
$this->assertStringContainsString('260529-659019537933390', $csv);
$this->assertStringNotContainsString('3306215028042824171', $csv);
$this->assertStringContainsString('2026-07-15', $csv);
}
public function test_importer_handles_excel_serial_dates_and_grouped_amounts(): void
{
$path = $this->writeFile('planting.xlsx');
$book = new Spreadsheet();
$sheet = $book->getActiveSheet();
$sheet->fromArray([
['日期', '订单编号', '财务售价', '财务运费', '财务佣金', '京喜差价', '扣款', '财务空包费', '财务打款金额', '笔数', '备注', '正确店铺', '负责人', '账户', '类型', '创建人', '标签', '所属利润中心', '负责平台', 'ID'],
[46174, 'ORDER-1', '1,234.56', 0, '0.5', 0, 0, 0, '1,235.06', 2, '', '天猫-亿家达旗舰店', '仝孟欣', '', 'CCDD', '齐静春', 'CCDD', '运营中心A', '', 'ID-1'],
// 无效日期(空)+ 无效订单号 → 跳过
['', '', 10, 0, 0, 0, 0, 0, 10, 1, '', '天猫-亿家达旗舰店', '', '', '', '', '', '', '', ''],
]);
(new Xlsx($book))->save($path);
$book->disconnectWorksheets();
$result = app(PlantingDetailImporter::class)->import($path, $this->company->id, true, null, 'test-xlsx');
$this->assertSame(1, $result['inserted']);
$this->assertSame(1, $result['skipped']);
$this->assertSame('2026-06-01', $result['date_min']);
$record = PlantingDetail::query()->where('company_id', $this->company->id)->firstOrFail();
$this->assertSame('2026-06-01', $record->bill_date->toDateString());
$this->assertSame('1234.5600', $record->finance_selling_price);
$this->assertSame('1235.0600', $record->finance_payment_amount);
$this->assertSame(2, $record->pieces);
$this->assertSame('CCDD', $record->type);
$this->assertSame('运营中心A', $record->profit_center);
}
/** 源表 ID 列存在「一个格子多个 ID、换行分隔」的行,导入要保留而不是报错截断。 */
public function test_importer_keeps_multi_line_external_ids(): void
{
$longId = implode("\n", ['10205993844217', '10205981846604', '10205993844217', '10210832117878']);
$path = $this->writeFile('planting-multi-id.csv');
$handle = fopen($path, 'wb');
fputcsv($handle, ['日期', '订单编号', '财务售价', '财务运费', '财务佣金', '京喜差价', '扣款', '财务空包费', '财务打款金额', '笔数', '备注', '正确店铺', '负责人', '账户', '类型', '创建人', '标签', '所属利润中心', '负责平台', 'ID']);
fputcsv($handle, ['2026-03-05', 'ORDER-9', '1356', '0', '0', '0', '0', '0', '1356', '1', '', '京喜-招财邸喜店工厂店', '李金林', '6228760405003956861', '', '骆冰', '', '运营中心A', '', $longId]);
fclose($handle);
$result = app(PlantingDetailImporter::class)->import($path, $this->company->id, true, null, 'test-multi-id');
$this->assertSame(1, $result['inserted']);
$record = PlantingDetail::query()->where('company_id', $this->company->id)->firstOrFail();
$this->assertSame($longId, $record->external_id);
}
/**
* 种菜报销工单同步:字段映射、日期=完成日-1、利润中心回填、重复执行不翻倍。
*/
public function test_sync_from_work_orders_maps_fields_and_dedupes(): void
{
$this->allowPage();
$this->fakeWorkOrderMirror();
// 历史行提供 店铺 → 利润中心 映射
PlantingDetail::query()->create([
'company_id' => $this->company->id,
'bill_date' => '2026-06-01',
'order_no' => 'HISTORY-1',
'store' => '天猫-亿家达旗舰店',
'profit_center' => '运营中心A',
'finance_payment_amount' => '1.0000',
]);
// 本月里没有对应工单的人工行:默认不删
$manual = PlantingDetail::query()->create([
'company_id' => $this->company->id,
'bill_date' => '2026-09-10',
'order_no' => 'MANUAL-ONLY',
'store' => '天猫-亿家达旗舰店',
'finance_payment_amount' => '9.0000',
]);
$result = app(PlantingWorkOrderSynchronizer::class)->sync('2026-09-01', '2026-09-30', $this->company->id);
// 只认「种菜报销 + 已完成 + 完成时间在 9/2~10/1」的两张工单
$this->assertSame(2, $result['orders']);
$this->assertSame(2, $result['inserted']);
$this->assertSame(0, $result['deleted']);
$first = PlantingDetail::query()->where('company_id', $this->company->id)
->where('order_no', '3316392626048007769')->firstOrFail();
$this->assertSame('2026-09-01', $first->bill_date->toDateString()); // 完成日 9-02 → 日期 9-01
$this->assertSame('37.7900', $first->finance_selling_price);
$this->assertSame('2.0000', $first->finance_commission);
$this->assertSame('37.7900', $first->finance_payment_amount);
$this->assertSame('天猫-亿家达旗舰店', $first->store);
$this->assertSame('黄文梳', $first->owner);
$this->assertSame('6214830551373771', $first->account);
$this->assertSame('CCDD', $first->type);
$this->assertSame('CCDD', $first->tag);
$this->assertSame('黄文梳', $first->creator);
$this->assertSame('1074872715022', $first->external_id);
$this->assertSame('运营中心A', $first->profit_center); // 从历史行回填
$this->assertSame(1, $first->pieces);
$this->assertSame(PlantingWorkOrderSynchronizer::SOURCE, $first->created_from);
$second = PlantingDetail::query()->where('company_id', $this->company->id)
->where('order_no', '260901-000000000000001')->firstOrFail();
$this->assertSame('2026-09-29', $second->bill_date->toDateString()); // 完成日 9-30
// 店铺在历史里没有利润中心,用店铺维度报表的 ERP 店铺资料补齐
$this->assertSame('运营中心G', $second->profit_center);
$this->assertSame(1, $result['profit_center_from_erp']);
// 再跑一遍:同月同订单号先删后写,不会翻倍
$again = app(PlantingWorkOrderSynchronizer::class)->sync('2026-09-01', '2026-09-30', $this->company->id);
$this->assertSame(2, $again['deleted']);
$this->assertSame(2, $again['inserted']);
$this->assertSame(1, PlantingDetail::query()->where('company_id', $this->company->id)
->where('order_no', '3316392626048007769')->count());
$this->assertSame(1, PlantingDetail::query()->where('company_id', $this->company->id)
->whereKey($manual->id)->count());
// dry-run 不写库
$dry = app(PlantingWorkOrderSynchronizer::class)->sync('2026-09-01', '2026-09-30', $this->company->id, true);
$this->assertTrue($dry['dry_run']);
$this->assertSame(2, $dry['orders']);
$this->assertSame(0, $dry['inserted']);
}
public function test_sync_force_clear_replaces_whole_month(): void
{
$this->allowPage();
$this->fakeWorkOrderMirror();
PlantingDetail::query()->create([
'company_id' => $this->company->id,
'bill_date' => '2026-09-10',
'order_no' => 'MANUAL-ONLY',
'finance_payment_amount' => '9.0000',
]);
$result = app(PlantingWorkOrderSynchronizer::class)
->sync('2026-09-01', '2026-09-30', $this->company->id, false, true);
$this->assertSame(1, $result['deleted']);
$this->assertSame(2, $result['inserted']);
$this->assertSame(0, PlantingDetail::query()->where('company_id', $this->company->id)
->where('order_no', 'MANUAL-ONLY')->count());
$this->assertSame(2, PlantingDetail::query()->where('company_id', $this->company->id)->count());
}
/** 造一个只有聚水潭协同工单镜像的 sqlite 业务镜像连接。 */
private function fakeWorkOrderMirror(): void
{
config(['upstream_mirrors.connection' => 'business_mirror']);
config(['database.connections.business_mirror' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]]);
DB::purge('business_mirror');
Schema::connection('business_mirror')->create('js_jst_work_order_info', function ($table): void {
$table->increments('Id');
$table->string('woNo')->nullable();
$table->string('woTypeId')->nullable();
$table->string('status')->nullable();
$table->string('createby')->nullable();
$table->string('completeTime')->nullable();
$table->text('businessFieldList')->nullable();
});
// 店铺维度报表用的 ERP 店铺资料:只给第二张工单的店铺,用来验证「历史没有就用 ERP 补」
Schema::connection('business_mirror')->create('ops_fys_shop_info', function ($table): void {
$table->increments('Id');
$table->string('erpShopName')->nullable();
$table->string('responsibleDepartment')->nullable();
});
DB::connection('business_mirror')->table('ops_fys_shop_info')->insert([
['erpShopName' => '拼多多-归来舍下住宅家具旗舰店', 'responsibleDepartment' => '运营中心G'],
]);
foreach ($this->workOrderFixtures() as $row) {
DB::connection('business_mirror')->table('js_jst_work_order_info')->insert($row);
}
}
/** @return list<array<string,mixed>> */
private function workOrderFixtures(): array
{
$planting = PlantingWorkOrderSynchronizer::WORK_TYPE_ID;
$fields = [
['fieldId' => 'orderField', 'type' => 'TEXT', 'value' => '3316392626048007769'],
['fieldId' => 'shopField', 'type' => 'SELECT', 'value' => '18501511', 'valueLabel' => '天猫-亿家达旗舰店'],
['fieldId' => 'NUMBERField1413856620250924131615', 'type' => 'NUMBER', 'value' => '37.79'],
['fieldId' => 'NUMBERField1413856620251230154457', 'type' => 'NUMBER', 'value' => '2'],
['fieldId' => 'NUMBERField1413856620251117101250', 'type' => 'NUMBER', 'value' => '37.79'],
['fieldId' => 'TEXTField1413856620251117180931', 'type' => 'TEXT', 'value' => '黄文梳'],
['fieldId' => 'TEXTField1413856620251117113451', 'type' => 'TEXT', 'value' => '6214830551373771'],
['fieldId' => 'TEXTField1413856620251113150546', 'type' => 'TEXT', 'value' => 'CCDD'],
['fieldId' => 'businessRemarkField', 'type' => 'TEXTAREA', 'value' => 'CCDD'],
['fieldId' => 'itemInfo', 'type' => 'FORM', 'childFieldList' => [[
['fieldId' => 'erpItemId', 'type' => 'TEXT', 'value' => '11961631'],
['fieldId' => 'platformItemId', 'type' => 'TEXT', 'value' => '1074872715022'],
]]],
];
$plain = [
['fieldId' => 'orderField', 'type' => 'TEXT', 'value' => '260901-000000000000001'],
['fieldId' => 'shopField', 'type' => 'SELECT', 'value' => '21355501', 'valueLabel' => '拼多多-归来舍下住宅家具旗舰店'],
['fieldId' => 'NUMBERField1413856620250924131615', 'type' => 'NUMBER', 'value' => '0.06'],
['fieldId' => 'NUMBERField1413856620251230154457', 'type' => 'NUMBER', 'value' => '0.5'],
['fieldId' => 'NUMBERField1413856620251117101250', 'type' => 'NUMBER', 'value' => '0.56'],
];
return [
[
'woNo' => 'GD565967381', 'woTypeId' => $planting, 'status' => 'CONFIRMED',
'createby' => '黄文梳', 'completeTime' => '2026-09-02 11:47:53',
'businessFieldList' => json_encode($fields, JSON_UNESCAPED_UNICODE),
],
[
'woNo' => 'GD999999999', 'woTypeId' => $planting, 'status' => 'CONFIRMED',
'createby' => '陆小凤', 'completeTime' => '2026-09-30 09:27:29',
'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE),
],
// 未完成 → 忽略
[
'woNo' => 'GD888888888', 'woTypeId' => $planting, 'status' => 'WAIT_CONFIRM',
'createby' => '陆小凤', 'completeTime' => '2026-09-15 09:27:29',
'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE),
],
// 别的工单类型 → 忽略
[
'woNo' => 'GD777777777', 'woTypeId' => 'ORDER_AFSALES', 'status' => 'CONFIRMED',
'createby' => '关雎', 'completeTime' => '2026-09-15 09:27:29',
'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE),
],
// 完成时间 10/2 → 日期 10/1,不在 9 月窗口 → 忽略
[
'woNo' => 'GD666666666', 'woTypeId' => $planting, 'status' => 'CONFIRMED',
'createby' => '陆小凤', 'completeTime' => '2026-10-02 09:27:29',
'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE),
],
];
}
private function csvFixture(): string
{
$path = $this->writeFile('planting.csv');
$rows = [
['日期', '订单编号', '财务售价', '财务运费', '财务佣金', '京喜差价', '扣款', '财务空包费', '财务打款金额', '笔数', '备注', '正确店铺', '负责人', '账户', '类型', '创建人', '标签', '所属利润中心', '负责平台', 'ID'],
['2026-06-01', '3306215028042824171', '2665.35', '0', '0', '0', '0', '0', '2665.35', '1', '', '阿里巴巴-常州乐畅家居有限公司', '叶颖龙', '6222031202029761854', '', '勇武', 'AABB', '运营中心B', '', '1056008452997'],
['2026-07-15', '260529-659019537933390', '0.01', '0', '0.5', '0', '0', '0', '0.51', '1', '', '拼多多-亿家达实木家具官方旗舰店', '屠锦超', '6222031211003375839', 'SSDD', '玄鸟', 'SSDD', '运营中心G', '', '959767499187'],
];
$handle = fopen($path, 'wb');
foreach ($rows as $row) {
fputcsv($handle, $row);
}
fclose($handle);
return $path;
}
private function writeFile(string $name): string
{
$path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('planting-', true) . '-' . $name;
$this->files[] = $path;
return $path;
}
private function allowPage(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
}
}