test: ga-common 서비스 + ga-batch MappingEngine 단위 테스트 37건 추가

- MenuTreeBuilderTest (6): 트리 빌드, sortOrder 정렬, 고아 노드 처리
- PermissionCheckerTest (4): userId null 단락, mapper 위임
- CommonCodeServiceTest (14): 그룹/상세 CRUD, 시스템 코드 잠금, 중복 검출
- MappingEngineTest (13): RECRUIT/MAINTAIN 라우팅, TRIM/UPPER/LOWER/DATE/
  DIVIDE/MULTIPLY/CODE_MAP 변환, 9가지 에러 분기

전체 단위 테스트 76건 (1+2단계 합산).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-05-10 20:53:30 +09:00
parent 8a0483cccc
commit 936d376794
4 changed files with 761 additions and 0 deletions
@@ -0,0 +1,382 @@
package com.ga.batch.settlement.transform;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
import com.ga.core.mapper.product.ContractMapper;
import com.ga.core.mapper.receive.ReceiveMapper;
import com.ga.core.vo.ledger.LedgerVO;
import com.ga.core.vo.product.ContractResp;
import com.ga.core.vo.product.ContractSearchParam;
import com.ga.core.vo.product.ContractVO;
import com.ga.core.vo.receive.CompanyFieldMappingVO;
import com.ga.core.vo.receive.ParseErrorLogVO;
import com.ga.core.vo.receive.RawCommissionDataVO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
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 MappingEngineTest {
private ReceiveMapper receiveMapper;
private ContractMapper contractMapper;
private RecruitLedgerMapper recruitMapper;
private MaintainLedgerMapper maintainMapper;
private MappingEngine engine;
@BeforeEach
void setUp() {
receiveMapper = mock(ReceiveMapper.class);
contractMapper = mock(ContractMapper.class);
recruitMapper = mock(RecruitLedgerMapper.class);
maintainMapper = mock(MaintainLedgerMapper.class);
engine = new MappingEngine(receiveMapper, contractMapper, recruitMapper, maintainMapper, new ObjectMapper());
}
/** 매핑 정의 1건 생성 헬퍼 */
private CompanyFieldMappingVO mapping(String src, String tgt, String dataType, String rule, String defVal, String required) {
CompanyFieldMappingVO m = new CompanyFieldMappingVO();
m.setSourceField(src);
m.setTargetField(tgt);
m.setDataType(dataType);
m.setTransformRule(rule);
m.setDefaultValue(defVal);
m.setIsRequired(required);
return m;
}
/** raw 1건 + 정상 mapping 정의 + 계약 매칭까지 갖춘 행복 경로 셋업 */
private RawCommissionDataVO basicRecruitRaw(String json) {
RawCommissionDataVO raw = new RawCommissionDataVO();
raw.setRawId(1000L);
raw.setCompanyCode("SAMSUNG");
raw.setSettleMonth("202405");
raw.setDataType("RECRUIT");
raw.setRawContent(json);
return raw;
}
private void setupContractMatch(String policyNo, Long contractId, Long agentId) {
ContractResp resp = new ContractResp();
resp.setContractId(contractId);
when(contractMapper.selectList(any(ContractSearchParam.class)))
.thenReturn(List.of(resp));
ContractVO vo = new ContractVO();
vo.setContractId(contractId);
vo.setAgentId(agentId);
vo.setPolicyNo(policyNo);
vo.setPremium(new BigDecimal("500000"));
vo.setPayCycle("MONTHLY");
when(contractMapper.selectById(contractId)).thenReturn(vo);
}
@Test
@DisplayName("RECRUIT 행복경로: TRIM/DATE/CODE_MAP 모두 적용 + recruitMapper.insert 호출")
void recruit_happyPath() {
String json = """
{"policyNo":" POL-001 ", "premium":"1000000", "amt":"75000",
"agentAmt":"45000", "compRate":"7.5", "payRate":"60",
"type":"WL_RAW"}
""";
RawCommissionDataVO raw = basicRecruitRaw(json);
when(receiveMapper.selectFieldMappings("SAMSUNG", "recruit_ledger")).thenReturn(List.of(
mapping("policyNo", "policy_no", "STRING", "TRIM", null, "Y"),
mapping("premium", "premium", "DECIMAL", null, null, "N"),
mapping("amt", "company_amount", "DECIMAL", null, null, "N"),
mapping("agentAmt", "agent_amount", "DECIMAL", null, null, "N"),
mapping("compRate", "company_rate", "DECIMAL", null, null, "N"),
mapping("payRate", "payout_rate", "DECIMAL", null, null, "N"),
mapping("type", "insurance_type", "STRING", "CODE_MAP", "INSURANCE_TYPE", "N")
));
when(receiveMapper.findTargetCode("SAMSUNG", "INSURANCE_TYPE", "WL_RAW")).thenReturn("WHOLE_LIFE");
setupContractMatch("POL-001", 100L, 10L);
Long ledgerId = engine.transform(raw);
// recruit insert 호출 / maintain 미호출
ArgumentCaptor<LedgerVO> cap = ArgumentCaptor.forClass(LedgerVO.class);
verify(recruitMapper).insert(cap.capture());
verify(maintainMapper, never()).insert(any());
LedgerVO led = cap.getValue();
assertThat(led.getContractId()).isEqualTo(100L);
assertThat(led.getAgentId()).isEqualTo(10L);
assertThat(led.getPolicyNo()).isEqualTo("POL-001"); // TRIM
assertThat(led.getCompanyCode()).isEqualTo("SAMSUNG");
assertThat(led.getInsuranceType()).isEqualTo("WHOLE_LIFE"); // CODE_MAP 변환
assertThat(led.getPremium()).isEqualByComparingTo("1000000");
assertThat(led.getCompanyAmount()).isEqualByComparingTo("75000");
assertThat(led.getAgentAmount()).isEqualByComparingTo("45000");
assertThat(led.getCompanyRate()).isEqualByComparingTo("7.5");
assertThat(led.getPayoutRate()).isEqualByComparingTo("60");
// 세금은 agent_amount × 3.3% 자동 계산: 45000 × 3.3% = 1485
assertThat(led.getTaxAmount()).isEqualByComparingTo("1485.00");
assertThat(led.getSettleMonth()).isEqualTo("202405");
assertThat(led.getStatus()).isEqualTo("CALCULATED");
assertThat(led.getReconcileStatus()).isEqualTo("PENDING");
// ledger 의 ID 는 insert 후 채워지지만, 반환값은 mapped LedgerId 그대로
assertThat(ledgerId).isEqualTo(led.getLedgerId());
}
@Test
@DisplayName("MAINTAIN 데이터타입은 maintainMapper.insert 로 라우팅")
void maintain_routedToMaintainMapper() {
String json = """
{"policyNo":"POL-002", "premium":"500000"}
""";
RawCommissionDataVO raw = new RawCommissionDataVO();
raw.setRawId(2000L);
raw.setCompanyCode("HANWHA");
raw.setSettleMonth("202405");
raw.setDataType("MAINTAIN");
raw.setRawContent(json);
when(receiveMapper.selectFieldMappings("HANWHA", "maintain_ledger")).thenReturn(List.of(
mapping("policyNo", "policy_no", "STRING", null, null, "Y"),
mapping("premium", "premium", "DECIMAL", null, null, "N")
));
setupContractMatch("POL-002", 200L, 20L);
engine.transform(raw);
verify(maintainMapper).insert(any(LedgerVO.class));
verify(recruitMapper, never()).insert(any());
}
@Test
@DisplayName("UPPER/LOWER/DIVIDE/MULTIPLY 변환 규칙 검증")
void multipleTransformRules() {
String json = """
{"upper":"hello", "lower":"WORLD",
"div":"100000", "mul":"5"}
""";
RawCommissionDataVO raw = basicRecruitRaw(json);
// policy_no 도 필수이므로 추가
String j2 = json.replace("{", "{\"pol\":\"POL-X\",");
raw.setRawContent(j2);
when(receiveMapper.selectFieldMappings("SAMSUNG", "recruit_ledger")).thenReturn(List.of(
mapping("pol", "policy_no", "STRING", "TRIM", null, "Y"),
// attr1 등 표준 외 필드는 mapped 에 들어가지만 LedgerVO 에 매핑되진 않음 — 변환만 검증
mapping("upper", "product_code", "STRING", "UPPER", null, "N"),
mapping("lower", "pay_method", "STRING", "LOWER", null, "N"),
mapping("div", "agent_amount", "DECIMAL", "DIVIDE", "100", "N"),
mapping("mul", "company_amount", "DECIMAL", "MULTIPLY", "1000","N")
));
setupContractMatch("POL-X", 300L, 30L);
engine.transform(raw);
ArgumentCaptor<LedgerVO> cap = ArgumentCaptor.forClass(LedgerVO.class);
verify(recruitMapper).insert(cap.capture());
LedgerVO led = cap.getValue();
assertThat(led.getProductCode()).isEqualTo("HELLO"); // UPPER
assertThat(led.getPayMethod()).isEqualTo("world"); // LOWER
// DIVIDE: 100000 / 100 = 1000.00
assertThat(led.getAgentAmount()).isEqualByComparingTo("1000.00");
// MULTIPLY: 5 × 1000 = 5000
assertThat(led.getCompanyAmount()).isEqualByComparingTo("5000");
}
@Test
@DisplayName("DATE 변환: defaultValue 의 패턴으로 파싱")
void dateTransform() {
// mapped 의 "contract_date" 등은 LedgerVO 에 직접 안 들어가므로,
// 변환만 일어나고 buildLedger 에서 기본값을 쓰는지 확인하기 어려움.
// 대신 변환 자체가 실패해서 에러가 안 나는지로 확인.
String json = """
{"pol":"POL-D", "dateField":"20240515"}
""";
RawCommissionDataVO raw = basicRecruitRaw(json);
raw.setRawContent(json);
when(receiveMapper.selectFieldMappings("SAMSUNG", "recruit_ledger")).thenReturn(List.of(
mapping("pol", "policy_no", "STRING", "TRIM", null, "Y"),
mapping("dateField", "contract_date", "DATE", "DATE", "yyyyMMdd", "N")
));
setupContractMatch("POL-D", 400L, 40L);
Long id = engine.transform(raw);
// 정상 처리 (예외 없음)
verify(recruitMapper).insert(any(LedgerVO.class));
verify(receiveMapper, never()).insertError(any());
}
@Test
@DisplayName("INVALID_INPUT: company_code 가 null 이면 에러 로그 + null 반환")
void invalidInput_nullCompanyCode() {
RawCommissionDataVO raw = new RawCommissionDataVO();
raw.setRawId(1L);
raw.setRawContent("{}");
raw.setDataType("RECRUIT");
// companyCode 누락
Long id = engine.transform(raw);
assertThat(id).isNull();
ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
verify(receiveMapper).insertError(cap.capture());
assertThat(cap.getValue().getErrorType()).isEqualTo("INVALID_INPUT");
verify(recruitMapper, never()).insert(any());
}
@Test
@DisplayName("UNKNOWN_TYPE: 지원하지 않는 data_type")
void unknownDataType() {
RawCommissionDataVO raw = new RawCommissionDataVO();
raw.setRawId(2L);
raw.setCompanyCode("SAMSUNG");
raw.setRawContent("{}");
raw.setDataType("STRANGE");
Long id = engine.transform(raw);
assertThat(id).isNull();
ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
verify(receiveMapper).insertError(cap.capture());
assertThat(cap.getValue().getErrorType()).isEqualTo("UNKNOWN_TYPE");
}
@Test
@DisplayName("NO_MAPPING: 매핑 정의가 비어있음")
void noMapping() {
RawCommissionDataVO raw = basicRecruitRaw("{}");
when(receiveMapper.selectFieldMappings("SAMSUNG", "recruit_ledger"))
.thenReturn(new ArrayList<>());
Long id = engine.transform(raw);
assertThat(id).isNull();
ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
verify(receiveMapper).insertError(cap.capture());
assertThat(cap.getValue().getErrorType()).isEqualTo("NO_MAPPING");
}
@Test
@DisplayName("JSON_PARSE: 잘못된 raw_content")
void jsonParseError() {
RawCommissionDataVO raw = basicRecruitRaw("{not-json");
when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
mapping("a", "policy_no", "STRING", null, null, "Y")
));
Long id = engine.transform(raw);
assertThat(id).isNull();
ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
verify(receiveMapper).insertError(cap.capture());
assertThat(cap.getValue().getErrorType()).isEqualTo("JSON_PARSE");
}
@Test
@DisplayName("REQUIRED: 필수 필드가 raw 에 없음")
void requiredMissing() {
RawCommissionDataVO raw = basicRecruitRaw("{\"other\":\"x\"}");
when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
mapping("policyNo", "policy_no", "STRING", "TRIM", null, "Y") // 필수
));
Long id = engine.transform(raw);
assertThat(id).isNull();
ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
verify(receiveMapper).insertError(cap.capture());
assertThat(cap.getValue().getErrorType()).isEqualTo("REQUIRED");
assertThat(cap.getValue().getErrorField()).isEqualTo("policy_no");
}
@Test
@DisplayName("NO_POLICY: policy_no 매핑이 아예 정의되지 않음")
void noPolicyMapping() {
RawCommissionDataVO raw = basicRecruitRaw("{\"premium\":\"100\"}");
when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
mapping("premium", "premium", "DECIMAL", null, null, "N")
));
Long id = engine.transform(raw);
assertThat(id).isNull();
ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
verify(receiveMapper).insertError(cap.capture());
assertThat(cap.getValue().getErrorType()).isEqualTo("NO_POLICY");
}
@Test
@DisplayName("CONTRACT_NOT_FOUND: policy_no 로 계약 매칭 실패")
void contractNotFound() {
RawCommissionDataVO raw = basicRecruitRaw("{\"policyNo\":\"POL-NONE\"}");
when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
mapping("policyNo", "policy_no", "STRING", "TRIM", null, "Y")
));
when(contractMapper.selectList(any(ContractSearchParam.class))).thenReturn(List.of());
Long id = engine.transform(raw);
assertThat(id).isNull();
ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
verify(receiveMapper).insertError(cap.capture());
assertThat(cap.getValue().getErrorType()).isEqualTo("CONTRACT_NOT_FOUND");
assertThat(cap.getValue().getErrorValue()).isEqualTo("POL-NONE");
}
@Test
@DisplayName("CODE_MAP: 매칭 실패 시 원본 값 그대로 사용")
void codeMap_fallback() {
String json = """
{"pol":"POL-CM", "type":"UNKNOWN_TYPE"}
""";
RawCommissionDataVO raw = basicRecruitRaw(json);
when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
mapping("pol", "policy_no", "STRING", "TRIM", null, "Y"),
mapping("type", "insurance_type", "STRING", "CODE_MAP", "INSURANCE_TYPE", "N")
));
when(receiveMapper.findTargetCode("SAMSUNG", "INSURANCE_TYPE", "UNKNOWN_TYPE")).thenReturn(null);
setupContractMatch("POL-CM", 500L, 50L);
engine.transform(raw);
ArgumentCaptor<LedgerVO> cap = ArgumentCaptor.forClass(LedgerVO.class);
verify(recruitMapper).insert(cap.capture());
// 매핑 미존재 → 원본 그대로
assertThat(cap.getValue().getInsuranceType()).isEqualTo("UNKNOWN_TYPE");
}
@Test
@DisplayName("계약값 폴백: premium/pay_method 가 mapped 에 없으면 contract 값 사용")
void buildLedger_fallbackToContract() {
// premium, pay_method 매핑 없음 → contract 의 값(500000, MONTHLY) 사용
RawCommissionDataVO raw = basicRecruitRaw("{\"pol\":\"POL-FB\"}");
when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
mapping("pol", "policy_no", "STRING", "TRIM", null, "Y")
));
setupContractMatch("POL-FB", 600L, 60L);
engine.transform(raw);
ArgumentCaptor<LedgerVO> cap = ArgumentCaptor.forClass(LedgerVO.class);
verify(recruitMapper).insert(cap.capture());
LedgerVO led = cap.getValue();
assertThat(led.getPremium()).isEqualByComparingTo("500000"); // contract.premium
assertThat(led.getPayMethod()).isEqualTo("MONTHLY"); // contract.payCycle
assertThat(led.getInsuranceType()).isEqualTo("WHOLE_LIFE"); // 기본값
assertThat(led.getCommissionYear()).isEqualTo(1); // 기본값
}
}