CaiWuXiTong/tests/Unit/EcommerceFinance/OnlineDaifaPricePaginationTest.php

235 lines
12 KiB
PHP

<?php
namespace Tests\Unit\EcommerceFinance;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Repositories\DingOnlineDaifaPriceMirrorRepository;
use Modules\EcommerceFinance\Repositories\ProductPurchasePriceMirrorRepository;
use Tests\TestCase;
/**
* 分页按来源分别筛选、各取前 N 条再合并后,结果必须和原先「两表整表合并后再排序」一致。
* 导出仍走整表合并口径,这里用导出顺序作为对照。
*/
class OnlineDaifaPricePaginationTest 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');
$this->createSchema();
$this->seedRows();
}
/** @return iterable<string,array{0:array<string,string>}> */
public static function filterProvider(): iterable
{
yield 'no filter' => [[]];
yield 'date range' => [['created_from' => '2026-10-03', 'created_to' => '2026-10-06']];
yield 'supplier' => [['supplier_name' => '供应商A']];
yield 'shop' => [['shop_name' => '店铺甲']];
yield 'keyword' => [['keyword' => '77']];
yield 'material' => [['material_no' => 'SKU-2']];
yield 'ding status only' => [['status' => 'COMPLETED']];
yield 'purchase status only' => [['status' => 'FINISHED']];
yield 'product id' => [['product_id' => 'PID-1']];
}
/** @dataProvider filterProvider */
public function test_pages_match_the_fully_merged_order_totals_and_summary(array $filters): void
{
$repository = app(DingOnlineDaifaPriceMirrorRepository::class);
$expected = array_map(
static fn (array $row): string => $row['source_type'] . ':' . $row['process_instance_id'] . ':' . $row['detail_index'],
iterator_to_array($repository->exportRows($filters), false),
);
$perPage = 4;
$first = $repository->paginate($filters, 1, $perPage);
$this->assertSame(count($expected), $first['total']);
$actual = [];
$pages = max((int) ceil($first['total'] / $perPage), 1) + 1; // one empty page past the end
for ($page = 1; $page <= $pages; $page++) {
foreach ($repository->paginate($filters, $page, $perPage)['data'] as $row) {
$actual[] = $row['source_type'] . ':' . $row['process_instance_id'] . ':' . $row['detail_index'];
}
}
$this->assertSame($expected, $actual);
$exported = iterator_to_array($repository->exportRows($filters), false);
$ding = array_values(array_filter($exported, static fn (array $r): bool => $r['source_type'] === 'ding'));
$summary = $first['summary'];
$this->assertSame(count($exported), $summary['count']);
$this->assertSame(count($exported) - count($ding), $summary['purchase_count']);
$this->assertSame(count($ding), $summary['matchable_count']);
$this->assertSame(
count(array_filter($ding, static fn (array $r): bool => ($r['matched_product']['matched'] ?? false) === true)),
$summary['matched_count'],
);
$this->assertSame(round(array_sum(array_column($ding, 'daifa_amount')), 2), $summary['daifa_amount']);
$this->assertSame(round(array_sum(array_column($ding, 'paid_amount')), 2), $summary['paid_amount']);
$this->assertSame(round(array_sum(array_column($ding, 'price_diff')), 2), $summary['price_diff']);
}
public function test_page_rows_keep_their_full_fields(): void
{
$data = app(DingOnlineDaifaPriceMirrorRepository::class)->paginate([], 1, 100)['data'];
$ding = collect($data)->first(fn (array $r): bool => $r['source_type'] === 'ding' && $r['process_instance_id'] === 'D1' && $r['detail_index'] === 2);
$this->assertSame('PID-1', $ding['product_id']);
$this->assertSame('供应商A', $ding['supplier_name']);
$this->assertSame(['status' => 'COMPLETED', 'result' => 'agree'], $ding['workflow']);
$purchase = collect($data)->first(fn (array $r): bool => $r['source_type'] === 'purchase' && $r['detail_index'] === 110);
$this->assertSame(9000, $purchase['purchase_order_id']);
$this->assertSame('店铺甲', $purchase['shop_name']);
$this->assertSame('77001', $purchase['shop_trade_no']);
$this->assertSame('已完成', $purchase['status_label']);
}
private function createSchema(): void
{
$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(DingOnlineDaifaPriceMirrorRepository::TARGET_TABLE, function (Blueprint $table): void {
$table->string('process_instance_id');
$table->unsignedInteger('detail_index');
$table->string('product_id')->nullable();
$table->string('product_name')->nullable();
$table->string('daifa_trade_no')->nullable();
$table->string('shop_trade_no')->nullable();
$table->string('shop_name')->nullable();
$table->string('payee_name')->nullable();
$table->string('supplier_name')->nullable();
$table->string('material_no')->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->string('workflow_created_at', 32)->nullable();
});
$schema->create('product_purchase_price_sync_states', function (Blueprint $table): void {
$table->string('dataset_key')->primary();
$table->string('status');
$table->unsignedTinyInteger('projection_version')->default(1);
$table->dateTime('initial_completed_at')->nullable();
$table->dateTime('last_succeeded_at')->nullable();
});
$schema->create('product_purchase_price_mirror_rows', function (Blueprint $table): void {
$table->unsignedBigInteger('item_id')->primary();
$table->unsignedBigInteger('purchase_order_id');
$table->unsignedBigInteger('source_order_no')->nullable();
$table->string('supplier_name')->nullable();
$table->string('sku_id')->nullable();
$table->string('style_id')->nullable();
$table->decimal('quantity', 20, 4)->nullable();
$table->decimal('unit_price', 20, 4)->nullable();
$table->string('document_status')->nullable();
$table->text('labels')->nullable();
$table->dateTime('document_created_at')->nullable();
$table->dateTime('finished_at')->nullable();
$table->dateTime('purchase_updated_at');
});
$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();
});
}
private function seedRows(): void
{
$db = DB::connection('business_mirror');
$db->table('business_mirror_table_states')->insert([
[
'dataset_key' => DingOnlineDaifaPriceMirrorRepository::DATASET_KEY,
'status' => 'succeeded',
'initial_completed_at' => '2026-10-01 00:00:00',
'last_succeeded_at' => '2026-10-28 09:00:00',
],
[
'dataset_key' => 'operations_replica.rpt.jst_Orders',
'status' => 'succeeded',
'initial_completed_at' => '2026-10-01 00:00:00',
'last_succeeded_at' => '2026-10-28 09:00:00',
],
]);
$db->table('product_purchase_price_sync_states')->insert([
'dataset_key' => ProductPurchasePriceMirrorRepository::DATASET_KEY,
'status' => 'succeeded',
'projection_version' => ProductPurchasePriceMirrorRepository::CURRENT_PROJECTION_VERSION,
'initial_completed_at' => '2026-10-01 00:00:00',
'last_succeeded_at' => '2026-10-28 10:00:00',
]);
$ding = [];
// 11 张审批、每张 1~3 条明细;部分创建时间与采购单完全相同,用来检验同一时间的先后。
for ($i = 1; $i <= 11; $i++) {
for ($d = 1; $d <= ($i % 3) + 1; $d++) {
$ding[] = [
'process_instance_id' => 'D' . $i,
'detail_index' => $d,
'product_id' => $i % 2 === 0 ? '' : 'PID-' . ($i % 4),
'product_name' => $i % 5 === 0 ? null : '商品' . $i,
'daifa_trade_no' => 'DF' . (770 + $i),
'shop_trade_no' => 'SO' . (100 + $i),
'shop_name' => $i % 2 === 0 ? '店铺甲' : '店铺乙',
'payee_name' => '收款人',
'supplier_name' => $i % 3 === 0 ? '供应商B' : '供应商A',
'material_no' => 'SKU-' . ($d + $i % 2),
'ship_qty' => 1,
'paid_amount' => 10 + $i + $d / 10,
'daifa_amount' => 8 + $i,
'price_diff' => 2 + $d / 10,
'workflow_payload' => '{"status":"COMPLETED","result":"agree"}',
'scheme_payload' => '[]',
'task_payload' => '[]',
'workflow_created_at' => sprintf('2026-10-%02d 10:00:00', ($i % 7) + 1),
];
}
}
$db->table(DingOnlineDaifaPriceMirrorRepository::TARGET_TABLE)->insert($ding);
$db->table('ops_jst_orders')->insert([
['Id' => 1, 'o_id' => 5001, 'so_id' => '77001', 'shop_name' => '店铺甲'],
['Id' => 2, 'o_id' => 5002, 'so_id' => '88002', 'shop_name' => '店铺乙'],
]);
$purchase = [];
$statuses = ['Confirmed', 'Finished', 'Cancelled'];
for ($i = 1; $i <= 14; $i++) {
$purchase[] = [
'item_id' => 100 + $i,
// 9001 与 10001 一起检验单号按文本排序(与原合并口径一致)
'purchase_order_id' => $i % 4 === 0 ? 10000 + $i : 9000 + ($i % 5),
'source_order_no' => $i % 3 === 0 ? null : 5000 + ($i % 2) + 1,
'supplier_name' => $i % 2 === 0 ? '供应商A' : '供应商C',
'sku_id' => 'SKU-' . ($i % 3),
'quantity' => 1,
'unit_price' => 5 + $i,
'document_status' => $statuses[$i % 3],
'document_created_at' => $i % 6 === 0 ? null : sprintf('2026-10-%02d 10:00:00', ($i % 7) + 1),
'purchase_updated_at' => sprintf('2026-10-%02d 09:00:00', ($i % 8) + 1),
];
}
$db->table('product_purchase_price_mirror_rows')->insert($purchase);
}
}