TimeCardChangeApprovalWriteBackServiceTest.java 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. package com.malk.service.workhours;
  2. import org.junit.jupiter.api.Test;
  3. import java.util.Arrays;
  4. import java.util.Collections;
  5. import java.util.HashMap;
  6. import java.util.LinkedHashMap;
  7. import java.util.List;
  8. import java.util.Map;
  9. import static org.junit.jupiter.api.Assertions.assertEquals;
  10. import static org.junit.jupiter.api.Assertions.assertSame;
  11. import static org.junit.jupiter.api.Assertions.assertThrows;
  12. import static org.junit.jupiter.api.Assertions.assertTrue;
  13. class TimeCardChangeApprovalWriteBackServiceTest {
  14. @Test
  15. void shouldSaveApprovalResultInSharedChangeSummary() {
  16. Map<String, Object> approved = billable("P-001", 8d, 6d, 8d, -2d);
  17. Map<String, Object> rejected = billable("P-002", 4d, 10d, 4d, 6d);
  18. Map<String, Object> detail = summary(approved, rejected);
  19. List<TimeCardChangeApprovalWriteBackService.ChangeRow> rows = Arrays.asList(
  20. row("P-001", 6d, -2d, true),
  21. row("P-002", 10d, 6d, false));
  22. Map<String, Object> update =
  23. TimeCardChangeApprovalWriteBackService.buildChangeSummaryUpdate(detail, rows);
  24. List<Map> actual = (List<Map>) update.get("tableField_mmczo634");
  25. assertEquals(6d, actual.get(0).get("numberField_mmczo636"));
  26. assertEquals(6d, actual.get(0).get("numberField_mmczo637"));
  27. assertEquals(-2d, actual.get(0).get("numberField_mmd80zae"));
  28. assertEquals(4d, actual.get(1).get("numberField_mmczo636"));
  29. assertEquals(4d, actual.get(1).get("numberField_mmczo637"));
  30. assertEquals(0d, actual.get(1).get("numberField_mmd80zae"));
  31. assertEquals(10d, update.get("numberField_mmeakgi4"));
  32. }
  33. @Test
  34. void shouldOnlyAccumulateChangeHoursInOriginalSummaryRows() {
  35. Map<String, Object> original = billable("P-001", 8d, 8d, 8d, 1d);
  36. List<TimeCardChangeApprovalWriteBackService.ChangeRow> rows = Collections.singletonList(
  37. row("P-001", 6d, -2d, true));
  38. List<Map> merged = TimeCardChangeApprovalWriteBackService.mergeDeltaRows(
  39. Collections.singletonList(original), rows,
  40. TimeCardChangeApprovalWriteBackService.BILLABLE);
  41. assertEquals(8d, merged.get(0).get("numberField_mmczo636"));
  42. assertEquals(8d, merged.get(0).get("numberField_mmczo637"));
  43. assertEquals(-1d, merged.get(0).get("numberField_mmd80zae"));
  44. }
  45. @Test
  46. void shouldGroupNonBillableDeltasByProjectWithoutChangingApprovedHours() {
  47. Map<String, Object> original = nonBillable("P-100", 4d, 4d, 0.5d);
  48. List<TimeCardChangeApprovalWriteBackService.ChangeRow> rows = Arrays.asList(
  49. row(TimeCardChangeApprovalWriteBackService.NON_BILLABLE,
  50. "P-100", 3d, -1d, true),
  51. row(TimeCardChangeApprovalWriteBackService.NON_BILLABLE,
  52. "P-100", 7d, 3d, true),
  53. row(TimeCardChangeApprovalWriteBackService.NON_BILLABLE,
  54. "P-100", 8d, 4d, false));
  55. List<Map> merged = TimeCardChangeApprovalWriteBackService.mergeDeltaRows(
  56. Collections.singletonList(original), rows,
  57. TimeCardChangeApprovalWriteBackService.NON_BILLABLE);
  58. assertEquals(1, merged.size());
  59. assertEquals("P-100", merged.get(0).get("textField_mmczo639"));
  60. assertEquals(4d, merged.get(0).get("numberField_mmczo63c"));
  61. assertEquals(4d, merged.get(0).get("numberField_mmczo63d"));
  62. assertEquals(2.5d, merged.get(0).get("numberField_mmeakgi9"));
  63. }
  64. @Test
  65. void shouldGroupOtherDeltasByActivityWithoutChangingApprovedHours() {
  66. Map<String, Object> original = other("Annual Leave", 8d, 8d, -1d);
  67. List<TimeCardChangeApprovalWriteBackService.ChangeRow> rows = Arrays.asList(
  68. row(TimeCardChangeApprovalWriteBackService.OTHER,
  69. "Annual Leave", 10d, 2d, true),
  70. row(TimeCardChangeApprovalWriteBackService.OTHER,
  71. "Annual Leave", 7.5d, -0.5d, true),
  72. row(TimeCardChangeApprovalWriteBackService.OTHER,
  73. "Annual Leave", 12d, 4d, false));
  74. List<Map> merged = TimeCardChangeApprovalWriteBackService.mergeDeltaRows(
  75. Collections.singletonList(original), rows,
  76. TimeCardChangeApprovalWriteBackService.OTHER);
  77. assertEquals(1, merged.size());
  78. assertEquals("Annual Leave", merged.get(0).get("selectField_mmeakgie"));
  79. assertEquals(8d, merged.get(0).get("numberField_mmeakgij"));
  80. assertEquals(8d, merged.get(0).get("numberField_mmeakgii"));
  81. assertEquals(0.5d, merged.get(0).get("numberField_mmeakgim"));
  82. }
  83. @Test
  84. void shouldMoveNegativeDeltaFromTargetDayToSubmitDay() {
  85. Map<String, Double> targetDeltas = new LinkedHashMap<>();
  86. targetDeltas.put("20260818", -2d);
  87. Map<String, Double> adjustments =
  88. TimeCardChangeApprovalWriteBackService.requiredAdjustments(
  89. targetDeltas, "20261007");
  90. assertEquals(2d, adjustments.get("20260818"));
  91. assertEquals(-2d, adjustments.get("20261007"));
  92. }
  93. @Test
  94. void shouldLeaveRequiredHoursUnchangedForSameDayNetZeroRedistribution() {
  95. Map<String, Double> targetDeltas = new LinkedHashMap<>();
  96. targetDeltas.put("20260818", 0d);
  97. Map<String, Double> adjustments =
  98. TimeCardChangeApprovalWriteBackService.requiredAdjustments(
  99. targetDeltas, "20261007");
  100. assertTrue(adjustments.isEmpty());
  101. }
  102. @Test
  103. void shouldKeepSignedLineDeltasForSubmissionDaySummary() {
  104. List<TimeCardChangeApprovalWriteBackService.ChangeRow> rows = Arrays.asList(
  105. row("P-001", 0d, -8d, true),
  106. row("P-002", 8d, 8d, true));
  107. List<Map> changes = TimeCardChangeApprovalWriteBackService.deltaRows(
  108. rows, TimeCardChangeApprovalWriteBackService.BILLABLE);
  109. assertEquals(2, changes.size());
  110. assertEquals(-8d, changes.get(0).get("numberField_mmd80zae"));
  111. assertEquals(8d, changes.get(1).get("numberField_mmd80zae"));
  112. }
  113. @Test
  114. void shouldNotRepeatBusinessWriteWhenCheckpointRetryIsNeeded() {
  115. String progressKey = "approval-retry-1";
  116. int[] actionCount = {0};
  117. int[] checkpointCount = {0};
  118. TimeCardChangeApprovalWriteBackService.clearLocalProgress(progressKey);
  119. try {
  120. assertThrows(IllegalStateException.class,
  121. () -> TimeCardChangeApprovalWriteBackService.runIdempotentStep(
  122. progressKey, 0, 0, () -> actionCount[0]++, completed -> {
  123. checkpointCount[0]++;
  124. throw new IllegalStateException("temporary checkpoint failure");
  125. }));
  126. int completed = TimeCardChangeApprovalWriteBackService.runIdempotentStep(
  127. progressKey, 0, 0, () -> actionCount[0]++, completedSteps -> checkpointCount[0]++);
  128. assertEquals(1, actionCount[0]);
  129. assertEquals(2, checkpointCount[0]);
  130. assertEquals(1, completed);
  131. } finally {
  132. TimeCardChangeApprovalWriteBackService.clearLocalProgress(progressKey);
  133. }
  134. }
  135. @Test
  136. void shouldSerializeChangeWriteBackForTheSameUser() {
  137. assertSame(TimeCardChangeApprovalWriteBackService.writeBackLock("user-1"),
  138. TimeCardChangeApprovalWriteBackService.writeBackLock("user-1"));
  139. }
  140. private static TimeCardChangeApprovalWriteBackService.ChangeRow row(
  141. String key, double target, double delta, boolean approved) {
  142. return row(TimeCardChangeApprovalWriteBackService.BILLABLE,
  143. key, target, delta, approved);
  144. }
  145. private static TimeCardChangeApprovalWriteBackService.ChangeRow row(
  146. TimeCardChangeApprovalWriteBackService.TableDef table, String key,
  147. double target, double delta, boolean approved) {
  148. return new TimeCardChangeApprovalWriteBackService.ChangeRow(
  149. table,
  150. "user-1", "20260818", key, "Project " + key, target, delta, approved);
  151. }
  152. private static Map<String, Object> billable(String key, double draft, double submitted,
  153. double approved, double change) {
  154. Map<String, Object> row = new HashMap<>();
  155. row.put("textField_mmacxew3", key);
  156. row.put("numberField_mmczo635", draft);
  157. row.put("numberField_mmczo636", submitted);
  158. row.put("numberField_mmczo637", approved);
  159. row.put("numberField_mmd80zae", change);
  160. return row;
  161. }
  162. private static Map<String, Object> nonBillable(String key, double submitted,
  163. double approved, double change) {
  164. Map<String, Object> row = new HashMap<>();
  165. row.put("textField_mmczo639", key);
  166. row.put("numberField_mmczo63c", submitted);
  167. row.put("numberField_mmczo63d", approved);
  168. row.put("numberField_mmeakgi9", change);
  169. return row;
  170. }
  171. private static Map<String, Object> other(String activity, double submitted,
  172. double approved, double change) {
  173. Map<String, Object> row = new HashMap<>();
  174. row.put("selectField_mmeakgie", activity);
  175. row.put("numberField_mmeakgij", submitted);
  176. row.put("numberField_mmeakgii", approved);
  177. row.put("numberField_mmeakgim", change);
  178. return row;
  179. }
  180. private static Map<String, Object> summary(Map<String, Object>... billableRows) {
  181. Map<String, Object> detail = new HashMap<>();
  182. detail.put("tableField_mmczo634", Arrays.asList(billableRows));
  183. detail.put("tableField_mmczo63h", Collections.emptyList());
  184. detail.put("tableField_mmeakgid", Collections.emptyList());
  185. return detail;
  186. }
  187. }