TimeCardChangeServiceTest.java 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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.TimeCardRepairSubmitRequest;
  11. import com.malk.server.workhours.TimeCardRepairSummaryUpsertRequest;
  12. import com.malk.server.workhours.WHConf;
  13. import com.malk.service.aliwork.YDClient;
  14. import org.junit.jupiter.api.Test;
  15. import org.mockito.ArgumentCaptor;
  16. import org.springframework.test.util.ReflectionTestUtils;
  17. import java.time.YearMonth;
  18. import java.time.ZoneId;
  19. import java.time.format.DateTimeFormatter;
  20. import java.util.Arrays;
  21. import java.util.Collections;
  22. import java.util.LinkedHashMap;
  23. import java.util.List;
  24. import java.util.Map;
  25. import static org.junit.jupiter.api.Assertions.assertEquals;
  26. import static org.junit.jupiter.api.Assertions.assertThrows;
  27. import static org.junit.jupiter.api.Assertions.assertTrue;
  28. import static org.mockito.ArgumentMatchers.any;
  29. import static org.mockito.ArgumentMatchers.eq;
  30. import static org.mockito.Mockito.mock;
  31. import static org.mockito.Mockito.never;
  32. import static org.mockito.Mockito.verify;
  33. import static org.mockito.Mockito.when;
  34. class TimeCardChangeServiceTest {
  35. private static final String USER = "user-1";
  36. private static final String DAY = "20260818";
  37. private static final String NEXT_DAY = "20260819";
  38. @Test
  39. void shouldSaveZeroTargetAsChangeDraftWithoutTouchingRepairType() {
  40. Fixture fixture = fixture();
  41. when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  42. .thenReturn(records(record("formal-1")), records());
  43. when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_id)))
  44. .thenReturn(detail(formalSummary(8d)));
  45. TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest();
  46. request.setSubmitterUserId(USER);
  47. request.setDayText(DAY);
  48. request.setFormData(summaryWithBillableRows(billable("P-001", 0d, 0d, 0d, 0d)));
  49. assertTrue(fixture.service.upsert(request));
  50. ArgumentCaptor<YDParam> captor = ArgumentCaptor.forClass(YDParam.class);
  51. verify(fixture.client).operateData(captor.capture(), eq(YDConf.FORM_OPERATION.upsert));
  52. Map<String, Object> condition = JSON.parseObject(captor.getValue().getSearchCondition(), Map.class);
  53. Map<String, Object> written = JSON.parseObject(captor.getValue().getFormDataJson(), Map.class);
  54. List<Map> rows = (List<Map>) written.get("tableField_mmczo634");
  55. assertEquals("变更", condition.get("fill-type"));
  56. assertEquals("变更", written.get("fill-type"));
  57. assertEquals("暂存", written.get("sync-status"));
  58. assertEquals(0d, ((Number) rows.get(0).get("numberField_mmczo635")).doubleValue());
  59. assertEquals(8d, ((Number) rows.get(0).get("numberField_mmczo637")).doubleValue());
  60. assertEquals(0d, ((Number) rows.get(0).get("numberField_mmd80zae")).doubleValue());
  61. }
  62. @Test
  63. void shouldRejectChangeWhenFormalDailyApprovedHoursAreNotPositive() {
  64. Fixture fixture = fixture();
  65. when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  66. .thenReturn(records(record("formal-1")));
  67. when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_id)))
  68. .thenReturn(detail(formalSummary(0d)));
  69. TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest();
  70. request.setSubmitterUserId(USER);
  71. request.setDayText(DAY);
  72. request.setFormData(summaryWithBillableRows(billable("P-001", 6d, 0d, 0d, 0d)));
  73. McException error = assertThrows(McException.class, () -> fixture.service.upsert(request));
  74. assertEquals("目标日期已审批工时(日)必须大于0,不能发起工时变更", error.getMessage());
  75. verify(fixture.client, never()).operateData(any(YDParam.class), any(YDConf.FORM_OPERATION.class));
  76. }
  77. @Test
  78. void shouldUseLatestApprovedChangeAsBaselineForRepeatedChange() {
  79. Fixture fixture = fixture();
  80. Map<String, Object> previousChange = summaryWithBillableRows(
  81. billable("P-001", 6d, 6d, 6d, -2d));
  82. previousChange.put("sync-status", "全部成功");
  83. when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  84. .thenReturn(records(record("formal-1")), records(record("change-1")));
  85. when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_id)))
  86. .thenReturn(detail(formalSummary(8d)), detail(previousChange));
  87. TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest();
  88. request.setSubmitterUserId(USER);
  89. request.setDayText(DAY);
  90. request.setFormData(summaryWithBillableRows(billable("P-001", 5d, 0d, 0d, 0d)));
  91. assertTrue(fixture.service.upsert(request));
  92. ArgumentCaptor<YDParam> captor = ArgumentCaptor.forClass(YDParam.class);
  93. verify(fixture.client).operateData(captor.capture(), eq(YDConf.FORM_OPERATION.upsert));
  94. Map<String, Object> written = JSON.parseObject(captor.getValue().getFormDataJson(), Map.class);
  95. List<Map> rows = (List<Map>) written.get("tableField_mmczo634");
  96. assertEquals(5d, ((Number) rows.get(0).get("numberField_mmczo635")).doubleValue());
  97. assertEquals(6d, ((Number) rows.get(0).get("numberField_mmczo637")).doubleValue());
  98. assertEquals(0d, ((Number) rows.get(0).get("numberField_mmd80zae")).doubleValue());
  99. }
  100. @Test
  101. void shouldSubmitOnlyChangedRowsAndKeepZeroTarget() {
  102. Fixture fixture = fixture();
  103. Map<String, Object> baseline = summaryWithBillableRows(
  104. billable("P-001", 8d, 8d, 8d, 0d),
  105. billable("P-002", 4d, 4d, 4d, 0d));
  106. baseline.put("sync-status", "暂存");
  107. Map<String, Object> submitted = summaryWithBillableRows(
  108. billable("P-001", 0d, 0d, 8d, -8d),
  109. billable("P-002", 4d, 4d, 4d, 0d));
  110. submitted.put("sync-status", "暂存");
  111. when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  112. .thenReturn(records(record("formal-1")), records(record("change-1")),
  113. records(record("formal-1")), records(record("change-1")));
  114. when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_id)))
  115. .thenReturn(detail(formalSummary(8d)), detail(baseline), detail(submitted),
  116. detail(formalSummary(8d)), detail(submitted));
  117. Map<String, Object> group = new LinkedHashMap<>();
  118. group.put("managerId", "manager-1");
  119. group.put("billableRows", Arrays.asList(
  120. changeRow("P-001", 0d), changeRow("P-002", 4d)));
  121. group.put("nonBillableRows", Collections.emptyList());
  122. Map<String, Object> groups = new LinkedHashMap<>();
  123. groups.put("manager-1", group);
  124. TimeCardRepairSubmitRequest request = new TimeCardRepairSubmitRequest();
  125. request.setSubmitterUserId(USER);
  126. request.setSubmitterName("Test User");
  127. request.setSourceMonthText("202608");
  128. request.setProjectGroups(groups);
  129. request.setOtherGroup(Collections.emptyMap());
  130. Map<String, Object> result = fixture.service.submit(request);
  131. assertEquals("审批中", result.get("status"));
  132. ArgumentCaptor<TimeCardApprovalStartRequest> approvalCaptor =
  133. ArgumentCaptor.forClass(TimeCardApprovalStartRequest.class);
  134. verify(fixture.approvalService).startApproval(approvalCaptor.capture());
  135. Map<String, Object> approval = JSON.parseObject(
  136. approvalCaptor.getValue().getFormDataJson(), Map.class);
  137. List<Map> approvalRows = (List<Map>) approval.get("tableField_mmae8t99");
  138. assertEquals("变更", approval.get("approval-fill-type"));
  139. assertEquals(1, approvalRows.size());
  140. assertEquals("P-001", approvalRows.get(0).get("textField_mmacxew3"));
  141. assertEquals(0d, ((Number) approvalRows.get(0).get("numberField_mmacxew9")).doubleValue());
  142. }
  143. @Test
  144. void shouldSubmitOppositeDayChangesWhenNetDeltaIsZero() {
  145. Fixture fixture = fixture();
  146. Map<String, Object> firstBaseline = summaryWithBillableRows(
  147. billable("P-001", 0d, 8d, 8d, 0d));
  148. firstBaseline.put("sync-status", "暂存");
  149. Map<String, Object> firstSubmitted = summaryWithBillableRows(
  150. billable("P-001", 0d, 0d, 8d, -8d));
  151. firstSubmitted.put("sync-status", "审批中");
  152. Map<String, Object> secondBaseline = summaryWithBillableRows(
  153. billable("P-001", 8d, 8d, 8d, 0d),
  154. billable("P-002", 0d, 0d, 0d, 0d));
  155. secondBaseline.put("sync-status", "暂存");
  156. Map<String, Object> secondSubmitted = summaryWithBillableRows(
  157. billable("P-001", 8d, 8d, 8d, 0d),
  158. billable("P-002", 8d, 8d, 0d, 8d));
  159. secondSubmitted.put("sync-status", "审批中");
  160. when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  161. .thenReturn(records(record("formal-1")), records(record("change-1")),
  162. records(record("formal-2")), records(record("change-2")));
  163. when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_id)))
  164. .thenReturn(detail(formalSummary(8d)), detail(firstBaseline), detail(firstSubmitted),
  165. detail(formalSummary(8d)), detail(secondBaseline), detail(secondSubmitted));
  166. Map<String, Object> group = new LinkedHashMap<>();
  167. group.put("managerId", "manager-1");
  168. group.put("billableRows", Arrays.asList(
  169. changeRow(DAY, "P-001", 0d), changeRow(NEXT_DAY, "P-002", 8d)));
  170. group.put("nonBillableRows", Collections.emptyList());
  171. Map<String, Object> groups = new LinkedHashMap<>();
  172. groups.put("manager-1", group);
  173. TimeCardRepairSubmitRequest request = new TimeCardRepairSubmitRequest();
  174. request.setSubmitterUserId(USER);
  175. request.setSubmitterName("Test User");
  176. request.setSourceMonthText("202608");
  177. request.setProjectGroups(groups);
  178. request.setOtherGroup(Collections.emptyMap());
  179. Map<String, Object> result = fixture.service.submit(request);
  180. assertEquals("审批中", result.get("status"));
  181. ArgumentCaptor<TimeCardApprovalStartRequest> approvalCaptor =
  182. ArgumentCaptor.forClass(TimeCardApprovalStartRequest.class);
  183. verify(fixture.approvalService).startApproval(approvalCaptor.capture());
  184. Map<String, Object> approval = JSON.parseObject(
  185. approvalCaptor.getValue().getFormDataJson(), Map.class);
  186. List<Map> approvalRows = (List<Map>) approval.get("tableField_mmae8t99");
  187. assertEquals(2, approvalRows.size());
  188. assertEquals(0d, ((Number) approvalRows.get(0).get("numberField_mmacxew9")).doubleValue());
  189. assertEquals(8d, ((Number) approvalRows.get(1).get("numberField_mmacxew9")).doubleValue());
  190. }
  191. @Test
  192. void shouldIgnoreRepairRowsDuringChangeResubmitScan() {
  193. Fixture fixture = fixture();
  194. Map<String, Object> repair = new LinkedHashMap<>();
  195. repair.put("fill-type", "补填");
  196. repair.put("sync-status", "审批中");
  197. when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
  198. .thenReturn(records(Collections.singletonMap("formData", repair)));
  199. TimeCardChangeService.ChangeResubmitResult result =
  200. fixture.service.resubmitRecentlyModifiedPending(7);
  201. assertEquals(0, result.getStarted());
  202. assertEquals(0, result.getFailed());
  203. verify(fixture.approvalService, never()).startApproval(any(TimeCardApprovalStartRequest.class));
  204. }
  205. private static Fixture fixture() {
  206. YDClient client = mock(YDClient.class);
  207. MonthClosingService closingService = mock(MonthClosingService.class);
  208. ReSubmitApprovalService approvalService = mock(ReSubmitApprovalService.class);
  209. when(closingService.status(any(MonthClosingCheckRequest.class)))
  210. .thenReturn(MonthClosingStatusResult.builder()
  211. .monthText(currentMonth()).closed(false).runningClosingCount(0).build());
  212. WHConf conf = new WHConf();
  213. conf.setYidaAppType("APP");
  214. conf.setYidaSystemToken("TOKEN");
  215. conf.setFormUuidWorkHoursSummary("formal-summary");
  216. conf.setFormUuidRepairWorkHoursSummary("shared-summary");
  217. conf.setRepairSummaryFillTypeField("fill-type");
  218. conf.setRepairSummarySyncStatusField("sync-status");
  219. conf.setRepairSummarySubmitDateField("submit-date");
  220. conf.setRepairSummaryBatchField("batch");
  221. conf.setRepairSummarySourceMonthField("source-month");
  222. conf.setApprovalFillTypeField("approval-fill-type");
  223. conf.setFormUuidApproval("project-approval");
  224. conf.setApprovalProcessCode("project-process");
  225. conf.setApprovalRepairSubmitDateField("approval-submit-date");
  226. conf.setApprovalRepairBatchField("approval-batch");
  227. conf.setApprovalRepairSourceMonthField("approval-source-month");
  228. conf.setFormUuidOtherApproval("other-approval");
  229. conf.setOtherApprovalProcessCode("other-process");
  230. conf.setOtherApprovalRepairSubmitDateField("other-submit-date");
  231. conf.setOtherApprovalRepairBatchField("other-batch");
  232. conf.setOtherApprovalRepairSourceMonthField("other-source-month");
  233. TimeCardChangeService service = new TimeCardChangeService();
  234. ReflectionTestUtils.setField(service, "ydClient", client);
  235. ReflectionTestUtils.setField(service, "whConf", conf);
  236. ReflectionTestUtils.setField(service, "monthClosingService", closingService);
  237. ReflectionTestUtils.setField(service, "reSubmitApprovalService", approvalService);
  238. return new Fixture(service, client, approvalService);
  239. }
  240. private static String currentMonth() {
  241. return YearMonth.now(ZoneId.of("Asia/Shanghai"))
  242. .format(DateTimeFormatter.ofPattern("yyyyMM"));
  243. }
  244. private static Map<String, Object> formalSummary(double dailyApproved) {
  245. Map<String, Object> result = summaryWithBillableRows(
  246. billable("P-001", 0d, 8d, 8d, 0d),
  247. billable("P-002", 0d, 4d, 4d, 0d));
  248. result.put("numberField_mmeakgi4", dailyApproved);
  249. return result;
  250. }
  251. private static Map<String, Object> summaryWithBillableRows(Map<String, Object>... rows) {
  252. Map<String, Object> result = new LinkedHashMap<>();
  253. result.put("tableField_mmczo634", Arrays.asList(rows));
  254. result.put("tableField_mmczo63h", Collections.emptyList());
  255. result.put("tableField_mmeakgid", Collections.emptyList());
  256. return result;
  257. }
  258. private static Map<String, Object> billable(String projectCode, double draft,
  259. double submitted, double approved, double change) {
  260. Map<String, Object> row = new LinkedHashMap<>();
  261. row.put("textField_mmacxew3", projectCode);
  262. row.put("textField_mmad6jcj", "Project " + projectCode);
  263. row.put("numberField_mmczo635", draft);
  264. row.put("numberField_mmczo636", submitted);
  265. row.put("numberField_mmczo637", approved);
  266. row.put("numberField_mmd80zae", change);
  267. return row;
  268. }
  269. private static Map<String, Object> changeRow(String projectCode, double hours) {
  270. return changeRow(DAY, projectCode, hours);
  271. }
  272. private static Map<String, Object> changeRow(String day, String projectCode, double hours) {
  273. Map<String, Object> row = new LinkedHashMap<>();
  274. row.put("dateStr", day);
  275. row.put("projectCode", projectCode);
  276. row.put("projectName", "Project " + projectCode);
  277. row.put("hours", hours);
  278. return row;
  279. }
  280. private static Map<String, Object> record(String id) {
  281. return Collections.singletonMap("formInstId", id);
  282. }
  283. private static DDR_New<Object> records(Map<String, Object>... items) {
  284. DDR_New<Object> result = new DDR_New<>();
  285. result.setData(Arrays.asList(items));
  286. return result;
  287. }
  288. private static DDR_New<Object> detail(Map<String, Object> data) {
  289. DDR_New<Object> result = new DDR_New<>();
  290. result.setFormData(data);
  291. return result;
  292. }
  293. private static final class Fixture {
  294. private final TimeCardChangeService service;
  295. private final YDClient client;
  296. private final ReSubmitApprovalService approvalService;
  297. private Fixture(TimeCardChangeService service, YDClient client,
  298. ReSubmitApprovalService approvalService) {
  299. this.service = service;
  300. this.client = client;
  301. this.approvalService = approvalService;
  302. }
  303. }
  304. }