1) 钉钉「线上代发业务报销」有 23 张已通过审批单的商品明细表格不叫“收款费用明细”(新版模板表格名为空、旧单叫“费用明细”), 同步时整单只剩一行空明细。按名称找不到时改取行内含“商品ID”列的明细表格;料号兼容新版“商品编码”。上线后对该数据集全量重同步。 2) 新增报销成本核查:按聚水潭订单由本行商品编码拆出的子商品,取普通商品资料成本价×发货数量求和为系统成本, 与代发订单金额比较,高出 0.01 元以上判“多报销”,另有“成本缺失”“无发货明细”;页面新增筛选、计数、多报销金额汇总及四列,导出同步。 涉及:金额核查展示(只读)、镜像全量重同步;不涉及迁移、权限和上游写入。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VANWYsNUbu2piMtCKymoyd
137 lines
7.2 KiB
PHP
137 lines
7.2 KiB
PHP
<?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']);
|
||
}
|
||
}
|