package com.malk.service.workhours; import com.alibaba.fastjson.JSON; import com.malk.server.aliwork.YDConf; import com.malk.server.aliwork.YDParam; import com.malk.server.dingtalk.DDR_New; import com.malk.server.common.McException; import com.malk.server.workhours.TimeCardDuplicateCleanupRequest; import com.malk.server.workhours.TimeCardDuplicateCleanupResult; import com.malk.server.workhours.TimeCardSummaryUpsertRequest; import com.malk.server.workhours.WHConf; import com.malk.service.aliwork.YDClient; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.test.util.ReflectionTestUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class TimeCardSummaryServiceTest { @Test void shouldUpsertBySubmitterIdAndDayText() { YDClient ydClient = mock(YDClient.class); WHConf whConf = new WHConf(); whConf.setYidaAppType("APP"); whConf.setYidaSystemToken("TOKEN"); whConf.setFormUuidWorkHoursSummary("FORM-SUMMARY"); TimeCardSummaryService service = new TimeCardSummaryService(); ReflectionTestUtils.setField(service, "ydClient", ydClient); ReflectionTestUtils.setField(service, "whConf", whConf); TimeCardSummaryUpsertRequest request = new TimeCardSummaryUpsertRequest(); request.setSubmitterUserId("user-1"); request.setDayText("20260716"); request.setFormData(Collections.singletonMap("numberField_hours", 8)); DDR_New empty = new DDR_New<>(); empty.setData(Collections.emptyList()); when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))) .thenReturn(empty); assertTrue(service.upsert(request)); ArgumentCaptor captor = ArgumentCaptor.forClass(YDParam.class); verify(ydClient).operateData(captor.capture(), eq(YDConf.FORM_OPERATION.upsert)); YDParam param = captor.getValue(); Map condition = JSON.parseObject(param.getSearchCondition(), Map.class); Map formData = JSON.parseObject(param.getFormDataJson(), Map.class); assertEquals("user-1", condition.get("textField_mmbffvd9")); assertEquals("20260716", condition.get("textField_mmbffvdb")); assertEquals("user-1", formData.get("textField_mmbffvd9")); assertEquals("20260716", formData.get("textField_mmbffvdb")); assertEquals(8, ((Number) formData.get("numberField_hours")).intValue()); } @Test void shouldNotOverwriteApprovedSummaryEvenWhenDailyTotalIsZero() { YDClient ydClient = mock(YDClient.class); WHConf whConf = new WHConf(); whConf.setFormUuidWorkHoursSummary("FORM-SUMMARY"); TimeCardSummaryService service = new TimeCardSummaryService(); ReflectionTestUtils.setField(service, "ydClient", ydClient); ReflectionTestUtils.setField(service, "whConf", whConf); Map item = summaryItem("APPROVED-ID", "user-1", "20260716"); Map data = (Map) item.get("formData"); data.put("numberField_mmeakgi4", 0); Map child = new LinkedHashMap<>(); child.put("numberField_mmczo637", 8); data.put("tableField_mmczo634", Collections.singletonList(child)); DDR_New existing = new DDR_New<>(); existing.setData(Collections.singletonList(item)); when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))) .thenReturn(existing); TimeCardSummaryUpsertRequest request = new TimeCardSummaryUpsertRequest(); request.setSubmitterUserId("user-1"); request.setDayText("20260716"); request.setFormData(Collections.singletonMap("numberField_mmd1smel", 8)); assertThrows(McException.class, () -> service.upsert(request)); verify(ydClient, never()).operateData(any(YDParam.class), eq(YDConf.FORM_OPERATION.upsert)); } @Test void shouldNotOverwriteApprovedRepairFromNormalSummary() { YDClient ydClient = mock(YDClient.class); WHConf whConf = new WHConf(); whConf.setFormUuidWorkHoursSummary("FORM-SUMMARY"); whConf.setFormUuidRepairWorkHoursSummary("FORM-REPAIR"); whConf.setRepairSummarySyncStatusField("repair-status"); TimeCardSummaryService service = new TimeCardSummaryService(); ReflectionTestUtils.setField(service, "ydClient", ydClient); ReflectionTestUtils.setField(service, "whConf", whConf); DDR_New empty = new DDR_New<>(); empty.setData(Collections.emptyList()); Map repair = summaryItem("REPAIR-ID", "user-1", "20260716"); ((Map) repair.get("formData")).put("repair-status", "已审批"); DDR_New approved = new DDR_New<>(); approved.setData(Collections.singletonList(repair)); when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))) .thenReturn(empty, approved); TimeCardSummaryUpsertRequest request = new TimeCardSummaryUpsertRequest(); request.setSubmitterUserId("user-1"); request.setDayText("20260716"); request.setFormData(Collections.singletonMap("numberField_mmd1smel", 8)); assertThrows(McException.class, () -> service.upsert(request)); verify(ydClient, never()).operateData(any(YDParam.class), eq(YDConf.FORM_OPERATION.upsert)); } @Test void shouldLockWholeFormalDayWhenOneChildIsSubmitted() { Map record = summaryItem("SUBMITTED-ID", "user-1", "20260716"); Map child = new LinkedHashMap<>(); child.put("numberField_mmczo636", 8); ((Map) record.get("formData")) .put("tableField_mmczo634", Collections.singletonList(child)); assertTrue(TimeCardSummaryService.isFormalRecordLocked(record)); } @Test void shouldValidateDuplicateCleanupWithoutDeletingInDryRun() { YDClient ydClient = mock(YDClient.class); WHConf whConf = new WHConf(); whConf.setYidaAppType("APP"); whConf.setYidaSystemToken("TOKEN"); whConf.setFormUuidWorkHoursSummary("FORM-SUMMARY"); TimeCardSummaryService service = new TimeCardSummaryService(); ReflectionTestUtils.setField(service, "ydClient", ydClient); ReflectionTestUtils.setField(service, "whConf", whConf); DDR_New queryResult = mock(DDR_New.class); when(queryResult.getData()).thenReturn(Arrays.asList( summaryItem("DELETE-ID", "user-1", "20260507"), summaryItem("KEEP-ID", "user-1", "20260507"))); when(ydClient.queryData( any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))) .thenReturn(queryResult); TimeCardDuplicateCleanupResult result = service.cleanupDuplicates(cleanupRequest(true)); assertEquals(1, result.getValidated()); assertEquals(0, result.getDeleted()); verify(ydClient, never()).operateData( any(YDParam.class), eq(YDConf.FORM_OPERATION.delete_batch)); } @Test void shouldRejectCleanupWhenCurrentDuplicateSetChanged() { TimeCardDuplicateCleanupRequest.Target target = cleanupRequest(true).getTargets().get(0); assertThrows( RuntimeException.class, () -> TimeCardSummaryService.validateCleanupTarget( target, Collections.singletonList("KEEP-ID"))); } @Test void shouldValidateDuplicateGroupWithThreeDeleteTargets() { TimeCardDuplicateCleanupRequest request = cleanupRequest(true); request.setTargets(new ArrayList<>(request.getTargets())); for (int index = 2; index <= 3; index++) { TimeCardDuplicateCleanupRequest.Target target = new TimeCardDuplicateCleanupRequest.Target(); target.setUserId("user-1"); target.setDayText("20260507"); target.setDeleteInstanceId("DELETE-ID-" + index); target.setKeepInstanceId("KEEP-ID"); request.getTargets().add(target); } TimeCardSummaryService.validateCleanupTargets( request.getTargets(), Arrays.asList("KEEP-ID", "DELETE-ID", "DELETE-ID-2", "DELETE-ID-3")); } @Test void shouldDeleteValidatedTargetAndVerifyKeeper() { YDClient ydClient = mock(YDClient.class); WHConf whConf = new WHConf(); whConf.setYidaAppType("APP"); whConf.setYidaSystemToken("TOKEN"); whConf.setFormUuidWorkHoursSummary("FORM-SUMMARY"); TimeCardSummaryService service = new TimeCardSummaryService(); ReflectionTestUtils.setField(service, "ydClient", ydClient); ReflectionTestUtils.setField(service, "whConf", whConf); DDR_New duplicateResult = mock(DDR_New.class); when(duplicateResult.getData()).thenReturn(Arrays.asList( summaryItem("DELETE-ID", "user-1", "20260507"), summaryItem("KEEP-ID", "user-1", "20260507"))); DDR_New keeperResult = mock(DDR_New.class); when(keeperResult.getData()).thenReturn(Collections.singletonList( summaryItem("KEEP-ID", "user-1", "20260507"))); when(ydClient.queryData( any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))) .thenReturn(duplicateResult, keeperResult); TimeCardDuplicateCleanupResult result = service.cleanupDuplicates(cleanupRequest(false)); ArgumentCaptor captor = ArgumentCaptor.forClass(YDParam.class); verify(ydClient).operateData( captor.capture(), eq(YDConf.FORM_OPERATION.delete_batch)); assertEquals(Collections.singletonList("DELETE-ID"), captor.getValue().getFormInstanceIdList()); assertEquals(1, result.getDeleted()); assertEquals(1, result.getVerified()); } private static TimeCardDuplicateCleanupRequest cleanupRequest(boolean dryRun) { TimeCardDuplicateCleanupRequest.Target target = new TimeCardDuplicateCleanupRequest.Target(); target.setUserId("user-1"); target.setDayText("20260507"); target.setDeleteInstanceId("DELETE-ID"); target.setKeepInstanceId("KEEP-ID"); TimeCardDuplicateCleanupRequest request = new TimeCardDuplicateCleanupRequest(); request.setDryRun(dryRun); request.setTargets(Collections.singletonList(target)); return request; } private static Map summaryItem(String instanceId, String userId, String dayText) { Map formData = new LinkedHashMap<>(); formData.put("textField_mmbffvd9", userId); formData.put("textField_mmbffvdb", dayText); Map item = new LinkedHashMap<>(); item.put("formInstanceId", instanceId); item.put("formData", formData); return item; } }