feat(services): implement user create/delete with PARTIAL face results

User create/delete with per-employee batch results, correct SUCCESS/FAILED rows, and PARTIAL when the user is created but face enrollment fails.
main
SYED MUSTUFA AHMED NAQVI 2026-08-27 10:00:42 +05:00
parent c4bd561811
commit 01ef651965
1 changed files with 153 additions and 24 deletions

View File

@ -6,6 +6,7 @@ public sealed class UserManagementService(
HikvisionIsapiClient hikvision,
EmployeePhotoService photos,
AttendanceMachineUserRepository machineUsers,
HrmsEmployeeService hrms,
OperationHistoryService history)
{
public static EmployeeSyncResult ValidateEmployee(HrmsEmployee employee, long siteId)
@ -28,8 +29,15 @@ public sealed class UserManagementService(
public async Task<EmployeeSyncResult> CreateUserWithFaceAsync(Device device, HrmsEmployee employee, long siteId,
IProgress<string>? progress, CancellationToken cancellationToken)
{
AppLogger.TraceEnter("USER_CREATE", nameof(CreateUserWithFaceAsync),
$"device={device.IpAddress} employee={employee.SerialNumber} siteId={siteId}");
var validation = ValidateEmployee(employee, siteId);
if (validation.OverallResult == "SKIPPED") return validation;
if (validation.OverallResult == "SKIPPED")
{
AppLogger.TraceExit("USER_CREATE", nameof(CreateUserWithFaceAsync),
$"employee={employee.SerialNumber} result=SKIPPED reason={validation.Reason}");
return validation;
}
var entry = new SyncHistoryEntry
{
@ -49,69 +57,118 @@ public sealed class UserManagementService(
Department = employee.DepartmentName
};
var userOnDevice = false;
try
{
progress?.Report("Checking if user already exists…");
if (await hikvision.UserExistsAsync(device, employee.SerialNumber, cancellationToken))
AppLogger.TraceEnter("USER_CREATE", "HikvisionIsapiClient.UserExistsAsync", $"employee={employee.SerialNumber}");
var userExists = await hikvision.UserExistsAsync(device, employee.SerialNumber, cancellationToken);
AppLogger.TraceExit("USER_CREATE", "HikvisionIsapiClient.UserExistsAsync",
$"employee={employee.SerialNumber} exists={userExists}");
if (userExists)
{
userOnDevice = true;
result.UserStatus = "Already exists";
result.OverallResult = "ALREADY_EXISTS";
result.UserCreated = true;
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";
result.Reason = "User and face already exist on device.";
await machineUsers.UpsertAsync(device.MachineId, employee.SerialNumber, employee.Name, cancellationToken);
await CompleteHistory(entry, result, cancellationToken);
AppLogger.TraceExit("USER_CREATE", nameof(CreateUserWithFaceAsync),
$"employee={employee.SerialNumber} result=ALREADY_EXISTS numOfFace={existingFaces}");
return result;
}
}
else
{
progress?.Report("Creating Hikvision UserInfo…");
AppLogger.TraceEnter("USER_CREATE", "HikvisionIsapiClient.CreateUserAsync", $"employee={employee.SerialNumber}");
var create = await hikvision.CreateUserAsync(device, employee, cancellationToken);
AppLogger.TraceExit("USER_CREATE", "HikvisionIsapiClient.CreateUserAsync",
$"employee={employee.SerialNumber} success={create.Success}");
if (!create.Success)
{
result.UserStatus = "Failed";
result.UserCreated = false;
result.OverallResult = "FAILED";
result.Reason = create.Reason;
await CompleteHistory(entry, result, cancellationToken);
AppLogger.TraceExit("USER_CREATE", nameof(CreateUserWithFaceAsync),
$"employee={employee.SerialNumber} result=FAILED reason={result.Reason}");
return result;
}
userOnDevice = true;
result.UserCreated = true;
result.UserStatus = "Created";
}
progress?.Report("Downloading employee photo…");
AppLogger.TraceEnter("USER_CREATE", "EmployeePhotoService.DownloadAndNormalizeAsync", $"employee={employee.SerialNumber}");
var photo = await photos.DownloadAndNormalizeAsync(employee, cancellationToken);
AppLogger.TraceExit("USER_CREATE", "EmployeePhotoService.DownloadAndNormalizeAsync",
$"employee={employee.SerialNumber} success={photo.Success}");
if (!photo.Success)
{
result.FaceStatus = "Failed";
result.OverallResult = "FAILED";
result.Reason = photo.Reason;
ApplyEnrollmentOutcome(result, userOnDevice, faceEnrolled: false, faceStatus: "Failed", verificationStatus: "Not requested",
failureReason: photo.Reason);
await CompleteHistory(entry, result, cancellationToken);
AppLogger.TraceExit("USER_CREATE", nameof(CreateUserWithFaceAsync),
$"employee={employee.SerialNumber} result={result.OverallResult} reason={result.Reason}");
return result;
}
progress?.Report("Uploading face…");
AppLogger.TraceEnter("USER_CREATE", "HikvisionIsapiClient.UploadFaceAsync", $"employee={employee.SerialNumber}");
var face = await hikvision.UploadFaceAsync(device, employee.SerialNumber, photo.Bytes, cancellationToken);
AppLogger.TraceExit("USER_CREATE", "HikvisionIsapiClient.UploadFaceAsync",
$"employee={employee.SerialNumber} success={face.Success}");
if (!face.Success)
{
result.FaceStatus = "Failed";
result.OverallResult = "FAILED";
result.Reason = face.Reason;
ApplyEnrollmentOutcome(result, userOnDevice, faceEnrolled: false, faceStatus: "Failed", verificationStatus: "Not requested",
failureReason: face.Reason);
await CompleteHistory(entry, result, cancellationToken);
AppLogger.TraceExit("USER_CREATE", nameof(CreateUserWithFaceAsync),
$"employee={employee.SerialNumber} result={result.OverallResult} reason={result.Reason}");
return result;
}
result.FaceStatus = "Uploaded";
progress?.Report("Verifying face enrollment…");
AppLogger.TraceEnter("USER_CREATE", "HikvisionIsapiClient.VerifyFaceAsync", $"employee={employee.SerialNumber}");
var verify = await hikvision.VerifyFaceAsync(device, employee.SerialNumber, cancellationToken);
result.VerificationStatus = verify.Success ? "Verified" : "Failed";
result.OverallResult = verify.Success ? "SUCCESS" : "FAILED";
result.Reason = verify.Reason;
AppLogger.TraceExit("USER_CREATE", "HikvisionIsapiClient.VerifyFaceAsync",
$"employee={employee.SerialNumber} success={verify.Success}");
ApplyEnrollmentOutcome(result, userOnDevice, verify.Success, faceStatus: result.FaceStatus,
verificationStatus: verify.Success ? "Verified" : "Failed", failureReason: verify.Reason);
if (verify.Success)
await machineUsers.UpsertAsync(device.MachineId, employee.SerialNumber, employee.Name, cancellationToken);
await CompleteHistory(entry, result, cancellationToken);
AppLogger.TraceExit("USER_CREATE", nameof(CreateUserWithFaceAsync),
$"employee={employee.SerialNumber} result={result.OverallResult} faceEnrolled={result.FaceEnrolled}");
return result;
}
catch (Exception ex)
{
result.OverallResult = "FAILED";
result.Reason = ex is InvalidOperationException ? ex.Message : "Device unavailable or operation failed.";
ApplyEnrollmentOutcome(result, userOnDevice, faceEnrolled: false, faceStatus: result.FaceStatus,
verificationStatus: result.VerificationStatus,
failureReason: ex is InvalidOperationException ? ex.Message : "Device unavailable or operation failed.");
if (!userOnDevice)
result.OverallResult = "FAILED";
await CompleteHistory(entry, result, cancellationToken);
AppLogger.TraceExit("USER_CREATE", nameof(CreateUserWithFaceAsync),
$"employee={employee.SerialNumber} result={result.OverallResult} reason={result.Reason}");
return result;
}
}
@ -120,6 +177,8 @@ public sealed class UserManagementService(
IProgress<string>? progress, CancellationToken cancellationToken)
{
var normalized = employeeNumbers.Where(e => !string.IsNullOrWhiteSpace(e)).Select(e => e.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
AppLogger.TraceEnter("USER_DELETE", nameof(DeleteUsersAsync),
$"device={device.IpAddress} count={normalized.Count} employees={string.Join(",", normalized)}");
var entry = new SyncHistoryEntry
{
Operation = SyncOperationKind.UserDeletion,
@ -140,27 +199,43 @@ public sealed class UserManagementService(
}
progress?.Report("Deleting users on device…");
var batchResult = await hikvision.DeleteUsersAsync(device, normalized, cancellationToken);
foreach (var employeeNo in normalized)
var employeeNames = await LoadEmployeeNamesAsync(device.MachineId, normalized, cancellationToken);
AppLogger.TraceEnter("USER_DELETE", "HikvisionIsapiClient.DeleteUsersAsync", $"device={device.IpAddress} count={normalized.Count}");
var deleteResults = await hikvision.DeleteUsersAsync(device, normalized, cancellationToken);
AppLogger.TraceExit("USER_DELETE", "HikvisionIsapiClient.DeleteUsersAsync",
$"success={deleteResults.Count(r => r.Result.Success)} failed={deleteResults.Count(r => !r.Result.Success)}");
foreach (var deleteResult in deleteResults)
{
if (batchResult.Success)
if (deleteResult.Result.Success)
{
await machineUsers.MarkDeletedAsync(device.MachineId, employeeNo, cancellationToken);
AppLogger.Info($"[USER_DELETE] employee={deleteResult.EmployeeNumber} result=SUCCESS");
await machineUsers.MarkDeletedAsync(device.MachineId, deleteResult.EmployeeNumber, cancellationToken);
results.Add(new EmployeeSyncResult
{
EmployeeNumber = employeeNo,
EmployeeNumber = deleteResult.EmployeeNumber,
EmployeeName = GetEmployeeName(employeeNames, deleteResult.EmployeeNumber),
UserStatus = "Deleted",
OverallResult = "SUCCESS"
FaceStatus = "Not applicable",
VerificationStatus = "Not requested",
OverallResult = "SUCCESS",
Reason = "User deleted successfully"
});
}
else
{
AppLogger.Warning($"[USER_DELETE] employee={deleteResult.EmployeeNumber} result=FAILED reason={deleteResult.Result.Reason}");
results.Add(new EmployeeSyncResult
{
EmployeeNumber = employeeNo,
EmployeeNumber = deleteResult.EmployeeNumber,
EmployeeName = GetEmployeeName(employeeNames, deleteResult.EmployeeNumber),
UserStatus = "Failed",
FaceStatus = "Not applicable",
VerificationStatus = "Not requested",
OverallResult = "FAILED",
Reason = batchResult.Reason
Reason = string.IsNullOrWhiteSpace(deleteResult.Result.Reason)
? "Device rejected the delete request."
: deleteResult.Result.Reason
});
}
}
@ -170,16 +245,70 @@ public sealed class UserManagementService(
entry.Failed = results.Count(r => r.OverallResult == "FAILED");
entry.Status = entry.Failed > 0 ? "Completed with errors" : "Completed";
await history.UpdateAsync(entry, cancellationToken);
AppLogger.TraceExit("USER_DELETE", nameof(DeleteUsersAsync),
$"success={entry.Success} failed={entry.Failed}");
return results;
}
private async Task<Dictionary<string, string>> LoadEmployeeNamesAsync(string machineId, IReadOnlyList<string> employeeNumbers, CancellationToken cancellationToken)
{
var names = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
try
{
var machineRows = await machineUsers.GetActiveByMachineAsync(machineId, cancellationToken);
foreach (var row in machineRows)
{
if (employeeNumbers.Contains(row.SerialNumber, StringComparer.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(row.EmployeeName))
names[row.SerialNumber] = row.EmployeeName;
}
}
catch (Exception ex)
{
AppLogger.Warning("User delete: unable to load local machine-user names. " + ex.Message);
}
foreach (var employeeNumber in employeeNumbers.Where(employeeNumber => !names.ContainsKey(employeeNumber)))
{
try
{
var employee = await hrms.FindBySerialNumberAsync(employeeNumber, cancellationToken);
if (employee is not null && !string.IsNullOrWhiteSpace(employee.Name))
names[employeeNumber] = employee.Name;
}
catch (Exception ex)
{
AppLogger.Warning($"User delete: unable to load HRMS employee name for {employeeNumber}. {ex.Message}");
}
}
return names;
}
private static string GetEmployeeName(IReadOnlyDictionary<string, string> employeeNames, string employeeNumber) =>
employeeNames.TryGetValue(employeeNumber, out var employeeName) ? employeeName : "";
private static void ApplyEnrollmentOutcome(
EmployeeSyncResult result,
bool userOnDevice,
bool faceEnrolled,
string faceStatus,
string verificationStatus,
string? failureReason) =>
FaceEnrollmentResults.Apply(result, userOnDevice, faceEnrolled, faceStatus, verificationStatus, failureReason);
private async Task CompleteHistory(SyncHistoryEntry entry, EmployeeSyncResult result, CancellationToken cancellationToken)
{
entry.CompletedAt = DateTime.Now;
entry.Success = result.OverallResult is "SUCCESS" or "ALREADY_EXISTS" ? 1 : 0;
entry.Failed = result.OverallResult == "FAILED" ? 1 : 0;
entry.Failed = result.OverallResult is "FAILED" or "PARTIAL" ? 1 : 0;
entry.Skipped = result.OverallResult == "SKIPPED" ? 1 : 0;
entry.Status = result.OverallResult == "FAILED" ? "Failed" : "Completed";
entry.Status = result.OverallResult switch
{
"FAILED" => "Failed",
"PARTIAL" => "Completed with errors",
_ => "Completed"
};
await history.UpdateAsync(entry, cancellationToken);
}
}