312 lines
13 KiB
PHP
312 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\EcommerceFinance;
|
|
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Modules\EcommerceFinance\Repositories\DingProcessDetailMirrorRepository;
|
|
use Modules\EcommerceFinance\Services\DingProcessDetailMirrorSynchronizer;
|
|
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
|
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
|
use RuntimeException;
|
|
use Tests\TestCase;
|
|
|
|
class DingProcessDetailMirrorSynchronizerTest extends TestCase
|
|
{
|
|
private FakeDingProcessDetailQuery $query;
|
|
|
|
private DingProcessDetailMirrorSynchronizer $synchronizer;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
config([
|
|
'database.connections.business_mirror' => [
|
|
'driver' => 'sqlite',
|
|
'database' => ':memory:',
|
|
'prefix' => '',
|
|
],
|
|
'upstream_mirrors.connection' => 'business_mirror',
|
|
'upstream_mirrors.ding_process_details' => [
|
|
'scope' => 'personal_permission',
|
|
'start_date' => '2026-01-01',
|
|
'batch_size' => 20,
|
|
'sync_interval_minutes' => 5,
|
|
'incremental_overlap_hours' => 48,
|
|
'processes' => [
|
|
'PROC-ACF09A14-B094-46B1-A9E2-8BB1F4E1FDCF' => [
|
|
'document_type' => '线上代发业务报销',
|
|
'detail_table' => '收款费用明细',
|
|
'detail_amount_fields' => ['代发订单金额'],
|
|
'amount_total_fields' => ['收款合计'],
|
|
'payment_table' => '付款明细',
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
DB::purge('business_mirror');
|
|
$this->createMirrorTables();
|
|
|
|
$this->query = new FakeDingProcessDetailQuery();
|
|
$factory = new class($this->query) extends UpstreamReadOnlyQueryFactory {
|
|
public function __construct(private readonly UpstreamReadOnlyQuery $query) {}
|
|
|
|
public function forSource(string $source): UpstreamReadOnlyQuery
|
|
{
|
|
return $this->query;
|
|
}
|
|
|
|
public function forSourceWithTimeout(string $source, int $timeoutSeconds): UpstreamReadOnlyQuery
|
|
{
|
|
return $this->query;
|
|
}
|
|
};
|
|
$this->app->instance(UpstreamReadOnlyQueryFactory::class, $factory);
|
|
$this->synchronizer = app(DingProcessDetailMirrorSynchronizer::class);
|
|
}
|
|
|
|
public function test_structured_sync_counts_a_multi_line_payment_once(): void
|
|
{
|
|
$this->query->workflows = [[
|
|
'processInstanceId' => 'process-daifa-1',
|
|
'processCode' => 'PROC-ACF09A14-B094-46B1-A9E2-8BB1F4E1FDCF',
|
|
'businessId' => '202605041811000260216',
|
|
'title' => '陈芝豹提交的线上代发业务报销',
|
|
'status' => 'COMPLETED',
|
|
'originatorUserId' => 'user-1',
|
|
'joinAt' => '2026-05-04 18:12:20',
|
|
'updateAt' => '2026-05-08 09:00:00',
|
|
]];
|
|
$this->query->scheme = $this->daifaScheme();
|
|
|
|
$result = $this->synchronizer->sync(true);
|
|
|
|
$this->assertSame('succeeded', $result['status']);
|
|
$this->assertSame(1, $result['workflows']);
|
|
$this->assertSame(14, $result['details']);
|
|
$this->assertSame(1, $result['payments']);
|
|
$this->assertEqualsWithDelta(1114.57, $result['amount_total'], 0.001);
|
|
$this->assertEqualsWithDelta(1114.57, $result['bank_amount_total'], 0.001);
|
|
$this->assertSame(14, DB::connection('business_mirror')
|
|
->table('ding_process_detail_mirror')->count());
|
|
$this->assertSame(1, DB::connection('business_mirror')
|
|
->table('ding_process_payment_mirror')->count());
|
|
$this->assertEqualsWithDelta(1114.57, (float) DB::connection('business_mirror')
|
|
->table('ding_process_detail_mirror')->sum('bank_amount'), 0.001);
|
|
|
|
$summary = app(DingProcessDetailMirrorRepository::class)
|
|
->paginate(['status' => '已完成'], 1, 50)['summary'];
|
|
$this->assertEqualsWithDelta(1114.57, $summary['amount_total'], 0.001);
|
|
$this->assertEqualsWithDelta(1114.57, $summary['bank_amount_total'], 0.001);
|
|
}
|
|
|
|
public function test_validation_failure_keeps_the_previous_published_rows(): void
|
|
{
|
|
$this->query->workflows = [[
|
|
'processInstanceId' => 'process-old',
|
|
'processCode' => 'PROC-ACF09A14-B094-46B1-A9E2-8BB1F4E1FDCF',
|
|
'businessId' => '202605041811000260216',
|
|
'title' => '陈芝豹提交的线上代发业务报销',
|
|
'status' => 'COMPLETED',
|
|
'originatorUserId' => 'user-1',
|
|
'joinAt' => '2026-05-04 18:12:20',
|
|
'updateAt' => '2026-05-08 09:00:00',
|
|
]];
|
|
$this->query->scheme = $this->daifaScheme();
|
|
$this->synchronizer->sync(true);
|
|
|
|
$this->query->scheme['收款合计'] = 999999;
|
|
$this->query->workflows = [[
|
|
'processInstanceId' => 'process-new',
|
|
'processCode' => 'PROC-ACF09A14-B094-46B1-A9E2-8BB1F4E1FDCF',
|
|
'businessId' => '202605051811000260217',
|
|
'title' => '陈芝豹提交的线上代发业务报销',
|
|
'status' => 'COMPLETED',
|
|
'originatorUserId' => 'user-1',
|
|
'joinAt' => '2026-05-05 18:12:20',
|
|
'updateAt' => '2026-05-09 09:00:00',
|
|
]];
|
|
|
|
try {
|
|
$this->synchronizer->sync(true);
|
|
$this->fail('明细合计与审批合计不一致时应停止发布。');
|
|
} catch (RuntimeException $exception) {
|
|
$this->assertStringContainsString('明细合计', $exception->getMessage());
|
|
}
|
|
|
|
$this->assertSame(14, DB::connection('business_mirror')
|
|
->table('ding_process_detail_mirror')->count());
|
|
$this->assertTrue(DB::connection('business_mirror')
|
|
->table('ding_process_detail_mirror')
|
|
->where('business_id', '202605041811000260216')
|
|
->exists());
|
|
$this->assertSame('failed', DB::connection('business_mirror')
|
|
->table('business_mirror_table_states')
|
|
->value('status'));
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
private function daifaScheme(): array
|
|
{
|
|
$amounts = [157, 153, 56.1, 63.1, 85, 90, 56.1, 63.1, 66.1, 61.67, 66.1, 65.1, 66.1, 66.1];
|
|
$details = array_map(static fn (float $amount): array => [
|
|
'rowValue' => [
|
|
['label' => '序号', 'value' => '1'],
|
|
['label' => '商品ID', 'value' => '877714260849'],
|
|
['label' => '代发订单金额', 'value' => (string) $amount],
|
|
['label' => '科目编码', 'value' => '1405.06'],
|
|
['label' => '科目名称', 'value' => '库存商品_线上代发'],
|
|
],
|
|
], $amounts);
|
|
$payment = [[
|
|
'rowValue' => [
|
|
['label' => '对应序号', 'value' => '1-2-3-4-5-6-7-8-9-10-11-12-13-14'],
|
|
['label' => '付款方式', 'value' => '银行卡'],
|
|
['label' => '付款金额', 'value' => '1114.57'],
|
|
['label' => '科目编码', 'value' => '1002.02.114'],
|
|
['label' => '科目名称', 'value' => '华夏银行-胡燕'],
|
|
['label' => '打款时间', 'value' => '2026-05-08'],
|
|
],
|
|
]];
|
|
|
|
return [
|
|
'收款费用明细' => json_encode($details, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
|
|
'收款合计' => 1114.57,
|
|
'付款明细' => json_encode($payment, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
|
|
];
|
|
}
|
|
|
|
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->string('process_instance_id', 100)->nullable();
|
|
$table->unsignedInteger('line_no');
|
|
$table->string('business_month', 6);
|
|
$table->string('originator_name', 100)->nullable();
|
|
$table->string('status', 20)->nullable();
|
|
$table->string('document_type', 100)->nullable();
|
|
$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);
|
|
$table->dateTime('synced_at', 6);
|
|
$table->timestamps();
|
|
$table->unique(['business_id', 'line_no']);
|
|
},
|
|
);
|
|
|
|
Schema::connection('business_mirror')->create(
|
|
'ding_process_payment_mirror',
|
|
function (Blueprint $table): void {
|
|
$table->bigIncrements('id');
|
|
$table->string('business_id', 32);
|
|
$table->string('process_instance_id', 100);
|
|
$table->unsignedInteger('payment_no');
|
|
$table->string('sequence_spec', 255)->nullable();
|
|
$table->decimal('amount', 18, 2);
|
|
$table->string('account_code', 64)->nullable();
|
|
$table->string('account_name', 255)->nullable();
|
|
$table->string('payment_method', 50)->nullable();
|
|
$table->string('paid_at', 50)->nullable();
|
|
$table->string('source_scope', 40);
|
|
$table->dateTime('synced_at', 6);
|
|
$table->timestamps();
|
|
$table->unique(['source_scope', 'business_id', 'payment_no']);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class FakeDingProcessDetailQuery extends UpstreamReadOnlyQuery
|
|
{
|
|
/** @var list<array<string,mixed>> */
|
|
public array $workflows = [];
|
|
|
|
/** @var array<string,mixed> */
|
|
public array $scheme = [];
|
|
|
|
/** @var list<array{sql:string,bindings:array}> */
|
|
public array $calls = [];
|
|
|
|
public function __construct()
|
|
{
|
|
parent::__construct('operations_replica');
|
|
}
|
|
|
|
public function scalar(string $sql, array $bindings = []): mixed
|
|
{
|
|
if (str_contains($sql, 'COUNT(*)') && str_contains($sql, 'rpt.ding_Workflow')) {
|
|
return count($this->workflows);
|
|
}
|
|
|
|
return parent::scalar($sql, $bindings);
|
|
}
|
|
|
|
public function select(string $sql, array $bindings = []): array
|
|
{
|
|
$this->calls[] = compact('sql', 'bindings');
|
|
|
|
if (str_contains($sql, 'FROM rpt.hr_employee')) {
|
|
return [['userid' => 'user-1', 'name' => '陈芝豹']];
|
|
}
|
|
if (str_contains($sql, 'JOIN rpt.ding_WorkflowScheme')
|
|
&& str_contains($sql, 'NOT IN')) {
|
|
return [];
|
|
}
|
|
if (str_contains($sql, 'FROM rpt.ding_Workflow w')) {
|
|
return $this->workflows;
|
|
}
|
|
if (str_contains($sql, 'FROM rpt.ding_WorkflowScheme')) {
|
|
$rows = [];
|
|
foreach ($this->scheme as $name => $value) {
|
|
$rows[] = [
|
|
'processInstanceId' => (string) ($this->workflows[0]['processInstanceId'] ?? ''),
|
|
'name' => $name,
|
|
'value' => $value,
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
return [];
|
|
}
|
|
}
|