Skip to content

Commit 99861ce

Browse files
budaidevadamsaghy
authored andcommitted
FINERACT-2418: add originator during the loan application
1 parent f5951f1 commit 99861ce

File tree

12 files changed

+767
-5
lines changed

12 files changed

+767
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanorigination.data;
20+
21+
import lombok.AllArgsConstructor;
22+
import lombok.Data;
23+
import lombok.NoArgsConstructor;
24+
25+
@Data
26+
@NoArgsConstructor
27+
@AllArgsConstructor
28+
public class LoanApplicationOriginatorData {
29+
30+
private Long id;
31+
private String externalId;
32+
private String name;
33+
private Long typeId;
34+
private Long channelTypeId;
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanorigination.exception;
20+
21+
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
22+
23+
public class LoanOriginatorCreationNotAllowedException extends AbstractPlatformDomainRuleException {
24+
25+
public LoanOriginatorCreationNotAllowedException(String externalId) {
26+
super("error.msg.loan.originator.creation.not.allowed", "Cannot create originator with externalId '" + externalId
27+
+ "' during loan application. Global configuration 'enable-originator-creation-during-loan-application' is disabled.",
28+
externalId);
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanorigination.serialization;
20+
21+
import static org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.CHANNEL_TYPE_CODE_NAME;
22+
import static org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.CHANNEL_TYPE_ID_PARAM;
23+
import static org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.EXTERNAL_ID_PARAM;
24+
import static org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.ORIGINATOR_TYPE_CODE_NAME;
25+
import static org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.ORIGINATOR_TYPE_ID_PARAM;
26+
27+
import com.google.gson.JsonElement;
28+
import com.google.gson.JsonObject;
29+
import java.util.ArrayList;
30+
import java.util.List;
31+
import lombok.RequiredArgsConstructor;
32+
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper;
33+
import org.apache.fineract.infrastructure.codes.exception.CodeValueNotFoundException;
34+
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
35+
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
36+
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
37+
import org.apache.fineract.portfolio.loanorigination.data.LoanApplicationOriginatorData;
38+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
39+
import org.springframework.stereotype.Component;
40+
41+
@Component
42+
@RequiredArgsConstructor
43+
@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true")
44+
public class LoanApplicationOriginatorDataValidator {
45+
46+
private static final String RESOURCE_NAME = "loan.originator";
47+
private static final String ID_PARAM = "id";
48+
private static final String NAME_PARAM = "name";
49+
50+
private final CodeValueRepositoryWrapper codeValueRepositoryWrapper;
51+
52+
public LoanApplicationOriginatorData validateAndExtract(JsonObject jsonObject) {
53+
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
54+
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource(RESOURCE_NAME);
55+
56+
final Long id = extractLong(jsonObject, ID_PARAM);
57+
final String externalId = extractString(jsonObject, EXTERNAL_ID_PARAM);
58+
59+
if (id == null && (externalId == null || externalId.isBlank())) {
60+
baseDataValidator.reset().parameter(ID_PARAM).failWithCode("or.externalId.required",
61+
"Either 'id' or 'externalId' must be provided for originator");
62+
}
63+
64+
final String name = extractString(jsonObject, NAME_PARAM);
65+
baseDataValidator.reset().parameter(NAME_PARAM).value(name).ignoreIfNull().notExceedingLengthOf(255);
66+
67+
final Long typeId = extractLong(jsonObject, ORIGINATOR_TYPE_ID_PARAM);
68+
if (typeId != null) {
69+
validateCodeValue(typeId, ORIGINATOR_TYPE_CODE_NAME, ORIGINATOR_TYPE_ID_PARAM, baseDataValidator);
70+
}
71+
72+
final Long channelTypeId = extractLong(jsonObject, CHANNEL_TYPE_ID_PARAM);
73+
if (channelTypeId != null) {
74+
validateCodeValue(channelTypeId, CHANNEL_TYPE_CODE_NAME, CHANNEL_TYPE_ID_PARAM, baseDataValidator);
75+
}
76+
77+
throwExceptionIfValidationWarningsExist(dataValidationErrors);
78+
79+
return new LoanApplicationOriginatorData(id, externalId, name, typeId, channelTypeId);
80+
}
81+
82+
private Long extractLong(JsonObject jsonObject, String paramName) {
83+
if (jsonObject.has(paramName)) {
84+
JsonElement element = jsonObject.get(paramName);
85+
if (!element.isJsonNull()) {
86+
try {
87+
return element.getAsLong();
88+
} catch (NumberFormatException e) {
89+
return null;
90+
}
91+
}
92+
}
93+
return null;
94+
}
95+
96+
private String extractString(JsonObject jsonObject, String paramName) {
97+
if (jsonObject.has(paramName)) {
98+
JsonElement element = jsonObject.get(paramName);
99+
if (!element.isJsonNull()) {
100+
return element.getAsString();
101+
}
102+
}
103+
return null;
104+
}
105+
106+
private void validateCodeValue(Long codeValueId, String codeName, String paramName, DataValidatorBuilder baseDataValidator) {
107+
try {
108+
this.codeValueRepositoryWrapper.findOneByCodeNameAndIdWithNotFoundDetection(codeName, codeValueId);
109+
} catch (CodeValueNotFoundException e) {
110+
baseDataValidator.reset().parameter(paramName).value(codeValueId).failWithCode("invalid.code.value",
111+
"Invalid code value id " + codeValueId + " for " + codeName);
112+
}
113+
}
114+
115+
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
116+
if (!dataValidationErrors.isEmpty()) {
117+
throw new PlatformApiDataValidationException(dataValidationErrors);
118+
}
119+
}
120+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanorigination.service;
20+
21+
import static org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants.ENABLE_ORIGINATOR_CREATION_DURING_LOAN_APPLICATION;
22+
import static org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.CHANNEL_TYPE_CODE_NAME;
23+
import static org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.ORIGINATOR_TYPE_CODE_NAME;
24+
25+
import com.google.gson.JsonArray;
26+
import com.google.gson.JsonElement;
27+
import com.google.gson.JsonObject;
28+
import java.util.HashSet;
29+
import java.util.Optional;
30+
import java.util.Set;
31+
import lombok.RequiredArgsConstructor;
32+
import lombok.extern.slf4j.Slf4j;
33+
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
34+
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper;
35+
import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationProperty;
36+
import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper;
37+
import org.apache.fineract.infrastructure.configuration.exception.GlobalConfigurationPropertyNotFoundException;
38+
import org.apache.fineract.infrastructure.core.domain.ExternalId;
39+
import org.apache.fineract.portfolio.loanaccount.service.LoanOriginatorLinkingService;
40+
import org.apache.fineract.portfolio.loanorigination.data.LoanApplicationOriginatorData;
41+
import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginator;
42+
import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMapping;
43+
import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMappingRepository;
44+
import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorRepository;
45+
import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorStatus;
46+
import org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorCreationNotAllowedException;
47+
import org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorNotActiveException;
48+
import org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorNotFoundException;
49+
import org.apache.fineract.portfolio.loanorigination.serialization.LoanApplicationOriginatorDataValidator;
50+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
51+
import org.springframework.stereotype.Service;
52+
import org.springframework.transaction.annotation.Transactional;
53+
54+
/**
55+
* Implementation of {@link LoanOriginatorLinkingService} that handles processing of originators during loan
56+
* application. This service is active only when the loan-origination module is enabled.
57+
*/
58+
@Slf4j
59+
@Service("loanOriginatorLinkingServiceImpl")
60+
@RequiredArgsConstructor
61+
@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true")
62+
public class LoanOriginatorLinkingServiceImpl implements LoanOriginatorLinkingService {
63+
64+
private final LoanOriginatorRepository loanOriginatorRepository;
65+
private final LoanOriginatorMappingRepository loanOriginatorMappingRepository;
66+
private final LoanApplicationOriginatorDataValidator validator;
67+
private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository;
68+
private final CodeValueRepositoryWrapper codeValueRepositoryWrapper;
69+
70+
@Transactional
71+
@Override
72+
public void processOriginatorsForLoanApplication(Long loanId, JsonArray originatorsArray) {
73+
if (originatorsArray == null || originatorsArray.isEmpty()) {
74+
return;
75+
}
76+
77+
log.debug("Processing {} originators for loan application {}", originatorsArray.size(), loanId);
78+
79+
Set<Long> attachedOriginatorIds = new HashSet<>();
80+
81+
for (JsonElement element : originatorsArray) {
82+
if (!element.isJsonObject()) {
83+
continue;
84+
}
85+
86+
JsonObject jsonObject = element.getAsJsonObject();
87+
LoanApplicationOriginatorData originatorData = validator.validateAndExtract(jsonObject);
88+
LoanOriginator originator = resolveOrCreateOriginator(originatorData);
89+
90+
if (attachedOriginatorIds.contains(originator.getId())) {
91+
log.debug("Originator {} already attached to loan {}, skipping duplicate", originator.getId(), loanId);
92+
continue;
93+
}
94+
95+
if (originator.getStatus() != LoanOriginatorStatus.ACTIVE) {
96+
throw new LoanOriginatorNotActiveException(originator.getId(), originator.getStatus().getValue());
97+
}
98+
99+
if (!loanOriginatorMappingRepository.existsByLoanIdAndOriginatorId(loanId, originator.getId())) {
100+
LoanOriginatorMapping mapping = LoanOriginatorMapping.create(loanId, originator);
101+
loanOriginatorMappingRepository.save(mapping);
102+
log.debug("Attached originator {} to loan {}", originator.getId(), loanId);
103+
}
104+
105+
attachedOriginatorIds.add(originator.getId());
106+
}
107+
}
108+
109+
private LoanOriginator resolveOrCreateOriginator(LoanApplicationOriginatorData originatorData) {
110+
if (originatorData.getId() != null) {
111+
return loanOriginatorRepository.findById(originatorData.getId())
112+
.orElseThrow(() -> new LoanOriginatorNotFoundException(originatorData.getId()));
113+
}
114+
115+
String externalId = originatorData.getExternalId();
116+
Optional<LoanOriginator> existingOriginator = loanOriginatorRepository.findByExternalId(new ExternalId(externalId));
117+
118+
if (existingOriginator.isPresent()) {
119+
return existingOriginator.get();
120+
}
121+
122+
if (!isOriginatorCreationDuringLoanApplicationEnabled()) {
123+
throw new LoanOriginatorCreationNotAllowedException(externalId);
124+
}
125+
126+
return createNewOriginator(originatorData);
127+
}
128+
129+
private boolean isOriginatorCreationDuringLoanApplicationEnabled() {
130+
try {
131+
GlobalConfigurationProperty config = globalConfigurationRepository
132+
.findOneByNameWithNotFoundDetection(ENABLE_ORIGINATOR_CREATION_DURING_LOAN_APPLICATION);
133+
return config.isEnabled();
134+
} catch (GlobalConfigurationPropertyNotFoundException e) {
135+
log.warn("Global configuration '{}' not found, defaulting to disabled", ENABLE_ORIGINATOR_CREATION_DURING_LOAN_APPLICATION);
136+
return false;
137+
}
138+
}
139+
140+
private LoanOriginator createNewOriginator(LoanApplicationOriginatorData data) {
141+
log.info("Creating new originator with externalId: {} during loan application", data.getExternalId());
142+
143+
CodeValue originatorType = resolveCodeValue(data.getTypeId(), ORIGINATOR_TYPE_CODE_NAME);
144+
CodeValue channelType = resolveCodeValue(data.getChannelTypeId(), CHANNEL_TYPE_CODE_NAME);
145+
146+
LoanOriginator originator = LoanOriginator.create(new ExternalId(data.getExternalId()), data.getName(), LoanOriginatorStatus.ACTIVE,
147+
originatorType, channelType);
148+
149+
return loanOriginatorRepository.saveAndFlush(originator);
150+
}
151+
152+
private CodeValue resolveCodeValue(Long codeValueId, String codeName) {
153+
if (codeValueId == null) {
154+
return null;
155+
}
156+
return codeValueRepositoryWrapper.findOneByCodeNameAndIdWithNotFoundDetection(codeName, codeValueId);
157+
}
158+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.service;
20+
21+
import com.google.gson.JsonArray;
22+
23+
public interface LoanOriginatorLinkingService {
24+
25+
/**
26+
* Process originators provided during loan application. Creates new originators if allowed by global config, then
27+
* attaches them to the loan.
28+
*
29+
* @param loanId
30+
* the loan ID to attach originators to
31+
* @param originatorsArray
32+
* JSON array of originator data from loan request
33+
*/
34+
void processOriginatorsForLoanApplication(Long loanId, JsonArray originatorsArray);
35+
}

0 commit comments

Comments
 (0)