['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_tag_filter_splits_ding_rows_by_payee(): void { DB::connection('business_mirror')->table(Repository::TARGET_TABLE)->where('process_instance_id', 'OK') ->update(['payee_name' => '宁波顾家望业家居有限公司']); $repository = app(Repository::class); $liuxiao = $repository->paginate(['tag' => Repository::TAG_LIUXIAO], 1, 100); $this->assertSame(['OK'], array_column($liuxiao['data'], 'process_instance_id')); $this->assertSame(Repository::TAG_LIUXIAO, $liuxiao['data'][0]['tag']); $other = $repository->paginate(['tag' => Repository::TAG_1688], 1, 100); $this->assertSame(3, $other['total']); $this->assertNotContains('OK', array_column($other['data'], 'process_instance_id')); $this->assertSame(0, $repository->paginate(['tag' => Repository::TAG_PURCHASE], 1, 100)['total']); $export = iterator_to_array($repository->exportRows(['tag' => Repository::TAG_LIUXIAO]), false); $this->assertSame(['OK'], array_column($export, 'process_instance_id')); } 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']); } }