blob: b2f4278d40e8dac499b6ed2ee58d60462036c9b5 [file] [log] [blame]
/*
* 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.cn.portfolio.service.internal.command.handler;
import org.apache.fineract.cn.individuallending.api.v1.domain.product.AccountDesignators;
import org.apache.fineract.cn.portfolio.api.v1.domain.AccountAssignment;
import org.apache.fineract.cn.portfolio.api.v1.domain.ChargeDefinition;
import org.apache.fineract.cn.portfolio.api.v1.domain.Product;
import org.apache.fineract.cn.portfolio.api.v1.events.EventConstants;
import org.apache.fineract.cn.portfolio.service.internal.command.ChangeEnablingOfProductCommand;
import org.apache.fineract.cn.portfolio.service.internal.command.ChangeProductCommand;
import org.apache.fineract.cn.portfolio.service.internal.command.CreateProductCommand;
import org.apache.fineract.cn.portfolio.service.internal.command.DeleteProductCommand;
import org.apache.fineract.cn.portfolio.service.internal.mapper.ChargeDefinitionMapper;
import org.apache.fineract.cn.portfolio.service.internal.mapper.ProductMapper;
import org.apache.fineract.cn.portfolio.service.internal.pattern.PatternFactoryRegistry;
import org.apache.fineract.cn.portfolio.service.internal.repository.CaseRepository;
import org.apache.fineract.cn.portfolio.service.internal.repository.ChargeDefinitionEntity;
import org.apache.fineract.cn.portfolio.service.internal.repository.ChargeDefinitionRepository;
import org.apache.fineract.cn.portfolio.service.internal.repository.ProductAccountAssignmentEntity;
import org.apache.fineract.cn.portfolio.service.internal.repository.ProductEntity;
import org.apache.fineract.cn.portfolio.service.internal.repository.ProductRepository;
import org.apache.fineract.cn.portfolio.service.internal.util.AccountingAdapter;
import org.apache.fineract.cn.products.spi.PatternFactory;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.fineract.cn.command.annotation.Aggregate;
import org.apache.fineract.cn.command.annotation.CommandHandler;
import org.apache.fineract.cn.command.annotation.CommandLogLevel;
import org.apache.fineract.cn.command.annotation.EventEmitter;
import org.apache.fineract.cn.lang.ServiceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Myrle Krantz
*/
@SuppressWarnings("unused")
@Aggregate
public class ProductCommandHandler {
private final PatternFactoryRegistry patternFactoryRegistry;
private final CaseRepository caseRepository;
private final ProductRepository productRepository;
private final ChargeDefinitionRepository chargeDefinitionRepository;
private final AccountingAdapter accountingAdapter;
@Autowired
public ProductCommandHandler(
final PatternFactoryRegistry patternFactoryRegistry,
final CaseRepository caseRepository,
final ProductRepository productRepository,
final ChargeDefinitionRepository chargeDefinitionRepository,
final AccountingAdapter accountingAdapter) {
super();
this.patternFactoryRegistry = patternFactoryRegistry;
this.caseRepository = caseRepository;
this.productRepository = productRepository;
this.chargeDefinitionRepository = chargeDefinitionRepository;
this.accountingAdapter = accountingAdapter;
}
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.POST_PRODUCT)
public String process(final CreateProductCommand createProductCommand) {
final PatternFactory patternFactory = patternFactoryRegistry
.getPatternFactoryForPackage(createProductCommand.getInstance().getPatternPackage())
.orElseThrow(IllegalArgumentException::new);
final ProductEntity productEntity = ProductMapper.map(createProductCommand.getInstance(), false);
this.productRepository.save(productEntity);
patternFactory.defaultConfigurableCharges().forEach(charge -> createChargeDefinition(productEntity, charge));
return createProductCommand.getInstance().getIdentifier();
}
private void createChargeDefinition(final ProductEntity productEntity, final ChargeDefinition chargeDefinition) {
final ChargeDefinitionEntity chargeDefinitionEntity =
ChargeDefinitionMapper.map(productEntity, chargeDefinition, null, null);
chargeDefinitionRepository.save(chargeDefinitionEntity);
}
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.PUT_PRODUCT)
public String process(final ChangeProductCommand changeProductCommand) {
final Product instance = changeProductCommand.getInstance();
if (caseRepository.existsByProductIdentifier(instance.getIdentifier()))
throw ServiceException
.conflict("Cases exist for product with the identifier '" + instance.getIdentifier() + "'. Product cannot be changed.");
final ProductEntity oldEntity = productRepository
.findByIdentifier(instance.getIdentifier())
.orElseThrow(() -> ServiceException.notFound("Product not found '" + instance.getIdentifier() + "'."));
final ProductEntity newEntity = ProductMapper.mapOverOldEntity(instance, oldEntity);
productRepository.save(newEntity);
return changeProductCommand.getInstance().getIdentifier();
}
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.DELETE_PRODUCT)
public String process(final DeleteProductCommand deleteProductCommand) {
final String productIdentifier = deleteProductCommand.getProductIdentifier();
final ProductEntity product = productRepository.findByIdentifier(productIdentifier)
.orElseThrow(() -> ServiceException.notFound("Instance with identifier ''{0}'' doesn''t exist.", productIdentifier));
if (product.getEnabled())
throw ServiceException.conflict("Cannot delete product with identifier ''{0}'', because it is enabled.", productIdentifier);
if (caseRepository.existsByProductIdentifier(productIdentifier))
throw ServiceException.conflict("Cannot delete product with identifier ''{0}'', because there are already cases defined on it.", productIdentifier);
productRepository.delete(product);
return deleteProductCommand.getProductIdentifier();
}
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.PUT_PRODUCT_ENABLE)
public String process(final ChangeEnablingOfProductCommand changeEnablingOfProductCommand)
{
final ProductEntity productEntity = this.productRepository.findByIdentifier(changeEnablingOfProductCommand.getProductIdentifier())
.orElseThrow(() -> ServiceException.notFound("Product not found '" + changeEnablingOfProductCommand.getProductIdentifier() + "'."));
//noinspection PointlessBooleanExpression
if (changeEnablingOfProductCommand.getEnabled() == true) {
final Set<AccountAssignment> accountAssignments = ProductMapper.map(productEntity).getAccountAssignments();
final Stream<ChargeDefinition> chargeDefinitions = chargeDefinitionRepository
.findByProductId(productEntity.getIdentifier())
.map(ChargeDefinitionMapper::map);
createAndAssignProductLossAllowanceAccountIfNecessary(productEntity, accountingAdapter);
final Set<String> accountAssignmentsRequiredButNotProvided
= AccountingAdapter.accountAssignmentsRequiredButNotProvided(accountAssignments, chargeDefinitions);
if (!accountAssignmentsRequiredButNotProvided.isEmpty())
throw ServiceException.conflict("Not ready to enable product ''{0}''. One or more of the charge definitions " +
"contains a designator for which no account assignment exists. Here are the unassigned designators ''{1}''",
changeEnablingOfProductCommand.getProductIdentifier(), accountAssignmentsRequiredButNotProvided);
final Set<String> accountAssignmentsMappedToNonexistentAccounts = accountingAdapter.accountAssignmentsMappedToNonexistentAccounts(accountAssignments);
if (!accountAssignmentsMappedToNonexistentAccounts.isEmpty())
throw ServiceException.conflict("Not ready to enable product ''{0}''. The following account assignments point " +
"to an account or ledger which does not exist ''{1}''.", changeEnablingOfProductCommand.getProductIdentifier(),
accountAssignmentsMappedToNonexistentAccounts);
}
productEntity.setEnabled(changeEnablingOfProductCommand.getEnabled());
this.productRepository.save(productEntity);
return changeEnablingOfProductCommand.getProductIdentifier();
}
static void createAndAssignProductLossAllowanceAccountIfNecessary(
final ProductEntity productEntity,
final AccountingAdapter accountingAdapter) {
final Map<String, ProductAccountAssignmentEntity> accountAssignmentEntityMap
= productEntity.getAccountAssignments().stream()
.collect(Collectors.toMap(ProductAccountAssignmentEntity::getDesignator, Function.identity()));
final Optional<ProductAccountAssignmentEntity> productLossAllowanceMapping
= Optional.ofNullable(accountAssignmentEntityMap.get(AccountDesignators.PRODUCT_LOSS_ALLOWANCE));
final boolean productLossAccountIsMappedToAccount = productLossAllowanceMapping
.map(x -> x.getType() == AccountingAdapter.IdentifierType.ACCOUNT)
.orElse(false);
if (productLossAccountIsMappedToAccount)
return; //Already done.
final boolean productLossAccountIsMappedToLedger = productLossAllowanceMapping
.map(x -> x.getType() == AccountingAdapter.IdentifierType.LEDGER)
.orElse(false);
if (productLossAccountIsMappedToLedger)
throw ServiceException.conflict("Not ready to enable product ''{0}''. The account assignment for product loss allowance is mapped to a ledger.",
productEntity.getIdentifier());
final String loanPrincipalLedgerMapping
= Optional.ofNullable(accountAssignmentEntityMap.get(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL))
.flatMap(x -> {
if (x.getType() != AccountingAdapter.IdentifierType.LEDGER)
return Optional.empty();
else
return Optional.of(x.getIdentifier());
})
.orElseThrow(() ->
ServiceException.conflict("Not ready to enable product ''{0}''. The customer loan principal account is not mapped to a ledger.",
productEntity.getIdentifier()));
final String accountIdentifier = accountingAdapter.createProductAccountForLedgerAssignment(
productEntity.getIdentifier(),
AccountDesignators.PRODUCT_LOSS_ALLOWANCE,
loanPrincipalLedgerMapping);
final AccountAssignment productLossAccountAssignment = new AccountAssignment(AccountDesignators.PRODUCT_LOSS_ALLOWANCE, accountIdentifier);
productEntity.getAccountAssignments().add(ProductMapper.map(productLossAccountAssignment, productEntity));
}
}