feat: person identity matching with detection, confirmation and audit trail

Implements enterprise-grade identity resolution (detect → suggest → confirm)
for Person ↔ User linking. Matches are detected automatically on person
creation and user account creation, then surfaced to organisers for explicit
confirmation or dismissal. No silent auto-linking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 12:50:25 +02:00
parent 239fe57a11
commit 4b182b449a
20 changed files with 1463 additions and 2 deletions

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('person_identity_matches', function (Blueprint $table) {
$table->ulid('id')->primary();
$table->foreignUlid('person_id')
->constrained('persons')
->cascadeOnDelete();
$table->foreignUlid('matched_user_id')
->constrained('users')
->cascadeOnDelete();
$table->string('matched_on');
$table->string('confidence');
$table->string('status')->default('pending');
$table->foreignUlid('resolved_by_user_id')
->nullable()
->constrained('users')
->nullOnDelete();
$table->timestamp('resolved_at')->nullable();
$table->timestamp('created_at')->nullable();
// Prevent duplicate match records for the same person+user pair
$table->unique(['person_id', 'matched_user_id']);
// Query indexes
$table->index(['person_id', 'status']);
$table->index(['matched_user_id', 'status']);
$table->index('status');
});
}
public function down(): void
{
Schema::dropIfExists('person_identity_matches');
}
};