diff --git a/modules/EcommerceFinance/Repositories/DingOnlineDaifaPriceMirrorRepository.php b/modules/EcommerceFinance/Repositories/DingOnlineDaifaPriceMirrorRepository.php index 4d4159f5..301c24b8 100644 --- a/modules/EcommerceFinance/Repositories/DingOnlineDaifaPriceMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/DingOnlineDaifaPriceMirrorRepository.php @@ -71,7 +71,7 @@ class DingOnlineDaifaPriceMirrorRepository $query->where('workflow_created_at', '>=', (string) $filters['created_from']); } if (($filters['created_to'] ?? '') !== '') { - $query->where('workflow_created_at', '<', (string) $filters['created_to'] . ' 99:99:99'); + $query->where('workflow_created_at', '<', \Illuminate\Support\Carbon::parse((string) $filters['created_to'])->addDay()->startOfDay()->format('Y-m-d H:i:s')); } $rows = $query->orderByDesc('workflow_created_at')->orderByDesc('process_instance_id')->orderByDesc('detail_index')->get(); @@ -143,7 +143,7 @@ class DingOnlineDaifaPriceMirrorRepository $daifa += (float) ($row->daifa_amount ?? 0); $paid += (float) ($row->paid_amount ?? 0); $diff += (float) ($row->price_diff ?? 0); - if (! empty($row->product_id)) { + if (! empty($row->product_id) && $this->cleanName($row->product_name ?? null) !== null) { $matched++; } } diff --git a/modules/EcommerceFinance/Services/DingPayableWorkflowMirrorSynchronizer.php b/modules/EcommerceFinance/Services/DingPayableWorkflowMirrorSynchronizer.php index 2f740388..233e88de 100644 --- a/modules/EcommerceFinance/Services/DingPayableWorkflowMirrorSynchronizer.php +++ b/modules/EcommerceFinance/Services/DingPayableWorkflowMirrorSynchronizer.php @@ -162,9 +162,10 @@ class DingPayableWorkflowMirrorSynchronizer $detailIndex++; } - if ($resolveProductName) { - $this->resolveProductNames($payloads); - } + } + + if ($resolveProductName && $payloads !== []) { + $this->resolveProductNames($payloads); } $nextCursor = $this->cursorFromWorkflow($workflows[array_key_last($workflows)]); @@ -182,7 +183,12 @@ class DingPayableWorkflowMirrorSynchronizer $updateColumns[] = 'product_name'; } } - $this->connection->transaction(function () use ($dataset, $target, $payloads, $cursor, $generation, $rows, $now, $upsertKeys, $updateColumns): void { + $this->connection->transaction(function () use ($dataset, $target, $payloads, $cursor, $generation, $rows, $now, $upsertKeys, $updateColumns, $extendColumns, $ids): void { + // A changed workflow may have fewer details or no longer match + // the approved status/payee. Replace its rows atomically. + if ($extendColumns && $ids !== []) { + $this->connection->table($target)->whereIn('process_instance_id', $ids)->delete(); + } if ($payloads !== []) { $this->connection->table($target)->upsert( $payloads, diff --git a/tests/Unit/EcommerceFinance/DingPayableWorkflowMirrorSynchronizerTest.php b/tests/Unit/EcommerceFinance/DingPayableWorkflowMirrorSynchronizerTest.php index 0018d5d6..1badf847 100644 --- a/tests/Unit/EcommerceFinance/DingPayableWorkflowMirrorSynchronizerTest.php +++ b/tests/Unit/EcommerceFinance/DingPayableWorkflowMirrorSynchronizerTest.php @@ -159,6 +159,54 @@ class DingPayableWorkflowMirrorSynchronizerTest extends TestCase $this->assertSame(2, DB::connection('business_mirror')->table('ding_payable_workflow_mirror')->count()); } + public function test_extended_workflow_update_removes_rows_that_no_longer_match(): void + { + config(['upstream_mirrors.ding_online_daifa_price' => [ + 'dataset_key' => 'operations_replica.rpt.ding_online_daifa_price', + 'target_table' => 'ding_online_daifa_price_mirror', + 'source_key' => 'operations_replica', 'process_code' => 'PROC-TEST', + 'extend_columns' => true, 'only_status' => 'COMPLETED', + 'column_map' => ['product_id' => '商品ID'], + ]]); + $sync = $this->synchronizer->forConfigKey('ding_online_daifa_price'); + $this->query->workflowBatches = [[$this->workflow('changed', now()->subHour()->format('Y-m-d H:i:s.u'))]]; + $sync->sync(true, 10, 0); + $table = DB::connection('business_mirror')->table('ding_online_daifa_price_mirror'); + $this->assertSame(1, $table->count()); + $this->query->workflowBatches = [[$this->workflow('changed', now()->format('Y-m-d H:i:s.u'), 'RUNNING')]]; + $sync->sync(false, 10, 0); + $this->assertSame(0, $table->count()); + } + + public function test_daifa_repository_includes_end_date_and_counts_only_resolved_products(): void + { + config(['upstream_mirrors.ding_online_daifa_price' => [ + 'dataset_key' => 'operations_replica.rpt.ding_online_daifa_price', + 'target_table' => 'ding_online_daifa_price_mirror', + 'source_key' => 'operations_replica', 'process_code' => 'PROC-TEST', + 'extend_columns' => true, 'column_map' => ['product_id' => '商品ID'], + ]]); + $this->query->workflowBatches = [[ + $this->workflow('A', '2026-08-01 00:00:00'), + $this->workflow('B', '2026-08-01 23:59:59'), + $this->workflow('C', '2026-08-02 00:00:00'), + ]]; + $this->synchronizer->forConfigKey('ding_online_daifa_price')->sync(true, 10, 0); + $db = DB::connection('business_mirror'); + $db->table('ding_online_daifa_price_mirror')->where('process_instance_id', 'A') + ->update(['product_id' => 'fixture-a', 'product_name' => '已匹配示例']); + $db->table('ding_online_daifa_price_mirror')->where('process_instance_id', 'B') + ->update(['product_id' => 'fixture-b', 'product_name' => null]); + $db->enableQueryLog(); + $result = app(\Modules\EcommerceFinance\Repositories\DingOnlineDaifaPriceMirrorRepository::class) + ->paginate(['created_from' => '2026-08-01', 'created_to' => '2026-08-01'], 1, 25); + $this->assertSame(2, $result['total']); + $this->assertSame(1, $result['summary']['matched_count']); + $this->assertSame(['B', 'A'], array_column($result['data'], 'process_instance_id')); + $queries = array_filter($db->getQueryLog(), fn ($q) => str_contains($q['query'], 'workflow_created_at')); + $this->assertContains('2026-08-02 00:00:00', array_values($queries)[0]['bindings']); + } + /** @return array */ private function workflow(string $id, string $updateAt, string $status = 'COMPLETED'): array {