CaiWuXiTong/tests/Unit/EcommerceFinance/CourierFreightReconciliationMirrorRepositoryTest.php

245 lines
12 KiB
PHP

<?php
namespace Tests\Unit\EcommerceFinance;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Repositories\CourierFreightReconciliationMirrorRepository;
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
use RuntimeException;
use Tests\TestCase;
class CourierFreightReconciliationMirrorRepositoryTest extends TestCase
{
private CourierFreightReconciliationMirrorRepository $repository;
protected function setUp(): void
{
parent::setUp();
config([
'database.connections.business_mirror' => [
'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '',
],
'upstream_mirrors.connection' => 'business_mirror',
'upstream_mirrors.datasets' => [
CourierFreightReconciliationMirrorRepository::DETAILS_DATASET => [
'source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'jst_order_express_details',
'target' => 'js_jst_order_express_details', 'keys' => ['id'], 'watermark' => 'updatedAt',
],
CourierFreightReconciliationMirrorRepository::EXTENSION_DATASET => [
'source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'Jst_SendData_View_extension',
'target' => 'js_jst_send_data_view_extension', 'keys' => ['Id'], 'watermark' => null,
],
CourierFreightReconciliationMirrorRepository::OUTBOUND_DATASET => [
'source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'jst_salesOutboundInfo',
'target' => 'js_jst_sales_outbound_info', 'keys' => ['Id'], 'watermark' => 'updateTime',
],
],
]);
DB::purge('business_mirror');
Schema::connection('business_mirror')->create('business_mirror_table_states', function (Blueprint $table): void {
$table->string('dataset_key')->primary();
$table->dateTime('initial_completed_at')->nullable();
$table->string('status')->nullable();
$table->dateTime('last_succeeded_at')->nullable();
$table->string('run_mode')->nullable();
});
Schema::connection('business_mirror')->create('js_jst_order_express_details', function (Blueprint $table): void {
$table->integer('id');
foreach (['updatedAt', 'so_id', 'logistic_id', 'logistic_company_name', 'platform_name', 'shop_name', 'warehouse_name', 'logistic_status_name'] as $column) {
$table->string($column)->nullable();
}
$table->dateTime('send_time')->nullable();
$table->decimal('weight', 20, 4)->nullable();
$table->decimal('f_weight', 20, 4)->nullable();
});
Schema::connection('business_mirror')->create('js_jst_send_data_view_extension', function (Blueprint $table): void {
$table->integer('Id');
$table->integer('io_id')->nullable();
$table->integer('qty')->nullable();
foreach (['l', 'w', 'h', 'volume'] as $column) {
$table->decimal($column, 20, 4)->nullable();
}
foreach (['so_id', 'sku_ids', 'sku_name', 'shop_name', 'shop_i_id', 'supplier_name', 'receiver_state', 'receiver_city', 'receiver_district', 'expressName', 'wareHoursName'] as $column) {
$table->string($column)->nullable();
}
});
Schema::connection('business_mirror')->create('js_jst_sales_outbound_info', function (Blueprint $table): void {
$table->integer('Id');
$table->integer('io_id')->nullable();
$table->decimal('freight', 20, 4)->nullable();
});
$this->repository = new CourierFreightReconciliationMirrorRepository(
new BusinessMirrorDatasetRegistry(),
new GenericBusinessMirrorRepository(app('db')),
);
}
public function test_it_rejects_an_unready_mirror(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('courier_freight_reconciliation_mirror_not_ready');
$this->repository->paginate([], 1, 25);
}
public function test_it_joins_workbook_fields_and_keeps_freight_aggregate_unique(): void
{
$this->markReady();
DB::connection('business_mirror')->table('js_jst_order_express_details')->insert([
'id' => 1, 'updatedAt' => '2026-09-01 01:00:00', 'so_id' => 'SO-1', 'logistic_id' => 'YT-001',
'logistic_company_name' => '圆通速递', 'platform_name' => '淘宝天猫', 'shop_name' => '店铺A',
'warehouse_name' => '仓库A', 'send_time' => '2026-09-01 00:02:06', 'weight' => 0.8,
'f_weight' => 0.7, 'logistic_status_name' => '已发货',
]);
DB::connection('business_mirror')->table('js_jst_send_data_view_extension')->insert([
['Id' => 10, 'io_id' => 100, 'so_id' => 'SO-1', 'qty' => 1, 'l' => 10, 'w' => 20, 'h' => 30,
'sku_ids' => 'SKU-OLD', 'sku_name' => '旧商品', 'shop_name' => '旧店铺', 'shop_i_id' => 'OLD-I',
'supplier_name' => '供应商A', 'receiver_state' => '江苏省', 'receiver_city' => '常州市',
'receiver_district' => '武进区', 'expressName' => '圆通旧', 'wareHoursName' => '旧仓', 'volume' => 6000],
['Id' => 11, 'io_id' => 100, 'so_id' => 'SO-1', 'qty' => 2, 'l' => 11, 'w' => 21, 'h' => 31,
'sku_ids' => 'SKU-1', 'sku_name' => '演示商品', 'shop_name' => '店铺A', 'shop_i_id' => 'ITEM-1',
'supplier_name' => '供应商A', 'receiver_state' => '江苏省', 'receiver_city' => '常州市',
'receiver_district' => '武进区', 'expressName' => '圆通速递', 'wareHoursName' => '仓库A', 'volume' => 7161],
]);
DB::connection('business_mirror')->table('js_jst_sales_outbound_info')->insert([
['Id' => 100, 'io_id' => 100, 'freight' => 0.5],
['Id' => 101, 'io_id' => 100, 'freight' => 0.8],
]);
$this->repository->summarize(['express_no' => 'YT', 'month' => '2026-09']);
$result = $this->repository->paginate(['express_no' => 'YT', 'month' => '2026-09'], 1, 25);
$this->assertSame(1, $result['total']);
$this->assertSame('YT-001', $result['data'][0]['waybill']);
$this->assertSame('ITEM-1', $result['data'][0]['product_id']);
$this->assertSame('SKU-1', $result['data'][0]['barcode']);
$this->assertSame('演示商品', $result['data'][0]['product_name']);
$this->assertSame('江苏省', $result['data'][0]['province']);
$this->assertSame(2, $result['data'][0]['quantity']);
$this->assertSame(0.8, $result['data'][0]['freight']);
$this->assertSame(0.8, $result['summary']['freight']);
$this->assertSame('fresh', $result['sync']['code']);
$this->assertArrayNotHasKey('receiver_address', $result['data'][0]);
$this->assertArrayNotHasKey('password', $result['data'][0]);
}
public function test_it_keeps_a_shipment_visible_when_optional_detail_rows_are_missing(): void
{
$this->markReady();
DB::connection('business_mirror')->table('js_jst_order_express_details')->insert([
'id' => 2, 'updatedAt' => '2026-09-02 01:00:00', 'so_id' => 'SO-2',
'logistic_id' => 'ZT-002', 'logistic_company_name' => '中通快递',
'platform_name' => '京东商城', 'shop_name' => '店铺B', 'warehouse_name' => '仓库B',
'send_time' => '2026-09-02 00:00:00', 'logistic_status_name' => '已发货',
]);
$this->repository->summarize(['express_no' => 'ZT-002']);
$result = $this->repository->paginate(['express_no' => 'ZT-002'], 1, 25);
$this->assertSame(1, $result['total']);
$this->assertSame('SO-2', $result['data'][0]['transaction_order_no']);
$this->assertSame('店铺B', $result['data'][0]['shop_name']);
$this->assertSame('仓库B', $result['data'][0]['warehouse']);
$this->assertNull($result['data'][0]['product_id']);
$this->assertNull($result['data'][0]['freight']);
}
public function test_pages_do_not_wait_for_full_aggregation_and_do_not_invent_totals(): void
{
$this->markReady();
$db = DB::connection('business_mirror');
$db->table('js_jst_order_express_details')->insert([
['id' => 1, 'logistic_id' => 'A'], ['id' => 2, 'logistic_id' => 'B'], ['id' => 3, 'logistic_id' => 'C'],
]);
$db->enableQueryLog();
$first = $this->repository->paginate([], 1, 2);
$second = $this->repository->paginate([], 2, 2);
$this->assertSame([3, 2], array_column($first['data'], 'detail_id'));
$this->assertSame([1], array_column($second['data'], 'detail_id'));
$this->assertTrue($first['has_more']);
$this->assertFalse($second['has_more']);
$this->assertNull($first['total']);
$this->assertNull($first['summary']);
$queries = strtolower(implode(' ', array_column($db->getQueryLog(), 'query')));
$db->disableQueryLog();
$this->assertStringNotContainsString('count(', $queries);
$this->assertStringNotContainsString('sum(', $queries);
}
public function test_summary_reuses_cache_and_serializes_expensive_requests(): void
{
$this->markReady();
$db = DB::connection('business_mirror');
$db->table('js_jst_order_express_details')->insert(['id' => 1, 'logistic_id' => 'A']);
$lock = Cache::lock('courier-freight-summary:calculation', 360);
$this->assertTrue($lock->get());
try {
$this->assertNull($this->repository->summarize([]));
} finally {
$lock->release();
}
$summary = $this->repository->summarize([]);
$this->assertSame(1, $summary['count']);
$this->assertArrayHasKey('computed_at', $summary);
$db->enableQueryLog();
$this->assertSame($summary, $this->repository->summarize(['month' => '', 'express_no' => '']));
$this->assertStringNotContainsString('sum(', strtolower(implode(' ', array_column($db->getQueryLog(), 'query'))));
$db->disableQueryLog();
$this->assertSame(0, $this->repository->summarize(['express_no' => 'MISSING'])['count']);
}
public function test_summary_request_defers_database_work_until_after_the_response(): void
{
$this->markReady();
$db = DB::connection('business_mirror');
$db->table('js_jst_order_express_details')->insert(['id' => 1, 'logistic_id' => 'A']);
$db->enableQueryLog();
$this->assertNull($this->repository->requestSummary([]));
$this->assertSame([], $db->getQueryLog());
$this->app->terminate();
$result = $this->repository->requestSummary([]);
$this->assertSame(1, $result['count']);
$db->disableQueryLog();
}
public function test_summary_lookup_keeps_full_long_order_identifiers_distinct(): void
{
$this->markReady();
$db = DB::connection('business_mirror');
$prefix = str_repeat('X', 64);
$db->table('js_jst_order_express_details')->insert(['id' => 1, 'logistic_id' => 'A', 'so_id' => $prefix . 'A']);
$db->table('js_jst_send_data_view_extension')->insert([
['Id' => 1, 'so_id' => $prefix . 'A', 'io_id' => 1],
['Id' => 2, 'so_id' => $prefix . 'B', 'io_id' => 2],
]);
$db->table('js_jst_sales_outbound_info')->insert([
['Id' => 1, 'io_id' => 1, 'freight' => 1], ['Id' => 2, 'io_id' => 2, 'freight' => 2],
]);
$summary = $this->repository->summarize([]);
$this->assertSame(1, $summary['count']);
$this->assertSame(1.0, $summary['freight']);
}
private function markReady(): void
{
$now = now()->toDateTimeString();
foreach ([
CourierFreightReconciliationMirrorRepository::DETAILS_DATASET,
CourierFreightReconciliationMirrorRepository::EXTENSION_DATASET,
CourierFreightReconciliationMirrorRepository::OUTBOUND_DATASET,
] as $dataset) {
DB::connection('business_mirror')->table('business_mirror_table_states')->insert([
'dataset_key' => $dataset, 'initial_completed_at' => '2026-09-01 09:00:00',
'status' => 'succeeded', 'last_succeeded_at' => $now,
]);
}
}
}