|
|
@@ -108,7 +108,7 @@ class TimeCardRepairServiceTest {
|
|
|
} finally {
|
|
|
release.countDown();
|
|
|
}
|
|
|
- assertEquals("repair-only", awaitReadyContext(service, repairContextRequest()).get("summaryReadMode"));
|
|
|
+ assertEquals("original-and-repair", awaitReadyContext(service, repairContextRequest()).get("summaryReadMode"));
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
@@ -122,7 +122,7 @@ class TimeCardRepairServiceTest {
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void shouldQueryOnlyRepairSummaryForConfirmedExternalUser() throws Exception {
|
|
|
+ void shouldQueryBothSummariesForConfirmedExternalUser() throws Exception {
|
|
|
YDClient client = mock(YDClient.class);
|
|
|
DDR_New<Object> response = new DDR_New<>();
|
|
|
response.setData(Collections.emptyList());
|
|
|
@@ -133,12 +133,12 @@ class TimeCardRepairServiceTest {
|
|
|
when(policy.getStaffType("user-1")).thenReturn("外部");
|
|
|
ReflectionTestUtils.setField(service, "personnelPolicy", policy);
|
|
|
Map<String, Object> context = awaitReadyContext(service, repairContextRequest());
|
|
|
- assertEquals("repair-only", context.get("summaryReadMode"));
|
|
|
+ assertEquals("original-and-repair", context.get("summaryReadMode"));
|
|
|
assertEquals("外部", context.get("staffType"));
|
|
|
assertTrue(((List<?>) context.get("originalRecords")).isEmpty());
|
|
|
- verify(client, never()).queryData(org.mockito.ArgumentMatchers.argThat(
|
|
|
+ verify(client, times(1)).queryData(org.mockito.ArgumentMatchers.argThat(
|
|
|
p -> "original-summary".equals(p.getFormUuid())), eq(YDConf.FORM_QUERY.retrieve_search_form));
|
|
|
- verify(client, times(1)).queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form));
|
|
|
+ verify(client, times(3)).queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form));
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
@@ -156,13 +156,13 @@ class TimeCardRepairServiceTest {
|
|
|
assertEquals("original-and-repair", awaitReadyContext(service, request).get("summaryReadMode"));
|
|
|
when(policy.getStaffType("user-1")).thenReturn("外部");
|
|
|
request.setStaffType("外部");
|
|
|
- assertEquals("repair-only", awaitReadyContext(service, request).get("summaryReadMode"));
|
|
|
- verify(client, times(1)).queryData(org.mockito.ArgumentMatchers.argThat(
|
|
|
+ assertEquals("original-and-repair", awaitReadyContext(service, request).get("summaryReadMode"));
|
|
|
+ verify(client, times(2)).queryData(org.mockito.ArgumentMatchers.argThat(
|
|
|
p -> "original-summary".equals(p.getFormUuid())), eq(YDConf.FORM_QUERY.retrieve_search_form));
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void shouldKeepRepairDayLockedForExternalSaveWithoutReadingOriginal() {
|
|
|
+ void shouldKeepRepairDayLockedForExternalSaveAfterCheckingOriginal() {
|
|
|
YDClient client = mock(YDClient.class);
|
|
|
MonthClosingService closing = mock(MonthClosingService.class);
|
|
|
when(closing.isLocked("202609")).thenReturn(true);
|
|
|
@@ -180,33 +180,39 @@ class TimeCardRepairServiceTest {
|
|
|
fd.put("numberField_mmd1smem", 8d);
|
|
|
DDR_New<Object> response = new DDR_New<>();
|
|
|
response.setData(Collections.singletonList(Collections.singletonMap("formData", fd)));
|
|
|
- when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))).thenReturn(response);
|
|
|
+ when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))).thenAnswer(invocation -> {
|
|
|
+ YDParam param = invocation.getArgument(0);
|
|
|
+ if ("repair-summary".equals(param.getFormUuid())) return response;
|
|
|
+ DDR_New<Object> original = new DDR_New<>();
|
|
|
+ original.setData(Collections.emptyList());
|
|
|
+ return original;
|
|
|
+ });
|
|
|
TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest();
|
|
|
request.setSubmitterUserId("user-1");
|
|
|
request.setDayText("20260930");
|
|
|
request.setFormData(new HashMap<>());
|
|
|
assertTrue(assertThrows(McException.class, () -> service.upsert(request)).getMessage().contains("已提交或已审批"));
|
|
|
- verify(client, never()).queryData(org.mockito.ArgumentMatchers.argThat(
|
|
|
+ verify(client, times(1)).queryData(org.mockito.ArgumentMatchers.argThat(
|
|
|
p -> "original-summary".equals(p.getFormUuid())), eq(YDConf.FORM_QUERY.retrieve_search_form));
|
|
|
}
|
|
|
|
|
|
- @Test
|
|
|
- void shouldSkipOnlyOriginalSourceGuardForExternalSubmission() {
|
|
|
+ @ParameterizedTest
|
|
|
+ @ValueSource(booleans = {false, true})
|
|
|
+ void shouldCheckOriginalSourceGuardForEveryStaffType(boolean external) {
|
|
|
YDClient client = mock(YDClient.class);
|
|
|
TimeCardRepairService service = repairContextService(client, mock(MonthClosingService.class), repairContextConf());
|
|
|
- ReflectionTestUtils.invokeMethod(service, "assertSourceDateAvailable", "user-1", "20260930", true);
|
|
|
- verifyNoInteractions(client);
|
|
|
Map<String, Object> fd = new HashMap<>();
|
|
|
fd.put("numberField_mmd1smem", 8d);
|
|
|
DDR_New<Object> response = new DDR_New<>();
|
|
|
response.setData(Collections.singletonList(Collections.singletonMap("formData", fd)));
|
|
|
when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))).thenReturn(response);
|
|
|
assertThrows(McException.class, () -> ReflectionTestUtils.invokeMethod(
|
|
|
- service, "assertSourceDateAvailable", "user-1", "20260930", false));
|
|
|
+ service, "assertSourceDateAvailable", "user-1", "20260930", external));
|
|
|
+ verify(client, times(1)).queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form));
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void shouldCheckRepairRowsButNeverOriginalDuringExternalSubmitValidation() {
|
|
|
+ void shouldCheckBothTablesDuringExternalSubmitValidation() {
|
|
|
YDClient client = mock(YDClient.class);
|
|
|
DDR_New<Object> response = new DDR_New<>();
|
|
|
response.setData(Collections.emptyList());
|
|
|
@@ -227,12 +233,99 @@ class TimeCardRepairServiceTest {
|
|
|
Collections.singletonMap("manager", group), Collections.emptyMap());
|
|
|
assertEquals(1, count);
|
|
|
verify(policy, times(1)).getStaffType("user-1");
|
|
|
- verify(client, never()).queryData(org.mockito.ArgumentMatchers.argThat(
|
|
|
+ verify(client, times(1)).queryData(org.mockito.ArgumentMatchers.argThat(
|
|
|
p -> "original-summary".equals(p.getFormUuid())), eq(YDConf.FORM_QUERY.retrieve_search_form));
|
|
|
verify(client, times(1)).queryData(org.mockito.ArgumentMatchers.argThat(
|
|
|
p -> "repair-summary".equals(p.getFormUuid())), eq(YDConf.FORM_QUERY.retrieve_search_form));
|
|
|
}
|
|
|
|
|
|
+ @Test
|
|
|
+ void shouldSeparateNormalAndConfirmedReentryContextsAndKeepOriginalProtection() throws Exception {
|
|
|
+ YDClient client = mock(YDClient.class);
|
|
|
+ Map<String, Object> reversal = reversalRecord();
|
|
|
+ when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
|
|
|
+ .thenAnswer(invocation -> {
|
|
|
+ YDParam param = invocation.getArgument(0);
|
|
|
+ Map<String, Object> data = "original-summary".equals(param.getFormUuid())
|
|
|
+ ? Collections.singletonMap("numberField_mmd1smem", 8d) : reversal;
|
|
|
+ DDR_New<Object> response = new DDR_New<>();
|
|
|
+ response.setData(Collections.singletonList(Collections.singletonMap("formData", data)));
|
|
|
+ return response;
|
|
|
+ });
|
|
|
+ TimeCardRepairService service = repairContextService(client, mock(MonthClosingService.class), repairContextConf());
|
|
|
+ TimeCardPersonnelPolicy policy = mock(TimeCardPersonnelPolicy.class);
|
|
|
+ when(policy.getStaffType("user-1")).thenReturn("外部");
|
|
|
+ ReflectionTestUtils.setField(service, "personnelPolicy", policy);
|
|
|
+ TimeCardRepairContextRequest request = repairContextRequest();
|
|
|
+ Map<String, Object> normal = awaitReadyContext(service, request);
|
|
|
+ assertEquals(1, ((List<?>) normal.get("originalRecords")).size());
|
|
|
+ request.setRepairMode("REENTRY");
|
|
|
+ Map<String, Object> reentry = awaitReadyContext(service, request);
|
|
|
+ assertEquals("repair-only", reentry.get("summaryReadMode"));
|
|
|
+ assertTrue(((List<?>) reentry.get("originalRecords")).isEmpty());
|
|
|
+ assertEquals(Collections.singletonList("20260930"), reentry.get("reentryAllowedDays"));
|
|
|
+ assertTrue(((List<?>) reentry.get("repairRecords")).isEmpty(), "冲销变更不能成为补填审批锁");
|
|
|
+ verify(client, times(1)).queryData(org.mockito.ArgumentMatchers.argThat(
|
|
|
+ p -> "original-summary".equals(p.getFormUuid())), eq(YDConf.FORM_QUERY.retrieve_search_form));
|
|
|
+ assertThrows(McException.class, () -> ReflectionTestUtils.invokeMethod(service,
|
|
|
+ "assertRepairSourceDateAvailable", "user-1", "20260930", "NORMAL", true));
|
|
|
+ ReflectionTestUtils.invokeMethod(service, "assertRepairSourceDateAvailable", "user-1", "20260930", "REENTRY", true);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldRejectRolledBackUnapprovedOrForgedReentryBeforeWrites() {
|
|
|
+ for (String invalid : new String[]{"missing", "batch", "sync", "user", "day", "positive", "internal", "mode"}) {
|
|
|
+ YDClient client = mock(YDClient.class);
|
|
|
+ Map<String, Object> reversal = reversalRecord();
|
|
|
+ if ("batch".equals(invalid)) reversal.put("batch", "unconfirmed");
|
|
|
+ if ("sync".equals(invalid)) reversal.put("sync", "部分成功");
|
|
|
+ if ("user".equals(invalid)) reversal.put("textField_mmbffvd9", "other-user");
|
|
|
+ if ("day".equals(invalid)) reversal.put("textField_mmbffvdb", "20260929");
|
|
|
+ if ("positive".equals(invalid)) reversal.put("numberField_mmeakgi4", 8d);
|
|
|
+ DDR_New<Object> response = new DDR_New<>();
|
|
|
+ response.setData("missing".equals(invalid) ? Collections.emptyList()
|
|
|
+ : Collections.singletonList(Collections.singletonMap("formData", reversal)));
|
|
|
+ when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))).thenReturn(response);
|
|
|
+ TimeCardRepairService service = repairContextService(client, mock(MonthClosingService.class), repairContextConf());
|
|
|
+ assertThrows(McException.class, () -> ReflectionTestUtils.invokeMethod(service,
|
|
|
+ "assertRepairSourceDateAvailable", "user-1", "20260930", "mode".equals(invalid) ? "FAKE" : "REENTRY",
|
|
|
+ !"internal".equals(invalid)), invalid);
|
|
|
+ verify(client, never()).operateData(any(YDParam.class), any());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void shouldBlockNormalExternalSaveAndAllSubmitCategoriesOnOriginalApprovedDay() {
|
|
|
+ for (String category : new String[]{"billableRows", "nonBillableRows", "otherRows"}) {
|
|
|
+ YDClient client = mock(YDClient.class);
|
|
|
+ DDR_New<Object> response = new DDR_New<>();
|
|
|
+ response.setData(Collections.singletonList(Collections.singletonMap("formData",
|
|
|
+ Collections.singletonMap("numberField_mmeakgi4", 8d))));
|
|
|
+ when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form))).thenReturn(response);
|
|
|
+ TimeCardRepairService service = repairContextService(client, mock(MonthClosingService.class), repairContextConf());
|
|
|
+ TimeCardPersonnelPolicy policy = mock(TimeCardPersonnelPolicy.class);
|
|
|
+ when(policy.getStaffType("user-1")).thenReturn("外部");
|
|
|
+ ReflectionTestUtils.setField(service, "personnelPolicy", policy);
|
|
|
+ TimeCardRepairSubmitRequest request = repairSubmitRequest();
|
|
|
+ request.setSourceMonthText("202609");
|
|
|
+ Map<String, Object> row = new HashMap<>();
|
|
|
+ row.put("dateStr", "20260930"); row.put("projectCode", "P"); row.put("hours", 8d);
|
|
|
+ Map<String, Object> group = Collections.singletonMap(category, Collections.singletonList(row));
|
|
|
+ assertThrows(McException.class, () -> ReflectionTestUtils.invokeMethod(service, "validateSubmitRows", request,
|
|
|
+ "otherRows".equals(category) ? Collections.emptyMap() : Collections.singletonMap("manager", group),
|
|
|
+ "otherRows".equals(category) ? group : Collections.emptyMap()));
|
|
|
+ verify(client, never()).operateData(any(YDParam.class), any());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map<String, Object> reversalRecord() {
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("fill-type", "变更"); data.put("batch", "10月调账-EXT-20261020"); data.put("sync", "全部成功");
|
|
|
+ data.put("textField_mmbffvd9", "user-1"); data.put("textField_mmbffvda", "202609");
|
|
|
+ data.put("textField_mmbffvdb", "20260930"); data.put("numberField_mmeakgi4", -8d);
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
@Test
|
|
|
void shouldReturnPendingImmediatelyThenExposeCompletedContext() throws Exception {
|
|
|
YDClient client = mock(YDClient.class);
|
|
|
@@ -399,6 +492,8 @@ class TimeCardRepairServiceTest {
|
|
|
conf.setFormUuidWorkHoursSummary("original-summary");
|
|
|
conf.setFormUuidRepairWorkHoursSummary("repair-summary");
|
|
|
conf.setRepairSummaryFillTypeField("fill-type");
|
|
|
+ conf.setRepairSummaryBatchField("batch");
|
|
|
+ conf.setRepairSummarySyncStatusField("sync");
|
|
|
return conf;
|
|
|
}
|
|
|
|