FINERACT-2081: Ability to assign settlement information to loan product when asset is externally owned

This commit is contained in:
Arnold Galovics
2025-01-14 13:55:46 +01:00
committed by Arnold Gálovics
parent 3481761e74
commit d2bdbf843a
31 changed files with 1715 additions and 5 deletions
+3
View File
@@ -610,6 +610,9 @@ configure(project.fineractJavaProjects) {
'com.google.truth:truth',
'com.google.truth.extensions:truth-java8-extension'
)
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
test {
@@ -65,6 +65,7 @@ import org.apache.fineract.client.services.DelinquencyRangeAndBucketsManagementA
import org.apache.fineract.client.services.DocumentsApiFixed;
import org.apache.fineract.client.services.EntityDataTableApi;
import org.apache.fineract.client.services.EntityFieldConfigurationApi;
import org.apache.fineract.client.services.ExternalAssetOwnerLoanProductAttributesApi;
import org.apache.fineract.client.services.ExternalAssetOwnersApi;
import org.apache.fineract.client.services.ExternalEventConfigurationApi;
import org.apache.fineract.client.services.ExternalServicesApi;
@@ -291,6 +292,7 @@ public final class FineractClient {
public final WorkingDaysApi workingDays;
public final ExternalAssetOwnersApi externalAssetOwners;
public final ExternalAssetOwnerLoanProductAttributesApi externalAssetOwnerLoanProductAttributes;
public final LoanAccountLockApi loanAccountLockApi;
private FineractClient(OkHttpClient okHttpClient, Retrofit retrofit) {
@@ -299,6 +301,7 @@ public final class FineractClient {
loanAccountLockApi = retrofit.create(LoanAccountLockApi.class);
externalAssetOwners = retrofit.create(ExternalAssetOwnersApi.class);
externalAssetOwnerLoanProductAttributes = retrofit.create(ExternalAssetOwnerLoanProductAttributesApi.class);
glClosures = retrofit.create(AccountingClosureApi.class);
accountingRules = retrofit.create(AccountingRulesApi.class);
accountNumberFormats = retrofit.create(AccountNumberFormatApi.class);
@@ -3644,6 +3644,23 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder createExternalAssetOwnerLoanProductAttribute(final Long loanProductId) {
this.actionName = "CREATE";
this.entityName = "EXTERNAL_ASSET_OWNER_LOAN_PRODUCT_ATTRIBUTE";
this.productId = loanProductId;
this.href = "/external-asset-owners/loan-product/" + loanProductId + "/attributes";
return this;
}
public CommandWrapperBuilder updateExternalAssetOwnerLoanProductAttribute(final Long loanProductId, final Long attributeId) {
this.actionName = "UPDATE";
this.entityName = "EXTERNAL_ASSET_OWNER_LOAN_PRODUCT_ATTRIBUTE";
this.productId = loanProductId;
this.entityId = attributeId;
this.href = "/external-asset-owners/loan-product/" + loanProductId + "/attributes/" + attributeId;
return this;
}
public CommandWrapperBuilder saleLoanToExternalAssetOwner(final Long loanId) {
this.actionName = "SALE";
this.entityName = "LOAN";
@@ -29,6 +29,7 @@ import org.apache.fineract.client.services.CurrencyApi;
import org.apache.fineract.client.services.DataTablesApi;
import org.apache.fineract.client.services.DefaultApi;
import org.apache.fineract.client.services.DelinquencyRangeAndBucketsManagementApi;
import org.apache.fineract.client.services.ExternalAssetOwnerLoanProductAttributesApi;
import org.apache.fineract.client.services.ExternalAssetOwnersApi;
import org.apache.fineract.client.services.ExternalEventConfigurationApi;
import org.apache.fineract.client.services.FundsApi;
@@ -214,6 +215,11 @@ public class ApiConfiguration {
return fineractClient.createService(ExternalAssetOwnersApi.class);
}
@Bean
public ExternalAssetOwnerLoanProductAttributesApi externalAssetOwnerLoanProductAttributesApi() {
return fineractClient.createService(ExternalAssetOwnerLoanProductAttributesApi.class);
}
@Bean
public BusinessStepConfigurationApi businessStepConfigurationApi() {
return fineractClient.createService(BusinessStepConfigurationApi.class);
+2
View File
@@ -36,6 +36,7 @@ dependencies {
implementation(
'org.springframework.boot:spring-boot-starter-web',
'org.springframework.boot:spring-boot-starter-security',
'org.springframework.boot:spring-boot-starter-cache',
'jakarta.ws.rs:jakarta.ws.rs-api',
'org.glassfish.jersey.media:jersey-media-multipart',
@@ -55,6 +56,7 @@ dependencies {
'org.mapstruct:mapstruct',
'io.github.resilience4j:resilience4j-spring-boot3',
'org.reflections:reflections:0.10.2',
)
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
@@ -0,0 +1,44 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.api;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributes;
public final class ExternalAssetOwnerLoanProductAttributesApiConstants {
private ExternalAssetOwnerLoanProductAttributesApiConstants() {
}
// parameters
private static final String loanProductIdParamName = "loanProductId";
private static final String attributeKeyParamName = "attributeKey";
private static final String attributeValueParamName = "attributeValue";
private static final String id = "id";
/**
* These parameters will match the class level parameters of {@link ExternalAssetOwnerLoanProductAttributes}. Where
* possible, we try to get response parameters to match those of request parameters.
*/
static final Set<String> RESPONSE_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(id, loanProductIdParamName, attributeKeyParamName, attributeValueParamName));
}
@@ -0,0 +1,131 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformUserRightsContext;
import org.apache.fineract.investor.config.InvestorModuleIsEnabledCondition;
import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesData;
import org.apache.fineract.investor.service.ExternalAssetOwnerLoanProductAttributesReadService;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
@Path("/v1/external-asset-owners/loan-product")
@Component
@Tag(name = "External Asset Owner Loan Product Attributes", description = "External Asset Owner Loan Product Attributes")
@RequiredArgsConstructor
@Conditional(InvestorModuleIsEnabledCondition.class)
public class ExternalAssetOwnerLoanProductAttributesApiResource {
private final PlatformUserRightsContext platformUserRightsContext;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final ExternalAssetOwnerLoanProductAttributesReadService externalAssetOwnerLoanProductAttributesReadService;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final DefaultToApiJsonSerializer<ExternalTransferLoanProductAttributesData> toApiJsonSerializer;
@POST
@Path("/{loanProductId}/attributes")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = ExternalAssetOwnerLoanProductAttributesApiResourceSwagger.PostExternalAssetOwnerLoanProductAttributeRequest.class)))
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "400", description = "Invalid Request"),
@ApiResponse(responseCode = "403", description = "Resource Already Exists"),
@ApiResponse(responseCode = "404", description = "Not Found"),
@ApiResponse(responseCode = "500", description = "Internal Server Error") })
public CommandProcessingResult postExternalAssetOwnerLoanProductAttribute(
@PathParam("loanProductId") @Parameter(description = "loanProductId") final Long loanProductId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
platformUserRightsContext.isAuthenticated();
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
CommandWrapper request = builder.createExternalAssetOwnerLoanProductAttribute(loanProductId).build();
return commandsSourceWritePlatformService.logCommandSource(request);
}
@GET
@Path("/{loanProductId}/attributes")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(tags = {
"External Asset Owner Loan Product Attributes" }, summary = "Retrieve All Loan Product Attributes", description = "Retrieves all Loan Product Attributes with a given loanProductId", parameters = {
@Parameter(name = "loanProductId", description = "loanProductId"),
@Parameter(name = "attributeKey", description = "attributeKey") })
@ApiResponses({ @ApiResponse(responseCode = "200", description = "A paginated group of loan product attributes is returned"),
@ApiResponse(responseCode = "404", description = "Not Found"),
@ApiResponse(responseCode = "500", description = "Internal Server Error") })
public Page<ExternalTransferLoanProductAttributesData> getExternalAssetOwnerLoanProductAttributes(@Context final UriInfo uriInfo,
@PathParam("loanProductId") @Parameter(description = "loanProductId") final Long loanProductId,
@QueryParam("attributeKey") @Parameter(description = "attributeKey") final String attributeKey) {
platformUserRightsContext.isAuthenticated();
return externalAssetOwnerLoanProductAttributesReadService.retrieveAllLoanProductAttributesByLoanProductId(loanProductId,
attributeKey);
}
@PUT
@Path("/{loanProductId}/attributes/{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = ExternalAssetOwnerLoanProductAttributesApiResourceSwagger.PutExternalAssetOwnerLoanProductAttributeRequest.class)))
@Operation(tags = {
"External Asset Owner Loan Product Attributes" }, summary = "Update a Loan Product Attribute", description = "Updates a loan product attribute with a given loan product id and attribute id", parameters = {
@Parameter(name = "loanProductId", description = "loanProductId"),
@Parameter(name = "attributeId", description = "attributeId") })
@ApiResponses({ @ApiResponse(responseCode = "200", description = "A loan product attribute filtered by id is returned"),
@ApiResponse(responseCode = "404", description = "Not Found"),
@ApiResponse(responseCode = "500", description = "Internal Server Error") })
public CommandProcessingResult updateLoanProductAttribute(
@PathParam("loanProductId") @Parameter(description = "loanProductId") final Long loanProductId,
@PathParam("id") @Parameter(description = "attributeId") final Long attributeId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
platformUserRightsContext.isAuthenticated();
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
CommandWrapper request = builder.updateExternalAssetOwnerLoanProductAttribute(loanProductId, attributeId).build();
return commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,52 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.api;
import io.swagger.v3.oas.annotations.media.Schema;
@SuppressWarnings({ "MemberName" })
final class ExternalAssetOwnerLoanProductAttributesApiResourceSwagger {
private ExternalAssetOwnerLoanProductAttributesApiResourceSwagger() {}
@Schema(description = "PostExternalAssetOwnerLoanProductAttributeRequest")
public static final class PostExternalAssetOwnerLoanProductAttributeRequest {
private PostExternalAssetOwnerLoanProductAttributeRequest() {}
@Schema(example = "SETTLEMENT_MODEL")
public String attributeKey;
@Schema(example = "DELAYED_SETTLEMENT")
public String attributeValue;
}
@Schema(description = "PutExternalAssetOwnerLoanProductAttributeRequest")
public static final class PutExternalAssetOwnerLoanProductAttributeRequest {
private PutExternalAssetOwnerLoanProductAttributeRequest() {}
@Schema(example = "SETTLEMENT_MODEL")
public String attributeKey;
@Schema(example = "DELAYED_SETTLEMENT_DISABLED")
public String attributeValue;
}
}
@@ -0,0 +1,27 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.data;
public final class ExternalAssetOwnerLoanProductAttributeRequestParameters {
private ExternalAssetOwnerLoanProductAttributeRequestParameters() {}
public static final String ATTRIBUTE_KEY = "attributeKey";
public static final String ATTRIBUTE_VALUE = "attributeValue";
}
@@ -0,0 +1,37 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.data;
import java.io.Serializable;
import lombok.Data;
import lombok.Getter;
/**
* Data object representing an external transfer loan product attribute
*/
@Data
@Getter
public class ExternalTransferLoanProductAttributesData implements Serializable {
private Long attributeId;
private Long loanProductId;
private String attributeKey;
private String attributeValue;
}
@@ -0,0 +1,26 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.data.attribute;
public interface ExternalAssetOwnerLoanProductAttribute {
String getAttributeKey();
String getAttributeValue();
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.data.attribute;
public enum SettlementModelExternalAssetOwnerLoanProductAttribute implements ExternalAssetOwnerLoanProductAttribute {
DEFAULT_SETTLEMENT("DEFAULT_SETTLEMENT"), DELAYED_SETTLEMENT("DELAYED_SETTLEMENT"),;
private final String attributeKey;
private final String attributeValue;
SettlementModelExternalAssetOwnerLoanProductAttribute(String attributeValue) {
this.attributeKey = "SETTLEMENT_MODEL";
this.attributeValue = attributeValue;
}
@Override
public String getAttributeKey() {
return attributeKey;
}
@Override
public String getAttributeValue() {
return attributeValue;
}
}
@@ -0,0 +1,45 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
@Getter
@Setter
@Entity
@NoArgsConstructor
@Table(name = "m_external_asset_owner_loan_product_configurable_attributes")
public class ExternalAssetOwnerLoanProductAttributes extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "loan_product_id", nullable = false)
private Long loanProductId;
@Column(name = "attribute_key", nullable = false)
private String attributeKey;
@Column(name = "attribute_value", nullable = false)
private String attributeValue;
}
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface ExternalAssetOwnerLoanProductAttributesRepository extends JpaRepository<ExternalAssetOwnerLoanProductAttributes, Long>,
JpaSpecificationExecutor<ExternalAssetOwnerLoanProductAttributes> {
@Query("SELECT CASE WHEN COUNT(attribute)>0 THEN TRUE ELSE FALSE END FROM ExternalAssetOwnerLoanProductAttributes attribute WHERE attribute.loanProductId =:loanProductId AND attribute.attributeKey =:attributeKey")
boolean existsByLoanProductIdAndKey(@Param("loanProductId") Long loanProductId, @Param("attributeKey") String attributeKey);
}
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
import org.springframework.http.HttpStatus;
public class ExternalAssetOwnerLoanProductAttributeAlreadyExistsException extends AbstractPlatformDomainRuleException {
private static final String errorCode = "error.msg.externalAssetOwnerLoanProductAttribute.exists";
public ExternalAssetOwnerLoanProductAttributeAlreadyExistsException(String errorMessage) {
super(errorCode, errorMessage, HttpStatus.FORBIDDEN);
}
}
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ExternalAssetOwnerLoanProductAttributeInvalidSettlementAttributeException extends AbstractPlatformDomainRuleException {
private static final String errorCode = "error.msg.externalAssetOwnerLoanProductAttribute.invalidSettlementAttribute";
public ExternalAssetOwnerLoanProductAttributeInvalidSettlementAttributeException(String errorMessage) {
super(errorCode, errorMessage);
}
}
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.springframework.http.HttpStatus;
public class ExternalAssetOwnerLoanProductAttributeNotFoundException extends AbstractPlatformResourceNotFoundException {
private static final String errorCode = "error.msg.externalAssetOwnerLoanProductAttribute.nonExistentAttribute";
public ExternalAssetOwnerLoanProductAttributeNotFoundException(final Long attributeId) {
super(errorCode, "Loan product attribute with id " + attributeId + " was not found", HttpStatus.NOT_FOUND);
}
}
@@ -0,0 +1,30 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ExternalAssetOwnerLoanProductAttributesException extends AbstractPlatformDomainRuleException {
private static final String errorCode = "error.msg.externalAssetOwnerLoanProductAttributes.general";
public ExternalAssetOwnerLoanProductAttributesException(String errorMessage) {
super(errorCode, errorMessage);
}
}
@@ -0,0 +1,39 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.service;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
@CommandType(entity = "EXTERNAL_ASSET_OWNER_LOAN_PRODUCT_ATTRIBUTE", action = "CREATE")
public class CreateExternalAssetOwnerLoanProductAttributeHandler implements NewCommandSourceHandler {
private final ExternalAssetOwnerLoanProductAttributesWriteService externalAssetOwnerLoanProductAttributesWriteService;
@Override
public CommandProcessingResult processCommand(JsonCommand command) {
return externalAssetOwnerLoanProductAttributesWriteService.createExternalAssetOwnerLoanProductAttribute(command);
}
}
@@ -0,0 +1,32 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.service;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesData;
import org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributes;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(config = MapstructMapperConfig.class)
public interface ExternalAssetOwnerLoanProductAttributesMapper {
@Mapping(target = "attributeId", source = "id")
ExternalTransferLoanProductAttributesData mapLoanProductAttributes(ExternalAssetOwnerLoanProductAttributes attributes);
}
@@ -0,0 +1,29 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.service;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesData;
public interface ExternalAssetOwnerLoanProductAttributesReadService {
Page<ExternalTransferLoanProductAttributesData> retrieveAllLoanProductAttributesByLoanProductId(Long loanProductId,
String attributeKey);
}
@@ -0,0 +1,86 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.service;
import jakarta.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesData;
import org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributes;
import org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributesRepository;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ExternalAssetOwnerLoanProductAttributesReadServiceImpl implements ExternalAssetOwnerLoanProductAttributesReadService {
private final ExternalAssetOwnerLoanProductAttributesRepository externalAssetOwnerLoanProductAttributesRepository;
private final LoanProductRepository loanProductRepository;
private final ExternalAssetOwnerLoanProductAttributesMapper mapper;
@Override
@Cacheable(cacheNames = "externalAssetOwnerLoanProductAttributes", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat(#loanProductId.toString() + #attributeKey)", unless = "#attributeKey == null")
public Page<ExternalTransferLoanProductAttributesData> retrieveAllLoanProductAttributesByLoanProductId(final Long loanProductId,
final String attributeKey) {
validateLoanProduct(loanProductId);
PageRequest pageRequest = PageRequest.of(0, 100, Sort.by("id"));
org.springframework.data.domain.Page<ExternalAssetOwnerLoanProductAttributes> pageOfAttributeData = externalAssetOwnerLoanProductAttributesRepository
.findAll(retrieveLoanProductAttributesByLoanProductIdAndAttributeKeySpecification(loanProductId, attributeKey),
pageRequest);
return new Page<>(pageOfAttributeData.getContent().stream().map(mapper::mapLoanProductAttributes).toList(),
pageOfAttributeData.getNumberOfElements());
}
private void validateLoanProduct(final Long loanProductId) {
if (loanProductId == null) {
throw new IllegalArgumentException("At least one of the following parameters must be provided: loanProductId");
}
if (!loanProductRepository.existsById(loanProductId)) {
throw new LoanProductNotFoundException(loanProductId);
}
}
public static Specification<ExternalAssetOwnerLoanProductAttributes> retrieveLoanProductAttributesByLoanProductIdAndAttributeKeySpecification(
final Long loanProductId, final String attributeKey) {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(root.get("loanProductId"), loanProductId));
if (StringUtils.isNotBlank(attributeKey)) {
predicates.add(cb.equal(root.get("attributeKey"), attributeKey));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
}
@@ -0,0 +1,30 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface ExternalAssetOwnerLoanProductAttributesWriteService {
CommandProcessingResult createExternalAssetOwnerLoanProductAttribute(JsonCommand command);
CommandProcessingResult updateExternalAssetOwnerLoanProductAttribute(JsonCommand command);
}
@@ -0,0 +1,193 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.service;
import static org.reflections.scanners.Scanners.SubTypes;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import jakarta.transaction.Transactional;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.investor.data.ExternalAssetOwnerLoanProductAttributeRequestParameters;
import org.apache.fineract.investor.data.attribute.ExternalAssetOwnerLoanProductAttribute;
import org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributes;
import org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributesRepository;
import org.apache.fineract.investor.exception.ExternalAssetOwnerLoanProductAttributeAlreadyExistsException;
import org.apache.fineract.investor.exception.ExternalAssetOwnerLoanProductAttributeInvalidSettlementAttributeException;
import org.apache.fineract.investor.exception.ExternalAssetOwnerLoanProductAttributeNotFoundException;
import org.apache.fineract.investor.exception.ExternalAssetOwnerLoanProductAttributesException;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException;
import org.reflections.Reflections;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Transactional
public class ExternalAssetOwnerLoanProductAttributesWriteServiceImpl implements ExternalAssetOwnerLoanProductAttributesWriteService {
private static final String INVESTOR_PATH = "org.apache.fineract.investor";
private final FromJsonHelper fromApiJsonHelper;
private final ExternalAssetOwnerLoanProductAttributesRepository externalAssetOwnerLoanProductAttributesRepository;
private final LoanProductRepository loanProductRepository;
private final Set<Class<?>> implementingClasses = new Reflections(INVESTOR_PATH)
.get(SubTypes.of(ExternalAssetOwnerLoanProductAttribute.class).asClass());
@Override
public CommandProcessingResult createExternalAssetOwnerLoanProductAttribute(JsonCommand command) {
final JsonElement json = fromApiJsonHelper.parse(command.json());
String attributeKey = fromApiJsonHelper.extractStringNamed(ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_KEY,
json);
String attributeValue = fromApiJsonHelper
.extractStringNamed(ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_VALUE, json);
Long loanProductId = command.getProductId();
validateLoanProductAttributeRequest(command.json(), attributeKey, attributeValue);
validateExternalAssetOwnerLoanProductAttribute(attributeKey, attributeValue);
validateLoanProductExistsAndAttributeDoesNotExist(loanProductId, attributeKey);
ExternalAssetOwnerLoanProductAttributes newAttribute = createExternalAssetOwnerLoanProductAttribute(loanProductId, attributeKey,
attributeValue);
externalAssetOwnerLoanProductAttributesRepository.saveAndFlush(newAttribute);
return buildResponseData(newAttribute);
}
@Override
@CacheEvict(cacheNames = "externalAssetOwnerLoanProductAttributes", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat(#command.getProductId().toString() + #attributeKey)")
public CommandProcessingResult updateExternalAssetOwnerLoanProductAttribute(JsonCommand command) {
final JsonElement json = fromApiJsonHelper.parse(command.json());
String attributeKey = fromApiJsonHelper.extractStringNamed(ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_KEY,
json);
String attributeValue = fromApiJsonHelper
.extractStringNamed(ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_VALUE, json);
Long loanProductId = command.getProductId();
Long attributeId = command.entityId();
validateLoanProductAttributeRequest(command.json(), attributeKey, attributeValue);
validateExternalAssetOwnerLoanProductAttribute(attributeKey, attributeValue);
validateLoanProductExists(loanProductId);
ExternalAssetOwnerLoanProductAttributes attributeToUpdate = getLoanProductAttribute(attributeId);
validateLoanProductAttributeKeysMatch(attributeKey, attributeToUpdate.getAttributeKey());
if (!attributeToUpdate.getAttributeValue().equals(attributeValue)) {
attributeToUpdate.setAttributeValue(attributeValue);
externalAssetOwnerLoanProductAttributesRepository.saveAndFlush(attributeToUpdate);
}
return buildResponseData(attributeToUpdate);
}
private void validateLoanProductAttributeRequest(String apiRequestBodyAsJson, String attributeKey, String attributeValue) {
final Set<String> requestParameters = new HashSet<>(
Arrays.asList(ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_KEY,
ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_VALUE));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, apiRequestBodyAsJson, requestParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanproductattribute");
baseDataValidator.reset().parameter(ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_KEY).value(attributeKey)
.notBlank().notExceedingLengthOf(255);
baseDataValidator.reset().parameter(ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_VALUE).value(attributeValue)
.notBlank().notExceedingLengthOf(255);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
private void validateLoanProductExistsAndAttributeDoesNotExist(Long loanProductId, String attributeKey) {
validateLoanProductExists(loanProductId);
validateLoanProductAttributeDoesNotExist(loanProductId, attributeKey);
}
private void validateLoanProductExists(Long loanProductId) {
if (!loanProductRepository.existsById(loanProductId)) {
throw new LoanProductNotFoundException(loanProductId);
}
}
private void validateLoanProductAttributeDoesNotExist(Long loanProductId, String attributeKey) {
if (externalAssetOwnerLoanProductAttributesRepository.existsByLoanProductIdAndKey(loanProductId, attributeKey)) {
throw new ExternalAssetOwnerLoanProductAttributeAlreadyExistsException(
"attributeKey already exists for the loanProductId: " + loanProductId + ". Use PUT call to UPDATE the attribute.");
}
}
private void validateLoanProductAttributeKeysMatch(String attributeKeyFromRequest, String attributeKeyFromDB) {
if (!attributeKeyFromRequest.equals(attributeKeyFromDB)) {
throw new ExternalAssetOwnerLoanProductAttributesException(
"The attribute key of requested update attribute does not match the attribute key from database.");
}
}
private void validateExternalAssetOwnerLoanProductAttribute(String attributeKey, String attributeValue) {
for (Class<?> implementingClass : implementingClasses) {
if (implementingClass.isEnum()) {
for (Object obj : implementingClass.getEnumConstants()) {
ExternalAssetOwnerLoanProductAttribute objEnum = (ExternalAssetOwnerLoanProductAttribute) obj;
if (objEnum.getAttributeKey().equals(attributeKey)
&& objEnum.getAttributeValue().equals(attributeValue.toUpperCase())) {
return;
}
}
}
}
throw new ExternalAssetOwnerLoanProductAttributeInvalidSettlementAttributeException(
"The given attribute key or attribute value is not valid.");
}
private ExternalAssetOwnerLoanProductAttributes getLoanProductAttribute(Long attributeId) {
Optional<ExternalAssetOwnerLoanProductAttributes> loanProductAttribute = externalAssetOwnerLoanProductAttributesRepository
.findById(attributeId);
if (loanProductAttribute.isEmpty()) {
throw new ExternalAssetOwnerLoanProductAttributeNotFoundException(attributeId);
}
return loanProductAttribute.get();
}
private ExternalAssetOwnerLoanProductAttributes createExternalAssetOwnerLoanProductAttribute(Long loanProductId, String attributeKey,
String attributeValue) {
ExternalAssetOwnerLoanProductAttributes attribute = new ExternalAssetOwnerLoanProductAttributes();
attribute.setLoanProductId(loanProductId);
attribute.setAttributeKey(attributeKey);
attribute.setAttributeValue(attributeValue);
return attribute;
}
private CommandProcessingResult buildResponseData(ExternalAssetOwnerLoanProductAttributes savedAttribute) {
return new CommandProcessingResultBuilder().withEntityId(savedAttribute.getLoanProductId()).build();
}
}
@@ -0,0 +1,39 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.service;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
@CommandType(entity = "EXTERNAL_ASSET_OWNER_LOAN_PRODUCT_ATTRIBUTE", action = "UPDATE")
public class UpdateExternalAssetOwnerLoanProductAttributeHandler implements NewCommandSourceHandler {
private final ExternalAssetOwnerLoanProductAttributesWriteService externalAssetOwnerLoanProductAttributesWriteService;
@Override
public CommandProcessingResult processCommand(JsonCommand command) {
return externalAssetOwnerLoanProductAttributesWriteService.updateExternalAssetOwnerLoanProductAttribute(command);
}
}
@@ -35,4 +35,5 @@
<include relativeToChangelogFile="true" file="parts/0011_set_datetime_precision.xml"/>
<include relativeToChangelogFile="true" file="parts/0012_add_external_asset_owner_transfer_index.xml"/>
<include relativeToChangelogFile="true" file="parts/0013_add_additional_asset_owner_indices.xml"/>
<include relativeToChangelogFile="true" file="parts/0014_add_external_asset_owner_loan_product_configurable_attributes.xml"/>
</databaseChangeLog>
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1" context="postgresql">
<!-- Create the m_external_asset_owner_loan_product_configurable_attributes table -->
<createTable tableName="m_external_asset_owner_loan_product_configurable_attributes">
<column autoIncrement="true" name="id" type="BIGINT" remarks="Internal ID">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="loan_product_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="attribute_key" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="attribute_value" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
</changeSet>
<changeSet author="fineract" id="1" context="mysql">
<!-- Create the m_external_asset_owner_loan_product_configurable_attributes table -->
<createTable tableName="m_external_asset_owner_loan_product_configurable_attributes">
<column autoIncrement="true" name="id" type="BIGINT" remarks="Internal ID">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="loan_product_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="attribute_key" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="attribute_value" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="DATETIME"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="DATETIME"/>
</createTable>
</changeSet>
<changeSet author="fineract" id="2">
<addForeignKeyConstraint baseColumnNames="created_by"
baseTableName="m_external_asset_owner_loan_product_configurable_attributes"
constraintName="FK_ext_asset_owner_lp_conf_attr_created_by"
deferrable="false"
initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_appuser" validate="true"/>
<addForeignKeyConstraint baseColumnNames="last_modified_by"
baseTableName="m_external_asset_owner_loan_product_configurable_attributes"
constraintName="FK_ext_asset_owner_lp_conf_attr_modified_by"
deferrable="false"
initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_appuser" validate="true"/>
<addForeignKeyConstraint baseColumnNames="loan_product_id"
baseTableName="m_external_asset_owner_loan_product_configurable_attributes"
constraintName="FK_ext_asset_owner_lp_conf_attr_loan_product_id"
deferrable="false"
initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_product_loan" validate="true"/>
</changeSet>
<changeSet author="fineract" id="3">
<insert tableName="m_permission">
<column name="grouping" value="loan_product_attribute"/>
<column name="code" value="CREATE_EXTERNAL_ASSET_OWNER_LOAN_PRODUCT_ATTRIBUTE"/>
<column name="entity_name" value="EXTERNAL_ASSET_OWNER_LOAN_PRODUCT_ATTRIBUTE"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="loan_product_attribute"/>
<column name="code" value="UPDATE_EXTERNAL_ASSET_OWNER_LOAN_PRODUCT_ATTRIBUTE"/>
<column name="entity_name" value="EXTERNAL_ASSET_OWNER_LOAN_PRODUCT_ATTRIBUTE"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,164 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.stream.Stream;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesData;
import org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributes;
import org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributesRepository;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
@ExtendWith(MockitoExtension.class)
public class ExternalAssetOwnerLoanProductAttributesReadServiceTest {
@Mock
private ExternalAssetOwnerLoanProductAttributesRepository externalAssetOwnerLoanProductAttributesRepository;
@Mock
private LoanProductRepository loanProductRepository;
@Mock
private ExternalAssetOwnerLoanProductAttributesMapper mapper;
private ExternalAssetOwnerLoanProductAttributesReadService underTest;
private int offset = 0;
private int limit = 100;
@BeforeEach
public void setUp() {
underTest = new ExternalAssetOwnerLoanProductAttributesReadServiceImpl(externalAssetOwnerLoanProductAttributesRepository,
loanProductRepository, mapper);
}
@ParameterizedTest
@MethodSource("testRetrieveAllLoanProductAttributesByLoanProductIdDataProvider")
public void testRetrieveAllLoanProductAttributesByLoanProductId(Long loanProductId, String attributeKey) {
// given
ExternalAssetOwnerLoanProductAttributes attributes = Mockito.mock(ExternalAssetOwnerLoanProductAttributes.class);
ExternalTransferLoanProductAttributesData data = Mockito.mock(ExternalTransferLoanProductAttributesData.class);
PageRequest pageRequest = PageRequest.of(offset, limit, Sort.by("id").ascending());
org.springframework.data.domain.Page<ExternalAssetOwnerLoanProductAttributes> attributesPage = new PageImpl<>(List.of(attributes),
pageRequest, 1);
when(externalAssetOwnerLoanProductAttributesRepository.findAll(any(Specification.class), eq(pageRequest)))
.thenReturn(attributesPage);
when(loanProductRepository.existsById(loanProductId)).thenReturn(true);
when(mapper.mapLoanProductAttributes(attributes)).thenReturn(data);
// when
Page<ExternalTransferLoanProductAttributesData> result = underTest.retrieveAllLoanProductAttributesByLoanProductId(loanProductId,
attributeKey);
// then
assertEquals(1, result.getTotalFilteredRecords());
assertEquals(data, result.getPageItems().get(0));
verify(loanProductRepository, times(1)).existsById(loanProductId);
verify(mapper, times(1)).mapLoanProductAttributes(attributes);
}
@Test
public void testRetrieveAllLoanProductAttributesByLoanProductIdAndInvalidAttributeKey() {
// given
Long loanProductId = 1L;
ExternalAssetOwnerLoanProductAttributes attributes = Mockito.mock(ExternalAssetOwnerLoanProductAttributes.class);
PageRequest pageRequest = PageRequest.of(offset, limit, Sort.by("id").ascending());
org.springframework.data.domain.Page<ExternalAssetOwnerLoanProductAttributes> attributesPage = new PageImpl<>(List.of(attributes),
pageRequest, 0);
when(externalAssetOwnerLoanProductAttributesRepository.findAll(any(Specification.class), eq(pageRequest)))
.thenReturn(attributesPage);
when(loanProductRepository.existsById(loanProductId)).thenReturn(true);
when(mapper.mapLoanProductAttributes(attributes)).thenReturn(null);
// when
Page<ExternalTransferLoanProductAttributesData> result = underTest.retrieveAllLoanProductAttributesByLoanProductId(loanProductId,
"BAD_KEY");
// then
assertEquals(1, result.getTotalFilteredRecords());
assertNull(result.getPageItems().get(0));
verify(externalAssetOwnerLoanProductAttributesRepository, times(1)).findAll(any(Specification.class), eq(pageRequest));
verify(loanProductRepository, times(1)).existsById(loanProductId);
verify(mapper, times(1)).mapLoanProductAttributes(attributes);
}
@Test
public void testRetrieveLoanProductAttributesDataByLoanProductIdIllegalArgument() {
// given
// when
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> underTest.retrieveAllLoanProductAttributesByLoanProductId(null, null));
// then
assertEquals("At least one of the following parameters must be provided: loanProductId", exception.getMessage());
verifyNoInteractions(externalAssetOwnerLoanProductAttributesRepository);
verifyNoInteractions(loanProductRepository);
verifyNoInteractions(mapper);
}
@Test
public void testRetrieveLoanProductAttributesDataByLoanProductIdNotFound() {
// given
Long loanProductId = 1L;
when(loanProductRepository.existsById(loanProductId)).thenReturn(false);
// when
LoanProductNotFoundException exception = assertThrows(LoanProductNotFoundException.class,
() -> underTest.retrieveAllLoanProductAttributesByLoanProductId(loanProductId, null));
// then
assertEquals("Loan product with identifier 1 does not exist", exception.getMessage());
verify(loanProductRepository, times(1)).existsById(loanProductId);
verifyNoInteractions(externalAssetOwnerLoanProductAttributesRepository);
verifyNoInteractions(mapper);
}
private static Stream<Arguments> testRetrieveAllLoanProductAttributesByLoanProductIdDataProvider() {
return Stream.of(Arguments.of(1L, "SETTLEMENT_MODEL"), Arguments.of(1L, null));
}
}
@@ -0,0 +1,391 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.service;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.gson.JsonElement;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Optional;
import java.util.stream.Stream;
import lombok.Setter;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.investor.data.ExternalAssetOwnerLoanProductAttributeRequestParameters;
import org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributes;
import org.apache.fineract.investor.domain.ExternalAssetOwnerLoanProductAttributesRepository;
import org.apache.fineract.investor.exception.ExternalAssetOwnerLoanProductAttributeAlreadyExistsException;
import org.apache.fineract.investor.exception.ExternalAssetOwnerLoanProductAttributeInvalidSettlementAttributeException;
import org.apache.fineract.investor.exception.ExternalAssetOwnerLoanProductAttributeNotFoundException;
import org.apache.fineract.investor.exception.ExternalAssetOwnerLoanProductAttributesException;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException;
import org.junit.Assert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class ExternalAssetOwnerLoanProductAttributesWriteServiceImplTest {
@Test
public void testCreateExternalAssetOwnerLoanProductAttributeHappyPath() {
TestContext testContext = new TestContext();
ArgumentCaptor<ExternalAssetOwnerLoanProductAttributes> loanProductAttributeArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerLoanProductAttributes.class);
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommandString, testContext.loanProductId, null);
when(testContext.externalAssetOwnerLoanProductAttributesRepository.existsByLoanProductIdAndKey(testContext.loanProductId,
testContext.attributeKey)).thenReturn(false);
when(testContext.loanProductRepository.existsById(testContext.loanProductId)).thenReturn(true);
// when
testContext.externalAssetOwnerLoanProductAttributesWriteService.createExternalAssetOwnerLoanProductAttribute(command);
// then
verify(testContext.externalAssetOwnerLoanProductAttributesRepository).existsByLoanProductIdAndKey(any(), any());
verify(testContext.externalAssetOwnerLoanProductAttributesRepository).saveAndFlush(loanProductAttributeArgumentCaptor.capture());
verify(testContext.loanProductRepository).existsById(testContext.loanProductId);
assertLoanProductAttributeValues(testContext, loanProductAttributeArgumentCaptor.getValue());
}
@Test
public void testUpdateExternalAssetOwnerLoanProductAttributeHappyPath() {
TestContext testContext = new TestContext();
ArgumentCaptor<ExternalAssetOwnerLoanProductAttributes> loanProductAttributeArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerLoanProductAttributes.class);
ExternalAssetOwnerLoanProductAttributes attributeInDB = new ExternalAssetOwnerLoanProductAttributes();
attributeInDB.setLoanProductId(testContext.loanProductId);
attributeInDB.setAttributeKey(testContext.attributeKey);
attributeInDB.setAttributeValue("DIFFERENT_VALUE");
attributeInDB.setId(1L);
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommandString, testContext.loanProductId, attributeInDB.getId());
when(testContext.loanProductRepository.existsById(testContext.loanProductId)).thenReturn(true);
when(testContext.externalAssetOwnerLoanProductAttributesRepository.findById(command.entityId()))
.thenReturn(Optional.of(attributeInDB));
testContext.externalAssetOwnerLoanProductAttributesWriteService.updateExternalAssetOwnerLoanProductAttribute(command);
// then
verify(testContext.loanProductRepository).existsById(testContext.loanProductId);
verify(testContext.externalAssetOwnerLoanProductAttributesRepository).findById(eq(command.entityId()));
verify(testContext.externalAssetOwnerLoanProductAttributesRepository).saveAndFlush(loanProductAttributeArgumentCaptor.capture());
}
@Test
public void testUpdateExternalAssetOwnerLoanProductAttributeUpdateNotRequired() {
TestContext testContext = new TestContext();
ArgumentCaptor<ExternalAssetOwnerLoanProductAttributes> loanProductAttributeArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerLoanProductAttributes.class);
ExternalAssetOwnerLoanProductAttributes attributeInDB = new ExternalAssetOwnerLoanProductAttributes();
attributeInDB.setLoanProductId(testContext.loanProductId);
attributeInDB.setAttributeKey(testContext.attributeKey);
attributeInDB.setAttributeValue(testContext.attributeValue);
attributeInDB.setId(1L);
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommandString, testContext.loanProductId, attributeInDB.getId());
when(testContext.loanProductRepository.existsById(testContext.loanProductId)).thenReturn(true);
when(testContext.externalAssetOwnerLoanProductAttributesRepository.findById(command.entityId()))
.thenReturn(Optional.of(attributeInDB));
testContext.externalAssetOwnerLoanProductAttributesWriteService.updateExternalAssetOwnerLoanProductAttribute(command);
// then
verify(testContext.loanProductRepository).existsById(testContext.loanProductId);
verify(testContext.externalAssetOwnerLoanProductAttributesRepository).findById(eq(command.entityId()));
verify(testContext.externalAssetOwnerLoanProductAttributesRepository, times(0))
.saveAndFlush(loanProductAttributeArgumentCaptor.capture());
}
@Test
public void testUpdateExternalAssetOwnerLoanProductAttributeOnAttributeThatDoesNotExist() {
TestContext testContext = new TestContext();
ArgumentCaptor<ExternalAssetOwnerLoanProductAttributes> loanProductAttributeArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerLoanProductAttributes.class);
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommandString, testContext.loanProductId, 1L);
when(testContext.loanProductRepository.existsById(testContext.loanProductId)).thenReturn(true);
when(testContext.externalAssetOwnerLoanProductAttributesRepository.findById(1L)).thenReturn(Optional.empty());
ExternalAssetOwnerLoanProductAttributeNotFoundException thrownException = Assert.assertThrows(
ExternalAssetOwnerLoanProductAttributeNotFoundException.class,
() -> testContext.externalAssetOwnerLoanProductAttributesWriteService
.updateExternalAssetOwnerLoanProductAttribute(command));
// then
verify(testContext.loanProductRepository).existsById(testContext.loanProductId);
verify(testContext.externalAssetOwnerLoanProductAttributesRepository).findById(eq(1L));
verify(testContext.externalAssetOwnerLoanProductAttributesRepository, times(0))
.saveAndFlush(loanProductAttributeArgumentCaptor.capture());
Assertions.assertEquals(thrownException.getMessage(), "Loan product attribute with id " + 1L + " was not found");
}
@Test
public void testUpdateExternalAssetOwnerLoanProductAttributeOnAttributeWithDifferentKeyValue() {
TestContext testContext = new TestContext();
ArgumentCaptor<ExternalAssetOwnerLoanProductAttributes> loanProductAttributeArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerLoanProductAttributes.class);
ExternalAssetOwnerLoanProductAttributes attributeInDB = new ExternalAssetOwnerLoanProductAttributes();
attributeInDB.setLoanProductId(testContext.loanProductId);
attributeInDB.setAttributeKey("DIFFERENT_KEY");
attributeInDB.setAttributeValue(testContext.attributeValue);
attributeInDB.setId(1L);
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommandString, testContext.loanProductId, attributeInDB.getId());
when(testContext.loanProductRepository.existsById(testContext.loanProductId)).thenReturn(true);
when(testContext.externalAssetOwnerLoanProductAttributesRepository.findById(command.entityId()))
.thenReturn(Optional.of(attributeInDB));
ExternalAssetOwnerLoanProductAttributesException thrownException = Assert.assertThrows(
ExternalAssetOwnerLoanProductAttributesException.class,
() -> testContext.externalAssetOwnerLoanProductAttributesWriteService
.updateExternalAssetOwnerLoanProductAttribute(command));
// then
verify(testContext.loanProductRepository).existsById(testContext.loanProductId);
verify(testContext.externalAssetOwnerLoanProductAttributesRepository).findById(eq(command.entityId()));
verify(testContext.externalAssetOwnerLoanProductAttributesRepository, times(0))
.saveAndFlush(loanProductAttributeArgumentCaptor.capture());
Assertions.assertEquals(thrownException.getMessage(),
"The attribute key of requested update attribute does not match the attribute key from database.");
}
@Test
public void testCreateExternalAssetOwnerLoanProductAttributeUsingDefaultSettlementValue() {
TestContext testContext = new TestContext();
ArgumentCaptor<ExternalAssetOwnerLoanProductAttributes> loanProductAttributeArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerLoanProductAttributes.class);
// given
final JsonElement jsonCommandElement = testContext.fromJsonHelper.parse(testContext.jsonCommandString);
final JsonCommand command = createJsonCommand(testContext.jsonCommandString, testContext.loanProductId, null);
when(testContext.externalAssetOwnerLoanProductAttributesRepository.existsByLoanProductIdAndKey(testContext.loanProductId,
testContext.attributeKey)).thenReturn(false);
when(testContext.loanProductRepository.existsById(testContext.loanProductId)).thenReturn(true);
when(testContext.fromApiJsonHelper.extractStringNamed(ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_VALUE,
jsonCommandElement)).thenReturn("DEFAULT_SETTLEMENT");
testContext.setAttributeValue("DEFAULT_SETTLEMENT");
// when
testContext.externalAssetOwnerLoanProductAttributesWriteService.createExternalAssetOwnerLoanProductAttribute(command);
// then
verify(testContext.externalAssetOwnerLoanProductAttributesRepository).existsByLoanProductIdAndKey(any(), any());
verify(testContext.externalAssetOwnerLoanProductAttributesRepository).saveAndFlush(loanProductAttributeArgumentCaptor.capture());
verify(testContext.loanProductRepository).existsById(testContext.loanProductId);
assertLoanProductAttributeValues(testContext, loanProductAttributeArgumentCaptor.getValue());
}
@ParameterizedTest
@MethodSource("externalAssetOwnerLoanProductAttributeApiRequestDataValidationErrors")
public void testExternalAssetOwnerLoanProductAttributeRequestWithApiDataValidationErrors(String testName, String attributeKey,
String attributeValue, String expectedErrorString) {
TestContext testContext = new TestContext();
final JsonElement jsonCommandElement = testContext.fromJsonHelper.parse(testContext.jsonCommandString);
final JsonCommand command = createJsonCommand(testContext.jsonCommandString, testContext.loanProductId, null);
when(testContext.fromApiJsonHelper.extractStringNamed(ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_KEY,
jsonCommandElement)).thenReturn(attributeKey);
when(testContext.fromApiJsonHelper.extractStringNamed(ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_VALUE,
jsonCommandElement)).thenReturn(attributeValue);
PlatformApiDataValidationException thrownException = Assert.assertThrows(PlatformApiDataValidationException.class,
() -> testContext.externalAssetOwnerLoanProductAttributesWriteService
.createExternalAssetOwnerLoanProductAttribute(command));
verify(testContext.externalAssetOwnerLoanProductAttributesRepository, times(0)).saveAndFlush(any());
verify(testContext.externalAssetOwnerLoanProductAttributesRepository, times(0)).existsByLoanProductIdAndKey(any(), any());
verify(testContext.loanProductRepository, times(0)).existsById(testContext.loanProductId);
Assertions.assertEquals(thrownException.getMessage(), expectedErrorString);
}
@Test
public void testCreateLoanProductAttributeExternalAssetOwnerExternalAssetOwnerLoanProductNotFound() {
TestContext testContext = new TestContext();
final JsonCommand command = createJsonCommand(testContext.jsonCommandString, testContext.loanProductId, null);
when(testContext.loanProductRepository.existsById(testContext.loanProductId)).thenReturn(false);
LoanProductNotFoundException thrownException = Assert.assertThrows(LoanProductNotFoundException.class,
() -> testContext.externalAssetOwnerLoanProductAttributesWriteService
.createExternalAssetOwnerLoanProductAttribute(command));
verify(testContext.externalAssetOwnerLoanProductAttributesRepository, times(0)).saveAndFlush(any());
verify(testContext.externalAssetOwnerLoanProductAttributesRepository, times(0)).existsByLoanProductIdAndKey(any(), any());
verify(testContext.loanProductRepository, times(1)).existsById(testContext.loanProductId);
Assertions.assertEquals(thrownException.getMessage(),
"Loan product with identifier " + testContext.loanProductId + " does not exist");
}
@Test
public void testCreateLoanProductAttributeExternalAssetOwnerExternalAssetOwnerLoanProductAlreadyHasAttribute() {
TestContext testContext = new TestContext();
final JsonCommand command = createJsonCommand(testContext.jsonCommandString, testContext.loanProductId, null);
when(testContext.externalAssetOwnerLoanProductAttributesRepository.existsByLoanProductIdAndKey(testContext.loanProductId,
testContext.attributeKey)).thenReturn(true);
when(testContext.loanProductRepository.existsById(testContext.loanProductId)).thenReturn(true);
ExternalAssetOwnerLoanProductAttributeAlreadyExistsException thrownException = Assert.assertThrows(
ExternalAssetOwnerLoanProductAttributeAlreadyExistsException.class,
() -> testContext.externalAssetOwnerLoanProductAttributesWriteService
.createExternalAssetOwnerLoanProductAttribute(command));
verify(testContext.externalAssetOwnerLoanProductAttributesRepository, times(0)).saveAndFlush(any());
verify(testContext.externalAssetOwnerLoanProductAttributesRepository, times(1)).existsByLoanProductIdAndKey(any(), any());
verify(testContext.loanProductRepository, times(1)).existsById(testContext.loanProductId);
Assertions.assertEquals(thrownException.getMessage(), "attributeKey already exists for the loanProductId: "
+ testContext.loanProductId + ". Use PUT call to UPDATE the attribute.");
}
@Test
public void testExternalAssetOwnerLoanProductAttributeInvalidKey() {
TestContext testContext = new TestContext();
final JsonCommand command = createJsonCommand(testContext.jsonCommandString, testContext.loanProductId, null);
final JsonElement jsonCommandElement = testContext.fromJsonHelper.parse(testContext.jsonCommandString);
when(testContext.fromApiJsonHelper.extractStringNamed(ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_KEY,
jsonCommandElement)).thenReturn("BAD_KEY");
ExternalAssetOwnerLoanProductAttributeInvalidSettlementAttributeException thrownException = Assert.assertThrows(
ExternalAssetOwnerLoanProductAttributeInvalidSettlementAttributeException.class,
() -> testContext.externalAssetOwnerLoanProductAttributesWriteService
.createExternalAssetOwnerLoanProductAttribute(command));
verify(testContext.externalAssetOwnerLoanProductAttributesRepository, times(0)).saveAndFlush(any());
verify(testContext.externalAssetOwnerLoanProductAttributesRepository, times(0)).existsByLoanProductIdAndKey(any(), any());
verify(testContext.loanProductRepository, times(0)).existsById(testContext.loanProductId);
Assertions.assertEquals(thrownException.getMessage(), "The given attribute key or attribute value is not valid.");
}
@Test
public void testExternalAssetOwnerLoanProductAttributeInvalidValue() {
TestContext testContext = new TestContext();
final JsonCommand command = createJsonCommand(testContext.jsonCommandString, testContext.loanProductId, null);
final JsonElement jsonCommandElement = testContext.fromJsonHelper.parse(testContext.jsonCommandString);
when(testContext.fromApiJsonHelper.extractStringNamed(ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_VALUE,
jsonCommandElement)).thenReturn("BAD_VALUE");
ExternalAssetOwnerLoanProductAttributeInvalidSettlementAttributeException thrownException = Assert.assertThrows(
ExternalAssetOwnerLoanProductAttributeInvalidSettlementAttributeException.class,
() -> testContext.externalAssetOwnerLoanProductAttributesWriteService
.createExternalAssetOwnerLoanProductAttribute(command));
verify(testContext.externalAssetOwnerLoanProductAttributesRepository, times(0)).saveAndFlush(any());
verify(testContext.externalAssetOwnerLoanProductAttributesRepository, times(0)).existsByLoanProductIdAndKey(any(), any());
verify(testContext.loanProductRepository, times(0)).existsById(testContext.loanProductId);
Assertions.assertEquals(thrownException.getMessage(), "The given attribute key or attribute value is not valid.");
}
private static Stream<Arguments> externalAssetOwnerLoanProductAttributeApiRequestDataValidationErrors() {
return Stream.of(Arguments.of("blankAttributeValue", "SETTLEMENT_MODEL", "", "Validation errors exist."),
Arguments.of("blankAttributeKey", "", "DELAYED_SETTLEMENT", "Validation errors exist."));
}
private void assertLoanProductAttributeValues(final TestContext testContext,
final ExternalAssetOwnerLoanProductAttributes loanProductAttribute) {
Assertions.assertEquals(testContext.loanProductId, loanProductAttribute.getLoanProductId());
Assertions.assertEquals(testContext.attributeKey, loanProductAttribute.getAttributeKey());
Assertions.assertEquals(testContext.attributeValue, loanProductAttribute.getAttributeValue());
}
/**
* Helper method to create {@link JsonCommand} object from json command string.
*
* @param jsonCommand
* the json command string
* @param loanProductId
* the loanProductId
* @return the {@link JsonCommand} object.
*/
private JsonCommand createJsonCommand(final String jsonCommand, final Long loanProductId, final Long resourceId) {
return new JsonCommand(null, jsonCommand, null, null, null, resourceId, null, null, null, null, null, null, null, loanProductId,
null, null, null, null);
}
@SuppressFBWarnings({ "VA_FORMAT_STRING_USES_NEWLINE" })
static class TestContext {
@Mock
private FromJsonHelper fromApiJsonHelper;
@Mock
private ExternalAssetOwnerLoanProductAttributesRepository externalAssetOwnerLoanProductAttributesRepository;
@Mock
private LoanProductRepository loanProductRepository;
@InjectMocks
private ExternalAssetOwnerLoanProductAttributesWriteServiceImpl externalAssetOwnerLoanProductAttributesWriteService;
private final FromJsonHelper fromJsonHelper = new FromJsonHelper();
private final Long loanProductId = Long.valueOf(RandomStringUtils.randomNumeric(2));
@Setter
private String attributeKey = "SETTLEMENT_MODEL";
@Setter
private String attributeValue = "DELAYED_SETTLEMENT";
private String jsonCommandString = String.format("""
{
"attributeKey": "%s",
"attributeValue": "%s"
}
""", attributeKey, attributeValue);
@SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
TestContext() {
MockitoAnnotations.openMocks(this);
stubFromApiJsonHelper();
}
private void stubFromApiJsonHelper() {
final JsonElement jsonCommandElement = fromJsonHelper.parse(jsonCommandString);
when(fromApiJsonHelper.parse(anyString())).thenReturn(jsonCommandElement);
when(fromApiJsonHelper.extractStringNamed(ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_KEY,
jsonCommandElement)).thenReturn(attributeKey);
when(fromApiJsonHelper.extractStringNamed(ExternalAssetOwnerLoanProductAttributeRequestParameters.ATTRIBUTE_VALUE,
jsonCommandElement)).thenReturn(attributeValue);
}
}
}
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanproduct.domain;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
@@ -36,4 +37,8 @@ public interface LoanProductRepository extends JpaRepository<LoanProduct, Long>,
List<LoanProduct> findByDelinquencyBucketNotNull();
LoanProduct findByExternalId(ExternalId externalId);
@Override
@Query("SELECT CASE WHEN COUNT(loanProduct)>0 THEN TRUE ELSE FALSE END FROM LoanProduct loanProduct WHERE loanProduct.id = :loanProductId")
boolean existsById(@NotNull @Param("loanProductId") Long loanProductId);
}
@@ -126,10 +126,10 @@ public class TwoFactorAuthenticationTest {
@Test
public void testTwofactorLogin() throws IOException, MessagingException {
assertEquals(greenMail.getReceivedMessages().length, 0);
assertEquals(0, greenMail.getReceivedMessages().length);
performServerPost(requestSpec, responseSpec,
"/fineract-provider/api/v1/twofactor?deliveryMethod=email&extendedToken=false&" + TENANT_IDENTIFIER, "", "");
assertEquals(greenMail.getReceivedMessages().length, 1);
assertEquals(1, greenMail.getReceivedMessages().length);
Pattern p = Pattern.compile("token is (.+).");
Matcher m = p.matcher((CharSequence) greenMail.getReceivedMessages()[0].getContent());
@@ -161,10 +161,10 @@ public class TwoFactorAuthenticationTest {
@Test
public void testTfaConfigSettings() throws IOException, MessagingException {
assertEquals(greenMail.getReceivedMessages().length, 0);
assertEquals(0, greenMail.getReceivedMessages().length);
performServerPost(requestSpec, responseSpec,
"/fineract-provider/api/v1/twofactor?deliveryMethod=email&extendedToken=false&" + TENANT_IDENTIFIER, "", "");
assertEquals(greenMail.getReceivedMessages().length, 1);
assertEquals(1, greenMail.getReceivedMessages().length);
Pattern p = Pattern.compile("token is (.+).");
Matcher m = p.matcher((CharSequence) greenMail.getReceivedMessages()[0].getContent());
@@ -202,7 +202,7 @@ public class TwoFactorAuthenticationTest {
// Login again
performServerPost(requestSpec, responseSpec,
"/fineract-provider/api/v1/twofactor?deliveryMethod=email&extendedToken=false&" + TENANT_IDENTIFIER, "", "");
assertEquals(greenMail.getReceivedMessages().length, 2);
assertEquals(2, greenMail.getReceivedMessages().length);
Matcher m2 = p.matcher((CharSequence) greenMail.getReceivedMessages()[1].getContent());