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.common.McException; import com.malk.server.dingtalk.DDR_New; import com.malk.server.workhours.MonthClosingCheckRequest; import com.malk.server.workhours.MonthClosingStatusResult; import com.malk.server.workhours.TimeCardApprovalStartRequest; import com.malk.server.workhours.TimeCardRepairSubmitRequest; import com.malk.server.workhours.TimeCardRepairSummaryUpsertRequest; 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.time.YearMonth; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Collections; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; 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.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; class TimeCardRepairServiceTest { @Test void shouldBlockRepairSubmissionWhenCurrentMonthClosingIsRunning() { assertCurrentMonthBlocksSubmit(false, 1); } @Test void shouldBlockRepairSubmissionWhenCurrentMonthIsClosed() { assertCurrentMonthBlocksSubmit(true, 0); } @Test void shouldAllowRepairSubmissionPastClosingCheckWhenCurrentMonthIsOpen() { MonthClosingService closingService = mock(MonthClosingService.class); YDClient client = mock(YDClient.class); TimeCardRepairSubmitRequest request = repairSubmitRequest(); String currentMonth = YearMonth.now(ZoneId.of("Asia/Shanghai")) .format(DateTimeFormatter.ofPattern("yyyyMM")); when(closingService.isClosed(request.getSourceMonthText())).thenReturn(true); when(closingService.status(any(MonthClosingCheckRequest.class))) .thenReturn(MonthClosingStatusResult.builder().monthText(currentMonth).build()); TimeCardRepairService service = new TimeCardRepairService(); ReflectionTestUtils.setField(service, "monthClosingService", closingService); ReflectionTestUtils.setField(service, "ydClient", client); McException error = assertThrows(McException.class, () -> service.submit(request)); assertEquals("没有需要提交的补填工时", error.getMessage()); verify(closingService).status(org.mockito.ArgumentMatchers.argThat( value -> currentMonth.equals(value.getMonthText()))); verifyNoInteractions(client); } private static void assertCurrentMonthBlocksSubmit(boolean closed, int runningCount) { MonthClosingService closingService = mock(MonthClosingService.class); YDClient client = mock(YDClient.class); TimeCardRepairSubmitRequest request = repairSubmitRequest(); when(closingService.isClosed(request.getSourceMonthText())).thenReturn(true); when(closingService.status(any(MonthClosingCheckRequest.class))) .thenReturn(MonthClosingStatusResult.builder() .closed(closed).runningClosingCount(runningCount).build()); TimeCardRepairService service = new TimeCardRepairService(); ReflectionTestUtils.setField(service, "monthClosingService", closingService); ReflectionTestUtils.setField(service, "ydClient", client); McException error = assertThrows(McException.class, () -> service.submit(request)); assertEquals("当前月份已封账或封账流程审批中,不可提交补填", error.getMessage()); verifyNoInteractions(client); } private static TimeCardRepairSubmitRequest repairSubmitRequest() { TimeCardRepairSubmitRequest request = new TimeCardRepairSubmitRequest(); request.setSubmitterUserId("user-1"); request.setSourceMonthText(YearMonth.now(ZoneId.of("Asia/Shanghai")) .minusMonths(1).format(DateTimeFormatter.ofPattern("yyyyMM"))); return request; } @Test void shouldAllowRepairWhenOriginalSummaryOnlyHasDraftHours() { Map formData = new HashMap<>(); formData.put("numberField_mmd1smel", 8d); Map record = new HashMap<>(); record.put("formData", formData); assertFalse(TimeCardRepairService.hasApprovedOrChangedHours(record)); } @Test void shouldBlockRepairWhenApprovedAndChangedHoursRemain() { Map formData = new HashMap<>(); formData.put("numberField_mmeakgi4", 8d); formData.put("numberField_mmeakgi5", -2d); assertTrue(TimeCardRepairService.hasApprovedOrChangedHours(formData)); } @Test void shouldBlockRepairWhenChangeFullyOffsetsApprovedHours() { Map formData = new HashMap<>(); formData.put("numberField_mmeakgi4", 8d); formData.put("numberField_mmeakgi5", -8d); assertTrue(TimeCardRepairService.hasApprovedOrChangedHours(formData)); } @Test void shouldBlockRepairWhenOriginalApprovalStatusIsApproved() { Map formData = new HashMap<>(); formData.put("selectField_mre1xz9g", "已审批"); assertTrue(TimeCardRepairService.hasApprovedOrChangedHours(formData)); } @Test void shouldNotSaveDraftOverApprovedRepairWithCorruptedDraftStatus() { YDClient client = mock(YDClient.class); MonthClosingService closingService = mock(MonthClosingService.class); WHConf conf = new WHConf(); conf.setFormUuidWorkHoursSummary("original-summary"); conf.setFormUuidRepairWorkHoursSummary("repair-summary"); conf.setRepairSummaryFillTypeField("fill-type"); conf.setRepairSummarySyncStatusField("repair-status"); when(closingService.isClosed("202608")).thenReturn(true); DDR_New noOriginal = new DDR_New<>(); noOriginal.setData(Collections.emptyList()); Map child = new HashMap<>(); child.put("numberField_mmeakgii", 8d); Map formData = new HashMap<>(); formData.put("repair-status", "暂存"); formData.put("textField_mmbffvd9", "user-1"); formData.put("textField_mmbffvdb", "20260818"); formData.put("tableField_mmeakgid", Collections.singletonList(child)); Map record = new HashMap<>(); record.put("formData", formData); DDR_New approvedRepair = new DDR_New<>(); approvedRepair.setData(Collections.singletonList(record)); when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))) .thenReturn(noOriginal, approvedRepair); TimeCardRepairService service = new TimeCardRepairService(); ReflectionTestUtils.setField(service, "ydClient", client); ReflectionTestUtils.setField(service, "whConf", conf); ReflectionTestUtils.setField(service, "monthClosingService", closingService); TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest(); request.setSubmitterUserId("user-1"); request.setDayText("20260818"); request.setFormData(Collections.singletonMap("numberField_mmd1smel", 8d)); assertThrows(McException.class, () -> service.upsert(request)); verify(client, never()).operateData(any(YDParam.class), eq(YDConf.FORM_OPERATION.upsert)); } @Test void shouldCalculateRepairDraftTotalsFromRowsInsteadOfPageTotals() { YDClient client = mock(YDClient.class); MonthClosingService closingService = mock(MonthClosingService.class); WHConf conf = new WHConf(); conf.setFormUuidWorkHoursSummary("original-summary"); conf.setFormUuidRepairWorkHoursSummary("repair-summary"); conf.setRepairSummaryFillTypeField("fill-type"); conf.setRepairSummarySyncStatusField("repair-status"); when(closingService.isClosed("202608")).thenReturn(true); DDR_New empty = new DDR_New<>(); empty.setData(Collections.emptyList()); when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))) .thenReturn(empty); TimeCardRepairService service = new TimeCardRepairService(); ReflectionTestUtils.setField(service, "ydClient", client); ReflectionTestUtils.setField(service, "whConf", conf); ReflectionTestUtils.setField(service, "monthClosingService", closingService); Map child = new HashMap<>(); child.put("numberField_mmczo635", 5d); Map payload = new HashMap<>(); payload.put("tableField_mmczo634", Collections.singletonList(child)); payload.put("numberField_mmeakgi4", 9d); payload.put("numberField_mmd1smel", 99d); TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest(); request.setSubmitterUserId("user-1"); request.setDayText("20260818"); request.setFormData(payload); assertTrue(service.upsert(request)); ArgumentCaptor captor = ArgumentCaptor.forClass(YDParam.class); verify(client).operateData(captor.capture(), eq(YDConf.FORM_OPERATION.upsert)); Map condition = JSON.parseObject(captor.getValue().getSearchCondition(), Map.class); Map written = JSON.parseObject(captor.getValue().getFormDataJson(), Map.class); assertEquals("补填", condition.get("fill-type")); assertEquals(5d, ((Number) written.get("numberField_mmd1smel")).doubleValue()); assertEquals(0d, ((Number) written.get("numberField_mmeakgi4")).doubleValue()); assertEquals("补填", written.get("fill-type")); assertEquals("暂存", written.get("repair-status")); } @Test void shouldIgnoreChangeRowsDuringRepairResubmitScan() { YDClient client = mock(YDClient.class); ReSubmitApprovalService approvalService = mock(ReSubmitApprovalService.class); WHConf conf = new WHConf(); conf.setFormUuidRepairWorkHoursSummary("shared-summary"); conf.setRepairSummaryFillTypeField("fill-type"); conf.setRepairSummarySyncStatusField("sync-status"); Map change = new HashMap<>(); change.put("fill-type", "变更"); change.put("sync-status", "审批中"); Map record = new HashMap<>(); record.put("formData", change); DDR_New summaries = new DDR_New<>(); summaries.setData(Collections.singletonList(record)); when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))) .thenReturn(summaries); TimeCardRepairService service = new TimeCardRepairService(); ReflectionTestUtils.setField(service, "ydClient", client); ReflectionTestUtils.setField(service, "whConf", conf); ReflectionTestUtils.setField(service, "reSubmitApprovalService", approvalService); TimeCardRepairService.RepairResubmitResult result = service.resubmitRecentlyModifiedPending(7); assertEquals(0, result.getStarted()); assertEquals(0, result.getFailed()); verifyNoInteractions(approvalService); } @Test void shouldTreatBlankLegacyTypeAndRepairTypeAsRepairOnly() { assertTrue(TimeCardRepairService.isRepairSummaryRecord( Collections.emptyMap(), "fill-type")); assertTrue(TimeCardRepairService.isRepairSummaryRecord( Collections.singletonMap("fill-type", "补填"), "fill-type")); assertFalse(TimeCardRepairService.isRepairSummaryRecord( Collections.singletonMap("fill-type", "变更"), "fill-type")); } @Test void shouldReportRepairBatchStillFailedAfterDeferredRetry() { YDClient client = mock(YDClient.class); ReSubmitApprovalService approvalService = mock(ReSubmitApprovalService.class); WHConf conf = new WHConf(); conf.setFormUuidRepairWorkHoursSummary("repair-summary"); conf.setFormUuidApproval("bill-approval"); conf.setFormUuidOtherApproval("other-approval"); conf.setRepairSummaryFillTypeField("fill-type"); conf.setRepairSummarySyncStatusField("repair-status"); conf.setRepairSummaryBatchField("repair-batch"); conf.setRepairSummarySubmitDateField("repair-submit-date"); conf.setApprovalFillTypeField("fill-type"); conf.setOtherApprovalRepairSubmitDateField("other-submit-date"); conf.setOtherApprovalRepairBatchField("other-batch"); conf.setOtherApprovalRepairSourceMonthField("other-source-month"); Map otherRow = new HashMap<>(); otherRow.put("numberField_mmeakgij", 8d); otherRow.put("numberField_mmeakgii", 0d); otherRow.put("selectField_mmeakgie", "培训"); Map formData = new HashMap<>(); formData.put("fill-type", "补填"); formData.put("repair-status", "审批中"); formData.put("repair-batch", "batch-1"); formData.put("repair-submit-date", 1787000000000L); formData.put("textField_mmbffvd9", "user-1"); formData.put("textField_mmbffvda", "202608"); formData.put("textField_mmbffvdb", "20260818"); formData.put("tableField_mmeakgid", Collections.singletonList(otherRow)); Map record = new HashMap<>(); record.put("formData", formData); DDR_New summaries = new DDR_New<>(); summaries.setData(Collections.singletonList(record)); DDR_New noApprovals = new DDR_New<>(); noApprovals.setData(Collections.emptyList()); when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))) .thenReturn(summaries); when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process))) .thenReturn(noApprovals); when(approvalService.startApproval(any(TimeCardApprovalStartRequest.class))) .thenThrow(new IllegalStateException("temporary failure")); TimeCardRepairService service = new TimeCardRepairService(); ReflectionTestUtils.setField(service, "ydClient", client); ReflectionTestUtils.setField(service, "whConf", conf); ReflectionTestUtils.setField(service, "reSubmitApprovalService", approvalService); TimeCardRepairService.RepairResubmitResult result = service.resubmitRecentlyModifiedPending(7); assertEquals(0, result.getStarted()); assertEquals(1, result.getFailed()); verify(approvalService, times(2)).startApproval(any(TimeCardApprovalStartRequest.class)); } }