CaiWuXiTong/tests/Unit/EcommerceFinance/OnlineDaifaCostAuditTest.php
limeng 43390be327 feat(线上代发价格): 识别改版明细表格 + 新增报销成本核查(多报销) (#841)
1) 钉钉「线上代发业务报销」有 23 张已通过审批单的商品明细表格不叫“收款费用明细”(新版模板表格名为空、旧单叫“费用明细”),
   同步时整单只剩一行空明细。按名称找不到时改取行内含“商品ID”列的明细表格;料号兼容新版“商品编码”。上线后对该数据集全量重同步。
2) 新增报销成本核查:按聚水潭订单由本行商品编码拆出的子商品,取普通商品资料成本价×发货数量求和为系统成本,
   与代发订单金额比较,高出 0.01 元以上判“多报销”,另有“成本缺失”“无发货明细”;页面新增筛选、计数、多报销金额汇总及四列,导出同步。

涉及:金额核查展示(只读)、镜像全量重同步;不涉及迁移、权限和上游写入。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VANWYsNUbu2piMtCKymoyd
2026-11-03 17:13:56 +08:00

137 lines
7.2 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\Unit\EcommerceFinance;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Repositories\DingOnlineDaifaPriceMirrorRepository as Repository;
use Tests\TestCase;
class OnlineDaifaCostAuditTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config([
'database.connections.business_mirror' => ['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => ''],
'upstream_mirrors.connection' => 'business_mirror',
]);
Cache::setDefaultDriver('array');
Cache::flush();
DB::purge('business_mirror');
$schema = Schema::connection('business_mirror');
$schema->create('business_mirror_table_states', function (Blueprint $table): void {
$table->string('dataset_key')->primary();
$table->string('status');
$table->dateTime('initial_completed_at')->nullable();
$table->dateTime('last_succeeded_at')->nullable();
});
$schema->create(Repository::TARGET_TABLE, function (Blueprint $table): void {
$table->string('process_instance_id');
$table->unsignedInteger('detail_index');
foreach (['product_id', 'product_name', 'daifa_trade_no', 'shop_trade_no', 'shop_name', 'payee_name', 'supplier_name', 'material_no'] as $column) {
$table->string($column)->nullable();
}
$table->integer('ship_qty')->nullable();
$table->decimal('paid_amount', 20, 2)->nullable();
$table->decimal('daifa_amount', 20, 2)->nullable();
$table->decimal('price_diff', 20, 2)->nullable();
$table->text('workflow_payload')->nullable();
$table->text('scheme_payload')->nullable();
$table->text('task_payload')->nullable();
$table->dateTime('workflow_created_at')->nullable();
});
$schema->create('ops_jst_orders', function (Blueprint $table): void {
$table->unsignedBigInteger('Id')->primary();
$table->unsignedBigInteger('o_id')->index();
$table->string('so_id')->nullable()->index();
$table->string('shop_name')->nullable();
$table->string('status')->nullable();
$table->text('labels')->nullable();
});
$schema->create('ops_jst_order_item', function (Blueprint $table): void {
$table->unsignedBigInteger('Id')->primary();
$table->unsignedBigInteger('o_id')->index();
$table->string('sku_id')->nullable();
$table->decimal('qty', 20, 4)->nullable();
$table->string('src_combine_sku_id')->nullable();
});
$schema->create('ops_jst_sku', function (Blueprint $table): void {
$table->unsignedBigInteger('autoid');
$table->string('sku_id');
$table->string('sku_type');
$table->decimal('cost_price', 18, 4)->nullable();
});
$db = DB::connection('business_mirror');
$db->table('business_mirror_table_states')->insert([
'dataset_key' => Repository::DATASET_KEY, 'status' => 'succeeded',
'initial_completed_at' => '2026-10-01 00:00:00', 'last_succeeded_at' => '2026-11-03 05:00:00',
]);
$ding = static fn (string $id, string $soId, string $material, ?float $amount): array => [
'process_instance_id' => $id, 'detail_index' => 0, 'shop_trade_no' => $soId, 'shop_name' => '京东-测试店',
'material_no' => $material, 'ship_qty' => 1, 'daifa_amount' => $amount,
'workflow_payload' => '{"status":"COMPLETED","result":"agree","businessId":"' . $id . '"}', 'scheme_payload' => '[]', 'task_payload' => '[]',
'workflow_created_at' => '2026-10-10 10:00:00',
];
$db->table(Repository::TARGET_TABLE)->insert([
$ding('OVER', '1001', 'ZH-CHAIR-2', 250.00), // 组合装:2 把椅子,成本 2×100=200,报 250 → 多报销 50
$ding('OK', '1002', 'DESK-B', 80.00), // 普通商品成本 80,报 80 → 正常
$ding('MISS', '1003', 'NOCOST', 30.00), // 成本价为 0 → 成本缺失
$ding('NONE', '9999', 'ANY', 10.00), // 聚水潭找不到订单 → 无发货明细
]);
$db->table('ops_jst_orders')->insert([
['Id' => 1, 'o_id' => 1, 'so_id' => '1001', 'shop_name' => 'x', 'status' => 'Sent', 'labels' => null],
['Id' => 2, 'o_id' => 2, 'so_id' => '1002', 'shop_name' => 'x', 'status' => 'Sent', 'labels' => null],
['Id' => 3, 'o_id' => 3, 'so_id' => '1003', 'shop_name' => 'x', 'status' => 'Sent', 'labels' => null],
]);
$db->table('ops_jst_order_item')->insert([
['Id' => 1, 'o_id' => 1, 'sku_id' => 'CHAIR-A', 'qty' => 2, 'src_combine_sku_id' => 'ZH-CHAIR-2'],
['Id' => 2, 'o_id' => 2, 'sku_id' => 'DESK-B', 'qty' => 1, 'src_combine_sku_id' => null],
['Id' => 3, 'o_id' => 3, 'sku_id' => 'NOCOST', 'qty' => 1, 'src_combine_sku_id' => null],
]);
$db->table('ops_jst_sku')->insert([
['autoid' => 1, 'sku_id' => 'CHAIR-A', 'sku_type' => 'normal', 'cost_price' => 90],
['autoid' => 2, 'sku_id' => 'CHAIR-A', 'sku_type' => 'normal', 'cost_price' => 100],
['autoid' => 3, 'sku_id' => 'DESK-B', 'sku_type' => 'normal', 'cost_price' => 80],
['autoid' => 4, 'sku_id' => 'NOCOST', 'sku_type' => 'normal', 'cost_price' => 0],
]);
}
public function test_cost_audit_splits_combo_and_flags_over_reimbursement(): void
{
$result = app(Repository::class)->paginate([], 1, 100);
$rows = collect($result['data'])->keyBy('process_instance_id');
$this->assertSame(Repository::AUDIT_OVER, $rows['OVER']['audit_result']);
$this->assertSame(200.0, $rows['OVER']['system_cost']);
$this->assertSame(50.0, $rows['OVER']['cost_diff']);
$this->assertSame('CHAIR-A×2×100=200.00', $rows['OVER']['audit_detail']);
$this->assertSame(Repository::AUDIT_OK, $rows['OK']['audit_result']);
$this->assertSame(0.0, $rows['OK']['cost_diff']);
$this->assertSame(Repository::AUDIT_COST_MISSING, $rows['MISS']['audit_result']);
$this->assertNull($rows['MISS']['cost_diff']);
$this->assertSame(Repository::AUDIT_NO_SHIPMENT, $rows['NONE']['audit_result']);
$this->assertSame(1, $result['summary']['audit_counts'][Repository::AUDIT_OVER]);
$this->assertSame(50.0, $result['summary']['over_amount']);
}
public function test_audit_filter_pages_and_exports_only_matching_rows(): void
{
$repository = app(Repository::class);
$result = $repository->paginate(['audit' => Repository::AUDIT_OVER], 1, 100);
$this->assertSame(1, $result['total']);
$this->assertSame(['OVER'], array_column($result['data'], 'process_instance_id'));
$export = iterator_to_array($repository->exportRows(['audit' => Repository::AUDIT_COST_MISSING]), false);
$this->assertSame(['MISS'], array_column($export, 'process_instance_id'));
$this->assertSame(Repository::AUDIT_COST_MISSING, $export[0]['audit_result']);
}
}