|
|
@ -4,10 +4,18 @@ import com.crm.lead.port.outbound.CreateOpportunityCmd; |
|
|
import com.crm.lead.port.outbound.OpportunityCreationException; |
|
|
import com.crm.lead.port.outbound.OpportunityCreationException; |
|
|
import com.crm.opportunity.domain.entity.Opportunity; |
|
|
import com.crm.opportunity.domain.entity.Opportunity; |
|
|
import com.crm.opportunity.domain.entity.OpportunityCustomer; |
|
|
import com.crm.opportunity.domain.entity.OpportunityCustomer; |
|
|
|
|
|
import com.crm.opportunity.domain.entity.OpportunityOplog; |
|
|
|
|
|
import com.crm.opportunity.domain.entity.OpportunityTeam; |
|
|
import com.crm.opportunity.domain.enums.OpportunityStatus; |
|
|
import com.crm.opportunity.domain.enums.OpportunityStatus; |
|
|
|
|
|
import com.crm.opportunity.domain.enums.TeamMemberPermission; |
|
|
import com.crm.opportunity.mapper.OpportunityCustomerMapper; |
|
|
import com.crm.opportunity.mapper.OpportunityCustomerMapper; |
|
|
import com.crm.opportunity.mapper.OpportunityMapper; |
|
|
import com.crm.opportunity.mapper.OpportunityMapper; |
|
|
|
|
|
import com.crm.opportunity.mapper.OpportunityOplogMapper; |
|
|
|
|
|
import com.crm.opportunity.mapper.OpportunityTeamMapper; |
|
|
import com.crm.opportunity.port.inbound.impl.OpportunityCreationPortImpl; |
|
|
import com.crm.opportunity.port.inbound.impl.OpportunityCreationPortImpl; |
|
|
|
|
|
import com.crm.rule.domain.entity.OpportunityStageNode; |
|
|
|
|
|
import com.crm.rule.domain.entity.OpportunityStageTemplate; |
|
|
|
|
|
import com.crm.rule.service.IOpportunityStageTemplateService; |
|
|
import org.junit.jupiter.api.DisplayName; |
|
|
import org.junit.jupiter.api.DisplayName; |
|
|
import org.junit.jupiter.api.Test; |
|
|
import org.junit.jupiter.api.Test; |
|
|
import org.junit.jupiter.api.extension.ExtendWith; |
|
|
import org.junit.jupiter.api.extension.ExtendWith; |
|
|
@ -17,6 +25,8 @@ import org.mockito.Mock; |
|
|
import org.mockito.junit.jupiter.MockitoExtension; |
|
|
import org.mockito.junit.jupiter.MockitoExtension; |
|
|
import org.springframework.dao.DuplicateKeyException; |
|
|
import org.springframework.dao.DuplicateKeyException; |
|
|
|
|
|
|
|
|
|
|
|
import java.util.List; |
|
|
|
|
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat; |
|
|
import static org.assertj.core.api.Assertions.assertThat; |
|
|
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
|
|
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
|
|
import static org.mockito.ArgumentMatchers.any; |
|
|
import static org.mockito.ArgumentMatchers.any; |
|
|
@ -38,9 +48,16 @@ class OpportunityCreationPortImplTest { |
|
|
private static final Long LEAD_ID = 9001L; |
|
|
private static final Long LEAD_ID = 9001L; |
|
|
private static final Long CUSTOMER_ID = 5001L; |
|
|
private static final Long CUSTOMER_ID = 5001L; |
|
|
private static final Long NEW_OPP_ID = 7001L; |
|
|
private static final Long NEW_OPP_ID = 7001L; |
|
|
|
|
|
private static final Long STAGE_TPL_VER_ID = 6601L; |
|
|
|
|
|
private static final Long FIRST_NODE_ID = 6611L; |
|
|
|
|
|
private static final Long OWNER_USER_ID = 100L; |
|
|
|
|
|
private static final Long OWNER_DEPT_ID = 200L; |
|
|
|
|
|
|
|
|
@Mock private OpportunityMapper oppMapper; |
|
|
@Mock private OpportunityMapper oppMapper; |
|
|
@Mock private OpportunityCustomerMapper customerMapper; |
|
|
@Mock private OpportunityCustomerMapper customerMapper; |
|
|
|
|
|
@Mock private OpportunityOplogMapper oplogMapper; |
|
|
|
|
|
@Mock private OpportunityTeamMapper teamMapper; |
|
|
|
|
|
@Mock private IOpportunityStageTemplateService stageTemplateService; |
|
|
|
|
|
|
|
|
@InjectMocks |
|
|
@InjectMocks |
|
|
private OpportunityCreationPortImpl port; |
|
|
private OpportunityCreationPortImpl port; |
|
|
@ -49,7 +66,23 @@ class OpportunityCreationPortImplTest { |
|
|
return new CreateOpportunityCmd( |
|
|
return new CreateOpportunityCmd( |
|
|
LEAD_ID, "商机A", "ind01", "甲方X", "意向客户名", customerId, |
|
|
LEAD_ID, "商机A", "ind01", "甲方X", "意向客户名", customerId, |
|
|
"110000", "110100", "备注", "线索A", "13800000000", "prod01", |
|
|
"110000", "110100", "备注", "线索A", "13800000000", "prod01", |
|
|
100L, 200L, 300L); |
|
|
OWNER_USER_ID, OWNER_DEPT_ID, 300L); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** 桂定阶段模板命中:部门解析到发布中版本 + 首节点。 */ |
|
|
|
|
|
private void stubStageTemplateHit() { |
|
|
|
|
|
OpportunityStageTemplate ver = new OpportunityStageTemplate(); |
|
|
|
|
|
ver.setId(STAGE_TPL_VER_ID); |
|
|
|
|
|
ver.setVersionNo("V1.0"); |
|
|
|
|
|
when(stageTemplateService.resolveBindingVersion(OWNER_DEPT_ID)).thenReturn(ver); |
|
|
|
|
|
OpportunityStageNode first = new OpportunityStageNode(); |
|
|
|
|
|
first.setId(FIRST_NODE_ID); |
|
|
|
|
|
first.setSeqNo(1); |
|
|
|
|
|
OpportunityStageNode second = new OpportunityStageNode(); |
|
|
|
|
|
second.setId(FIRST_NODE_ID + 1); |
|
|
|
|
|
second.setSeqNo(2); |
|
|
|
|
|
when(stageTemplateService.listNodesOfVersion(STAGE_TPL_VER_ID)) |
|
|
|
|
|
.thenReturn(List.of(first, second)); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** insert 时给实体回填自增 id(模拟 MyBatis-Plus useGeneratedKeys)。 */ |
|
|
/** insert 时给实体回填自增 id(模拟 MyBatis-Plus useGeneratedKeys)。 */ |
|
|
@ -133,6 +166,83 @@ class OpportunityCreationPortImplTest { |
|
|
assertThat(saved.getCityCode()).isEqualTo("110100"); |
|
|
assertThat(saved.getCityCode()).isEqualTo("110100"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
|
@DisplayName("阶段落位:命中默认模板 → current_stage_id=首节点 + 锁模板版本 id/号(票 02 步 2)") |
|
|
|
|
|
void create_stagePlacement_bindsFirstNode() { |
|
|
|
|
|
when(oppMapper.selectCount(any())).thenReturn(0L); |
|
|
|
|
|
stubStageTemplateHit(); |
|
|
|
|
|
ArgumentCaptor<Opportunity> insCap = ArgumentCaptor.forClass(Opportunity.class); |
|
|
|
|
|
doAnswer(inv -> { |
|
|
|
|
|
((Opportunity) inv.getArgument(0)).setId(NEW_OPP_ID); |
|
|
|
|
|
return 1; |
|
|
|
|
|
}).when(oppMapper).insert(insCap.capture()); |
|
|
|
|
|
|
|
|
|
|
|
port.createOpportunity(cmd(null)); |
|
|
|
|
|
|
|
|
|
|
|
Opportunity saved = insCap.getValue(); |
|
|
|
|
|
assertThat(saved.getCurrentStageId()).isEqualTo(FIRST_NODE_ID); |
|
|
|
|
|
assertThat(saved.getStageTemplateId()).isEqualTo(STAGE_TPL_VER_ID); |
|
|
|
|
|
assertThat(saved.getStageTemplateVersion()).isEqualTo("V1.0"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
|
@DisplayName("阶段落位:无发布中模板(resolveBindingVersion 返 null)→ 三字段空,商机仍建成(容忍空走法 A)") |
|
|
|
|
|
void create_noStageTemplate_tolerated() { |
|
|
|
|
|
when(oppMapper.selectCount(any())).thenReturn(0L); |
|
|
|
|
|
when(stageTemplateService.resolveBindingVersion(OWNER_DEPT_ID)).thenReturn(null); |
|
|
|
|
|
ArgumentCaptor<Opportunity> insCap = ArgumentCaptor.forClass(Opportunity.class); |
|
|
|
|
|
doAnswer(inv -> { |
|
|
|
|
|
((Opportunity) inv.getArgument(0)).setId(NEW_OPP_ID); |
|
|
|
|
|
return 1; |
|
|
|
|
|
}).when(oppMapper).insert(insCap.capture()); |
|
|
|
|
|
|
|
|
|
|
|
Long id = port.createOpportunity(cmd(null)); |
|
|
|
|
|
|
|
|
|
|
|
assertThat(id).isEqualTo(NEW_OPP_ID); |
|
|
|
|
|
Opportunity saved = insCap.getValue(); |
|
|
|
|
|
assertThat(saved.getCurrentStageId()).isNull(); |
|
|
|
|
|
assertThat(saved.getStageTemplateId()).isNull(); |
|
|
|
|
|
assertThat(saved.getStageTemplateVersion()).isNull(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
|
@DisplayName("初始操作日志:写一条 ROW_ADD/ROW_CHANGE/SYSTEM「由线索转入创建」(票 02 步 3)") |
|
|
|
|
|
void create_writesInitialOplog() { |
|
|
|
|
|
when(oppMapper.selectCount(any())).thenReturn(0L); |
|
|
|
|
|
stubInsertAssignsId(); |
|
|
|
|
|
|
|
|
|
|
|
port.createOpportunity(cmd(null)); |
|
|
|
|
|
|
|
|
|
|
|
ArgumentCaptor<OpportunityOplog> logCap = ArgumentCaptor.forClass(OpportunityOplog.class); |
|
|
|
|
|
verify(oplogMapper).insert(logCap.capture()); |
|
|
|
|
|
OpportunityOplog log = logCap.getValue(); |
|
|
|
|
|
assertThat(log.getOppId()).isEqualTo(NEW_OPP_ID); |
|
|
|
|
|
assertThat(log.getOpKind()).isEqualTo("ROW_ADD"); |
|
|
|
|
|
assertThat(log.getLogType()).isEqualTo("ROW_CHANGE"); |
|
|
|
|
|
assertThat(log.getOpSource()).isEqualTo("SYSTEM"); |
|
|
|
|
|
assertThat(log.getOpDesc()).contains("由线索转入创建"); |
|
|
|
|
|
assertThat(log.getOpUserId()).isEqualTo(OWNER_USER_ID); |
|
|
|
|
|
assertThat(log.getOpTime()).isNotNull(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
|
@DisplayName("团队成员:插一条领取人=商机负责人 project_role_01/READ_WRITE(票 02 步 4)") |
|
|
|
|
|
void create_insertsOwnerTeamMember() { |
|
|
|
|
|
when(oppMapper.selectCount(any())).thenReturn(0L); |
|
|
|
|
|
stubInsertAssignsId(); |
|
|
|
|
|
|
|
|
|
|
|
port.createOpportunity(cmd(null)); |
|
|
|
|
|
|
|
|
|
|
|
ArgumentCaptor<OpportunityTeam> teamCap = ArgumentCaptor.forClass(OpportunityTeam.class); |
|
|
|
|
|
verify(teamMapper).insert(teamCap.capture()); |
|
|
|
|
|
OpportunityTeam member = teamCap.getValue(); |
|
|
|
|
|
assertThat(member.getOpportunityId()).isEqualTo(NEW_OPP_ID); |
|
|
|
|
|
assertThat(member.getUserId()).isEqualTo(OWNER_USER_ID); |
|
|
|
|
|
assertThat(member.getProjectRole()).isEqualTo("project_role_01"); |
|
|
|
|
|
assertThat(member.getPermission()).isEqualTo(TeamMemberPermission.READ_WRITE.getValue()); |
|
|
|
|
|
assertThat(member.getDeleteKey()).isEqualTo(0L); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
@Test |
|
|
@Test |
|
|
@DisplayName("并发窗口:预检通过后 insert 撞 DB 唯一键 → 转译为 OpportunityCreationException") |
|
|
@DisplayName("并发窗口:预检通过后 insert 撞 DB 唯一键 → 转译为 OpportunityCreationException") |
|
|
void create_concurrentDuplicateKey_translated() { |
|
|
void create_concurrentDuplicateKey_translated() { |
|
|
|