feat: complete person identity matching system with fuzzy detection, revert, and manual link

Implements the full identity matching engine: email matching (HIGH confidence),
fuzzy name matching with Levenshtein distance (MEDIUM confidence, upgradable to
HIGH with DOB tiebreaker), manual link/unlink, revert confirmed matches, and
automatic detection via PersonObserver. Includes 33 comprehensive tests, frontend
integration with confirm/dismiss/unlink UI, and match indicators in the persons list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 08:44:24 +02:00
parent 7932e53daf
commit eb1a0ac666
30 changed files with 1941 additions and 399 deletions

View File

@@ -0,0 +1,24 @@
<?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::table('users', function (Blueprint $table) {
$table->date('date_of_birth')->nullable()->after('last_name');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('date_of_birth');
});
}
};

View File

@@ -0,0 +1,59 @@
<?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::table('person_identity_matches', function (Blueprint $table) {
$table->json('match_details')->nullable()->after('status');
$table->foreignUlid('confirmed_by_user_id')
->nullable()
->after('match_details')
->constrained('users')
->nullOnDelete();
$table->timestamp('confirmed_at')->nullable()->after('confirmed_by_user_id');
$table->foreignUlid('dismissed_by_user_id')
->nullable()
->after('confirmed_at')
->constrained('users')
->nullOnDelete();
$table->timestamp('dismissed_at')->nullable()->after('dismissed_by_user_id');
$table->foreignUlid('reverted_by_user_id')
->nullable()
->after('dismissed_at')
->constrained('users')
->nullOnDelete();
$table->timestamp('reverted_at')->nullable()->after('reverted_by_user_id');
});
}
public function down(): void
{
Schema::table('person_identity_matches', function (Blueprint $table) {
$table->dropForeign(['confirmed_by_user_id']);
$table->dropForeign(['dismissed_by_user_id']);
$table->dropForeign(['reverted_by_user_id']);
$table->dropColumn([
'match_details',
'confirmed_by_user_id',
'confirmed_at',
'dismissed_by_user_id',
'dismissed_at',
'reverted_by_user_id',
'reverted_at',
]);
});
}
};