[ '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> */ 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 */ 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 */ 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 */ 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, ]; } }