blob: 09ee4f167e17954e50a2917f541e7749518d37bf [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.portfolio.client.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
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.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.apache.fineract.portfolio.charge.exception.ChargeCannotBeAppliedToException;
import org.apache.fineract.portfolio.client.api.ClientApiConstants;
import org.apache.fineract.portfolio.client.data.ClientChargeDataValidator;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientCharge;
import org.apache.fineract.portfolio.client.domain.ClientChargePaidBy;
import org.apache.fineract.portfolio.client.domain.ClientChargeRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.ClientTransaction;
import org.apache.fineract.portfolio.client.domain.ClientTransactionRepository;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.apache.fineract.useradministration.domain.AppUser;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
@Service
public class ClientChargeWritePlatformServiceJpaRepositoryImpl implements ClientChargeWritePlatformService {
private final static Logger logger = LoggerFactory.getLogger(ClientChargeWritePlatformServiceJpaRepositoryImpl.class);
private final PlatformSecurityContext context;
private final ChargeRepositoryWrapper chargeRepository;
private final ClientRepositoryWrapper clientRepository;
private final ClientChargeDataValidator clientChargeDataValidator;
private final ConfigurationDomainService configurationDomainService;
private final HolidayRepositoryWrapper holidayRepository;
private final WorkingDaysRepositoryWrapper workingDaysRepository;
private final ClientChargeRepositoryWrapper clientChargeRepository;
private final ClientTransactionRepository clientTransactionRepository;
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
@Autowired
public ClientChargeWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context,
final ChargeRepositoryWrapper chargeRepository, final ClientChargeDataValidator clientChargeDataValidator,
final ClientRepositoryWrapper clientRepository, final HolidayRepositoryWrapper holidayRepositoryWrapper,
final ConfigurationDomainService configurationDomainService, final ClientChargeRepositoryWrapper clientChargeRepository,
final WorkingDaysRepositoryWrapper workingDaysRepository, final ClientTransactionRepository clientTransactionRepository,
final PaymentDetailWritePlatformService paymentDetailWritePlatformService,
final JournalEntryWritePlatformService journalEntryWritePlatformService) {
this.context = context;
this.chargeRepository = chargeRepository;
this.clientChargeDataValidator = clientChargeDataValidator;
this.clientRepository = clientRepository;
this.holidayRepository = holidayRepositoryWrapper;
this.configurationDomainService = configurationDomainService;
this.clientChargeRepository = clientChargeRepository;
this.workingDaysRepository = workingDaysRepository;
this.clientTransactionRepository = clientTransactionRepository;
this.paymentDetailWritePlatformService = paymentDetailWritePlatformService;
this.journalEntryWritePlatformService = journalEntryWritePlatformService;
}
@Override
public CommandProcessingResult addCharge(Long clientId, JsonCommand command) {
try {
this.clientChargeDataValidator.validateAdd(command.json());
final Client client = clientRepository.getActiveClientInUserScope(clientId);
final Long chargeDefinitionId = command.longValueOfParameterNamed(ClientApiConstants.chargeIdParamName);
final Charge charge = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId);
// validate for client charge
if (!charge.isClientCharge()) {
final String errorMessage = "Charge with identifier " + charge.getId() + " cannot be applied to a Client";
throw new ChargeCannotBeAppliedToException("client", errorMessage, charge.getId());
}
final ClientCharge clientCharge = ClientCharge.createNew(client, charge, command);
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat());
validateDueDateOnWorkingDay(clientCharge, fmt);
this.clientChargeRepository.save(clientCharge);
return new CommandProcessingResultBuilder() //
.withEntityId(clientCharge.getId()) //
.withOfficeId(clientCharge.getClient().getOffice().getId()) //
.withClientId(clientCharge.getClient().getId()) //
.build();
} catch (DataIntegrityViolationException dve) {
handleDataIntegrityIssues(clientId, null, dve);
return CommandProcessingResult.empty();
}
}
@Override
public CommandProcessingResult payCharge(Long clientId, Long clientChargeId, JsonCommand command) {
try {
this.clientChargeDataValidator.validatePayCharge(command.json());
final Client client = this.clientRepository.getActiveClientInUserScope(clientId);
final ClientCharge clientCharge = this.clientChargeRepository.findOneWithNotFoundDetection(clientChargeId);
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
final LocalDate transactionDate = command.localDateValueOfParameterNamed(ClientApiConstants.transactionDateParamName);
final BigDecimal amountPaid = command.bigDecimalValueOfParameterNamed(ClientApiConstants.amountParamName);
final Money chargePaid = Money.of(clientCharge.getCurrency(), amountPaid);
// Validate business rules for payment
validatePaymentTransaction(client, clientCharge, fmt, transactionDate, amountPaid);
// pay the charge
clientCharge.pay(chargePaid);
// create Payment Transaction
final Map<String, Object> changes = new LinkedHashMap<>();
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
ClientTransaction clientTransaction = ClientTransaction.payCharge(client, client.getOffice(), paymentDetail, transactionDate,
chargePaid, clientCharge.getCurrency().getCode(), getAppUserIfPresent());
this.clientTransactionRepository.saveAndFlush(clientTransaction);
// update charge paid by associations
final ClientChargePaidBy chargePaidBy = ClientChargePaidBy.instance(clientTransaction, clientCharge, amountPaid);
clientTransaction.getClientChargePaidByCollection().add(chargePaidBy);
// generate accounting entries
generateAccountingEntries(clientTransaction);
return new CommandProcessingResultBuilder() //
.withTransactionId(clientTransaction.getId().toString())//
.withEntityId(clientCharge.getId()) //
.withOfficeId(clientCharge.getClient().getOffice().getId()) //
.withClientId(clientCharge.getClient().getId()).build();
} catch (DataIntegrityViolationException dve) {
handleDataIntegrityIssues(clientId, clientChargeId, dve);
return CommandProcessingResult.empty();
}
}
private void generateAccountingEntries(ClientTransaction clientTransaction) {
Map<String, Object> accountingBridgeData = clientTransaction.toMapData();
journalEntryWritePlatformService.createJournalEntriesForClientTransactions(accountingBridgeData);
}
@Override
public CommandProcessingResult waiveCharge(Long clientId, Long clientChargeId) {
try {
final Client client = this.clientRepository.getActiveClientInUserScope(clientId);
final ClientCharge clientCharge = this.clientChargeRepository.findOneWithNotFoundDetection(clientChargeId);
final LocalDate transactionDate = DateUtils.getLocalDateOfTenant();
// Validate business rules for payment
validateWaiverTransaction(client, clientCharge);
// waive the charge
Money waivedAmount = clientCharge.waive();
// create Waiver Transaction
ClientTransaction clientTransaction = ClientTransaction.waiver(client, client.getOffice(), transactionDate, waivedAmount,
clientCharge.getCurrency().getCode(), getAppUserIfPresent());
this.clientTransactionRepository.save(clientTransaction);
// update charge paid by associations
final ClientChargePaidBy chargePaidBy = ClientChargePaidBy.instance(clientTransaction, clientCharge, waivedAmount.getAmount());
clientTransaction.getClientChargePaidByCollection().add(chargePaidBy);
return new CommandProcessingResultBuilder().withTransactionId(clientTransaction.getId().toString())//
.withEntityId(clientCharge.getId()) //
.withOfficeId(clientCharge.getClient().getOffice().getId()) //
.withClientId(clientCharge.getClient().getId()) //
.build();
} catch (DataIntegrityViolationException dve) {
handleDataIntegrityIssues(clientId, clientChargeId, dve);
return CommandProcessingResult.empty();
}
}
@Override
public CommandProcessingResult deleteCharge(Long clientId, Long clientChargeId) {
try {
final Client client = this.clientRepository.getActiveClientInUserScope(clientId);
final ClientCharge clientCharge = this.clientChargeRepository.findOneWithNotFoundDetection(clientChargeId);
// Validate business rules for charge deletion
validateChargeDeletion(client, clientCharge);
// delete the charge
clientChargeRepository.delete(clientCharge);
return new CommandProcessingResultBuilder() //
.withEntityId(clientCharge.getId()) //
.withOfficeId(clientCharge.getClient().getOffice().getId()) //
.withClientId(clientCharge.getClient().getId()) //
.build();
} catch (DataIntegrityViolationException dve) {
handleDataIntegrityIssues(clientId, clientChargeId, dve);
return CommandProcessingResult.empty();
}
}
/**
* Validates transaction to ensure that <br>
* charge is active <br>
* transaction date is valid (between client activation and todays date)
* <br>
* charge is not already paid or waived <br>
* amount is not more than total due
*
* @param client
* @param clientCharge
* @param fmt
* @param transactionDate
* @param amountPaid
* @param requiresTransactionDateValidation
* if set to false, transaction date specific validation is
* skipped
* @param requiresTransactionAmountValidation
* if set to false transaction amount validation is skipped
* @return
*/
private void validatePaymentDateAndAmount(final Client client, final ClientCharge clientCharge, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal amountPaid, final boolean requiresTransactionDateValidation,
final boolean requiresTransactionAmountValidation) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(ClientApiConstants.CLIENT_CHARGES_RESOURCE_NAME);
if (clientCharge.isNotActive()) {
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("charge.is.not.active");
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
}
if (requiresTransactionDateValidation) {
validateTransactionDateOnWorkingDay(transactionDate, clientCharge, fmt);
if (client.getActivationLocalDate() != null && transactionDate.isBefore(client.getActivationLocalDate())) {
baseDataValidator.reset().parameter(ClientApiConstants.transactionDateParamName).value(transactionDate.toString(fmt))
.failWithCodeNoParameterAddedToErrorCode("transaction.before.activationDate");
throw new PlatformApiDataValidationException(dataValidationErrors);
}
if (DateUtils.isDateInTheFuture(transactionDate)) {
baseDataValidator.reset().parameter(ClientApiConstants.transactionDateParamName).value(transactionDate.toString(fmt))
.failWithCodeNoParameterAddedToErrorCode("transaction.is.futureDate");
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
// validate charge is not already paid or waived
if (clientCharge.isWaived()) {
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.charge.is.already.waived");
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
} else if (clientCharge.isPaid()) {
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.charge.is.paid");
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
}
if (requiresTransactionAmountValidation) {
final Money chargePaid = Money.of(clientCharge.getCurrency(), amountPaid);
if (!clientCharge.getAmountOutstanding().isGreaterThanOrEqualTo(chargePaid)) {
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.charge.amount.paid.in.access");
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
}
}
}
public void validateWaiverTransaction(final Client client, final ClientCharge clientCharge) {
DateTimeFormatter fmt = null;
LocalDate transactionDate = null;
BigDecimal amountPaid = null;
boolean requiresTransactionDateValidation = false;
boolean requiresTransactionAmountValidation = false;
validatePaymentDateAndAmount(client, clientCharge, fmt, transactionDate, amountPaid, requiresTransactionDateValidation,
requiresTransactionAmountValidation);
}
public void validatePaymentTransaction(final Client client, final ClientCharge clientCharge, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal amountPaid) {
boolean requiresTransactionDateValidation = true;
boolean requiresTransactionAmountValidation = true;
validatePaymentDateAndAmount(client, clientCharge, fmt, transactionDate, amountPaid, requiresTransactionDateValidation,
requiresTransactionAmountValidation);
}
public void validateChargeDeletion(final Client client, final ClientCharge clientCharge) {
DateTimeFormatter fmt = null;
LocalDate transactionDate = null;
BigDecimal amountPaid = null;
boolean requiresTransactionDateValidation = false;
boolean requiresTransactionAmountValidation = false;
validatePaymentDateAndAmount(client, clientCharge, fmt, transactionDate, amountPaid, requiresTransactionDateValidation,
requiresTransactionAmountValidation);
}
/**
* @param clientId
* @return
*/
@Override
public CommandProcessingResult updateCharge(@SuppressWarnings("unused") Long clientId,
@SuppressWarnings("unused") JsonCommand command) {
// functionality not yet supported
return null;
}
@Override
@SuppressWarnings("unused")
public CommandProcessingResult inactivateCharge(Long clientId, Long clientChargeId) {
// functionality not yet supported
return null;
}
/**
* Ensures that the charge due date is not on a holiday or a non working day
*
* @param clientCharge
* @param fmt
*/
private void validateDueDateOnWorkingDay(final ClientCharge clientCharge, final DateTimeFormatter fmt) {
validateActivityDateFallOnAWorkingDay(clientCharge.getDueLocalDate(), clientCharge.getOfficeId(),
ClientApiConstants.dueAsOfDateParamName, "charge.due.date.is.on.holiday", "charge.due.date.is.a.non.workingday", fmt);
}
/**
* Ensures that the charge transaction date (for payments) is not on a
* holiday or a non working day
*
* @param savingsAccountCharge
* @param fmt
*/
private void validateTransactionDateOnWorkingDay(final LocalDate transactionDate, final ClientCharge clientCharge,
final DateTimeFormatter fmt) {
validateActivityDateFallOnAWorkingDay(transactionDate, clientCharge.getOfficeId(), ClientApiConstants.transactionDateParamName,
"transaction.not.allowed.transaction.date.is.on.holiday", "transaction.not.allowed.transaction.date.is.a.non.workingday",
fmt);
}
/**
* @param date
* @param officeId
* @param jsonPropertyName
* @param errorMessageFragment
* @param fmt
*/
private void validateActivityDateFallOnAWorkingDay(final LocalDate date, final Long officeId, final String jsonPropertyName,
final String errorMessageFragmentForActivityOnHoliday, final String errorMessageFragmentForActivityOnNonWorkingDay,
final DateTimeFormatter fmt) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(ClientApiConstants.CLIENT_CHARGES_RESOURCE_NAME);
if (date != null) {
// transaction date should not be on a holiday or non working day
if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() && this.holidayRepository.isHoliday(officeId, date)) {
baseDataValidator.reset().parameter(jsonPropertyName).value(date.toString(fmt))
.failWithCodeNoParameterAddedToErrorCode(errorMessageFragmentForActivityOnHoliday);
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
}
if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled()
&& !this.workingDaysRepository.isWorkingDay(date)) {
baseDataValidator.reset().parameter(jsonPropertyName).value(date.toString(fmt))
.failWithCodeNoParameterAddedToErrorCode(errorMessageFragmentForActivityOnNonWorkingDay);
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
}
}
}
private AppUser getAppUserIfPresent() {
AppUser user = null;
if (this.context != null) {
user = this.context.getAuthenticatedUserIfPresent();
}
return user;
}
private void handleDataIntegrityIssues(@SuppressWarnings("unused") final Long clientId, final Long clientChargeId,
final DataIntegrityViolationException dve) {
final Throwable realCause = dve.getMostSpecificCause();
if (realCause.getMessage().contains("FK_m_client_charge_paid_by_m_client_charge")) {
throw new PlatformDataIntegrityException("error.msg.client.charge.cannot.be.deleted",
"Client charge with id `" + clientChargeId + "` cannot be deleted as transactions have been made on the same",
"clientChargeId", clientChargeId); }
logger.error(dve.getMessage(), dve);
throw new PlatformDataIntegrityException("error.msg.client.charges.unknown.data.integrity.issue",
"Unknown data integrity issue with resource.");
}
}