diff --git a/DbToDevicePlanning.cs b/DbToDevicePlanning.cs new file mode 100644 index 0000000..a4589f5 --- /dev/null +++ b/DbToDevicePlanning.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace HanvonF710XAttendanceService +{ + /// + /// Pure planning logic for DB_TO_DEVICE jobs (testable without device/DB I/O). + /// + internal static class DbToDevicePlanning + { + public const int TemplateLoadBatchSize = 50; + + public static bool ShouldIgnoreSourceDevice(TemplateTransferMode mode) + { + return mode == TemplateTransferMode.DB_TO_DEVICE; + } + + public static List NormalizeEmployeeIds(IEnumerable employeeIds) + { + if (employeeIds == null) + { + return new List(); + } + + return employeeIds + .Where(id => !string.IsNullOrWhiteSpace(id)) + .Select(id => id.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + /// + /// Resolves which employee serial numbers should be synced from HRMS DB to target devices. + /// + public static List ResolveEmployeeIds( + TemplateTransferJob job, + Func, List> loadByDepartmentIds, + Func> loadRegisteredOnTargets) + { + if (job == null) + { + return new List(); + } + + if (job.EmpIds != null && job.EmpIds.Count > 0) + { + return NormalizeEmployeeIds(job.EmpIds); + } + + if (job.DepartmentIds != null && job.DepartmentIds.Count > 0) + { + if (loadByDepartmentIds != null) + { + var fromDepartments = loadByDepartmentIds(job.DepartmentIds); + if (fromDepartments != null && fromDepartments.Count > 0) + { + return NormalizeEmployeeIds(fromDepartments); + } + } + } + + return NormalizeEmployeeIds(loadRegisteredOnTargets != null ? loadRegisteredOnTargets() : null); + } + + public static int ComputeTemplateLoadBatchCount(int employeeCount) + { + if (employeeCount <= 0) + { + return 0; + } + + return (employeeCount + TemplateLoadBatchSize - 1) / TemplateLoadBatchSize; + } + + public static int ComputeLastTemplateLoadBatchSize(int employeeCount) + { + if (employeeCount <= 0) + { + return 0; + } + + int remainder = employeeCount % TemplateLoadBatchSize; + return remainder == 0 ? TemplateLoadBatchSize : remainder; + } + } +} + diff --git a/DbToDevicePlanningTests.cs b/DbToDevicePlanningTests.cs new file mode 100644 index 0000000..2325e1f --- /dev/null +++ b/DbToDevicePlanningTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; + +namespace HanvonF710XAttendanceService +{ + /// + /// Lightweight unit tests for DB_TO_DEVICE planning logic (no MSTest dependency). + /// + internal static class DbToDevicePlanningTests + { + public static int RunAll() + { + int failed = 0; + failed += Run("1 DB_TO_DEVICE ignores source device", TestDbToDeviceIgnoresSource); + failed += Run("2 DEVICE_TO_DB requires source device", TestDeviceToDbRequiresSource); + failed += Run("3 DB_TO_DEVICE allows empty SourceIp planning", TestDbToDeviceAllowsEmptySourceIp); + failed += Run("4 EmpIds only selection", TestEmpIdsFilter); + failed += Run("5 DepartmentIds selection", TestDepartmentIdsFallback); + failed += Run("6 empty EmpIds uses registered employees", TestEmptyEmpIdsUsesRegistered); + failed += Run("7 normalize employee ids", TestNormalizeEmployeeIds); + failed += Run("8 DEVICE_TO_DEVICE requires source device", TestDeviceToDeviceRequiresSource); + failed += Run("9 template load batches for 673 employees", TestTemplateLoadBatchCount673); + return failed; + } + + private static int Run(string name, Func test) + { + try + { + if (!test()) + { + Console.WriteLine("FAIL: " + name); + return 1; + } + Console.WriteLine("PASS: " + name); + return 0; + } + catch (Exception ex) + { + Console.WriteLine("FAIL: " + name + " err=" + ex.Message); + return 1; + } + } + + private static bool TestDbToDeviceIgnoresSource() + { + return DbToDevicePlanning.ShouldIgnoreSourceDevice(TemplateTransferMode.DB_TO_DEVICE); + } + + private static bool TestDeviceToDbRequiresSource() + { + return !DbToDevicePlanning.ShouldIgnoreSourceDevice(TemplateTransferMode.DEVICE_TO_DB); + } + + private static bool TestDeviceToDeviceRequiresSource() + { + return !DbToDevicePlanning.ShouldIgnoreSourceDevice(TemplateTransferMode.DEVICE_TO_DEVICE); + } + + private static bool TestDbToDeviceAllowsEmptySourceIp() + { + var job = new TemplateTransferJob("1", true, "", new List { "192.168.90.223" }, + new List { "5" }, null); + var ids = DbToDevicePlanning.ResolveEmployeeIds(job, _ => new List(), () => new List { "5" }); + return ids.Count == 1 && ids[0] == "5"; + } + + private static bool TestOtherModesRequireSource() + { + return TestDeviceToDbRequiresSource() && TestDeviceToDeviceRequiresSource(); + } + + private static bool TestEmpIdsFilter() + { + var job = new TemplateTransferJob("1", true, "10.0.1.3", new List { "192.168.90.223" }, + new List { "5", "55", "96" }, null); + + var ids = DbToDevicePlanning.ResolveEmployeeIds( + job, + _ => new List { "999" }, + () => new List { "999" }); + + return ids.Count == 3 + && ids.Contains("5") + && ids.Contains("55") + && ids.Contains("96"); + } + + private static bool TestDepartmentIdsFallback() + { + var job = new TemplateTransferJob("1", true, "", new List { "192.168.90.223" }, + null, new List { "10", "20" }); + + bool deptCalled = false; + var ids = DbToDevicePlanning.ResolveEmployeeIds( + job, + departmentIds => + { + deptCalled = true; + return new List { "100", "200" }; + }, + () => new List { "300" }); + + return deptCalled && ids.Count == 2 && ids.Contains("100") && ids.Contains("200"); + } + + private static bool TestEmptyEmpIdsUsesRegistered() + { + var job = new TemplateTransferJob("1", true, "", new List { "192.168.90.223" }, + null, null); + + var ids = DbToDevicePlanning.ResolveEmployeeIds( + job, + _ => new List(), + () => new List { "5", "55" }); + + return ids.Count == 2 && ids.Contains("5") && ids.Contains("55"); + } + + private static bool TestNormalizeEmployeeIds() + { + var ids = DbToDevicePlanning.NormalizeEmployeeIds(new[] { " 5 ", "5", "", null, "96" }); + return ids.Count == 2 && ids[0] == "5" && ids[1] == "96"; + } + + private static bool TestTemplateLoadBatchCount673() + { + return DbToDevicePlanning.ComputeTemplateLoadBatchCount(673) == 14 + && DbToDevicePlanning.ComputeLastTemplateLoadBatchSize(673) == 23; + } + } +} diff --git a/DeviceSettingsConfig.cs b/DeviceSettingsConfig.cs index 7edfbb4..f2cdafe 100644 --- a/DeviceSettingsConfig.cs +++ b/DeviceSettingsConfig.cs @@ -97,7 +97,7 @@ namespace HanvonF710XAttendanceService var enabled = TryParseBool((string)jobEl.Attribute("Enabled"), out var jEnabled) && jEnabled; var sourceIp = (string)jobEl.Attribute("SourceIp") ?? (string)jobEl.Attribute("SourceIP") ?? ""; - if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(sourceIp)) + if (string.IsNullOrWhiteSpace(id)) { continue; } @@ -124,7 +124,20 @@ namespace HanvonF710XAttendanceService .ToList(); } - ttConfig.Jobs.Add(new TemplateTransferJob(id.Trim(), enabled, sourceIp.Trim(), targets, empIds)); + var departmentIdsAttr = (string)jobEl.Attribute("DepartmentIds") + ?? (string)jobEl.Attribute("DepartmentIDs") + ?? (string)jobEl.Attribute("DeptIds"); + List departmentIds = null; + if (!string.IsNullOrWhiteSpace(departmentIdsAttr)) + { + departmentIds = departmentIdsAttr + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(v => v.Trim()) + .Where(v => v.Length > 0) + .ToList(); + } + + ttConfig.Jobs.Add(new TemplateTransferJob(id.Trim(), enabled, sourceIp.Trim(), targets, empIds, departmentIds)); } parsed.TemplateTransfer = ttConfig; @@ -188,13 +201,14 @@ namespace HanvonF710XAttendanceService internal sealed class TemplateTransferJob { - public TemplateTransferJob(string id, bool enabled, string sourceIp, List targetIps, List empIds) + public TemplateTransferJob(string id, bool enabled, string sourceIp, List targetIps, List empIds, List departmentIds = null) { Id = id ?? ""; Enabled = enabled; SourceIp = sourceIp ?? ""; TargetIps = targetIps ?? new List(); EmpIds = empIds ?? new List(); + DepartmentIds = departmentIds ?? new List(); } public string Id { get; } @@ -202,6 +216,7 @@ namespace HanvonF710XAttendanceService public string SourceIp { get; } public List TargetIps { get; } public List EmpIds { get; } + public List DepartmentIds { get; } } }