ApprovalWriteBackServiceTest.java 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. package com.malk.service.workhours;
  2. import com.malk.server.dingtalk.DDR_New;
  3. import com.malk.server.aliwork.YDConf;
  4. import com.malk.server.aliwork.YDParam;
  5. import com.malk.server.workhours.ApprovalWriteBackResult;
  6. import com.malk.server.workhours.WHConf;
  7. import com.malk.service.aliwork.YDClient;
  8. import org.junit.jupiter.api.Test;
  9. import org.springframework.test.util.ReflectionTestUtils;
  10. import java.util.ArrayList;
  11. import java.util.Collections;
  12. import java.util.HashMap;
  13. import java.util.List;
  14. import java.util.Map;
  15. import static org.junit.jupiter.api.Assertions.assertEquals;
  16. import static org.junit.jupiter.api.Assertions.assertFalse;
  17. import static org.junit.jupiter.api.Assertions.assertTrue;
  18. import static org.junit.jupiter.api.Assertions.assertThrows;
  19. import static org.mockito.ArgumentMatchers.any;
  20. import static org.mockito.ArgumentMatchers.eq;
  21. import static org.mockito.Mockito.mock;
  22. import static org.mockito.Mockito.times;
  23. import static org.mockito.Mockito.verify;
  24. import static org.mockito.Mockito.when;
  25. class ApprovalWriteBackServiceTest {
  26. @Test
  27. void shouldReportApprovalStatusWriteFailureToTimer() {
  28. YDClient client = mock(YDClient.class);
  29. WHConf conf = new WHConf();
  30. conf.setOtherApprovalSyncStatusField("status-field");
  31. conf.setOtherApprovalSyncTotalField("total-field");
  32. conf.setOtherApprovalSyncFailField("fail-field");
  33. Map<String, Object> approvalRow = new HashMap<>();
  34. approvalRow.put("textField_mmd1wu9f", "user-1");
  35. approvalRow.put("textField_mmd8g655", "20260818");
  36. approvalRow.put("selectField_mmeakgie", "培训");
  37. approvalRow.put("numberField_mmd1wu9e", 8d);
  38. Map<String, Object> approvalData = new HashMap<>();
  39. approvalData.put("tableField_mmd1wu9h", Collections.singletonList(approvalRow));
  40. DDR_New<Object> response = new DDR_New<>();
  41. response.setData(approvalData);
  42. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process_id)))
  43. .thenReturn(response);
  44. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  45. .thenThrow(new IllegalStateException("day record unavailable"));
  46. when(client.operateData(any(YDParam.class), eq(YDConf.FORM_OPERATION.update)))
  47. .thenThrow(new IllegalStateException("status update failed"));
  48. ApprovalWriteBackService service = new ApprovalWriteBackService();
  49. ReflectionTestUtils.setField(service, "ydClient", client);
  50. ReflectionTestUtils.setField(service, "whConf", conf);
  51. assertThrows(IllegalStateException.class, () -> service.writeBack("approval-1", 1));
  52. }
  53. @Test
  54. void shouldLeaveFailedDayForDeferredTimerRetryWithoutImmediateRetry() {
  55. YDClient client = mock(YDClient.class);
  56. WHConf conf = new WHConf();
  57. conf.setFormUuidOtherApproval("other-approval");
  58. Map<String, Object> approvalRow = new HashMap<>();
  59. approvalRow.put("textField_mmd1wu9f", "user-1");
  60. approvalRow.put("textField_mmd8g655", "20260818");
  61. approvalRow.put("selectField_mmeakgie", "培训");
  62. approvalRow.put("numberField_mmd1wu9e", 8d);
  63. Map<String, Object> approvalData = new HashMap<>();
  64. approvalData.put("tableField_mmd1wu9h", Collections.singletonList(approvalRow));
  65. DDR_New<Object> approvalResponse = new DDR_New<>();
  66. approvalResponse.setData(approvalData);
  67. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process_id)))
  68. .thenReturn(approvalResponse);
  69. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  70. .thenThrow(new IllegalStateException("temporary query failure"));
  71. ApprovalWriteBackService service = new ApprovalWriteBackService();
  72. ReflectionTestUtils.setField(service, "ydClient", client);
  73. ReflectionTestUtils.setField(service, "whConf", conf);
  74. ApprovalWriteBackResult result = service.writeBack("approval-1", 1);
  75. assertEquals(1, result.getFailCount());
  76. verify(client, times(1)).queryData(
  77. any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form));
  78. }
  79. @Test
  80. void shouldPreferApprovedStatus() {
  81. assertEquals("已审批", ApprovalWriteBackService.resolveSummaryApprovalStatus(8, 8));
  82. assertEquals("已审批", ApprovalWriteBackService.resolveSummaryApprovalStatus(4, 4));
  83. assertEquals("已审批", ApprovalWriteBackService.resolveSummaryApprovalStatus(0, 4));
  84. }
  85. @Test
  86. void shouldUseInApprovalWhenSubmittedHoursRemain() {
  87. assertEquals("审批中", ApprovalWriteBackService.resolveSummaryApprovalStatus(8, 0));
  88. assertEquals("审批中", ApprovalWriteBackService.resolveSummaryApprovalStatus(8, 4));
  89. }
  90. @Test
  91. void shouldFallBackToDraftWhenNoSubmittedOrApprovedHours() {
  92. assertEquals("暂存", ApprovalWriteBackService.resolveSummaryApprovalStatus(0, 0));
  93. }
  94. @Test
  95. void shouldRestoreSubmittedHoursWhenApprovalCompletes() {
  96. assertEquals(8.0,
  97. ApprovalWriteBackService.resolveSubmittedAfterWriteBack(true, 0, 8));
  98. }
  99. @Test
  100. void shouldKeepLargerSubmittedHoursWhenApprovalCompletes() {
  101. assertEquals(12.0,
  102. ApprovalWriteBackService.resolveSubmittedAfterWriteBack(true, 12, 8));
  103. }
  104. @Test
  105. void shouldClearSubmittedHoursWhenApprovalIsRejected() {
  106. assertEquals(0.0,
  107. ApprovalWriteBackService.resolveSubmittedAfterWriteBack(false, 8, 8));
  108. }
  109. @Test
  110. void shouldFallBackToWholeApprovalWhenHistoricalDetailOpinionIsBlank() {
  111. assertEquals(true, ApprovalWriteBackService.isDetailApproved(true, ""));
  112. assertEquals(true, ApprovalWriteBackService.isDetailApproved(true, null));
  113. assertEquals(true, ApprovalWriteBackService.isDetailApproved(true, "同意"));
  114. assertEquals(false, ApprovalWriteBackService.isDetailApproved(true, "拒绝"));
  115. assertEquals(false, ApprovalWriteBackService.isDetailApproved(false, ""));
  116. }
  117. @Test
  118. void shouldExtractFullFormDataFromProcessDetail() {
  119. DDR_New response = new DDR_New();
  120. Map<String, Object> formData = new HashMap<>();
  121. formData.put("tableField_mmae8t99", new ArrayList<>(63));
  122. response.setData(formData);
  123. Map extracted = ApprovalWriteBackService.extractProcessFormData(response);
  124. assertEquals(formData, extracted);
  125. }
  126. @Test
  127. void shouldExtractFullFormDataFromLegacyProcessResult() {
  128. DDR_New response = new DDR_New();
  129. Map<String, Object> formData = new HashMap<>();
  130. formData.put("tableField_mmae8t99", new ArrayList<>(63));
  131. Map<String, Object> result = new HashMap<>();
  132. result.put("data", formData);
  133. response.setResult(result);
  134. Map extracted = ApprovalWriteBackService.extractProcessFormData(response);
  135. assertEquals(formData, extracted);
  136. }
  137. @Test
  138. void shouldRecognizeWorkApprovalWithOnlyNonBillableRows() {
  139. Map<String, Object> row = new HashMap<>();
  140. row.put("textField_mmd1wu9d", "PROJECT-1");
  141. Map<String, Object> formData = new HashMap<>();
  142. formData.put("tableField_mmd1wu9h", Collections.singletonList(row));
  143. assertEquals(false, ApprovalWriteBackService.isOtherApprovalForm(formData));
  144. }
  145. @Test
  146. void shouldRecognizeOtherApprovalByActivityField() {
  147. Map<String, Object> row = new HashMap<>();
  148. row.put("selectField_mmeakgie", "培训");
  149. Map<String, Object> formData = new HashMap<>();
  150. formData.put("tableField_mmd1wu9h", Collections.singletonList(row));
  151. assertEquals(true, ApprovalWriteBackService.isOtherApprovalForm(formData));
  152. }
  153. @Test
  154. void shouldReadOtherApprovalOpinionFromActualField() {
  155. Map<String, Object> row = new HashMap<>();
  156. row.put("selectField_mmeakgie", "培训");
  157. row.put("radioField_mpwhk294", "不同意");
  158. assertFalse(RepairApprovalWriteBackService.isApprovedRow(row, false, true));
  159. row.put("radioField_mpwhk294", "同意");
  160. assertTrue(RepairApprovalWriteBackService.isApprovedRow(row, false, true));
  161. }
  162. @Test
  163. void shouldReadNonBillableApprovalOpinionFromActualField() {
  164. Map<String, Object> row = new HashMap<>();
  165. row.put("textField_mmd1wu9d", "PROJECT-1");
  166. row.put("radioField_mpwhk295", "不同意");
  167. assertFalse(RepairApprovalWriteBackService.isApprovedRow(row, false, true));
  168. }
  169. @Test
  170. void shouldResumeRepairWriteBackFromPersistedCheckpoint() {
  171. assertEquals(0, RepairApprovalWriteBackService.resolveCompletedSteps("", 0, 0, 4));
  172. assertEquals(0, RepairApprovalWriteBackService.resolveCompletedSteps("部分失败", 3, 2, 4));
  173. assertEquals(1, RepairApprovalWriteBackService.resolveCompletedSteps("部分失败", 4, 3, 4));
  174. assertEquals(3, RepairApprovalWriteBackService.resolveCompletedSteps("部分失败", 4, 1, 4));
  175. assertEquals(4, RepairApprovalWriteBackService.resolveCompletedSteps("部分失败", 4, 0, 4));
  176. assertEquals(0, RepairApprovalWriteBackService.resolveCompletedSteps("全部成功", 4, 0, 4));
  177. }
  178. @Test
  179. void shouldPreserveSavedDraftHoursWhenRepairIsSubmitted() {
  180. Map<String, Object> existing = new HashMap<>();
  181. existing.put("projectCode", "P-001");
  182. existing.put("draftHours", 4d);
  183. existing.put("submittedHours", 4d);
  184. existing.put("clientName", "Client A");
  185. java.util.List<Map> rows = new java.util.ArrayList<>();
  186. rows.add(existing);
  187. Map<String, Object> submitted = new HashMap<>();
  188. submitted.put("projectCode", "P-001");
  189. submitted.put("submittedHours", 4d);
  190. TimeCardRepairService.mergeSubmittedRow(
  191. rows, submitted, "projectCode", "submittedHours", 4d);
  192. assertEquals(1, rows.size());
  193. assertEquals(4d, rows.get(0).get("draftHours"));
  194. assertEquals(4d, rows.get(0).get("submittedHours"));
  195. assertEquals("Client A", rows.get(0).get("clientName"));
  196. }
  197. @Test
  198. void shouldRecalculateRepairMainTotalsFromAllChildTables() {
  199. Map<String, Object> billable = new HashMap<>();
  200. billable.put("numberField_mmczo635", 0d);
  201. billable.put("numberField_mmczo636", 4d);
  202. billable.put("numberField_mmczo637", 0d);
  203. Map<String, Object> nonBillable = new HashMap<>();
  204. nonBillable.put("numberField_mmczo63b", 1d);
  205. nonBillable.put("numberField_mmczo63c", 0d);
  206. Map<String, Object> other = new HashMap<>();
  207. other.put("numberField_mmeakgik", 0d);
  208. other.put("numberField_mmeakgij", 2d);
  209. Map<String, Object> snapshot = new HashMap<>();
  210. snapshot.put("tableField_mmczo634", Collections.singletonList(billable));
  211. snapshot.put("tableField_mmczo63h", Collections.singletonList(nonBillable));
  212. snapshot.put("tableField_mmeakgid", Collections.singletonList(other));
  213. Map<String, Object> update = new HashMap<>();
  214. TimeCardRepairService.applySummaryTotals(update, snapshot);
  215. assertEquals(4d, update.get("numberField_mmacxewa"));
  216. assertEquals(1d, update.get("numberField_mmad6jbx"));
  217. assertEquals(2d, update.get("numberField_mmd1smeq"));
  218. assertEquals(1d, update.get("numberField_mmd1smel"));
  219. assertEquals(6d, update.get("numberField_mmd1smem"));
  220. }
  221. @Test
  222. void shouldUseDetailIdentityForRepairResubmitCoverage() {
  223. assertEquals("billable|u1|20260805|P-001",
  224. TimeCardRepairService.coverageKey("billable", "u1", "20260805", "P-001"));
  225. }
  226. @Test
  227. void shouldRecalculateRepairTotalsFromUpdatedApprovalSnapshot() {
  228. Map<String, Object> billable = new HashMap<>();
  229. billable.put("textField_mmacxew3", "P-001");
  230. billable.put("numberField_mmczo635", 8d);
  231. billable.put("numberField_mmczo636", 8d);
  232. billable.put("numberField_mmczo637", 0d);
  233. billable.put("numberField_mmczo637_value", 0d);
  234. billable.put("textField_mkov5ozd", "Project D4001");
  235. Map<String, Object> other = new HashMap<>();
  236. other.put("selectField_mmeakgie", "Time off in lieu of overtime");
  237. other.put("numberField_mmeakgik", 8d);
  238. other.put("numberField_mmeakgij", 8d);
  239. other.put("numberField_mmeakgii", 8d);
  240. Map<String, Object> detail = new HashMap<>();
  241. detail.put("tableField_mmczo634", Collections.singletonList(billable));
  242. detail.put("tableField_mmeakgid", Collections.singletonList(other));
  243. Map<String, Object> update = RepairApprovalWriteBackService.buildRepairSummaryUpdate(
  244. detail, false, true, "P-001", 8d, true);
  245. assertEquals(8d, update.get("numberField_mmacxewa"));
  246. assertEquals(8d, update.get("numberField_mmacxewg"));
  247. assertEquals(16d, update.get("numberField_mmd1smem"));
  248. assertEquals(16d, update.get("numberField_mmeakgi4"));
  249. assertEquals(16d, update.get("numberField_mmd1smel"));
  250. assertEquals("全部成功", RepairApprovalWriteBackService.resolveRepairSummaryStatus(update));
  251. Map updatedBillable = ((java.util.List<Map>) update.get("tableField_mmczo634")).get(0);
  252. assertEquals(8d, updatedBillable.get("numberField_mmczo637"));
  253. assertFalse(updatedBillable.containsKey("numberField_mmczo637_value"));
  254. assertEquals("Project D4001", updatedBillable.get("textField_mkov5ozd"));
  255. }
  256. @Test
  257. void shouldAggregateBillableAndNonBillableChangesByProjectCode() {
  258. List<RepairApprovalWriteBackService.Row> rows = new ArrayList<>();
  259. rows.add(repairRow("P-001", "Project A", 4d, true));
  260. rows.add(repairRow("P-001", "Project A", 6d, true));
  261. rows.add(repairRow("P-001", "Project A", 3d, false));
  262. rows.add(repairRow("P-002", "Project B", 5d, false));
  263. List<Map> billable = RepairApprovalWriteBackService.childChanges(rows, true);
  264. List<Map> nonBillable = RepairApprovalWriteBackService.childChanges(rows, false);
  265. assertEquals(1, billable.size());
  266. assertEquals("P-001", billable.get(0).get("textField_mmacxew3"));
  267. assertEquals("Project A", billable.get(0).get("selectField_mmad6jci"));
  268. assertEquals(10d, billable.get(0).get("numberField_mmd80zae"));
  269. assertEquals(2, nonBillable.size());
  270. assertEquals("P-001", nonBillable.get(0).get("textField_mmczo639"));
  271. assertEquals("Project A", nonBillable.get(0).get("selectField_mmczo638"));
  272. assertEquals(3d, nonBillable.get(0).get("numberField_mmeakgi9"));
  273. assertEquals("P-002", nonBillable.get(1).get("textField_mmczo639"));
  274. assertEquals(5d, nonBillable.get(1).get("numberField_mmeakgi9"));
  275. }
  276. private static RepairApprovalWriteBackService.Row repairRow(
  277. String projectCode, String projectName, double hours, boolean billable) {
  278. RepairApprovalWriteBackService.Row row = new RepairApprovalWriteBackService.Row();
  279. row.key = projectCode;
  280. row.projectName = projectName;
  281. row.hours = hours;
  282. row.billable = billable;
  283. row.approve = true;
  284. return row;
  285. }
  286. @Test
  287. void shouldKeepRepairSummaryPendingUntilAllCategoriesAreApproved() {
  288. Map<String, Object> totals = new HashMap<>();
  289. totals.put("numberField_mmd1smem", 16d);
  290. totals.put("numberField_mmeakgi4", 8d);
  291. assertEquals("审批中", RepairApprovalWriteBackService.resolveRepairSummaryStatus(totals));
  292. }
  293. @Test
  294. void shouldInitializeCreatedOriginalSummaryAsDraft() {
  295. Map<String, Object> update = new HashMap<>();
  296. RepairApprovalWriteBackService.putDraftSummaryStatus(update, "summaryStatus");
  297. assertEquals("暂存", update.get("summaryStatus"));
  298. }
  299. @Test
  300. void shouldOnlySkipMissingRequiredHoursForRepairSourceDay() {
  301. assertTrue(RepairApprovalWriteBackService.shouldSkipMissingRequiredRecord(true, 0));
  302. assertFalse(RepairApprovalWriteBackService.shouldSkipMissingRequiredRecord(false, 0));
  303. assertFalse(RepairApprovalWriteBackService.shouldSkipMissingRequiredRecord(true, 2));
  304. }
  305. @Test
  306. void shouldNeverTreatEmptyOrMalformedApprovalDetailsAsSuccess() {
  307. assertEquals("全部失败", ApprovalWriteBackService.determineSyncStatus(0, 0));
  308. assertEquals("全部失败", ApprovalWriteBackService.determineSyncStatus(1, 1));
  309. assertEquals("部分失败", ApprovalWriteBackService.determineSyncStatus(3, 1));
  310. assertEquals("全部成功", ApprovalWriteBackService.determineSyncStatus(3, 0));
  311. }
  312. }