[ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ], 'upstream_mirrors.connection' => 'business_mirror', 'upstream_mirrors.ding_process_details.scope' => 'personal_permission', ]); DB::purge('business_mirror'); $this->createMirrorTables(); $this->seedRows(); $this->repository = app(DingProcessDetailMirrorRepository::class); } public function test_paginate_returns_rows_summary_and_filter_options(): void { $result = $this->repository->paginate([], 1, 50); $this->assertSame(4, $result['total']); $this->assertCount(4, $result['data']); $this->assertSame(4, $result['summary']['rows']); $this->assertSame(2, $result['summary']['approvals']); $this->assertEqualsWithDelta(650.5, $result['summary']['amount_total'], 0.001); $this->assertEqualsWithDelta(650.5, $result['summary']['bank_amount_total'], 0.001); $this->assertSame('personal_permission', $result['scope']); $options = $this->repository->filterOptions(); $this->assertSame(['2026-02', '2026-01'], $options['months']); $this->assertContains('公摊日常报销', $options['document_types']); $this->assertContains('应付申请流程', $options['document_types']); $this->assertContains('已完成', $options['statuses']); $this->assertContains('审批中', $options['statuses']); } public function test_paginate_applies_month_status_and_keyword_filters(): void { $this->assertSame( 2, $this->repository->paginate(['month' => '2026-01'], 1, 50)['total'], ); $this->assertSame( 2, $this->repository->paginate(['status' => '审批中'], 1, 50)['total'], ); $this->assertSame( 1, $this->repository->paginate(['keyword' => '办公'], 1, 50)['total'], ); } public function test_paginate_applies_document_type_originator_and_business_id_filters(): void { $this->assertSame( 2, $this->repository->paginate([ 'document_type' => '应付申请流程', 'originator_name' => '李四', ], 1, 50)['total'], ); $this->assertSame( 2, $this->repository->paginate(['business_id' => '0002'], 1, 50)['total'], ); } public function test_export_rows_returns_every_filtered_row(): void { $rows = iterator_to_array($this->repository->exportRows(['status' => '已完成'])); $this->assertCount(2, $rows); $this->assertSame('202601011200000000001', (string) $rows[0]['business_id']); } public function test_not_ready_when_state_is_missing(): void { DB::connection('business_mirror') ->table('business_mirror_table_states') ->where('dataset_key', 'ding.process_details.personal_permission') ->delete(); $this->assertFalse($this->repository->ready()); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('ding_process_detail_mirror_not_ready'); $this->repository->paginate([], 1, 50); } private function seedRows(): void { $now = now(); $rows = [ $this->row('202601011200000000001', 1, '202601', '张三', '已完成', '公摊日常报销', '办公用品采购', 100.00), $this->row('202601011200000000001', 2, '202601', '张三', '已完成', '公摊日常报销', '交通费', 50.50), $this->row('202602031200000000002', 1, '202602', '李四', '审批中', '应付申请流程', '供应商货款', 200.00), $this->row('202602031200000000002', 2, '202602', '李四', '审批中', '应付申请流程', '供应商货款', 300.00), ]; DB::connection('business_mirror') ->table('ding_process_detail_mirror') ->insert($rows); DB::connection('business_mirror') ->table('business_mirror_table_states') ->insert([ 'dataset_key' => 'ding.process_details.personal_permission', 'source_key' => 'dingtalk_oa', 'source_object' => 'oa approval detail (personal permission scope)', 'target_table' => 'ding_process_detail_mirror', 'status' => 'succeeded', 'run_mode' => 'full', 'generation' => '00000000-0000-0000-0000-000000000001', 'initial_completed_at' => $now, 'last_full_succeeded_at' => $now, 'last_attempted_at' => $now, 'last_succeeded_at' => $now, 'last_upserted_rows' => 4, 'mirror_row_count' => 4, 'created_at' => $now, 'updated_at' => $now, ]); } /** @return array */ private function row( string $businessId, int $lineNo, string $month, string $originator, string $status, string $documentType, string $purpose, float $amount, ): array { $now = now(); return [ 'business_id' => $businessId, 'line_no' => $lineNo, 'business_month' => $month, 'originator_name' => $originator, 'status' => $status, 'document_type' => $documentType, 'purpose' => $purpose, 'account_code' => '5602.04', 'account_name' => '管理费用_办公费', 'amount' => $amount, 'bank_account_code' => '1002.01.001', 'bank_account_name' => '华夏银行-测试', 'bank_amount' => $amount, 'payment_method' => '银行卡', 'paid_at' => '2026-01-02 00:00', 'source_scope' => 'personal_permission', 'synced_at' => $now, 'created_at' => $now, 'updated_at' => $now, ]; } private function createMirrorTables(): void { Schema::connection('business_mirror')->create( 'business_mirror_table_states', function (Blueprint $table): void { $table->string('dataset_key')->primary(); $table->string('source_key'); $table->string('source_object'); $table->string('target_table'); $table->string('status')->default('pending'); $table->string('run_mode')->nullable(); $table->string('generation')->nullable(); $table->json('cursor')->nullable(); $table->json('high_watermark')->nullable(); $table->dateTime('window_started_at')->nullable(); $table->dateTime('window_ends_at')->nullable(); $table->dateTime('initial_completed_at')->nullable(); $table->dateTime('last_full_succeeded_at')->nullable(); $table->dateTime('last_attempted_at')->nullable(); $table->dateTime('last_succeeded_at')->nullable(); $table->unsignedBigInteger('last_upserted_rows')->default(0); $table->unsignedBigInteger('mirror_row_count')->default(0); $table->string('last_error_code')->nullable(); $table->timestamps(); }, ); Schema::connection('business_mirror')->create( 'ding_process_detail_mirror', function (Blueprint $table): void { $table->bigIncrements('id'); $table->string('business_id', 32); $table->unsignedInteger('line_no'); $table->string('business_month', 6)->index(); $table->string('originator_name', 100)->nullable()->index(); $table->string('status', 20)->nullable()->index(); $table->string('document_type', 100)->nullable()->index(); $table->text('purpose')->nullable(); $table->string('account_code', 64)->nullable(); $table->string('account_name', 255)->nullable(); $table->decimal('amount', 18, 2)->nullable(); $table->string('bank_account_code', 255)->nullable(); $table->string('bank_account_name', 255)->nullable(); $table->decimal('bank_amount', 18, 2)->nullable(); $table->string('payment_method', 50)->nullable(); $table->string('paid_at', 50)->nullable(); $table->string('source_scope', 40)->default('personal_permission')->index(); $table->dateTime('synced_at', 6); $table->timestamps(); $table->unique(['business_id', 'line_no']); }, ); } }