feat(sync): update user sync engine and departmental sync for face outcomes

Wires the sync engine and departmental sync to the new result model and face-enrollment verification behavior.
main
SYED MUSTUFA AHMED NAQVI 2026-08-27 10:00:13 +05:00
parent f1e20ddf0b
commit c4bd561811
2 changed files with 107 additions and 13 deletions

View File

@ -11,6 +11,8 @@ public sealed class DepartmentalUserSyncService(
IProgress<UserSyncProgress>? progress, CancellationToken cancellationToken) IProgress<UserSyncProgress>? progress, CancellationToken cancellationToken)
{ {
var list = employees.ToList(); var list = employees.ToList();
AppLogger.TraceEnter("USER_SYNC", nameof(SyncAsync),
$"device={device.IpAddress} count={list.Count} context={contextSummary}");
var entry = new SyncHistoryEntry var entry = new SyncHistoryEntry
{ {
Operation = SyncOperationKind.DepartmentalUserSync, Operation = SyncOperationKind.DepartmentalUserSync,
@ -24,10 +26,12 @@ public sealed class DepartmentalUserSyncService(
var results = await engine.RunAsync(device, list, initialEnrollment: true, progress, cancellationToken); var results = await engine.RunAsync(device, list, initialEnrollment: true, progress, cancellationToken);
entry.CompletedAt = DateTime.Now; entry.CompletedAt = DateTime.Now;
entry.Success = results.Count(r => r.OverallResult is "SUCCESS" or "ALREADY_EXISTS"); entry.Success = results.Count(r => r.OverallResult is "SUCCESS" or "ALREADY_EXISTS");
entry.Failed = results.Count(r => r.OverallResult == "FAILED"); entry.Failed = results.Count(r => r.OverallResult is "FAILED" or "PARTIAL");
entry.Skipped = results.Count(r => r.OverallResult == "SKIPPED"); entry.Skipped = results.Count(r => r.OverallResult == "SKIPPED");
entry.Status = entry.Failed > 0 ? "Completed with errors" : "Completed"; entry.Status = entry.Failed > 0 ? "Completed with errors" : "Completed";
await history.UpdateAsync(entry, cancellationToken); await history.UpdateAsync(entry, cancellationToken);
AppLogger.TraceExit("USER_SYNC", nameof(SyncAsync),
$"success={entry.Success} failed={entry.Failed} skipped={entry.Skipped}");
return (entry, results); return (entry, results);
} }
} }

View File

@ -7,47 +7,103 @@ public sealed class UserSyncEngine(HikvisionIsapiClient hikvision, EmployeePhoto
public async Task<IReadOnlyList<EmployeeSyncResult>> RunAsync(Device device, IEnumerable<HrmsEmployee> employees, bool initialEnrollment, public async Task<IReadOnlyList<EmployeeSyncResult>> RunAsync(Device device, IEnumerable<HrmsEmployee> employees, bool initialEnrollment,
IProgress<UserSyncProgress>? progress, CancellationToken cancellationToken) IProgress<UserSyncProgress>? progress, CancellationToken cancellationToken)
{ {
var results = new List<EmployeeSyncResult>();
var source = employees.ToList(); var source = employees.ToList();
AppLogger.TraceEnter("USER_SYNC", nameof(RunAsync),
$"device={device.IpAddress} count={source.Count} initialEnrollment={initialEnrollment}");
var results = new List<EmployeeSyncResult>();
for (var index = 0; index < source.Count; index++) for (var index = 0; index < source.Count; index++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var employee = source[index]; var employee = source[index];
AppLogger.TraceEnter("USER_SYNC", "ProcessEmployee", $"employee={employee.SerialNumber} index={index + 1}/{source.Count}");
progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Creating user…", results)); progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Creating user…", results));
var result = new EmployeeSyncResult { Employee = employee, EmployeeNumber = employee.SerialNumber, EmployeeName = employee.Name, Department = employee.DepartmentName }; var result = new EmployeeSyncResult { Employee = employee, EmployeeNumber = employee.SerialNumber, EmployeeName = employee.Name, Department = employee.DepartmentName };
var userOnDevice = false;
try try
{ {
if (await hikvision.UserExistsAsync(device, employee.SerialNumber, cancellationToken)) var exists = await hikvision.UserExistsAsync(device, employee.SerialNumber, cancellationToken);
if (exists)
{
userOnDevice = true;
result.UserCreated = true;
result.UserStatus = "Already exists"; result.UserStatus = "Already exists";
if (initialEnrollment)
{
var existingFaces = await hikvision.TryGetUserFaceCountAsync(device, employee.SerialNumber, cancellationToken);
if (existingFaces is >= 1)
{
result.FaceStatus = "Already enrolled";
result.VerificationStatus = "Verified";
result.FaceEnrolled = true;
result.OverallResult = "ALREADY_EXISTS";
results.Add(result);
continue;
}
}
}
else else
{ {
var user = await hikvision.CreateUserAsync(device, employee, cancellationToken); var user = await hikvision.CreateUserAsync(device, employee, cancellationToken);
if (!user.Success) { result.UserStatus = "Failed"; result.OverallResult = "FAILED"; result.Reason = user.Reason; results.Add(result); continue; } if (!user.Success)
{
result.UserStatus = "Failed";
result.OverallResult = "FAILED";
result.Reason = user.Reason;
results.Add(result);
continue;
}
userOnDevice = true;
result.UserCreated = true;
result.UserStatus = "Created"; result.UserStatus = "Created";
} }
if (!initialEnrollment) { result.OverallResult = result.UserStatus == "Already exists" ? "ALREADY_EXISTS" : "SUCCESS"; results.Add(result); continue; } if (!initialEnrollment)
{
result.OverallResult = result.UserStatus == "Already exists" ? "ALREADY_EXISTS" : "SUCCESS";
results.Add(result);
continue;
}
progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Downloading employee photo…", results)); progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Downloading employee photo…", results));
var photo = await photos.DownloadAndNormalizeAsync(employee, cancellationToken); var photo = await photos.DownloadAndNormalizeAsync(employee, cancellationToken);
if (!photo.Success) { result.FaceStatus = "Failed"; result.OverallResult = "FAILED"; result.Reason = photo.Reason; results.Add(result); continue; } if (!photo.Success)
{
FaceEnrollmentResults.Apply(result, userOnDevice, false, "Failed", "Not requested", photo.Reason);
results.Add(result);
continue;
}
progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Uploading face…", results)); progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Uploading face…", results));
var face = await hikvision.UploadFaceAsync(device, employee.SerialNumber, photo.Bytes, cancellationToken); var face = await hikvision.UploadFaceAsync(device, employee.SerialNumber, photo.Bytes, cancellationToken);
if (!face.Success) { result.FaceStatus = "Failed"; result.OverallResult = "FAILED"; result.Reason = face.Reason; results.Add(result); continue; } if (!face.Success)
{
FaceEnrollmentResults.Apply(result, userOnDevice, false, "Failed", "Not requested", face.Reason);
results.Add(result);
continue;
}
result.FaceStatus = "Uploaded"; result.FaceStatus = "Uploaded";
progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Verifying face enrollment…", results)); progress?.Report(CreateProgress(index, source.Count, employee.SerialNumber, "Verifying face enrollment…", results));
var verification = await hikvision.VerifyFaceAsync(device, employee.SerialNumber, cancellationToken); var verification = await hikvision.VerifyFaceAsync(device, employee.SerialNumber, cancellationToken);
result.VerificationStatus = verification.Success ? "Verified" : "Failed"; FaceEnrollmentResults.Apply(result, userOnDevice, verification.Success, result.FaceStatus,
result.OverallResult = verification.Success ? "SUCCESS" : "FAILED"; verification.Success ? "Verified" : "Failed", verification.Reason);
result.Reason = verification.Reason;
} }
catch (Exception ex) catch (Exception ex)
{ {
result.OverallResult = "FAILED"; FaceEnrollmentResults.Apply(result, userOnDevice, false, result.FaceStatus, result.VerificationStatus,
result.Reason = ex is InvalidOperationException ? ex.Message : "Device unavailable or operation failed."; ex is InvalidOperationException ? ex.Message : "Device unavailable or operation failed.");
if (!userOnDevice)
result.OverallResult = "FAILED";
} }
AppLogger.TraceExit("USER_SYNC", "ProcessEmployee",
$"employee={employee.SerialNumber} result={result.OverallResult}");
results.Add(result); results.Add(result);
} }
progress?.Report(CreateProgress(source.Count, source.Count, "", "Completed", results)); progress?.Report(CreateProgress(source.Count, source.Count, "", "Completed", results));
AppLogger.TraceExit("USER_SYNC", nameof(RunAsync),
$"success={results.Count(r => r.OverallResult is "SUCCESS" or "ALREADY_EXISTS")} partial={results.Count(r => r.OverallResult == "PARTIAL")} failed={results.Count(r => r.OverallResult == "FAILED")}");
return results; return results;
} }
@ -55,5 +111,39 @@ public sealed class UserSyncEngine(HikvisionIsapiClient hikvision, EmployeePhoto
new(completed, total, employee, operation, new(completed, total, employee, operation,
results.Count(r => r.UserStatus == "Created"), results.Count(r => r.UserStatus == "Already exists"), results.Count(r => r.UserStatus == "Created"), results.Count(r => r.UserStatus == "Already exists"),
results.Count(r => r.FaceStatus == "Uploaded"), results.Count(r => r.FaceStatus == "Failed"), results.Count(r => r.FaceStatus == "Uploaded"), results.Count(r => r.FaceStatus == "Failed"),
results.Count(r => r.OverallResult == "SKIPPED"), results.Count(r => r.OverallResult == "FAILED")); results.Count(r => r.OverallResult == "SKIPPED"), results.Count(r => r.OverallResult is "FAILED" or "PARTIAL"));
}
internal static class FaceEnrollmentResults
{
internal static void Apply(
EmployeeSyncResult result,
bool userOnDevice,
bool faceEnrolled,
string faceStatus,
string verificationStatus,
string? failureReason)
{
result.UserCreated = userOnDevice;
result.FaceEnrolled = faceEnrolled;
result.FaceStatus = faceStatus;
result.VerificationStatus = verificationStatus;
if (faceEnrolled)
{
result.OverallResult = "SUCCESS";
result.Reason = "";
return;
}
if (userOnDevice)
{
result.OverallResult = "PARTIAL";
result.Reason = string.IsNullOrWhiteSpace(failureReason) ? "Face enrollment failed." : failureReason;
return;
}
result.OverallResult = "FAILED";
result.Reason = string.IsNullOrWhiteSpace(failureReason) ? "User creation failed." : failureReason;
}
} }