TimeCardRepairServiceTest.java 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866
  1. package com.malk.service.workhours;
  2. import com.alibaba.fastjson.JSON;
  3. import com.malk.server.aliwork.YDConf;
  4. import com.malk.server.aliwork.YDParam;
  5. import com.malk.server.common.McException;
  6. import com.malk.server.dingtalk.DDR_New;
  7. import com.malk.server.workhours.MonthClosingCheckRequest;
  8. import com.malk.server.workhours.MonthClosingStatusResult;
  9. import com.malk.server.workhours.TimeCardApprovalStartRequest;
  10. import com.malk.server.workhours.TimeCardRepairContextRequest;
  11. import com.malk.server.workhours.TimeCardRepairSubmitRequest;
  12. import com.malk.server.workhours.TimeCardRepairSummaryUpsertRequest;
  13. import com.malk.server.workhours.WHConf;
  14. import com.malk.service.aliwork.YDClient;
  15. import org.junit.jupiter.api.Test;
  16. import org.junit.jupiter.params.ParameterizedTest;
  17. import org.junit.jupiter.params.provider.ValueSource;
  18. import org.mockito.ArgumentCaptor;
  19. import org.springframework.test.util.ReflectionTestUtils;
  20. import java.time.YearMonth;
  21. import java.time.LocalDate;
  22. import java.time.ZoneId;
  23. import java.time.format.DateTimeFormatter;
  24. import java.util.Collections;
  25. import java.util.HashMap;
  26. import java.util.List;
  27. import java.util.Map;
  28. import java.util.concurrent.CountDownLatch;
  29. import java.util.concurrent.TimeUnit;
  30. import static org.junit.jupiter.api.Assertions.assertEquals;
  31. import static org.junit.jupiter.api.Assertions.assertFalse;
  32. import static org.junit.jupiter.api.Assertions.assertThrows;
  33. import static org.junit.jupiter.api.Assertions.assertTrue;
  34. import static org.mockito.ArgumentMatchers.any;
  35. import static org.mockito.ArgumentMatchers.eq;
  36. import static org.mockito.Mockito.mock;
  37. import static org.mockito.Mockito.doCallRealMethod;
  38. import static org.mockito.Mockito.never;
  39. import static org.mockito.Mockito.times;
  40. import static org.mockito.Mockito.verify;
  41. import static org.mockito.Mockito.verifyNoInteractions;
  42. import static org.mockito.Mockito.when;
  43. class TimeCardRepairServiceTest {
  44. @Test
  45. void shouldCheckSourceEmploymentDatesForAllRepairCategoriesAndAllowLaterSubmission() {
  46. for (String category : new String[]{"billableRows", "nonBillableRows", "otherRows"}) {
  47. YDClient client = mock(YDClient.class);
  48. DDR_New<Object> response = new DDR_New<>();
  49. response.setData(Collections.emptyList());
  50. when(client.queryData(any(YDParam.class), any())).thenReturn(response);
  51. TimeCardRepairService service = repairContextService(client, mock(MonthClosingService.class), repairContextConf());
  52. TimeCardPersonnelPolicy policy = mock(TimeCardPersonnelPolicy.class);
  53. when(policy.getStaffType("user-1")).thenReturn("外部");
  54. when(policy.getEmploymentDates("user-1")).thenReturn(Collections.singletonMap(
  55. "end", java.time.LocalDate.of(2026, 9, 15)));
  56. ReflectionTestUtils.setField(service, "personnelPolicy", policy);
  57. TimeCardRepairSubmitRequest request = new TimeCardRepairSubmitRequest();
  58. request.setSubmitterUserId("user-1");
  59. request.setSourceMonthText("202609");
  60. Map<String, Object> row = new HashMap<>();
  61. row.put("dateStr", "20260916");
  62. row.put("projectCode", "P");
  63. row.put("hours", 8d);
  64. Map<String, Object> group = Collections.singletonMap(category, Collections.singletonList(row));
  65. Map projects = "otherRows".equals(category) ? Collections.emptyMap() : Collections.singletonMap("manager", group);
  66. Map other = "otherRows".equals(category) ? group : Collections.emptyMap();
  67. assertThrows(McException.class, () -> ReflectionTestUtils.invokeMethod(service,
  68. "validateSubmitRows", request, projects, other));
  69. verifyNoInteractions(client);
  70. row.remove("dateStr");
  71. row.put("dayText", "20260915");
  72. assertEquals(1, (int) ReflectionTestUtils.invokeMethod(service,
  73. "validateSubmitRows", request, projects, other));
  74. }
  75. }
  76. private static TimeCardPersonnelPolicy internalPolicy() {
  77. TimeCardPersonnelPolicy policy = mock(TimeCardPersonnelPolicy.class);
  78. when(policy.getStaffType(any())).thenReturn("内部");
  79. return policy;
  80. }
  81. @Test
  82. void shouldReturnPendingWhilePersonnelLookupIsStillRunning() throws Exception {
  83. YDClient client = mock(YDClient.class);
  84. DDR_New<Object> response = new DDR_New<>();
  85. response.setData(Collections.emptyList());
  86. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))).thenReturn(response);
  87. TimeCardRepairService service = repairContextService(client, mock(MonthClosingService.class), repairContextConf());
  88. CountDownLatch started = new CountDownLatch(1);
  89. CountDownLatch release = new CountDownLatch(1);
  90. TimeCardPersonnelPolicy policy = mock(TimeCardPersonnelPolicy.class);
  91. when(policy.getStaffType("user-1")).thenAnswer(invocation -> {
  92. started.countDown();
  93. assertTrue(release.await(2, TimeUnit.SECONDS));
  94. return "外部";
  95. });
  96. ReflectionTestUtils.setField(service, "personnelPolicy", policy);
  97. try {
  98. assertEquals(false, service.context(repairContextRequest()).get("ready"));
  99. assertTrue(started.await(1, TimeUnit.SECONDS));
  100. verifyNoInteractions(client);
  101. } finally {
  102. release.countDown();
  103. }
  104. assertEquals("original-and-repair", awaitReadyContext(service, repairContextRequest()).get("summaryReadMode"));
  105. }
  106. @Test
  107. void shouldRejectForgedExternalAttributeBeforeAnySummaryQuery() throws Exception {
  108. YDClient client = mock(YDClient.class);
  109. TimeCardRepairService service = repairContextService(client, mock(MonthClosingService.class), repairContextConf());
  110. TimeCardRepairContextRequest request = repairContextRequest();
  111. request.setStaffType("外部");
  112. assertTrue(awaitContextFailure(service, request).getMessage().contains("人员属性已变化"));
  113. verifyNoInteractions(client);
  114. }
  115. @Test
  116. void shouldQueryBothSummariesForConfirmedExternalUser() throws Exception {
  117. YDClient client = mock(YDClient.class);
  118. DDR_New<Object> response = new DDR_New<>();
  119. response.setData(Collections.emptyList());
  120. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  121. .thenReturn(response);
  122. TimeCardRepairService service = repairContextService(client, mock(MonthClosingService.class), repairContextConf());
  123. TimeCardPersonnelPolicy policy = mock(TimeCardPersonnelPolicy.class);
  124. when(policy.getStaffType("user-1")).thenReturn("外部");
  125. ReflectionTestUtils.setField(service, "personnelPolicy", policy);
  126. Map<String, Object> context = awaitReadyContext(service, repairContextRequest());
  127. assertEquals("original-and-repair", context.get("summaryReadMode"));
  128. assertEquals("外部", context.get("staffType"));
  129. assertTrue(((List<?>) context.get("originalRecords")).isEmpty());
  130. verify(client, times(1)).queryData(org.mockito.ArgumentMatchers.argThat(
  131. p -> "original-summary".equals(p.getFormUuid())), eq(YDConf.FORM_QUERY.retrieve_search_form));
  132. verify(client, times(3)).queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form));
  133. }
  134. @Test
  135. void shouldSeparateContextCacheWhenPersonnelAttributeChanges() throws Exception {
  136. YDClient client = mock(YDClient.class);
  137. DDR_New<Object> response = new DDR_New<>();
  138. response.setData(Collections.emptyList());
  139. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))).thenReturn(response);
  140. TimeCardRepairService service = repairContextService(client, mock(MonthClosingService.class), repairContextConf());
  141. TimeCardPersonnelPolicy policy = mock(TimeCardPersonnelPolicy.class);
  142. when(policy.getStaffType("user-1")).thenReturn("内部");
  143. ReflectionTestUtils.setField(service, "personnelPolicy", policy);
  144. TimeCardRepairContextRequest request = repairContextRequest();
  145. request.setStaffType("内部");
  146. assertEquals("original-and-repair", awaitReadyContext(service, request).get("summaryReadMode"));
  147. when(policy.getStaffType("user-1")).thenReturn("外部");
  148. request.setStaffType("外部");
  149. assertEquals("original-and-repair", awaitReadyContext(service, request).get("summaryReadMode"));
  150. verify(client, times(2)).queryData(org.mockito.ArgumentMatchers.argThat(
  151. p -> "original-summary".equals(p.getFormUuid())), eq(YDConf.FORM_QUERY.retrieve_search_form));
  152. }
  153. @Test
  154. void shouldKeepRepairDayLockedForExternalSaveAfterCheckingOriginal() {
  155. YDClient client = mock(YDClient.class);
  156. MonthClosingService closing = mock(MonthClosingService.class);
  157. when(closing.isLocked("202609")).thenReturn(true);
  158. when(closing.status(any(MonthClosingCheckRequest.class)))
  159. .thenReturn(MonthClosingStatusResult.builder().build());
  160. WHConf conf = repairContextConf();
  161. conf.setRepairSummarySyncStatusField("sync");
  162. TimeCardRepairService service = repairContextService(client, closing, conf);
  163. TimeCardPersonnelPolicy policy = mock(TimeCardPersonnelPolicy.class);
  164. when(policy.getStaffType("user-1")).thenReturn("外部");
  165. ReflectionTestUtils.setField(service, "personnelPolicy", policy);
  166. Map<String, Object> fd = new HashMap<>();
  167. fd.put("fill-type", "补填");
  168. fd.put("sync", "审批中");
  169. fd.put("numberField_mmd1smem", 8d);
  170. DDR_New<Object> response = new DDR_New<>();
  171. response.setData(Collections.singletonList(Collections.singletonMap("formData", fd)));
  172. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))).thenAnswer(invocation -> {
  173. YDParam param = invocation.getArgument(0);
  174. if ("repair-summary".equals(param.getFormUuid())) return response;
  175. DDR_New<Object> original = new DDR_New<>();
  176. original.setData(Collections.emptyList());
  177. return original;
  178. });
  179. TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest();
  180. request.setSubmitterUserId("user-1");
  181. request.setDayText("20260930");
  182. request.setFormData(new HashMap<>());
  183. assertTrue(assertThrows(McException.class, () -> service.upsert(request)).getMessage().contains("已提交或已审批"));
  184. verify(client, times(1)).queryData(org.mockito.ArgumentMatchers.argThat(
  185. p -> "original-summary".equals(p.getFormUuid())), eq(YDConf.FORM_QUERY.retrieve_search_form));
  186. }
  187. @ParameterizedTest
  188. @ValueSource(booleans = {false, true})
  189. void shouldCheckOriginalSourceGuardForEveryStaffType(boolean external) {
  190. YDClient client = mock(YDClient.class);
  191. TimeCardRepairService service = repairContextService(client, mock(MonthClosingService.class), repairContextConf());
  192. Map<String, Object> fd = new HashMap<>();
  193. fd.put("numberField_mmd1smem", 8d);
  194. DDR_New<Object> response = new DDR_New<>();
  195. response.setData(Collections.singletonList(Collections.singletonMap("formData", fd)));
  196. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))).thenReturn(response);
  197. assertThrows(McException.class, () -> ReflectionTestUtils.invokeMethod(
  198. service, "assertSourceDateAvailable", "user-1", "20260930", external));
  199. verify(client, times(1)).queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form));
  200. }
  201. @Test
  202. void shouldCheckBothTablesDuringExternalSubmitValidation() {
  203. YDClient client = mock(YDClient.class);
  204. DDR_New<Object> response = new DDR_New<>();
  205. response.setData(Collections.emptyList());
  206. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))).thenReturn(response);
  207. TimeCardRepairService service = repairContextService(client, mock(MonthClosingService.class), repairContextConf());
  208. TimeCardPersonnelPolicy policy = mock(TimeCardPersonnelPolicy.class);
  209. when(policy.getStaffType("user-1")).thenReturn("外部");
  210. ReflectionTestUtils.setField(service, "personnelPolicy", policy);
  211. TimeCardRepairSubmitRequest request = new TimeCardRepairSubmitRequest();
  212. request.setSubmitterUserId("user-1");
  213. request.setSourceMonthText("202609");
  214. Map<String, Object> row = new HashMap<>();
  215. row.put("dateStr", "20260930");
  216. row.put("projectCode", "P");
  217. row.put("hours", 8d);
  218. Map<String, Object> group = Collections.singletonMap("billableRows", Collections.singletonList(row));
  219. Object count = ReflectionTestUtils.invokeMethod(service, "validateSubmitRows", request,
  220. Collections.singletonMap("manager", group), Collections.emptyMap());
  221. assertEquals(1, count);
  222. verify(policy, times(1)).getStaffType("user-1");
  223. verify(client, times(1)).queryData(org.mockito.ArgumentMatchers.argThat(
  224. p -> "original-summary".equals(p.getFormUuid())), eq(YDConf.FORM_QUERY.retrieve_search_form));
  225. verify(client, times(1)).queryData(org.mockito.ArgumentMatchers.argThat(
  226. p -> "repair-summary".equals(p.getFormUuid())), eq(YDConf.FORM_QUERY.retrieve_search_form));
  227. }
  228. @Test
  229. void shouldSeparateNormalAndConfirmedReentryContextsAndKeepOriginalProtection() throws Exception {
  230. YDClient client = mock(YDClient.class);
  231. Map<String, Object> reversal = reversalRecord();
  232. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  233. .thenAnswer(invocation -> {
  234. YDParam param = invocation.getArgument(0);
  235. Map<String, Object> data = "original-summary".equals(param.getFormUuid())
  236. ? Collections.singletonMap("numberField_mmd1smem", 8d) : reversal;
  237. DDR_New<Object> response = new DDR_New<>();
  238. response.setData(Collections.singletonList(Collections.singletonMap("formData", data)));
  239. return response;
  240. });
  241. TimeCardRepairService service = repairContextService(client, mock(MonthClosingService.class), repairContextConf());
  242. TimeCardPersonnelPolicy policy = mock(TimeCardPersonnelPolicy.class);
  243. when(policy.getStaffType("user-1")).thenReturn("外部");
  244. ReflectionTestUtils.setField(service, "personnelPolicy", policy);
  245. TimeCardRepairContextRequest request = repairContextRequest();
  246. Map<String, Object> normal = awaitReadyContext(service, request);
  247. assertEquals(1, ((List<?>) normal.get("originalRecords")).size());
  248. request.setRepairMode("REENTRY");
  249. Map<String, Object> reentry = awaitReadyContext(service, request);
  250. assertEquals("repair-only", reentry.get("summaryReadMode"));
  251. assertTrue(((List<?>) reentry.get("originalRecords")).isEmpty());
  252. assertEquals(Collections.singletonList("20260930"), reentry.get("reentryAllowedDays"));
  253. assertTrue(((List<?>) reentry.get("repairRecords")).isEmpty(), "冲销变更不能成为补填审批锁");
  254. verify(client, times(1)).queryData(org.mockito.ArgumentMatchers.argThat(
  255. p -> "original-summary".equals(p.getFormUuid())), eq(YDConf.FORM_QUERY.retrieve_search_form));
  256. assertThrows(McException.class, () -> ReflectionTestUtils.invokeMethod(service,
  257. "assertRepairSourceDateAvailable", "user-1", "20260930", "NORMAL", true));
  258. ReflectionTestUtils.invokeMethod(service, "assertRepairSourceDateAvailable", "user-1", "20260930", "REENTRY", true);
  259. }
  260. @Test
  261. void shouldRejectRolledBackUnapprovedOrForgedReentryBeforeWrites() {
  262. for (String invalid : new String[]{"missing", "batch", "sync", "user", "day", "positive", "internal", "mode"}) {
  263. YDClient client = mock(YDClient.class);
  264. Map<String, Object> reversal = reversalRecord();
  265. if ("batch".equals(invalid)) reversal.put("batch", "unconfirmed");
  266. if ("sync".equals(invalid)) reversal.put("sync", "部分成功");
  267. if ("user".equals(invalid)) reversal.put("textField_mmbffvd9", "other-user");
  268. if ("day".equals(invalid)) reversal.put("textField_mmbffvdb", "20260929");
  269. if ("positive".equals(invalid)) reversal.put("numberField_mmeakgi4", 8d);
  270. DDR_New<Object> response = new DDR_New<>();
  271. response.setData("missing".equals(invalid) ? Collections.emptyList()
  272. : Collections.singletonList(Collections.singletonMap("formData", reversal)));
  273. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))).thenReturn(response);
  274. TimeCardRepairService service = repairContextService(client, mock(MonthClosingService.class), repairContextConf());
  275. assertThrows(McException.class, () -> ReflectionTestUtils.invokeMethod(service,
  276. "assertRepairSourceDateAvailable", "user-1", "20260930", "mode".equals(invalid) ? "FAKE" : "REENTRY",
  277. !"internal".equals(invalid)), invalid);
  278. verify(client, never()).operateData(any(YDParam.class), any());
  279. }
  280. }
  281. @Test
  282. void shouldBlockNormalExternalSaveAndAllSubmitCategoriesOnOriginalApprovedDay() {
  283. for (String category : new String[]{"billableRows", "nonBillableRows", "otherRows"}) {
  284. YDClient client = mock(YDClient.class);
  285. DDR_New<Object> response = new DDR_New<>();
  286. response.setData(Collections.singletonList(Collections.singletonMap("formData",
  287. Collections.singletonMap("numberField_mmeakgi4", 8d))));
  288. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))).thenReturn(response);
  289. TimeCardRepairService service = repairContextService(client, mock(MonthClosingService.class), repairContextConf());
  290. TimeCardPersonnelPolicy policy = mock(TimeCardPersonnelPolicy.class);
  291. when(policy.getStaffType("user-1")).thenReturn("外部");
  292. ReflectionTestUtils.setField(service, "personnelPolicy", policy);
  293. TimeCardRepairSubmitRequest request = repairSubmitRequest();
  294. request.setSourceMonthText("202609");
  295. Map<String, Object> row = new HashMap<>();
  296. row.put("dateStr", "20260930"); row.put("projectCode", "P"); row.put("hours", 8d);
  297. Map<String, Object> group = Collections.singletonMap(category, Collections.singletonList(row));
  298. assertThrows(McException.class, () -> ReflectionTestUtils.invokeMethod(service, "validateSubmitRows", request,
  299. "otherRows".equals(category) ? Collections.emptyMap() : Collections.singletonMap("manager", group),
  300. "otherRows".equals(category) ? group : Collections.emptyMap()));
  301. verify(client, never()).operateData(any(YDParam.class), any());
  302. }
  303. }
  304. private static Map<String, Object> reversalRecord() {
  305. Map<String, Object> data = new HashMap<>();
  306. data.put("fill-type", "变更"); data.put("batch", "10月调账-EXT-20261020"); data.put("sync", "全部成功");
  307. data.put("textField_mmbffvd9", "user-1"); data.put("textField_mmbffvda", "202609");
  308. data.put("textField_mmbffvdb", "20260930"); data.put("numberField_mmeakgi4", -8d);
  309. return data;
  310. }
  311. @Test
  312. void shouldReturnPendingImmediatelyThenExposeCompletedContext() throws Exception {
  313. YDClient client = mock(YDClient.class);
  314. MonthClosingService closingService = mock(MonthClosingService.class);
  315. WHConf conf = repairContextConf();
  316. CountDownLatch queriesStarted = new CountDownLatch(2);
  317. CountDownLatch releaseQueries = new CountDownLatch(1);
  318. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  319. .thenAnswer(invocation -> {
  320. YDParam param = invocation.getArgument(0);
  321. queriesStarted.countDown();
  322. if (!releaseQueries.await(2, TimeUnit.SECONDS)) {
  323. throw new IllegalStateException("parallel context queries did not start");
  324. }
  325. Map<String, Object> formData = new HashMap<>();
  326. if ("repair-summary".equals(param.getFormUuid())) formData.put("fill-type", "补填");
  327. Map<String, Object> record = new HashMap<>();
  328. record.put("formInstId", param.getFormUuid() + "-instance");
  329. record.put("formData", formData);
  330. record.put("instValue", "redundant payload");
  331. DDR_New<Object> response = new DDR_New<>();
  332. response.setData(Collections.singletonList(record));
  333. return response;
  334. });
  335. TimeCardRepairService service = repairContextService(client, closingService, conf);
  336. TimeCardRepairContextRequest request = repairContextRequest();
  337. Map<String, Object> first = service.context(request);
  338. assertEquals(false, first.get("ready"));
  339. assertEquals(2_000L, first.get("retryAfterMillis"));
  340. assertTrue(queriesStarted.await(2, TimeUnit.SECONDS));
  341. Map<String, Object> second = service.context(request);
  342. assertEquals(false, second.get("ready"));
  343. releaseQueries.countDown();
  344. Map<String, Object> completed = awaitReadyContext(service, request);
  345. assertEquals(true, completed.get("ready"));
  346. assertEquals(1, ((List<?>) completed.get("originalRecords")).size());
  347. assertEquals(1, ((List<?>) completed.get("repairRecords")).size());
  348. Map<?, ?> compactRecord = (Map<?, ?>) ((List<?>) completed.get("originalRecords")).get(0);
  349. assertEquals("original-summary-instance", compactRecord.get("formInstId"));
  350. assertFalse(compactRecord.containsKey("instValue"));
  351. assertFalse(completed.containsKey("closed"));
  352. verify(client, times(2)).queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form));
  353. verifyNoInteractions(closingService);
  354. }
  355. @Test
  356. void shouldReuseCompletedRepairContextWithinTtl() throws Exception {
  357. YDClient client = mock(YDClient.class);
  358. MonthClosingService closingService = mock(MonthClosingService.class);
  359. DDR_New<Object> response = new DDR_New<>();
  360. response.setData(Collections.emptyList());
  361. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  362. .thenReturn(response);
  363. TimeCardRepairService service = repairContextService(client, closingService, repairContextConf());
  364. TimeCardRepairContextRequest request = repairContextRequest();
  365. service.context(request);
  366. Map<String, Object> first = awaitReadyContext(service, request);
  367. Map<String, Object> second = service.context(request);
  368. assertEquals(first, second);
  369. verify(client, times(2)).queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form));
  370. verifyNoInteractions(closingService);
  371. }
  372. @Test
  373. void shouldStartDifferentUserContextsWithoutWaitingForEachOther() throws Exception {
  374. YDClient client = mock(YDClient.class);
  375. MonthClosingService closingService = mock(MonthClosingService.class);
  376. CountDownLatch queriesStarted = new CountDownLatch(4);
  377. CountDownLatch releaseQueries = new CountDownLatch(1);
  378. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  379. .thenAnswer(invocation -> {
  380. queriesStarted.countDown();
  381. if (!releaseQueries.await(2, TimeUnit.SECONDS)) {
  382. throw new IllegalStateException("different user context queries did not start");
  383. }
  384. DDR_New<Object> response = new DDR_New<>();
  385. response.setData(Collections.emptyList());
  386. return response;
  387. });
  388. TimeCardRepairService service = repairContextService(client, closingService, repairContextConf());
  389. TimeCardRepairContextRequest firstRequest = repairContextRequest();
  390. TimeCardRepairContextRequest secondRequest = repairContextRequest();
  391. secondRequest.setSubmitterUserId("user-2");
  392. assertEquals(false, service.context(firstRequest).get("ready"));
  393. assertEquals(false, service.context(secondRequest).get("ready"));
  394. assertTrue(queriesStarted.await(2, TimeUnit.SECONDS));
  395. releaseQueries.countDown();
  396. assertEquals(true, awaitReadyContext(service, firstRequest).get("ready"));
  397. assertEquals(true, awaitReadyContext(service, secondRequest).get("ready"));
  398. verify(client, times(4)).queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form));
  399. verifyNoInteractions(closingService);
  400. }
  401. private static Map<String, Object> awaitReadyContext(TimeCardRepairService service,
  402. TimeCardRepairContextRequest request) throws InterruptedException {
  403. for (int attempt = 0; attempt < 200; attempt++) {
  404. Map<String, Object> result = service.context(request);
  405. if (Boolean.TRUE.equals(result.get("ready"))) return result;
  406. Thread.sleep(10L);
  407. }
  408. throw new AssertionError("context did not become ready");
  409. }
  410. @Test
  411. void shouldPropagateRepairContextQueryFailureWithoutPartialResult() throws Exception {
  412. YDClient client = mock(YDClient.class);
  413. MonthClosingService closingService = mock(MonthClosingService.class);
  414. CountDownLatch queryStarted = new CountDownLatch(1);
  415. CountDownLatch releaseQuery = new CountDownLatch(1);
  416. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  417. .thenAnswer(invocation -> {
  418. queryStarted.countDown();
  419. if (!releaseQuery.await(2, TimeUnit.SECONDS)) {
  420. throw new IllegalStateException("failed context query was not released");
  421. }
  422. throw new McException("TEMPORARY_FAILURE", "宜搭查询超时");
  423. });
  424. TimeCardRepairService service = repairContextService(client, closingService, repairContextConf());
  425. TimeCardRepairContextRequest request = repairContextRequest();
  426. assertEquals(false, service.context(request).get("ready"));
  427. assertTrue(queryStarted.await(2, TimeUnit.SECONDS));
  428. releaseQuery.countDown();
  429. McException error = awaitContextFailure(service, request);
  430. assertEquals("宜搭查询超时", error.getMessage());
  431. verifyNoInteractions(closingService);
  432. }
  433. private static McException awaitContextFailure(TimeCardRepairService service,
  434. TimeCardRepairContextRequest request) throws InterruptedException {
  435. for (int attempt = 0; attempt < 200; attempt++) {
  436. try {
  437. service.context(request);
  438. } catch (McException error) {
  439. return error;
  440. }
  441. Thread.sleep(10L);
  442. }
  443. throw new AssertionError("context failure was not propagated");
  444. }
  445. private static TimeCardRepairService repairContextService(YDClient client,
  446. MonthClosingService closingService,
  447. WHConf conf) {
  448. TimeCardRepairService service = new TimeCardRepairService();
  449. ReflectionTestUtils.setField(service, "personnelPolicy", internalPolicy());
  450. ReflectionTestUtils.setField(service, "ydClient", client);
  451. ReflectionTestUtils.setField(service, "monthClosingService", closingService);
  452. ReflectionTestUtils.setField(service, "whConf", conf);
  453. return service;
  454. }
  455. private static WHConf repairContextConf() {
  456. WHConf conf = new WHConf();
  457. conf.setYidaAppType("app");
  458. conf.setYidaSystemToken("token");
  459. conf.setFormUuidWorkHoursSummary("original-summary");
  460. conf.setFormUuidRepairWorkHoursSummary("repair-summary");
  461. conf.setRepairSummaryFillTypeField("fill-type");
  462. conf.setRepairSummaryBatchField("batch");
  463. conf.setRepairSummarySyncStatusField("sync");
  464. return conf;
  465. }
  466. private static TimeCardRepairContextRequest repairContextRequest() {
  467. TimeCardRepairContextRequest request = new TimeCardRepairContextRequest();
  468. request.setSubmitterUserId("user-1");
  469. request.setSourceMonthText("202609");
  470. return request;
  471. }
  472. @ParameterizedTest
  473. @ValueSource(booleans = {false, true})
  474. void shouldBlockSavingWhenCurrentMonthClosingRunsOrCompleted(boolean closed) {
  475. MonthClosingService closingService = mock(MonthClosingService.class);
  476. when(closingService.isLocked("202608")).thenReturn(true);
  477. when(closingService.status(any(MonthClosingCheckRequest.class)))
  478. .thenReturn(MonthClosingStatusResult.builder().closed(closed)
  479. .runningClosingCount(closed ? 0 : 1).build());
  480. TimeCardRepairService service = new TimeCardRepairService();
  481. ReflectionTestUtils.setField(service, "personnelPolicy", internalPolicy());
  482. ReflectionTestUtils.setField(service, "monthClosingService", closingService);
  483. TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest();
  484. request.setSubmitterUserId("user-1");
  485. request.setDayText("20260818");
  486. assertEquals("当前月份已封账或封账流程审批中,不可保存补填",
  487. assertThrows(McException.class, () -> service.upsert(request)).getMessage());
  488. }
  489. @Test
  490. void shouldAllowSubmissionPastSourceClosingCheckWhileClosingIsRunning() {
  491. MonthClosingService closingService = mock(MonthClosingService.class);
  492. YDClient client = mock(YDClient.class);
  493. TimeCardRepairSubmitRequest request = repairSubmitRequest();
  494. doCallRealMethod().when(closingService).isLocked(request.getSourceMonthText());
  495. when(closingService.status(any(MonthClosingCheckRequest.class))).thenAnswer(invocation -> {
  496. MonthClosingCheckRequest query = invocation.getArgument(0);
  497. return MonthClosingStatusResult.builder().monthText(query.getMonthText())
  498. .runningClosingCount(request.getSourceMonthText().equals(query.getMonthText()) ? 1 : 0)
  499. .build();
  500. });
  501. TimeCardRepairService service = new TimeCardRepairService();
  502. ReflectionTestUtils.setField(service, "personnelPolicy", internalPolicy());
  503. ReflectionTestUtils.setField(service, "monthClosingService", closingService);
  504. ReflectionTestUtils.setField(service, "ydClient", client);
  505. McException error = assertThrows(McException.class, () -> service.submit(request));
  506. assertEquals("没有需要提交的补填工时", error.getMessage());
  507. verifyNoInteractions(client);
  508. }
  509. @Test
  510. void shouldRejectSaveAndSubmitWhenSourceHasNoClosingProcess() {
  511. MonthClosingService closingService = mock(MonthClosingService.class);
  512. YDClient client = mock(YDClient.class);
  513. TimeCardRepairService service = new TimeCardRepairService();
  514. ReflectionTestUtils.setField(service, "personnelPolicy", internalPolicy());
  515. ReflectionTestUtils.setField(service, "monthClosingService", closingService);
  516. ReflectionTestUtils.setField(service, "ydClient", client);
  517. TimeCardRepairSummaryUpsertRequest draft = new TimeCardRepairSummaryUpsertRequest();
  518. draft.setSubmitterUserId("user-1");
  519. draft.setDayText("20260920");
  520. assertEquals("补填只能保存已发起封账或已完成封账的月份",
  521. assertThrows(McException.class, () -> service.upsert(draft)).getMessage());
  522. assertEquals("补填只能提交已发起封账或已完成封账的月份",
  523. assertThrows(McException.class, () -> service.submit(repairSubmitRequest())).getMessage());
  524. verifyNoInteractions(client);
  525. }
  526. @Test
  527. void shouldBlockRepairSubmissionWhenCurrentMonthClosingIsRunning() {
  528. assertCurrentMonthBlocksSubmit(false, 1);
  529. }
  530. @Test
  531. void shouldBlockRepairSubmissionWhenCurrentMonthIsClosed() {
  532. assertCurrentMonthBlocksSubmit(true, 0);
  533. }
  534. @Test
  535. void shouldAllowRepairSubmissionPastClosingCheckWhenCurrentMonthIsOpen() {
  536. MonthClosingService closingService = mock(MonthClosingService.class);
  537. YDClient client = mock(YDClient.class);
  538. TimeCardRepairSubmitRequest request = repairSubmitRequest();
  539. String currentMonth = YearMonth.now(ZoneId.of("Asia/Shanghai"))
  540. .format(DateTimeFormatter.ofPattern("yyyyMM"));
  541. when(closingService.isLocked(request.getSourceMonthText())).thenReturn(true);
  542. when(closingService.status(any(MonthClosingCheckRequest.class)))
  543. .thenReturn(MonthClosingStatusResult.builder().monthText(currentMonth).build());
  544. TimeCardRepairService service = new TimeCardRepairService();
  545. ReflectionTestUtils.setField(service, "personnelPolicy", internalPolicy());
  546. ReflectionTestUtils.setField(service, "monthClosingService", closingService);
  547. ReflectionTestUtils.setField(service, "ydClient", client);
  548. McException error = assertThrows(McException.class, () -> service.submit(request));
  549. assertEquals("没有需要提交的补填工时", error.getMessage());
  550. verify(closingService).status(org.mockito.ArgumentMatchers.argThat(
  551. value -> currentMonth.equals(value.getMonthText())));
  552. verifyNoInteractions(client);
  553. }
  554. private static void assertCurrentMonthBlocksSubmit(boolean closed, int runningCount) {
  555. MonthClosingService closingService = mock(MonthClosingService.class);
  556. YDClient client = mock(YDClient.class);
  557. TimeCardRepairSubmitRequest request = repairSubmitRequest();
  558. when(closingService.isLocked(request.getSourceMonthText())).thenReturn(true);
  559. when(closingService.status(any(MonthClosingCheckRequest.class)))
  560. .thenReturn(MonthClosingStatusResult.builder()
  561. .closed(closed).runningClosingCount(runningCount).build());
  562. TimeCardRepairService service = new TimeCardRepairService();
  563. ReflectionTestUtils.setField(service, "personnelPolicy", internalPolicy());
  564. ReflectionTestUtils.setField(service, "monthClosingService", closingService);
  565. ReflectionTestUtils.setField(service, "ydClient", client);
  566. McException error = assertThrows(McException.class, () -> service.submit(request));
  567. assertEquals("当前月份已封账或封账流程审批中,不可提交补填", error.getMessage());
  568. verifyNoInteractions(client);
  569. }
  570. private static TimeCardRepairSubmitRequest repairSubmitRequest() {
  571. TimeCardRepairSubmitRequest request = new TimeCardRepairSubmitRequest();
  572. request.setSubmitterUserId("user-1");
  573. request.setSourceMonthText(YearMonth.now(ZoneId.of("Asia/Shanghai"))
  574. .minusMonths(1).format(DateTimeFormatter.ofPattern("yyyyMM")));
  575. return request;
  576. }
  577. @Test
  578. void shouldAllowRepairWhenOriginalSummaryOnlyHasDraftHours() {
  579. Map<String, Object> formData = new HashMap<>();
  580. formData.put("numberField_mmd1smel", 8d);
  581. Map<String, Object> record = new HashMap<>();
  582. record.put("formData", formData);
  583. assertFalse(TimeCardRepairService.hasApprovedOrChangedHours(record));
  584. }
  585. @Test
  586. void shouldBlockRepairWhenApprovedAndChangedHoursRemain() {
  587. Map<String, Object> formData = new HashMap<>();
  588. formData.put("numberField_mmeakgi4", 8d);
  589. formData.put("numberField_mmeakgi5", -2d);
  590. assertTrue(TimeCardRepairService.hasApprovedOrChangedHours(formData));
  591. }
  592. @Test
  593. void shouldBlockRepairWhenChangeFullyOffsetsApprovedHours() {
  594. Map<String, Object> formData = new HashMap<>();
  595. formData.put("numberField_mmeakgi4", 8d);
  596. formData.put("numberField_mmeakgi5", -8d);
  597. assertTrue(TimeCardRepairService.hasApprovedOrChangedHours(formData));
  598. }
  599. @Test
  600. void shouldBlockRepairWhenOriginalApprovalStatusIsApproved() {
  601. Map<String, Object> formData = new HashMap<>();
  602. formData.put("selectField_mre1xz9g", "已审批");
  603. assertTrue(TimeCardRepairService.hasApprovedOrChangedHours(formData));
  604. }
  605. @Test
  606. void shouldNotSaveDraftOverApprovedRepairWithCorruptedDraftStatus() {
  607. YDClient client = mock(YDClient.class);
  608. MonthClosingService closingService = mock(MonthClosingService.class);
  609. WHConf conf = new WHConf();
  610. conf.setFormUuidWorkHoursSummary("original-summary");
  611. conf.setFormUuidRepairWorkHoursSummary("repair-summary");
  612. conf.setRepairSummaryFillTypeField("fill-type");
  613. conf.setRepairSummarySyncStatusField("repair-status");
  614. when(closingService.isLocked("202608")).thenReturn(true);
  615. when(closingService.status(any(MonthClosingCheckRequest.class)))
  616. .thenReturn(MonthClosingStatusResult.builder().build());
  617. DDR_New<Object> noOriginal = new DDR_New<>();
  618. noOriginal.setData(Collections.emptyList());
  619. Map<String, Object> child = new HashMap<>();
  620. child.put("numberField_mmeakgii", 8d);
  621. Map<String, Object> formData = new HashMap<>();
  622. formData.put("repair-status", "暂存");
  623. formData.put("textField_mmbffvd9", "user-1");
  624. formData.put("textField_mmbffvdb", "20260818");
  625. formData.put("tableField_mmeakgid", Collections.singletonList(child));
  626. Map<String, Object> record = new HashMap<>();
  627. record.put("formData", formData);
  628. DDR_New<Object> approvedRepair = new DDR_New<>();
  629. approvedRepair.setData(Collections.singletonList(record));
  630. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  631. .thenReturn(noOriginal, approvedRepair);
  632. TimeCardRepairService service = new TimeCardRepairService();
  633. ReflectionTestUtils.setField(service, "personnelPolicy", internalPolicy());
  634. ReflectionTestUtils.setField(service, "ydClient", client);
  635. ReflectionTestUtils.setField(service, "whConf", conf);
  636. ReflectionTestUtils.setField(service, "monthClosingService", closingService);
  637. TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest();
  638. request.setSubmitterUserId("user-1");
  639. request.setDayText("20260818");
  640. request.setFormData(Collections.singletonMap("numberField_mmd1smel", 8d));
  641. assertThrows(McException.class, () -> service.upsert(request));
  642. verify(client, never()).operateData(any(YDParam.class), eq(YDConf.FORM_OPERATION.upsert));
  643. }
  644. @ParameterizedTest
  645. @ValueSource(booleans = {false, true})
  646. void shouldCalculateRepairDraftTotalsWhenSourceClosingIsRunningOrCompleted(boolean closed) {
  647. YDClient client = mock(YDClient.class);
  648. MonthClosingService closingService = mock(MonthClosingService.class);
  649. WHConf conf = new WHConf();
  650. conf.setFormUuidWorkHoursSummary("original-summary");
  651. conf.setFormUuidRepairWorkHoursSummary("repair-summary");
  652. conf.setRepairSummaryFillTypeField("fill-type");
  653. conf.setRepairSummarySyncStatusField("repair-status");
  654. doCallRealMethod().when(closingService).isLocked("202608");
  655. when(closingService.status(any(MonthClosingCheckRequest.class))).thenAnswer(invocation -> {
  656. String month = ((MonthClosingCheckRequest) invocation.getArgument(0)).getMonthText();
  657. boolean source = "202608".equals(month);
  658. return MonthClosingStatusResult.builder().monthText(month)
  659. .closed(source && closed).runningClosingCount(source && !closed ? 1 : 0).build();
  660. });
  661. DDR_New<Object> empty = new DDR_New<>();
  662. empty.setData(Collections.emptyList());
  663. Map<String, Object> stored = new HashMap<>();
  664. Map<String, Object> storedRecord = new HashMap<>();
  665. storedRecord.put("formInstId", "saved-repair");
  666. storedRecord.put("formData", stored);
  667. DDR_New<Object> saved = new DDR_New<>();
  668. saved.setData(Collections.singletonList(storedRecord));
  669. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  670. .thenReturn(empty, empty, saved);
  671. when(client.operateData(any(YDParam.class), eq(YDConf.FORM_OPERATION.upsert))).thenAnswer(invocation -> {
  672. stored.putAll(JSON.parseObject(((YDParam) invocation.getArgument(0)).getFormDataJson(), Map.class));
  673. return true;
  674. });
  675. DDR_New<Object> storedDetail = new DDR_New<>();
  676. storedDetail.setFormData(stored);
  677. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_id))).thenReturn(storedDetail);
  678. TimeCardRepairService service = new TimeCardRepairService();
  679. ReflectionTestUtils.setField(service, "personnelPolicy", internalPolicy());
  680. ReflectionTestUtils.setField(service, "ydClient", client);
  681. ReflectionTestUtils.setField(service, "whConf", conf);
  682. ReflectionTestUtils.setField(service, "monthClosingService", closingService);
  683. Map<String, Object> child = new HashMap<>();
  684. child.put("numberField_mmczo635", 5d);
  685. Map<String, Object> payload = new HashMap<>();
  686. conf.setRepairSummarySourceMonthField("source-month");
  687. conf.setRepairSummarySubmitDateField("submit-date");
  688. payload.put("tableField_mmczo634", Collections.singletonList(child));
  689. payload.put("numberField_mmeakgi4", 9d);
  690. payload.put("numberField_mmd1smel", 99d);
  691. TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest();
  692. request.setSubmitterUserId("user-1");
  693. request.setDayText("20260818");
  694. request.setFormData(payload);
  695. assertTrue(service.upsert(request));
  696. ArgumentCaptor<YDParam> captor = ArgumentCaptor.forClass(YDParam.class);
  697. verify(client).operateData(captor.capture(), eq(YDConf.FORM_OPERATION.upsert));
  698. Map<String, Object> condition = JSON.parseObject(captor.getValue().getSearchCondition(), Map.class);
  699. Map<String, Object> written = JSON.parseObject(captor.getValue().getFormDataJson(), Map.class);
  700. assertEquals("补填", condition.get("fill-type"));
  701. assertEquals(5d, ((Number) written.get("numberField_mmd1smel")).doubleValue());
  702. assertEquals(0d, ((Number) written.get("numberField_mmeakgi4")).doubleValue());
  703. assertEquals("补填", written.get("fill-type"));
  704. assertEquals("暂存", written.get("repair-status"));
  705. assertEquals("202608", written.get("source-month"));
  706. assertEquals(LocalDate.now(ZoneId.of("Asia/Shanghai")).atStartOfDay(ZoneId.of("Asia/Shanghai"))
  707. .toInstant().toEpochMilli(), ((Number) written.get("submit-date")).longValue());
  708. }
  709. @Test
  710. void shouldIgnoreChangeRowsDuringRepairResubmitScan() {
  711. YDClient client = mock(YDClient.class);
  712. ReSubmitApprovalService approvalService = mock(ReSubmitApprovalService.class);
  713. WHConf conf = new WHConf();
  714. conf.setFormUuidRepairWorkHoursSummary("shared-summary");
  715. conf.setRepairSummaryFillTypeField("fill-type");
  716. conf.setRepairSummarySyncStatusField("sync-status");
  717. Map<String, Object> change = new HashMap<>();
  718. change.put("fill-type", "变更");
  719. change.put("sync-status", "审批中");
  720. Map<String, Object> record = new HashMap<>();
  721. record.put("formData", change);
  722. DDR_New<Object> summaries = new DDR_New<>();
  723. summaries.setData(Collections.singletonList(record));
  724. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  725. .thenReturn(summaries);
  726. TimeCardRepairService service = new TimeCardRepairService();
  727. ReflectionTestUtils.setField(service, "personnelPolicy", internalPolicy());
  728. ReflectionTestUtils.setField(service, "ydClient", client);
  729. ReflectionTestUtils.setField(service, "whConf", conf);
  730. ReflectionTestUtils.setField(service, "reSubmitApprovalService", approvalService);
  731. TimeCardRepairService.RepairResubmitResult result =
  732. service.resubmitRecentlyModifiedPending(7);
  733. assertEquals(0, result.getStarted());
  734. assertEquals(0, result.getFailed());
  735. verifyNoInteractions(approvalService);
  736. }
  737. @Test
  738. void shouldTreatBlankLegacyTypeAndRepairTypeAsRepairOnly() {
  739. assertTrue(TimeCardRepairService.isRepairSummaryRecord(
  740. Collections.emptyMap(), "fill-type"));
  741. assertTrue(TimeCardRepairService.isRepairSummaryRecord(
  742. Collections.singletonMap("fill-type", "补填"), "fill-type"));
  743. assertFalse(TimeCardRepairService.isRepairSummaryRecord(
  744. Collections.singletonMap("fill-type", "变更"), "fill-type"));
  745. }
  746. @Test
  747. void shouldReportRepairBatchStillFailedAfterDeferredRetry() {
  748. YDClient client = mock(YDClient.class);
  749. ReSubmitApprovalService approvalService = mock(ReSubmitApprovalService.class);
  750. WHConf conf = new WHConf();
  751. conf.setFormUuidRepairWorkHoursSummary("repair-summary");
  752. conf.setFormUuidApproval("bill-approval");
  753. conf.setFormUuidOtherApproval("other-approval");
  754. conf.setRepairSummaryFillTypeField("fill-type");
  755. conf.setRepairSummarySyncStatusField("repair-status");
  756. conf.setRepairSummaryBatchField("repair-batch");
  757. conf.setRepairSummarySubmitDateField("repair-submit-date");
  758. conf.setApprovalFillTypeField("fill-type");
  759. conf.setOtherApprovalRepairSubmitDateField("other-submit-date");
  760. conf.setOtherApprovalRepairBatchField("other-batch");
  761. conf.setOtherApprovalRepairSourceMonthField("other-source-month");
  762. Map<String, Object> otherRow = new HashMap<>();
  763. otherRow.put("numberField_mmeakgij", 8d);
  764. otherRow.put("numberField_mmeakgii", 0d);
  765. otherRow.put("selectField_mmeakgie", "培训");
  766. Map<String, Object> formData = new HashMap<>();
  767. formData.put("fill-type", "补填");
  768. formData.put("repair-status", "审批中");
  769. formData.put("repair-batch", "batch-1");
  770. formData.put("repair-submit-date", 1787000000000L);
  771. formData.put("textField_mmbffvd9", "user-1");
  772. formData.put("textField_mmbffvda", "202608");
  773. formData.put("textField_mmbffvdb", "20260818");
  774. formData.put("tableField_mmeakgid", Collections.singletonList(otherRow));
  775. Map<String, Object> record = new HashMap<>();
  776. record.put("formData", formData);
  777. DDR_New<Object> summaries = new DDR_New<>();
  778. summaries.setData(Collections.singletonList(record));
  779. DDR_New<Object> noApprovals = new DDR_New<>();
  780. noApprovals.setData(Collections.emptyList());
  781. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  782. .thenReturn(summaries);
  783. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process)))
  784. .thenReturn(noApprovals);
  785. when(approvalService.startApproval(any(TimeCardApprovalStartRequest.class)))
  786. .thenThrow(new IllegalStateException("temporary failure"));
  787. TimeCardRepairService service = new TimeCardRepairService();
  788. ReflectionTestUtils.setField(service, "personnelPolicy", internalPolicy());
  789. ReflectionTestUtils.setField(service, "ydClient", client);
  790. ReflectionTestUtils.setField(service, "whConf", conf);
  791. ReflectionTestUtils.setField(service, "reSubmitApprovalService", approvalService);
  792. TimeCardRepairService.RepairResubmitResult result =
  793. service.resubmitRecentlyModifiedPending(7);
  794. assertEquals(0, result.getStarted());
  795. assertEquals(1, result.getFailed());
  796. verify(approvalService, times(2)).startApproval(any(TimeCardApprovalStartRequest.class));
  797. }
  798. }