| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 |
- 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.Arrays;
- import java.util.Collections;
- import java.util.LinkedHashMap;
- import java.util.List;
- 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 TimeCardChangeServiceTest {
- private static final String USER = "user-1";
- private static final String DAY = "20260818";
- private static final String NEXT_DAY = "20260819";
- @Test
- void shouldSaveZeroTargetAsChangeDraftWithoutTouchingRepairType() {
- Fixture fixture = fixture();
- when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
- .thenReturn(records(record("formal-1")), records());
- when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_id)))
- .thenReturn(detail(formalSummary(8d)));
- TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest();
- request.setSubmitterUserId(USER);
- request.setDayText(DAY);
- request.setFormData(summaryWithBillableRows(billable("P-001", 0d, 0d, 0d, 0d)));
- assertTrue(fixture.service.upsert(request));
- ArgumentCaptor<YDParam> captor = ArgumentCaptor.forClass(YDParam.class);
- verify(fixture.client).operateData(captor.capture(), eq(YDConf.FORM_OPERATION.upsert));
- Map<String, Object> condition = JSON.parseObject(captor.getValue().getSearchCondition(), Map.class);
- Map<String, Object> written = JSON.parseObject(captor.getValue().getFormDataJson(), Map.class);
- List<Map> rows = (List<Map>) written.get("tableField_mmczo634");
- assertEquals("变更", condition.get("fill-type"));
- assertEquals("变更", written.get("fill-type"));
- assertEquals("暂存", written.get("sync-status"));
- assertEquals(0d, ((Number) rows.get(0).get("numberField_mmczo635")).doubleValue());
- assertEquals(8d, ((Number) rows.get(0).get("numberField_mmczo637")).doubleValue());
- assertEquals(0d, ((Number) rows.get(0).get("numberField_mmd80zae")).doubleValue());
- }
- @Test
- void shouldRejectChangeWhenFormalDailyApprovedHoursAreNotPositive() {
- Fixture fixture = fixture();
- when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
- .thenReturn(records(record("formal-1")));
- when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_id)))
- .thenReturn(detail(formalSummary(0d)));
- TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest();
- request.setSubmitterUserId(USER);
- request.setDayText(DAY);
- request.setFormData(summaryWithBillableRows(billable("P-001", 6d, 0d, 0d, 0d)));
- McException error = assertThrows(McException.class, () -> fixture.service.upsert(request));
- assertEquals("目标日期已审批工时(日)必须大于0,不能发起工时变更", error.getMessage());
- verify(fixture.client, never()).operateData(any(YDParam.class), any(YDConf.FORM_OPERATION.class));
- }
- @Test
- void shouldUseLatestApprovedChangeAsBaselineForRepeatedChange() {
- Fixture fixture = fixture();
- Map<String, Object> previousChange = summaryWithBillableRows(
- billable("P-001", 6d, 6d, 6d, -2d));
- previousChange.put("sync-status", "全部成功");
- when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
- .thenReturn(records(record("formal-1")), records(record("change-1")));
- when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_id)))
- .thenReturn(detail(formalSummary(8d)), detail(previousChange));
- TimeCardRepairSummaryUpsertRequest request = new TimeCardRepairSummaryUpsertRequest();
- request.setSubmitterUserId(USER);
- request.setDayText(DAY);
- request.setFormData(summaryWithBillableRows(billable("P-001", 5d, 0d, 0d, 0d)));
- assertTrue(fixture.service.upsert(request));
- ArgumentCaptor<YDParam> captor = ArgumentCaptor.forClass(YDParam.class);
- verify(fixture.client).operateData(captor.capture(), eq(YDConf.FORM_OPERATION.upsert));
- Map<String, Object> written = JSON.parseObject(captor.getValue().getFormDataJson(), Map.class);
- List<Map> rows = (List<Map>) written.get("tableField_mmczo634");
- assertEquals(5d, ((Number) rows.get(0).get("numberField_mmczo635")).doubleValue());
- assertEquals(6d, ((Number) rows.get(0).get("numberField_mmczo637")).doubleValue());
- assertEquals(0d, ((Number) rows.get(0).get("numberField_mmd80zae")).doubleValue());
- }
- @Test
- void shouldSubmitOnlyChangedRowsAndKeepZeroTarget() {
- Fixture fixture = fixture();
- Map<String, Object> baseline = summaryWithBillableRows(
- billable("P-001", 8d, 8d, 8d, 0d),
- billable("P-002", 4d, 4d, 4d, 0d));
- baseline.put("sync-status", "暂存");
- Map<String, Object> submitted = summaryWithBillableRows(
- billable("P-001", 0d, 0d, 8d, -8d),
- billable("P-002", 4d, 4d, 4d, 0d));
- submitted.put("sync-status", "暂存");
- when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
- .thenReturn(records(record("formal-1")), records(record("change-1")),
- records(record("formal-1")), records(record("change-1")));
- when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_id)))
- .thenReturn(detail(formalSummary(8d)), detail(baseline), detail(submitted),
- detail(formalSummary(8d)), detail(submitted));
- Map<String, Object> group = new LinkedHashMap<>();
- group.put("managerId", "manager-1");
- group.put("billableRows", Arrays.asList(
- changeRow("P-001", 0d), changeRow("P-002", 4d)));
- group.put("nonBillableRows", Collections.emptyList());
- Map<String, Object> groups = new LinkedHashMap<>();
- groups.put("manager-1", group);
- TimeCardRepairSubmitRequest request = new TimeCardRepairSubmitRequest();
- request.setSubmitterUserId(USER);
- request.setSubmitterName("Test User");
- request.setSourceMonthText("202608");
- request.setProjectGroups(groups);
- request.setOtherGroup(Collections.emptyMap());
- Map<String, Object> result = fixture.service.submit(request);
- assertEquals("审批中", result.get("status"));
- ArgumentCaptor<TimeCardApprovalStartRequest> approvalCaptor =
- ArgumentCaptor.forClass(TimeCardApprovalStartRequest.class);
- verify(fixture.approvalService).startApproval(approvalCaptor.capture());
- Map<String, Object> approval = JSON.parseObject(
- approvalCaptor.getValue().getFormDataJson(), Map.class);
- List<Map> approvalRows = (List<Map>) approval.get("tableField_mmae8t99");
- assertEquals("变更", approval.get("approval-fill-type"));
- assertEquals(1, approvalRows.size());
- assertEquals("P-001", approvalRows.get(0).get("textField_mmacxew3"));
- assertEquals(0d, ((Number) approvalRows.get(0).get("numberField_mmacxew9")).doubleValue());
- }
- @Test
- void shouldSubmitOppositeDayChangesWhenNetDeltaIsZero() {
- Fixture fixture = fixture();
- Map<String, Object> firstBaseline = summaryWithBillableRows(
- billable("P-001", 0d, 8d, 8d, 0d));
- firstBaseline.put("sync-status", "暂存");
- Map<String, Object> firstSubmitted = summaryWithBillableRows(
- billable("P-001", 0d, 0d, 8d, -8d));
- firstSubmitted.put("sync-status", "审批中");
- Map<String, Object> secondBaseline = summaryWithBillableRows(
- billable("P-001", 8d, 8d, 8d, 0d),
- billable("P-002", 0d, 0d, 0d, 0d));
- secondBaseline.put("sync-status", "暂存");
- Map<String, Object> secondSubmitted = summaryWithBillableRows(
- billable("P-001", 8d, 8d, 8d, 0d),
- billable("P-002", 8d, 8d, 0d, 8d));
- secondSubmitted.put("sync-status", "审批中");
- when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
- .thenReturn(records(record("formal-1")), records(record("change-1")),
- records(record("formal-2")), records(record("change-2")));
- when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_id)))
- .thenReturn(detail(formalSummary(8d)), detail(firstBaseline), detail(firstSubmitted),
- detail(formalSummary(8d)), detail(secondBaseline), detail(secondSubmitted));
- Map<String, Object> group = new LinkedHashMap<>();
- group.put("managerId", "manager-1");
- group.put("billableRows", Arrays.asList(
- changeRow(DAY, "P-001", 0d), changeRow(NEXT_DAY, "P-002", 8d)));
- group.put("nonBillableRows", Collections.emptyList());
- Map<String, Object> groups = new LinkedHashMap<>();
- groups.put("manager-1", group);
- TimeCardRepairSubmitRequest request = new TimeCardRepairSubmitRequest();
- request.setSubmitterUserId(USER);
- request.setSubmitterName("Test User");
- request.setSourceMonthText("202608");
- request.setProjectGroups(groups);
- request.setOtherGroup(Collections.emptyMap());
- Map<String, Object> result = fixture.service.submit(request);
- assertEquals("审批中", result.get("status"));
- ArgumentCaptor<TimeCardApprovalStartRequest> approvalCaptor =
- ArgumentCaptor.forClass(TimeCardApprovalStartRequest.class);
- verify(fixture.approvalService).startApproval(approvalCaptor.capture());
- Map<String, Object> approval = JSON.parseObject(
- approvalCaptor.getValue().getFormDataJson(), Map.class);
- List<Map> approvalRows = (List<Map>) approval.get("tableField_mmae8t99");
- assertEquals(2, approvalRows.size());
- assertEquals(0d, ((Number) approvalRows.get(0).get("numberField_mmacxew9")).doubleValue());
- assertEquals(8d, ((Number) approvalRows.get(1).get("numberField_mmacxew9")).doubleValue());
- }
- @Test
- void shouldIgnoreRepairRowsDuringChangeResubmitScan() {
- Fixture fixture = fixture();
- Map<String, Object> repair = new LinkedHashMap<>();
- repair.put("fill-type", "补填");
- repair.put("sync-status", "审批中");
- when(fixture.client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
- .thenReturn(records(Collections.singletonMap("formData", repair)));
- TimeCardChangeService.ChangeResubmitResult result =
- fixture.service.resubmitRecentlyModifiedPending(7);
- assertEquals(0, result.getStarted());
- assertEquals(0, result.getFailed());
- verify(fixture.approvalService, never()).startApproval(any(TimeCardApprovalStartRequest.class));
- }
- private static Fixture fixture() {
- YDClient client = mock(YDClient.class);
- MonthClosingService closingService = mock(MonthClosingService.class);
- ReSubmitApprovalService approvalService = mock(ReSubmitApprovalService.class);
- when(closingService.status(any(MonthClosingCheckRequest.class)))
- .thenReturn(MonthClosingStatusResult.builder()
- .monthText(currentMonth()).closed(false).runningClosingCount(0).build());
- WHConf conf = new WHConf();
- conf.setYidaAppType("APP");
- conf.setYidaSystemToken("TOKEN");
- conf.setFormUuidWorkHoursSummary("formal-summary");
- conf.setFormUuidRepairWorkHoursSummary("shared-summary");
- conf.setRepairSummaryFillTypeField("fill-type");
- conf.setRepairSummarySyncStatusField("sync-status");
- conf.setRepairSummarySubmitDateField("submit-date");
- conf.setRepairSummaryBatchField("batch");
- conf.setRepairSummarySourceMonthField("source-month");
- conf.setApprovalFillTypeField("approval-fill-type");
- conf.setFormUuidApproval("project-approval");
- conf.setApprovalProcessCode("project-process");
- conf.setApprovalRepairSubmitDateField("approval-submit-date");
- conf.setApprovalRepairBatchField("approval-batch");
- conf.setApprovalRepairSourceMonthField("approval-source-month");
- conf.setFormUuidOtherApproval("other-approval");
- conf.setOtherApprovalProcessCode("other-process");
- conf.setOtherApprovalRepairSubmitDateField("other-submit-date");
- conf.setOtherApprovalRepairBatchField("other-batch");
- conf.setOtherApprovalRepairSourceMonthField("other-source-month");
- TimeCardChangeService service = new TimeCardChangeService();
- ReflectionTestUtils.setField(service, "ydClient", client);
- ReflectionTestUtils.setField(service, "whConf", conf);
- ReflectionTestUtils.setField(service, "monthClosingService", closingService);
- ReflectionTestUtils.setField(service, "reSubmitApprovalService", approvalService);
- return new Fixture(service, client, approvalService);
- }
- private static String currentMonth() {
- return YearMonth.now(ZoneId.of("Asia/Shanghai"))
- .format(DateTimeFormatter.ofPattern("yyyyMM"));
- }
- private static Map<String, Object> formalSummary(double dailyApproved) {
- Map<String, Object> result = summaryWithBillableRows(
- billable("P-001", 0d, 8d, 8d, 0d),
- billable("P-002", 0d, 4d, 4d, 0d));
- result.put("numberField_mmeakgi4", dailyApproved);
- return result;
- }
- private static Map<String, Object> summaryWithBillableRows(Map<String, Object>... rows) {
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("tableField_mmczo634", Arrays.asList(rows));
- result.put("tableField_mmczo63h", Collections.emptyList());
- result.put("tableField_mmeakgid", Collections.emptyList());
- return result;
- }
- private static Map<String, Object> billable(String projectCode, double draft,
- double submitted, double approved, double change) {
- Map<String, Object> row = new LinkedHashMap<>();
- row.put("textField_mmacxew3", projectCode);
- row.put("textField_mmad6jcj", "Project " + projectCode);
- row.put("numberField_mmczo635", draft);
- row.put("numberField_mmczo636", submitted);
- row.put("numberField_mmczo637", approved);
- row.put("numberField_mmd80zae", change);
- return row;
- }
- private static Map<String, Object> changeRow(String projectCode, double hours) {
- return changeRow(DAY, projectCode, hours);
- }
- private static Map<String, Object> changeRow(String day, String projectCode, double hours) {
- Map<String, Object> row = new LinkedHashMap<>();
- row.put("dateStr", day);
- row.put("projectCode", projectCode);
- row.put("projectName", "Project " + projectCode);
- row.put("hours", hours);
- return row;
- }
- private static Map<String, Object> record(String id) {
- return Collections.singletonMap("formInstId", id);
- }
- private static DDR_New<Object> records(Map<String, Object>... items) {
- DDR_New<Object> result = new DDR_New<>();
- result.setData(Arrays.asList(items));
- return result;
- }
- private static DDR_New<Object> detail(Map<String, Object> data) {
- DDR_New<Object> result = new DDR_New<>();
- result.setFormData(data);
- return result;
- }
- private static final class Fixture {
- private final TimeCardChangeService service;
- private final YDClient client;
- private final ReSubmitApprovalService approvalService;
- private Fixture(TimeCardChangeService service, YDClient client,
- ReSubmitApprovalService approvalService) {
- this.service = service;
- this.client = client;
- this.approvalService = approvalService;
- }
- }
- }
|