245 lines
12 KiB
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,
|
|
]);
|
|
}
|
|
}
|
|
}
|