MonthClosingServiceTest.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. package com.malk.service.workhours;
  2. import com.malk.server.aliwork.YDConf;
  3. import com.malk.server.aliwork.YDParam;
  4. import com.malk.server.common.McException;
  5. import com.malk.server.dingtalk.DDR_New;
  6. import com.malk.server.workhours.MonthClosingCheckRequest;
  7. import com.malk.server.workhours.MonthClosingConf;
  8. import com.malk.server.workhours.MonthClosingStatusResult;
  9. import com.malk.server.workhours.MonthClosingValidationResult;
  10. import com.malk.server.workhours.WHConf;
  11. import com.malk.service.aliwork.YDClient;
  12. import org.junit.jupiter.api.Test;
  13. import org.mockito.invocation.InvocationOnMock;
  14. import org.springframework.test.util.ReflectionTestUtils;
  15. import java.util.ArrayList;
  16. import java.util.Arrays;
  17. import java.util.Collections;
  18. import java.util.HashMap;
  19. import java.util.List;
  20. import java.util.Map;
  21. import static org.junit.jupiter.api.Assertions.assertEquals;
  22. import static org.junit.jupiter.api.Assertions.assertFalse;
  23. import static org.junit.jupiter.api.Assertions.assertThrows;
  24. import static org.junit.jupiter.api.Assertions.assertTrue;
  25. import static org.mockito.ArgumentMatchers.any;
  26. import static org.mockito.ArgumentMatchers.eq;
  27. import static org.mockito.Mockito.mock;
  28. import static org.mockito.Mockito.verifyNoInteractions;
  29. import static org.mockito.Mockito.when;
  30. class MonthClosingServiceTest {
  31. private static final String CLOSING_FORM = "FORM-CLOSING";
  32. private static final String PROJECT_FORM = "FORM-PROJECT-APPROVAL";
  33. private static final String OTHER_FORM = "FORM-OTHER-APPROVAL";
  34. private static final String SUMMARY_FORM = "FORM-SUMMARY";
  35. private static final String REPAIR_SUMMARY_FORM = "FORM-REPAIR-SUMMARY";
  36. private static final String CLOSING_MONTH_FIELD = "dateField_closing_month";
  37. private static final String CLOSING_STATUS_FIELD = "radioField_closing_status";
  38. private static final String SYNC_STATUS_FIELD = "selectField_sync_status";
  39. private static final String SUMMARY_STATUS_FIELD = "selectField_summary_status";
  40. private static final String APPROVAL_MONTH_FIELD = "textField_mmd2lv0y";
  41. private static final String FILL_TYPE_FIELD = "radioField_fill_type";
  42. private static final String PROJECT_REPAIR_SUBMIT_DATE_FIELD = "dateField_project_repair_submit";
  43. private static final String OTHER_REPAIR_SUBMIT_DATE_FIELD = "dateField_other_repair_submit";
  44. private static final String REPAIR_SUMMARY_SUBMIT_DATE_FIELD = "dateField_repair_summary_submit";
  45. private static final String REPAIR_SUMMARY_STATUS_FIELD = "selectField_repair_summary_status";
  46. @Test
  47. void shouldRejectInvalidMonthBeforeQueryingYida() {
  48. YDClient ydClient = mock(YDClient.class);
  49. MonthClosingService service = service(ydClient);
  50. assertThrows(
  51. McException.class,
  52. () -> service.status(request("202613")));
  53. verifyNoInteractions(ydClient);
  54. }
  55. @Test
  56. void shouldAllowClosingWhenNoApprovalIsPending() {
  57. YDClient ydClient = mock(YDClient.class);
  58. when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
  59. .thenReturn(response(Collections.emptyList()));
  60. MonthClosingService service = service(ydClient);
  61. MonthClosingValidationResult result = service.validate(request("202608"));
  62. assertTrue(result.isAllowed());
  63. assertFalse(result.isAlreadyClosed());
  64. assertEquals(0, result.getProjectRunningCount());
  65. assertEquals(0, result.getOtherRunningCount());
  66. assertEquals(0, result.getWriteBackPendingCount());
  67. assertEquals(0, result.getSummaryPendingCount());
  68. }
  69. @Test
  70. void shouldBlockClosingWhenProjectApprovalIsRunning() {
  71. YDClient ydClient = mock(YDClient.class);
  72. when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
  73. .thenAnswer(this::queryResponse);
  74. MonthClosingService service = service(ydClient);
  75. MonthClosingValidationResult result = service.validate(request("202608"));
  76. assertFalse(result.isAllowed());
  77. assertEquals(1, result.getProjectRunningCount());
  78. assertTrue(result.getMessage().contains("项目工时 1 张"));
  79. }
  80. @Test
  81. void shouldTreatApprovedClosingProcessAsClosed() {
  82. YDClient ydClient = mock(YDClient.class);
  83. when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process)))
  84. .thenAnswer(invocation -> {
  85. YDParam param = invocation.getArgument(0);
  86. if (CLOSING_FORM.equals(param.getFormUuid())
  87. && "COMPLETED".equals(param.getInstanceStatus())) {
  88. Map<String, Object> formData = new HashMap<>();
  89. formData.put(CLOSING_MONTH_FIELD, "2026-08-01");
  90. formData.put(CLOSING_STATUS_FIELD, "封账");
  91. Map<String, Object> item = new HashMap<>();
  92. item.put("approvedResult", "agree");
  93. item.put("modifiedTimeGMT", 1_777_590_000_000L);
  94. item.put("formData", formData);
  95. return response(Collections.singletonList(item));
  96. }
  97. return response(Collections.emptyList());
  98. });
  99. MonthClosingService service = service(ydClient);
  100. MonthClosingStatusResult result = service.status(request("202608"));
  101. assertTrue(result.isClosed());
  102. assertEquals(1, result.getApprovedClosingCount());
  103. assertEquals("封账", result.getCurrentStatus());
  104. }
  105. @Test
  106. void shouldTreatLatestApprovedReopeningAsOpen() {
  107. YDClient ydClient = mock(YDClient.class);
  108. when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process)))
  109. .thenAnswer(invocation -> {
  110. YDParam param = invocation.getArgument(0);
  111. if (CLOSING_FORM.equals(param.getFormUuid())
  112. && "COMPLETED".equals(param.getInstanceStatus())) {
  113. return response(Arrays.asList(
  114. closingAction("启封", 1_777_676_400_000L),
  115. closingAction("封账", 1_777_590_000_000L)));
  116. }
  117. return response(Collections.emptyList());
  118. });
  119. MonthClosingService service = service(ydClient);
  120. MonthClosingStatusResult result = service.status(request("202608"));
  121. assertFalse(result.isClosed());
  122. assertEquals("启封", result.getCurrentStatus());
  123. assertEquals(1, result.getApprovedClosingCount());
  124. assertEquals(1, result.getApprovedReopeningCount());
  125. }
  126. @Test
  127. void shouldAllowReopeningOnlyWhenMonthIsClosed() {
  128. YDClient ydClient = mock(YDClient.class);
  129. when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
  130. .thenAnswer(invocation -> {
  131. YDParam param = invocation.getArgument(0);
  132. if (CLOSING_FORM.equals(param.getFormUuid())
  133. && "COMPLETED".equals(param.getInstanceStatus())) {
  134. return response(Collections.singletonList(
  135. closingAction("封账", 1_777_590_000_000L)));
  136. }
  137. return response(Collections.emptyList());
  138. });
  139. MonthClosingService service = service(ydClient);
  140. MonthClosingValidationResult result = service.validate(request("202608", "启封"));
  141. assertTrue(result.isAllowed());
  142. assertEquals("启封", result.getRequestedStatus());
  143. assertEquals("封账", result.getCurrentStatus());
  144. }
  145. @Test
  146. void shouldBlockReopeningWhenMonthIsAlreadyOpen() {
  147. YDClient ydClient = mock(YDClient.class);
  148. when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
  149. .thenReturn(response(Collections.emptyList()));
  150. MonthClosingService service = service(ydClient);
  151. MonthClosingValidationResult result = service.validate(request("202608", "启封"));
  152. assertFalse(result.isAllowed());
  153. assertTrue(result.getMessage().contains("当前未封账"));
  154. }
  155. @Test
  156. void shouldNormalizeDateFieldValuesToMonth() {
  157. assertEquals("202608", MonthClosingService.monthFromDateValue("2026-08-01"));
  158. assertEquals(
  159. "202608",
  160. MonthClosingService.monthFromDateValue(
  161. java.time.Instant.parse("2026-08-01T00:00:00Z").toEpochMilli()));
  162. }
  163. @Test
  164. void shouldBlockCompletedApprovalWhoseWriteBackIsNotSuccessful() {
  165. YDClient ydClient = mock(YDClient.class);
  166. when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
  167. .thenAnswer(invocation -> {
  168. YDParam param = invocation.getArgument(0);
  169. YDConf.FORM_QUERY queryType = invocation.getArgument(1);
  170. if (queryType == YDConf.FORM_QUERY.retrieve_search_process
  171. && PROJECT_FORM.equals(param.getFormUuid())
  172. && "COMPLETED".equals(param.getInstanceStatus())) {
  173. Map<String, Object> formData = new HashMap<>();
  174. formData.put(SYNC_STATUS_FIELD, "部分失败");
  175. Map<String, Object> item = new HashMap<>();
  176. item.put("approvedResult", "agree");
  177. item.put("formData", formData);
  178. return response(Collections.singletonList(item));
  179. }
  180. return response(Collections.emptyList());
  181. });
  182. MonthClosingService service = service(ydClient);
  183. MonthClosingValidationResult result = service.validate(request("202608"));
  184. assertFalse(result.isAllowed());
  185. assertEquals(1, result.getWriteBackPendingCount());
  186. }
  187. @Test
  188. void shouldAssignRunningRepairApprovalToSubmitMonthInsteadOfSourceMonth() {
  189. YDClient ydClient = mock(YDClient.class);
  190. when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
  191. .thenAnswer(invocation -> {
  192. YDParam param = invocation.getArgument(0);
  193. if (PROJECT_FORM.equals(param.getFormUuid())
  194. && "RUNNING".equals(param.getInstanceStatus())) {
  195. return response(Collections.singletonList(
  196. repairApproval(PROJECT_REPAIR_SUBMIT_DATE_FIELD,
  197. "2026-09-15", "", "")));
  198. }
  199. return response(Collections.emptyList());
  200. });
  201. MonthClosingService service = service(ydClient);
  202. MonthClosingValidationResult september = service.validate(request("202609"));
  203. MonthClosingValidationResult august = service.validate(request("202608"));
  204. assertFalse(september.isAllowed());
  205. assertEquals(1, september.getProjectRunningCount());
  206. assertTrue(august.isAllowed());
  207. }
  208. @Test
  209. void shouldBlockClosingWhenOtherRepairApprovalAwaitsWriteBack() {
  210. YDClient ydClient = mock(YDClient.class);
  211. when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
  212. .thenAnswer(invocation -> {
  213. YDParam param = invocation.getArgument(0);
  214. if (OTHER_FORM.equals(param.getFormUuid())
  215. && "COMPLETED".equals(param.getInstanceStatus())) {
  216. return response(Collections.singletonList(
  217. repairApproval(OTHER_REPAIR_SUBMIT_DATE_FIELD,
  218. "2026-09-15", "agree", "部分失败")));
  219. }
  220. return response(Collections.emptyList());
  221. });
  222. MonthClosingService service = service(ydClient);
  223. MonthClosingValidationResult result = service.validate(request("202609"));
  224. assertFalse(result.isAllowed());
  225. assertEquals(1, result.getWriteBackPendingCount());
  226. }
  227. @Test
  228. void shouldAllowClosingAfterRepairApprovalAndSummaryWriteBackFinish() {
  229. YDClient ydClient = mock(YDClient.class);
  230. when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
  231. .thenAnswer(invocation -> {
  232. YDParam param = invocation.getArgument(0);
  233. if (OTHER_FORM.equals(param.getFormUuid())
  234. && "COMPLETED".equals(param.getInstanceStatus())) {
  235. return response(Collections.singletonList(
  236. repairApproval(OTHER_REPAIR_SUBMIT_DATE_FIELD,
  237. "2026-09-15", "agree", "全部成功")));
  238. }
  239. if (REPAIR_SUMMARY_FORM.equals(param.getFormUuid())) {
  240. Map<String, Object> data = new HashMap<>();
  241. data.put(REPAIR_SUMMARY_SUBMIT_DATE_FIELD, "2026-09-15");
  242. data.put(REPAIR_SUMMARY_STATUS_FIELD, "全部成功");
  243. Map<String, Object> item = new HashMap<>();
  244. item.put("formData", data);
  245. return response(Collections.singletonList(item));
  246. }
  247. return response(Collections.emptyList());
  248. });
  249. MonthClosingService service = service(ydClient);
  250. MonthClosingValidationResult result = service.validate(request("202609"));
  251. assertTrue(result.isAllowed());
  252. assertEquals(0, result.getWriteBackPendingCount());
  253. assertEquals(0, result.getSummaryPendingCount());
  254. }
  255. @Test
  256. void shouldBlockClosingWhileRepairSummaryWriteBackIsPartial() {
  257. YDClient ydClient = mock(YDClient.class);
  258. when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
  259. .thenAnswer(invocation -> {
  260. YDParam param = invocation.getArgument(0);
  261. if (REPAIR_SUMMARY_FORM.equals(param.getFormUuid())) {
  262. Map<String, Object> data = new HashMap<>();
  263. data.put(REPAIR_SUMMARY_SUBMIT_DATE_FIELD, "2026-09-15");
  264. data.put(REPAIR_SUMMARY_STATUS_FIELD, "部分成功");
  265. Map<String, Object> item = new HashMap<>();
  266. item.put("formData", data);
  267. return response(Collections.singletonList(item));
  268. }
  269. return response(Collections.emptyList());
  270. });
  271. MonthClosingService service = service(ydClient);
  272. MonthClosingValidationResult result = service.validate(request("202609"));
  273. MonthClosingValidationResult previousMonth = service.validate(request("202608"));
  274. assertFalse(result.isAllowed());
  275. assertEquals(1, result.getSummaryPendingCount());
  276. assertTrue(previousMonth.isAllowed());
  277. }
  278. @Test
  279. void shouldKeepBlockingClosingForNormalSummaryInApproval() {
  280. YDClient ydClient = mock(YDClient.class);
  281. when(ydClient.queryData(any(YDParam.class), any(YDConf.FORM_QUERY.class)))
  282. .thenAnswer(invocation -> {
  283. YDParam param = invocation.getArgument(0);
  284. if (SUMMARY_FORM.equals(param.getFormUuid())) {
  285. Map<String, Object> data = new HashMap<>();
  286. data.put("textField_mmbffvda", "202609");
  287. data.put(SUMMARY_STATUS_FIELD, "审批中");
  288. Map<String, Object> item = new HashMap<>();
  289. item.put("formData", data);
  290. return response(Collections.singletonList(item));
  291. }
  292. return response(Collections.emptyList());
  293. });
  294. MonthClosingService service = service(ydClient);
  295. MonthClosingValidationResult result = service.validate(request("202609"));
  296. assertFalse(result.isAllowed());
  297. assertEquals(1, result.getSummaryPendingCount());
  298. }
  299. private static Map<String, Object> repairApproval(String submitDateField,
  300. String submitDate, String result,
  301. String syncStatus) {
  302. Map<String, Object> formData = new HashMap<>();
  303. formData.put(APPROVAL_MONTH_FIELD, "202608");
  304. formData.put(FILL_TYPE_FIELD, "补填");
  305. formData.put(submitDateField, submitDate);
  306. formData.put(SYNC_STATUS_FIELD, syncStatus);
  307. Map<String, Object> item = new HashMap<>();
  308. item.put("approvedResult", result);
  309. item.put("formData", formData);
  310. return item;
  311. }
  312. private DDR_New queryResponse(InvocationOnMock invocation) {
  313. YDParam param = invocation.getArgument(0);
  314. YDConf.FORM_QUERY queryType = invocation.getArgument(1);
  315. if (queryType == YDConf.FORM_QUERY.retrieve_search_process
  316. && PROJECT_FORM.equals(param.getFormUuid())
  317. && "RUNNING".equals(param.getInstanceStatus())) {
  318. Map<String, Object> item = new HashMap<>();
  319. item.put("processInstanceId", "PROC-RUNNING");
  320. return response(Collections.singletonList(item));
  321. }
  322. return response(Collections.emptyList());
  323. }
  324. private MonthClosingService service(YDClient ydClient) {
  325. WHConf whConf = new WHConf();
  326. whConf.setYidaAppType("APP-TEST");
  327. whConf.setYidaSystemToken("TOKEN-TEST");
  328. whConf.setFormUuidApproval(PROJECT_FORM);
  329. whConf.setFormUuidOtherApproval(OTHER_FORM);
  330. whConf.setFormUuidWorkHoursSummary(SUMMARY_FORM);
  331. whConf.setFormUuidRepairWorkHoursSummary(REPAIR_SUMMARY_FORM);
  332. whConf.setApprovalSyncStatusField(SYNC_STATUS_FIELD);
  333. whConf.setOtherApprovalSyncStatusField(SYNC_STATUS_FIELD);
  334. whConf.setSummaryApprovalStatusField(SUMMARY_STATUS_FIELD);
  335. whConf.setApprovalFillTypeField(FILL_TYPE_FIELD);
  336. whConf.setApprovalRepairSubmitDateField(PROJECT_REPAIR_SUBMIT_DATE_FIELD);
  337. whConf.setOtherApprovalRepairSubmitDateField(OTHER_REPAIR_SUBMIT_DATE_FIELD);
  338. whConf.setRepairSummarySubmitDateField(REPAIR_SUMMARY_SUBMIT_DATE_FIELD);
  339. whConf.setRepairSummarySyncStatusField(REPAIR_SUMMARY_STATUS_FIELD);
  340. MonthClosingConf closingConf = new MonthClosingConf();
  341. closingConf.setFormUuid(CLOSING_FORM);
  342. closingConf.setMonthDateField(CLOSING_MONTH_FIELD);
  343. closingConf.setStatusField(CLOSING_STATUS_FIELD);
  344. MonthClosingService service = new MonthClosingService();
  345. ReflectionTestUtils.setField(service, "ydClient", ydClient);
  346. ReflectionTestUtils.setField(service, "whConf", whConf);
  347. ReflectionTestUtils.setField(service, "closingConf", closingConf);
  348. return service;
  349. }
  350. private static MonthClosingCheckRequest request(String monthText) {
  351. return request(monthText, "封账");
  352. }
  353. private static MonthClosingCheckRequest request(String monthText, String closingStatus) {
  354. return MonthClosingCheckRequest.builder()
  355. .monthText(monthText)
  356. .closingStatus(closingStatus)
  357. .build();
  358. }
  359. private static Map<String, Object> closingAction(String status, long modifiedTime) {
  360. Map<String, Object> formData = new HashMap<>();
  361. formData.put(CLOSING_MONTH_FIELD, "2026-08-01");
  362. formData.put(CLOSING_STATUS_FIELD, status);
  363. Map<String, Object> item = new HashMap<>();
  364. item.put("approvedResult", "agree");
  365. item.put("modifiedTimeGMT", modifiedTime);
  366. item.put("formData", formData);
  367. return item;
  368. }
  369. private static DDR_New response(List<Map> data) {
  370. DDR_New response = new DDR_New();
  371. response.setData(new ArrayList<>(data));
  372. response.setTotalCount(data.size());
  373. return response;
  374. }
  375. }