package com.malk.service.workhours; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class TimeCardChangeApprovalWriteBackServiceTest { @Test void shouldSaveApprovalResultInSharedChangeSummary() { Map approved = billable("P-001", 8d, 6d, 8d, -2d); Map rejected = billable("P-002", 4d, 10d, 4d, 6d); Map detail = summary(approved, rejected); List rows = Arrays.asList( row("P-001", 6d, -2d, true), row("P-002", 10d, 6d, false)); Map update = TimeCardChangeApprovalWriteBackService.buildChangeSummaryUpdate(detail, rows); List actual = (List) update.get("tableField_mmczo634"); assertEquals(6d, actual.get(0).get("numberField_mmczo636")); assertEquals(6d, actual.get(0).get("numberField_mmczo637")); assertEquals(-2d, actual.get(0).get("numberField_mmd80zae")); assertEquals(4d, actual.get(1).get("numberField_mmczo636")); assertEquals(4d, actual.get(1).get("numberField_mmczo637")); assertEquals(0d, actual.get(1).get("numberField_mmd80zae")); assertEquals(10d, update.get("numberField_mmeakgi4")); } @Test void shouldOnlyAccumulateChangeHoursInOriginalSummaryRows() { Map original = billable("P-001", 8d, 8d, 8d, 1d); List rows = Collections.singletonList( row("P-001", 6d, -2d, true)); List merged = TimeCardChangeApprovalWriteBackService.mergeDeltaRows( Collections.singletonList(original), rows, TimeCardChangeApprovalWriteBackService.BILLABLE); assertEquals(8d, merged.get(0).get("numberField_mmczo636")); assertEquals(8d, merged.get(0).get("numberField_mmczo637")); assertEquals(-1d, merged.get(0).get("numberField_mmd80zae")); } @Test void shouldGroupNonBillableDeltasByProjectWithoutChangingApprovedHours() { Map original = nonBillable("P-100", 4d, 4d, 0.5d); List rows = Arrays.asList( row(TimeCardChangeApprovalWriteBackService.NON_BILLABLE, "P-100", 3d, -1d, true), row(TimeCardChangeApprovalWriteBackService.NON_BILLABLE, "P-100", 7d, 3d, true), row(TimeCardChangeApprovalWriteBackService.NON_BILLABLE, "P-100", 8d, 4d, false)); List merged = TimeCardChangeApprovalWriteBackService.mergeDeltaRows( Collections.singletonList(original), rows, TimeCardChangeApprovalWriteBackService.NON_BILLABLE); assertEquals(1, merged.size()); assertEquals("P-100", merged.get(0).get("textField_mmczo639")); assertEquals(4d, merged.get(0).get("numberField_mmczo63c")); assertEquals(4d, merged.get(0).get("numberField_mmczo63d")); assertEquals(2.5d, merged.get(0).get("numberField_mmeakgi9")); } @Test void shouldGroupOtherDeltasByActivityWithoutChangingApprovedHours() { Map original = other("Annual Leave", 8d, 8d, -1d); List rows = Arrays.asList( row(TimeCardChangeApprovalWriteBackService.OTHER, "Annual Leave", 10d, 2d, true), row(TimeCardChangeApprovalWriteBackService.OTHER, "Annual Leave", 7.5d, -0.5d, true), row(TimeCardChangeApprovalWriteBackService.OTHER, "Annual Leave", 12d, 4d, false)); List merged = TimeCardChangeApprovalWriteBackService.mergeDeltaRows( Collections.singletonList(original), rows, TimeCardChangeApprovalWriteBackService.OTHER); assertEquals(1, merged.size()); assertEquals("Annual Leave", merged.get(0).get("selectField_mmeakgie")); assertEquals(8d, merged.get(0).get("numberField_mmeakgij")); assertEquals(8d, merged.get(0).get("numberField_mmeakgii")); assertEquals(0.5d, merged.get(0).get("numberField_mmeakgim")); } @Test void shouldMoveNegativeDeltaFromTargetDayToSubmitDay() { Map targetDeltas = new LinkedHashMap<>(); targetDeltas.put("20260818", -2d); Map adjustments = TimeCardChangeApprovalWriteBackService.requiredAdjustments( targetDeltas, "20261007"); assertEquals(2d, adjustments.get("20260818")); assertEquals(-2d, adjustments.get("20261007")); } @Test void shouldLeaveRequiredHoursUnchangedForSameDayNetZeroRedistribution() { Map targetDeltas = new LinkedHashMap<>(); targetDeltas.put("20260818", 0d); Map adjustments = TimeCardChangeApprovalWriteBackService.requiredAdjustments( targetDeltas, "20261007"); assertTrue(adjustments.isEmpty()); } @Test void shouldKeepSignedLineDeltasForSubmissionDaySummary() { List rows = Arrays.asList( row("P-001", 0d, -8d, true), row("P-002", 8d, 8d, true)); List changes = TimeCardChangeApprovalWriteBackService.deltaRows( rows, TimeCardChangeApprovalWriteBackService.BILLABLE); assertEquals(2, changes.size()); assertEquals(-8d, changes.get(0).get("numberField_mmd80zae")); assertEquals(8d, changes.get(1).get("numberField_mmd80zae")); } @Test void shouldNotRepeatBusinessWriteWhenCheckpointRetryIsNeeded() { String progressKey = "approval-retry-1"; int[] actionCount = {0}; int[] checkpointCount = {0}; TimeCardChangeApprovalWriteBackService.clearLocalProgress(progressKey); try { assertThrows(IllegalStateException.class, () -> TimeCardChangeApprovalWriteBackService.runIdempotentStep( progressKey, 0, 0, () -> actionCount[0]++, completed -> { checkpointCount[0]++; throw new IllegalStateException("temporary checkpoint failure"); })); int completed = TimeCardChangeApprovalWriteBackService.runIdempotentStep( progressKey, 0, 0, () -> actionCount[0]++, completedSteps -> checkpointCount[0]++); assertEquals(1, actionCount[0]); assertEquals(2, checkpointCount[0]); assertEquals(1, completed); } finally { TimeCardChangeApprovalWriteBackService.clearLocalProgress(progressKey); } } @Test void shouldSerializeChangeWriteBackForTheSameUser() { assertSame(TimeCardChangeApprovalWriteBackService.writeBackLock("user-1"), TimeCardChangeApprovalWriteBackService.writeBackLock("user-1")); } private static TimeCardChangeApprovalWriteBackService.ChangeRow row( String key, double target, double delta, boolean approved) { return row(TimeCardChangeApprovalWriteBackService.BILLABLE, key, target, delta, approved); } private static TimeCardChangeApprovalWriteBackService.ChangeRow row( TimeCardChangeApprovalWriteBackService.TableDef table, String key, double target, double delta, boolean approved) { return new TimeCardChangeApprovalWriteBackService.ChangeRow( table, "user-1", "20260818", key, "Project " + key, target, delta, approved); } private static Map billable(String key, double draft, double submitted, double approved, double change) { Map row = new HashMap<>(); row.put("textField_mmacxew3", key); row.put("numberField_mmczo635", draft); row.put("numberField_mmczo636", submitted); row.put("numberField_mmczo637", approved); row.put("numberField_mmd80zae", change); return row; } private static Map nonBillable(String key, double submitted, double approved, double change) { Map row = new HashMap<>(); row.put("textField_mmczo639", key); row.put("numberField_mmczo63c", submitted); row.put("numberField_mmczo63d", approved); row.put("numberField_mmeakgi9", change); return row; } private static Map other(String activity, double submitted, double approved, double change) { Map row = new HashMap<>(); row.put("selectField_mmeakgie", activity); row.put("numberField_mmeakgij", submitted); row.put("numberField_mmeakgii", approved); row.put("numberField_mmeakgim", change); return row; } private static Map summary(Map... billableRows) { Map detail = new HashMap<>(); detail.put("tableField_mmczo634", Arrays.asList(billableRows)); detail.put("tableField_mmczo63h", Collections.emptyList()); detail.put("tableField_mmeakgid", Collections.emptyList()); return detail; } }