CaiWuXiTong/tests/Unit/EcommerceFinance/NonJingxiRefundMirrorRepositoryTest.php

239 lines
12 KiB
PHP

<?php
namespace Tests\Unit\EcommerceFinance;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
use Modules\EcommerceFinance\Repositories\NonJingxiRefundMirrorRepository;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
use Tests\TestCase;
class NonJingxiRefundMirrorRepositoryTest extends TestCase
{
private NonJingxiRefundMirrorRepository $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' => $this->definitions(),
]);
DB::purge('business_mirror');
$this->createTables();
$this->markReady();
$generic = new GenericBusinessMirrorRepository(app('db'));
$this->repository = new NonJingxiRefundMirrorRepository(
new BusinessMirrorDatasetRegistry(),
$generic,
);
}
public function test_ready_mirror_supports_second_page_filters_and_current_page_enrichment(): void
{
$db = DB::connection('business_mirror');
$db->table('ops_jst_return_refund_info')->insert([
$this->refundInfo(1, 101, '2026-08-19 10:00:00', '天猫', '一号店'),
$this->refundInfo(2, 102, '2026-08-20 10:00:00', '抖音', '二号店'),
$this->refundInfo(3, 103, '2026-08-21 10:00:00', 'jingxi', '京喜店'),
]);
$db->table('ops_jst_return_refund_item')->insert([
$this->refundItem(11, 101, 'SKU-1', 'SUB-1'),
$this->refundItem(12, 102, 'SKU-2', 'SUB-2'),
$this->refundItem(13, 103, 'SKU-JX', 'SUB-JX'),
]);
$db->table('ops_jst_orders')->insert([
['Id' => 1, 'o_id' => 8901, 'receiver_zip' => 'ZIP-1'],
['Id' => 2, 'o_id' => 8902, 'receiver_zip' => 'ZIP-2'],
]);
$db->table('ops_jst_order_item')->insert([
['Id' => 1, 'o_id' => 8901, 'outer_oi_id' => 'SUB-1', 'shop_i_id' => 'ITEM-1', 'shop_sku_id' => 'SHOP-SKU-1', 'i_id' => 'STYLE-1', 'sku_id' => 'SKU-1', 'buyer_paid_amount' => 88],
['Id' => 2, 'o_id' => 8902, 'outer_oi_id' => 'SUB-2', 'shop_i_id' => 'ITEM-2', 'shop_sku_id' => 'SHOP-SKU-2', 'i_id' => 'STYLE-2', 'sku_id' => 'SKU-2', 'buyer_paid_amount' => 99],
]);
$db->table('ops_jst_sku')->insert([
['autoid' => 1, 'sku_id' => 'SKU-1', 'sku_type' => 'normal', 'cost_price' => 20],
['autoid' => 2, 'sku_id' => 'SKU-2', 'sku_type' => 'normal', 'cost_price' => 30],
]);
$db->table('ops_fys_shop_info')->insert([
['Id' => 1, 'erpShopName' => '一号店', 'responsibleDepartment' => '部门甲'],
['Id' => 2, 'erpShopName' => '二号店', 'responsibleDepartment' => '部门乙'],
]);
$filters = ['from' => '2026-08-18', 'to' => '2026-08-21', 'order_no' => '', 'shop_name' => '', 'sku_id' => ''];
$page2 = $this->repository->paginate($filters, 2, 1);
$this->assertSame(2, $page2['total']);
$this->assertSame(11, $page2['data'][0]->Id);
$this->assertSame('ZIP-1', $page2['data'][0]->originPostcode);
$this->assertSame('ITEM-1', $page2['data'][0]->shop_i_id);
$this->assertSame(88, $page2['data'][0]->buyer_paid_amount);
$this->assertSame(20.0, $page2['data'][0]->unitCostGoods);
$this->assertSame(40.0, $page2['data'][0]->refundCostGoods);
$this->assertSame('部门甲', $page2['data'][0]->responsibleDepartment);
$filters['shop_name'] = '二号';
$this->assertSame(1, $this->repository->paginate($filters, 1, 25)['total']);
$filters['shop_name'] = '';
$filters['shop_name'] = '%';
$this->assertSame(0, $this->repository->paginate($filters, 1, 25)['total']);
$filters['shop_name'] = '';
$filters['sku_id'] = 'SKU-1';
$this->assertSame(1, $this->repository->paginate($filters, 1, 25)['total']);
$filters['sku_id'] = '';
$filters['order_no'] = '8902';
$this->assertSame(1, $this->repository->paginate($filters, 1, 25)['total']);
$filters['order_no'] = '';
$exportRows = iterator_to_array($this->repository->exportRows($filters, 1));
$this->assertCount(2, $exportRows);
$this->assertSame(12, $exportRows[0]->Id);
$this->assertSame(11, $exportRows[1]->Id);
}
public function test_sync_status_distinguishes_rotation_from_initial_sync(): void
{
$states = $this->repository->states();
foreach ($states as &$state) {
$state['status'] = 'running';
$state['last_succeeded_at'] = '2026-08-21 12:00:00';
}
unset($state);
$this->assertSame('syncing', $this->repository->syncStatus(
$states,
Carbon::parse('2026-08-21 12:05:00'),
)['code']);
$states[NonJingxiRefundMirrorRepository::INFO_DATASET]['initial_completed_at'] = null;
$this->assertSame('initializing', $this->repository->syncStatus($states)['code']);
}
public function test_query_is_blocked_when_a_required_lookup_index_is_missing(): void
{
Schema::connection('business_mirror')->table(
'ops_jst_return_refund_info',
static fn (Blueprint $table) => $table->dropIndex(['as_date', 'Id']),
);
$this->assertFalse($this->repository->queryReady());
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('business_mirror_dataset_not_ready');
$this->repository->paginate(
['from' => '2026-08-18', 'to' => '2026-08-21', 'order_no' => '', 'shop_name' => '', 'sku_id' => ''],
1,
25,
);
}
/** @return array<string,array<string,mixed>> */
private function definitions(): array
{
return [
NonJingxiRefundMirrorRepository::INFO_DATASET => $this->definition('jst_returnAndRefundInfo', 'ops_jst_return_refund_info'),
NonJingxiRefundMirrorRepository::ITEM_DATASET => $this->definition('jst_returnAndRefundItem', 'ops_jst_return_refund_item'),
'operations_replica.rpt.jst_Orders' => $this->definition('jst_Orders', 'ops_jst_orders'),
'operations_replica.rpt.jst_OrderItem' => $this->definition('jst_OrderItem', 'ops_jst_order_item'),
'operations_replica.rpt.jst_sku' => $this->definition('jst_sku', 'ops_jst_sku'),
'operations_replica.rpt.fys_shopInfo' => $this->definition('fys_shopInfo', 'ops_fys_shop_info'),
];
}
/** @return array<string,mixed> */
private function definition(string $object, string $target): array
{
return ['source_key' => 'operations_replica', 'schema' => 'rpt', 'object' => $object, 'target' => $target, 'keys' => ['Id']];
}
private function createTables(): void
{
$schema = Schema::connection('business_mirror');
$schema->create('business_mirror_table_states', function (Blueprint $table): void {
$table->string('dataset_key')->primary();
$table->string('status')->default('succeeded');
$table->dateTime('initial_completed_at')->nullable();
$table->dateTime('last_succeeded_at')->nullable();
});
$schema->create('ops_jst_return_refund_info', function (Blueprint $table): void {
$table->integer('Id'); $table->integer('as_id'); $table->dateTime('as_date'); $table->string('outer_as_id')->nullable();
$table->integer('o_id'); $table->string('so_id')->nullable(); $table->string('type')->nullable(); $table->string('node')->nullable();
$table->string('shop_type')->nullable(); $table->string('shop_name')->nullable(); $table->string('shop_site')->nullable();
$table->decimal('refund', 18, 2)->nullable(); $table->decimal('receive_refund', 18, 2)->nullable();
$table->decimal('buyer_apply_refund', 18, 2)->nullable(); $table->dateTime('createTime')->nullable();
$table->string('status')->nullable(); $table->string('shop_status')->nullable(); $table->dateTime('created')->nullable();
$table->dateTime('confirm_date')->nullable();
$table->index(['as_date', 'Id']);
$table->index(['o_id', 'Id']);
$table->index(['as_id', 'Id']);
});
$schema->create('ops_jst_return_refund_item', function (Blueprint $table): void {
$table->integer('Id'); $table->integer('as_id'); $table->string('outer_oi_id')->nullable();
$table->string('shop_sku_id')->nullable(); $table->string('i_id')->nullable(); $table->string('sku_id')->nullable();
$table->integer('qty')->nullable(); $table->decimal('shop_amount', 18, 2)->nullable();
$table->string('type')->nullable(); $table->integer('r_qty')->nullable();
$table->index(['as_id', 'Id']);
});
$schema->create('ops_jst_orders', function (Blueprint $table): void {
$table->integer('Id'); $table->integer('o_id'); $table->string('receiver_zip')->nullable();
$table->index(['o_id', 'Id']);
});
$schema->create('ops_jst_order_item', function (Blueprint $table): void {
$table->integer('Id'); $table->integer('o_id'); $table->string('outer_oi_id');
$table->string('shop_i_id')->nullable(); $table->string('shop_sku_id')->nullable(); $table->string('i_id')->nullable();
$table->string('sku_id')->nullable(); $table->decimal('buyer_paid_amount', 18, 2)->nullable();
$table->index(['o_id', 'Id']);
});
$schema->create('ops_jst_sku', function (Blueprint $table): void {
$table->integer('autoid'); $table->string('sku_id'); $table->string('sku_type'); $table->decimal('cost_price', 18, 2)->nullable();
});
$schema->create('ops_fys_shop_info', function (Blueprint $table): void {
$table->integer('Id'); $table->string('erpShopName'); $table->string('responsibleDepartment')->nullable();
});
}
private function markReady(): void
{
foreach (array_keys($this->definitions()) as $key) {
DB::connection('business_mirror')->table('business_mirror_table_states')->insert([
'dataset_key' => $key,
'status' => 'succeeded',
'initial_completed_at' => '2026-08-15 00:00:00',
'last_succeeded_at' => '2026-08-21 12:00:00',
]);
}
}
/** @return array<string,mixed> */
private function refundInfo(int $id, int $asId, string $date, string $platform, string $shopName): array
{
return [
'Id' => $id, 'as_id' => $asId, 'as_date' => $date, 'outer_as_id' => 'AS-' . $asId,
'o_id' => 8900 + $id, 'so_id' => 'SO-' . $id, 'type' => '退款', 'node' => '完成',
'shop_type' => '自营', 'shop_name' => $shopName, 'shop_site' => $platform,
'refund' => 100, 'receive_refund' => 80, 'buyer_apply_refund' => 100,
'createTime' => $date, 'status' => '完成', 'shop_status' => '完成',
'created' => $date, 'confirm_date' => $date,
];
}
/** @return array<string,mixed> */
private function refundItem(int $id, int $asId, string $skuId, string $outerOrderItemId): array
{
return [
'Id' => $id, 'as_id' => $asId, 'outer_oi_id' => $outerOrderItemId,
'shop_sku_id' => 'SHOP-' . $skuId, 'i_id' => 'STYLE-' . $id, 'sku_id' => $skuId,
'qty' => 2, 'shop_amount' => 80, 'type' => '退货退款', 'r_qty' => 2,
];
}
}