From 81358416cbef3f8e4a8cf546955010deb2f0fe5e Mon Sep 17 00:00:00 2001 From: Zoltan Nebli Date: Mon, 22 Jun 2020 16:15:22 +0200 Subject: [PATCH] FINERACT add release onhold interop transaction --- .../core/data/CommandProcessingResult.java | 5 + .../data/InteropAccountData.java | 36 +-- .../data/InteropIdentifierData.java | 2 +- .../domain/InteropIdentifier.java | 21 +- .../domain/InteropIdentifierRepository.java | 5 +- .../domain/InteropTransferActionType.java | 2 +- .../serialization/InteropDataValidator.java | 10 + .../service/InteropService.java | 3 + .../service/InteropServiceImpl.java | 270 +++++++++--------- .../savings/domain/SavingsAccount.java | 16 +- ...WritePlatformServiceJpaRepositoryImpl.java | 2 +- .../migrations/core_db/V352__interop_init.sql | 3 + 12 files changed, 207 insertions(+), 168 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java index f45690284..8b014a0d5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java @@ -148,6 +148,11 @@ public class CommandProcessingResult implements Serializable { this.subResourceId = null; } + protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId, + final Map changesOnly, long clientId) { + this(commandId, officeId, null, clientId, null, null, null, resourceId, null, changesOnly, null, null, null, null, null); + } + public Long commandId() { return this.commandId; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropAccountData.java index fd0737ec4..2cf8ef298 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropAccountData.java @@ -70,8 +70,8 @@ public class InteropAccountData extends CommandProcessingResult { String productName, String shortProductName, String currency, BigDecimal accountBalance, BigDecimal availableBalance, SavingsAccountStatusType status, SavingsAccountSubStatusEnum subStatus, AccountType accountType, DepositAccountType depositType, LocalDate activatedOn, LocalDate statusUpdateOn, LocalDate withdrawnOn, LocalDate balanceOn, - List identifiers) { - super(resourceId, officeId, commandId, changesOnly); + List identifiers, long clientId) { + super(resourceId, officeId, commandId, changesOnly, clientId); this.accountId = accountId; this.savingProductId = productId; this.productName = productName; @@ -93,9 +93,9 @@ public class InteropAccountData extends CommandProcessingResult { InteropAccountData(String accountId, String productId, String productName, String shortProductName, String currency, BigDecimal accountBalance, BigDecimal availableBalance, SavingsAccountStatusType status, SavingsAccountSubStatusEnum subStatus, AccountType accountType, DepositAccountType depositType, LocalDate activatedOn, LocalDate statusUpdateOn, LocalDate withdrawnOn, - LocalDate balanceOn, List identifiers) { + LocalDate balanceOn, List identifiers, long clientId) { this(null, null, null, null, accountId, productId, productName, shortProductName, currency, accountBalance, availableBalance, - status, subStatus, accountType, depositType, activatedOn, statusUpdateOn, withdrawnOn, balanceOn, identifiers); + status, subStatus, accountType, depositType, activatedOn, statusUpdateOn, withdrawnOn, balanceOn, identifiers, clientId); } public static InteropAccountData build(SavingsAccount account) { @@ -114,28 +114,28 @@ public class InteropAccountData extends CommandProcessingResult { return new InteropAccountData(account.getExternalId(), product.getId().toString(), product.getName(), product.getShortName(), account.getCurrency().getCode(), account.getAccountBalance(), account.getWithdrawableBalance(), account.getStatus(), subStatus, account.getAccountType(), account.depositAccountType(), account.getActivationLocalDate(), - calcStatusUpdateOn(account), account.getWithdrawnOnDate(), account.retrieveLastTransactionDate(), ids); + calcStatusUpdateOn(account), account.getWithdrawnOnDate(), account.retrieveLastTransactionDate(), ids, + account.getClient().getId()); } private static LocalDate calcStatusUpdateOn(@NotNull SavingsAccount account) { - LocalDate date = account.getClosedOnDate(); - if (date != null) { - return date; + if (account.getClosedOnDate() != null) { + return account.getClosedOnDate(); } - if ((date = account.getWithdrawnOnDate()) != null) { - return date; + if (account.getWithdrawnOnDate() != null) { + return account.getWithdrawnOnDate(); } - if ((date = account.getActivationLocalDate()) != null) { - return date; + if (account.getActivationLocalDate() != null) { + return account.getActivationLocalDate(); } - if ((date = account.getRejectedOnDate()) != null) { - return date; + if (account.getRejectedOnDate() != null) { + return account.getRejectedOnDate(); } - if ((date = account.getApprovedOnDate()) != null) { - return date; + if (account.getApprovedOnDate() != null) { + return account.getApprovedOnDate(); } - if ((date = account.getSubmittedOnDate()) != null) { - return date; + if (account.getSubmittedOnDate() != null) { + return account.getSubmittedOnDate(); } return null; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropIdentifierData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropIdentifierData.java index 50a64615c..84b7ac8ed 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropIdentifierData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropIdentifierData.java @@ -55,6 +55,6 @@ public class InteropIdentifierData { } public static InteropIdentifierData build(InteropIdentifier identifier) { - return new InteropIdentifierData(identifier.getType(), identifier.getValue(), identifier.getSubValueOrType()); + return new InteropIdentifierData(identifier.getType(), identifier.getValue(), identifier.getSubType()); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifier.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifier.java index eb9ab9cde..d1c09fae4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifier.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifier.java @@ -19,6 +19,7 @@ package org.apache.fineract.interoperation.domain; import java.util.Date; +import java.util.Objects; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; @@ -51,7 +52,7 @@ public class InteropIdentifier extends AbstractPersistableCustom { private String value; @Column(name = "sub_value_or_type", length = 128) - private String subValueOrType; + private String subType; @Column(name = "created_by", nullable = false, length = 32) private String createdBy; @@ -69,12 +70,12 @@ public class InteropIdentifier extends AbstractPersistableCustom { protected InteropIdentifier() {} - public InteropIdentifier(@NotNull SavingsAccount account, @NotNull InteropIdentifierType type, @NotNull String value, - String subValueOrType, @NotNull String createdBy, @NotNull Date createdOn) { + public InteropIdentifier(@NotNull SavingsAccount account, @NotNull InteropIdentifierType type, @NotNull String value, String subType, + @NotNull String createdBy, @NotNull Date createdOn) { this.account = account; this.type = type; this.value = value; - this.subValueOrType = subValueOrType; + this.subType = subType; this.createdBy = createdBy; this.createdOn = createdOn; } @@ -108,12 +109,12 @@ public class InteropIdentifier extends AbstractPersistableCustom { this.value = value; } - public String getSubValueOrType() { - return subValueOrType; + public String getSubType() { + return subType; } - public void setSubValueOrType(String subValueOrType) { - this.subValueOrType = subValueOrType; + public void setSubType(String subType) { + this.subType = subType; } public String getCreatedBy() { @@ -168,14 +169,14 @@ public class InteropIdentifier extends AbstractPersistableCustom { if (!value.equals(that.value)) { return false; } - return subValueOrType != null ? subValueOrType.equals(that.subValueOrType) : that.subValueOrType == null; + return Objects.equals(subType, that.subType); } @Override public int hashCode() { int result = type.hashCode(); result = 31 * result + value.hashCode(); - result = 31 * result + (subValueOrType != null ? subValueOrType.hashCode() : 0); + result = 31 * result + (subType != null ? subType.hashCode() : 0); return result; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifierRepository.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifierRepository.java index a20bcd1b5..a2b59f18c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifierRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifierRepository.java @@ -23,4 +23,7 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; @Repository -public interface InteropIdentifierRepository extends JpaRepository, JpaSpecificationExecutor {} +public interface InteropIdentifierRepository extends JpaRepository, JpaSpecificationExecutor { + + InteropIdentifier findOneByTypeAndValueAndSubType(InteropIdentifierType idType, String value, String subType); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropTransferActionType.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropTransferActionType.java index 288e2e9d0..951108be7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropTransferActionType.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropTransferActionType.java @@ -20,5 +20,5 @@ package org.apache.fineract.interoperation.domain; public enum InteropTransferActionType { - PREPARE, CREATE,; + PREPARE, CREATE, RELEASE; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/serialization/InteropDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/serialization/InteropDataValidator.java index 6ac5ad4ad..11ac7c9b8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/serialization/InteropDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/serialization/InteropDataValidator.java @@ -46,6 +46,16 @@ public class InteropDataValidator { this.jsonHelper = fromJsonHelper; } + public InteropTransferRequestData validateAndParseTransferRequest(JsonCommand command) { + final DataValidatorBuilder dataValidator = new DataValidatorBuilder(new ArrayList<>()).resource("interoperation.transfer"); + JsonObject element = extractJsonObject(command); + + InteropTransferRequestData result = InteropTransferRequestData.validateAndParse(dataValidator, element, jsonHelper); + throwExceptionIfValidationWarningsExist(dataValidator); + + return result; + } + public InteropIdentifierRequestData validateAndParseCreateIdentifier(@NotNull InteropIdentifierType idType, @NotNull String idValue, String subIdOrType, JsonCommand command) { final DataValidatorBuilder dataValidator = new DataValidatorBuilder(new ArrayList<>()).resource("interoperation.identifier"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropService.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropService.java index 284e2dc4d..c46bae879 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropService.java @@ -71,4 +71,7 @@ public interface InteropService { @NotNull InteropTransferResponseData commitTransfer(@NotNull JsonCommand command); + + @NotNull + InteropTransferResponseData releaseTransfer(@NotNull JsonCommand command); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java index fcfe9397d..9db2f0ed4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java @@ -20,7 +20,11 @@ package org.apache.fineract.interoperation.service; import static org.apache.fineract.interoperation.util.InteropUtil.DEFAULT_LOCALE; import static org.apache.fineract.interoperation.util.InteropUtil.DEFAULT_ROUTING_CODE; -import static org.springframework.data.jpa.domain.Specification.where; +import static org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail.instance; +import static org.apache.fineract.portfolio.savings.SavingsAccountTransactionType.AMOUNT_HOLD; +import static org.apache.fineract.portfolio.savings.SavingsAccountTransactionType.DEPOSIT; +import static org.apache.fineract.portfolio.savings.SavingsAccountTransactionType.WITHDRAWAL; +import static org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction.releaseAmount; import java.math.BigDecimal; import java.time.ZoneId; @@ -28,10 +32,6 @@ import java.util.Date; import java.util.List; import java.util.Locale; import java.util.function.Predicate; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Path; -import javax.persistence.criteria.Root; import javax.validation.constraints.NotNull; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.api.JsonCommand; @@ -59,11 +59,11 @@ import org.apache.fineract.interoperation.serialization.InteropDataValidator; import org.apache.fineract.interoperation.util.MathUtil; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepository; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.note.domain.Note; import org.apache.fineract.portfolio.note.domain.NoteRepository; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; -import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; import org.apache.fineract.portfolio.paymenttype.domain.PaymentType; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; @@ -84,7 +84,6 @@ import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -115,7 +114,7 @@ public class InteropServiceImpl implements InteropService { ApplicationCurrencyRepository applicationCurrencyRepository, NoteRepository noteRepository, PaymentTypeRepository paymentTypeRepository, InteropIdentifierRepository identifierRepository, SavingsHelper savingsHelper, SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper, - SavingsAccountDomainService savingsAccountService, PaymentDetailWritePlatformService paymentDetailWritePlatformService) { + SavingsAccountDomainService savingsAccountService) { this.securityContext = securityContext; this.dataValidator = interopDataValidator; this.savingsAccountRepository = savingsAccountRepository; @@ -133,8 +132,7 @@ public class InteropServiceImpl implements InteropService { @Override @Transactional public InteropAccountData getAccountDetails(@NotNull String accountId) { - SavingsAccount savingsAccount = validateAndGetSavingAccount(accountId); - return InteropAccountData.build(savingsAccount); + return InteropAccountData.build(validateAndGetSavingAccount(accountId)); } @NotNull @@ -171,17 +169,22 @@ public class InteropServiceImpl implements InteropService { } @NotNull - @Override @Transactional + @Override public InteropIdentifierAccountResponseData getAccountByIdentifier(@NotNull InteropIdentifierType idType, @NotNull String idValue, String subIdOrType) { InteropIdentifier identifier = findIdentifier(idType, idValue, subIdOrType); + if (identifier == null) { + throw new UnsupportedOperationException( + "Account not found for identifier " + idType + "/" + idValue + (subIdOrType == null ? "" : ("/" + subIdOrType))); + } + return InteropIdentifierAccountResponseData.build(identifier.getAccount().getExternalId()); } @NotNull - @Override @Transactional(propagation = Propagation.MANDATORY) + @Override public InteropIdentifierAccountResponseData registerAccountIdentifier(@NotNull InteropIdentifierType idType, @NotNull String idValue, String subIdOrType, @NotNull JsonCommand command) { InteropIdentifierRequestData request = dataValidator.validateAndParseCreateIdentifier(idType, idValue, subIdOrType, command); @@ -198,12 +201,16 @@ public class InteropServiceImpl implements InteropService { return InteropIdentifierAccountResponseData.build(savingsAccount.getExternalId()); } - @Override @NotNull @Transactional(propagation = Propagation.MANDATORY) + @Override public InteropIdentifierAccountResponseData deleteAccountIdentifier(@NotNull InteropIdentifierType idType, @NotNull String idValue, String subIdOrType) { InteropIdentifier identifier = findIdentifier(idType, idValue, subIdOrType); + if (identifier == null) { + throw new UnsupportedOperationException( + "Account not found for identifier " + idType + "/" + idValue + (subIdOrType == null ? "" : ("/" + subIdOrType))); + } String accountId = identifier.getAccount().getExternalId(); @@ -226,7 +233,8 @@ public class InteropServiceImpl implements InteropService { // always Payer InteropTransactionRequestData request = dataValidator.validateAndParseCreateRequest(command); - validateAndGetSavingAccount(request); + // TODO: error handling + SavingsAccount savingsAccount = validateAndGetSavingAccount(request); return InteropTransactionRequestResponseData.build(command.commandId(), request.getTransactionCode(), InteropActionState.ACCEPTED, request.getExpiration(), request.getExtensionList(), request.getRequestCode()); @@ -243,12 +251,18 @@ public class InteropServiceImpl implements InteropService { public InteropQuoteResponseData createQuote(@NotNull JsonCommand command) { InteropQuoteRequestData request = dataValidator.validateAndParseCreateQuote(command); - // TODO: error handling SavingsAccount savingsAccount = validateAndGetSavingAccount(request); SavingsAccountTransactionType transactionType = request.getTransactionRole().getTransactionType(); - BigDecimal fee = transactionType.isDebit() ? savingsAccount.calculateWithdrawalFee(request.getAmount().getAmount()) - : BigDecimal.ZERO; + final BigDecimal fee; + if (transactionType.isDebit()) { + fee = savingsAccount.calculateWithdrawalFee(request.getAmount().getAmount()); + if (MathUtil.isLessThan(savingsAccount.getWithdrawableBalance(), request.getAmount().getAmount().add(fee))) { + throw new UnsupportedOperationException("Not enough balance amount for requested amount plus fees!"); + } + } else { + fee = BigDecimal.ZERO; + } return InteropQuoteResponseData.build(command.commandId(), request.getTransactionCode(), InteropActionState.ACCEPTED, request.getExpiration(), request.getExtensionList(), request.getQuoteCode(), @@ -264,32 +278,37 @@ public class InteropServiceImpl implements InteropService { @NotNull @Transactional(propagation = Propagation.MANDATORY) public InteropTransferResponseData prepareTransfer(@NotNull JsonCommand command) { - InteropTransferRequestData request = dataValidator.validateAndParsePrepareTransfer(command); - - // TODO: error handling - // TODO: REVERSE - SavingsAccount savingsAccount = validateAndGetSavingAccount(request); - - BigDecimal total = validateTransfer(request, savingsAccount); - + InteropTransferRequestData request = dataValidator.validateAndParseTransferRequest(command); String transferCode = request.getTransferCode(); LocalDateTime transactionDate = DateUtils.getLocalDateTimeOfTenant(); - if (MathUtil.isGreaterThanZero(total)) { + + // TODO validate request fee/comission and account quote amount + // matching, at CREATE it is debited anyway + + SavingsAccountTransactionType transactionType = request.getTransactionRole().getTransactionType(); + if (transactionType.isDebit()) { + SavingsAccount savingsAccount = validateAndGetSavingAccount(request); + BigDecimal total = calculateTotalTransferAmount(request, savingsAccount); + if (MathUtil.isLessThan(savingsAccount.getWithdrawableBalance(), total)) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("Not enough balance amount for requested amount plus fees!"); } - if (findTransaction(savingsAccount, transferCode, SavingsAccountTransactionType.AMOUNT_HOLD) != null) { + if (findTransaction(savingsAccount, transferCode, AMOUNT_HOLD.getValue()) != null) { throw new UnsupportedOperationException("Transfer amount was already put on hold " + transferCode); } - PaymentDetail paymentDetail = PaymentDetail.instance(findPaymentType(), savingsAccount.getExternalId(), null, getRoutingCode(), - transferCode, null); - AppUser appUser = getLoginUser(); - SavingsAccountTransaction transaction = SavingsAccountTransaction.holdAmount(savingsAccount, savingsAccount.office(), - paymentDetail, transactionDate.toLocalDate(), Money.of(savingsAccount.getCurrency(), total), new Date(), appUser); + PaymentDetail paymentDetail = instance(findPaymentType(), savingsAccount.getExternalId(), null, getRoutingCode(), transferCode, + null); + SavingsAccountTransaction holdTransaction = SavingsAccountTransaction.holdAmount(savingsAccount, savingsAccount.office(), + paymentDetail, transactionDate.toLocalDate(), Money.of(savingsAccount.getCurrency(), total), new Date(), + getLoginUser()); + MonetaryCurrency accountCurrency = savingsAccount.getCurrency().copy(); + holdTransaction.updateRunningBalance( + Money.of(accountCurrency, savingsAccount.getWithdrawableBalance().subtract(holdTransaction.getAmount()))); + holdTransaction.updateCumulativeBalanceAndDates(accountCurrency, transactionDate.toLocalDate()); savingsAccount.holdAmount(total); - savingsAccount.addTransaction(transaction); + savingsAccount.addTransaction(holdTransaction); savingsAccountRepository.save(savingsAccount); } @@ -302,55 +321,52 @@ public class InteropServiceImpl implements InteropService { @NotNull @Transactional(propagation = Propagation.MANDATORY) public InteropTransferResponseData commitTransfer(@NotNull JsonCommand command) { - InteropTransferRequestData request = dataValidator.validateAndParseCreateTransfer(command); - SavingsAccountTransactionType transactionType = request.getTransactionRole().getTransactionType(); - boolean debit = transactionType.isDebit(); - - // TODO: error handling - // TODO: REVERSE + InteropTransferRequestData request = dataValidator.validateAndParseTransferRequest(command); + boolean isDebit = request.getTransactionRole().getTransactionType().isDebit(); SavingsAccount savingsAccount = validateAndGetSavingAccount(request); - - validateTransfer(request, savingsAccount); - String transferCode = request.getTransferCode(); - if (findTransaction(savingsAccount, transferCode, - debit ? SavingsAccountTransactionType.WITHDRAWAL : SavingsAccountTransactionType.DEPOSIT) != null) { - throw new UnsupportedOperationException("Transfer was already committed " + transferCode); - } - PaymentDetail paymentDetail = PaymentDetail.instance(findPaymentType(), savingsAccount.getExternalId(), null, getRoutingCode(), - transferCode, null); + if (findTransaction(savingsAccount, transferCode, (isDebit ? WITHDRAWAL : DEPOSIT).getValue()) != null) { + throw new UnsupportedOperationException("Transfer was already committed with code: " + transferCode); + } LocalDateTime transactionDateTime = DateUtils.getLocalDateTimeOfTenant(); LocalDate transactionDate = transactionDateTime.toLocalDate(); - Date createdDate = new Date(); - - SavingsAccountTransaction holdTransaction = findTransaction(savingsAccount, transferCode, - SavingsAccountTransactionType.AMOUNT_HOLD); - if (holdTransaction != null && holdTransaction.getReleaseIdOfHoldAmountTransaction() == null) { - AppUser appUser = getLoginUser(); - - SavingsAccountTransaction releaseTransaction = SavingsAccountTransaction.releaseAmount(holdTransaction, transactionDate, - createdDate, appUser); - releaseTransaction = savingsAccountTransactionRepository.saveAndFlush(releaseTransaction); - holdTransaction.updateReleaseId(releaseTransaction.getId()); - - savingsAccount.releaseAmount(holdTransaction.getAmount()); - savingsAccount.addTransaction(releaseTransaction); - - savingsAccountRepository.save(savingsAccount); - } - - BigDecimal amount = request.getAmount().getAmount(); DateTimeFormatter fmt = getDateTimeFormatter(command); - SavingsAccountTransaction transaction; - if (debit) { + + if (isDebit) { + SavingsAccountTransaction holdTransaction = findTransaction(savingsAccount, transferCode, AMOUNT_HOLD.getValue()); + if (holdTransaction == null) { + throw new UnsupportedOperationException("Missing onhold transaction for transfer: " + transferCode); + } + + BigDecimal totalTransferAmount = calculateTotalTransferAmount(request, savingsAccount); + if (holdTransaction.getAmount().compareTo(totalTransferAmount) != 0) { + throw new UnsupportedOperationException("Transfer request amount plus fees does not match with onhold amount!"); + } + + if (MathUtil.isLessThan(savingsAccount.getWithdrawableBalance().add(holdTransaction.getAmount()), totalTransferAmount)) { + throw new UnsupportedOperationException("Not enough balance amount!"); + } + + if (holdTransaction.getReleaseIdOfHoldAmountTransaction() == null) { + SavingsAccountTransaction releaseTransaction = savingsAccountTransactionRepository + .saveAndFlush(releaseAmount(holdTransaction, transactionDate, new Date(), getLoginUser())); + holdTransaction.updateReleaseId(releaseTransaction.getId()); + savingsAccount.releaseOnHoldAmount(holdTransaction.getAmount()); + savingsAccount.addTransaction(releaseTransaction); + + savingsAccountRepository.save(savingsAccount); + } + SavingsTransactionBooleanValues transactionValues = new SavingsTransactionBooleanValues(false, true, true, false, false); - transaction = savingsAccountService.handleWithdrawal(savingsAccount, fmt, transactionDate, amount, paymentDetail, + transaction = savingsAccountService.handleWithdrawal(savingsAccount, fmt, transactionDate, request.getAmount().getAmount(), + instance(findPaymentType(), savingsAccount.getExternalId(), null, getRoutingCode(), transferCode, null), transactionValues); } else { - transaction = savingsAccountService.handleDeposit(savingsAccount, fmt, transactionDate, amount, paymentDetail, false, true); + transaction = savingsAccountService.handleDeposit(savingsAccount, fmt, transactionDate, request.getAmount().getAmount(), + instance(findPaymentType(), savingsAccount.getExternalId(), null, getRoutingCode(), transferCode, null), false, true); } String note = request.getNote(); @@ -362,7 +378,36 @@ public class InteropServiceImpl implements InteropService { request.getExpiration(), request.getExtensionList(), request.getTransferCode(), transactionDateTime); } - // Util + @Override + @Transactional(propagation = Propagation.MANDATORY) + public @NotNull InteropTransferResponseData releaseTransfer(@NotNull JsonCommand command) { + InteropTransferRequestData request = dataValidator.validateAndParseTransferRequest(command); + SavingsAccount savingsAccount = validateAndGetSavingAccount(request); + + LocalDateTime transactionDateTime = DateUtils.getLocalDateTimeOfTenant(); + SavingsAccountTransaction holdTransaction = findTransaction(savingsAccount, request.getTransferCode(), AMOUNT_HOLD.getValue()); + + if (holdTransaction != null && holdTransaction.getReleaseIdOfHoldAmountTransaction() == null) { + SavingsAccountTransaction releaseTransaction = releaseAmount(holdTransaction, transactionDateTime.toLocalDate(), new Date(), + getLoginUser()); + MonetaryCurrency accountCurrency = savingsAccount.getCurrency().copy(); + releaseTransaction.updateRunningBalance( + Money.of(accountCurrency, savingsAccount.getWithdrawableBalance().add(holdTransaction.getAmount()))); + releaseTransaction.updateCumulativeBalanceAndDates(accountCurrency, transactionDateTime.toLocalDate()); + releaseTransaction = savingsAccountTransactionRepository.saveAndFlush(releaseTransaction); + holdTransaction.updateReleaseId(releaseTransaction.getId()); + + savingsAccount.releaseOnHoldAmount(holdTransaction.getAmount()); + savingsAccount.addTransaction(releaseTransaction); + + savingsAccountRepository.save(savingsAccount); + } else { + throw new UnsupportedOperationException("No active onhold transaction exists with transferCode " + request.getTransferCode()); + } + + return InteropTransferResponseData.build(command.commandId(), request.getTransactionCode(), InteropActionState.ACCEPTED, + request.getExpiration(), request.getExtensionList(), request.getTransferCode(), transactionDateTime); + } private SavingsAccount validateAndGetSavingAccount(String accountId) { SavingsAccount savingsAccount = savingsAccountRepository.findByExternalId(accountId); @@ -377,44 +422,38 @@ public class InteropServiceImpl implements InteropService { SavingsAccount savingsAccount = validateAndGetSavingAccount(request.getAccountId()); savingsAccount.setHelpers(savingsAccountTransactionSummaryWrapper, savingsHelper); - ApplicationCurrency currency = currencyRepository.findOneByCode(request.getAmount().getCurrency()); - if (!savingsAccount.getCurrency().getCode().equals(currency.getCode())) { - throw new UnsupportedOperationException(); + ApplicationCurrency requestCurrency = currencyRepository.findOneByCode(request.getAmount().getCurrency()); + if (!savingsAccount.getCurrency().getCode().equals(requestCurrency.getCode())) { + throw new UnsupportedOperationException("Account and request has different currencies!"); } SavingsAccountTransactionType transactionType = request.getTransactionRole().getTransactionType(); if (!savingsAccount.isTransactionAllowed(transactionType, request.getExpirationLocalDate())) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("Transaction not allowed on account!"); } request.normalizeAmounts(savingsAccount.getCurrency()); - if (transactionType.isDebit() && MathUtil.isLessThan(savingsAccount.getWithdrawableBalance(), request.getAmount().getAmount())) { - throw new UnsupportedOperationException(); - } return savingsAccount; } - private BigDecimal validateTransfer(@NotNull InteropTransferRequestData request, @NotNull SavingsAccount savingsAccount) { - BigDecimal amount = request.getAmount().getAmount(); - SavingsAccountTransactionType transactionType = request.getTransactionRole().getTransactionType(); - - BigDecimal total = transactionType.isDebit() ? amount : MathUtil.negate(amount); - MoneyData fspFee = request.getFspFee(); - if (fspFee != null) { - if (!savingsAccount.getCurrency().getCode().equals(fspFee.getCurrency())) { + private BigDecimal calculateTotalTransferAmount(@NotNull InteropTransferRequestData request, @NotNull SavingsAccount savingsAccount) { + BigDecimal total = request.getAmount().getAmount(); + MoneyData requestFee = request.getFspFee(); + if (requestFee != null) { + if (!savingsAccount.getCurrency().getCode().equals(requestFee.getCurrency())) { throw new UnsupportedOperationException(); } // TODO: compare with calculated quote fee - total = MathUtil.add(total, fspFee.getAmount()); + total = MathUtil.add(total, requestFee.getAmount()); } - MoneyData fspCommission = request.getFspCommission(); - if (fspCommission != null) { - if (!savingsAccount.getCurrency().getCode().equals(fspCommission.getCurrency())) { + MoneyData requestCommission = request.getFspCommission(); + if (requestCommission != null) { + if (!savingsAccount.getCurrency().getCode().equals(requestCommission.getCurrency())) { throw new UnsupportedOperationException(); } // TODO: compare with calculated quote commission - total = MathUtil.subtractToZero(total, fspCommission.getAmount()); + total = MathUtil.subtractToZero(total, requestCommission.getAmount()); } return total; } @@ -437,50 +476,23 @@ public class InteropServiceImpl implements InteropService { for (PaymentType paymentType : paymentTypes) { if (!paymentType.isCashPayment()) { return paymentType; - // TODO: for now first not cash is retured: - // 1. must be added as initial setup, - // 2. if more than one non-cashe type added then update this - // code } + // TODO: for now first not cash is retured: + // 1. must be added as initial setup, + // 2. if more than one non-cashe type added then update this code } return null; } - SavingsAccountTransaction findTransaction(@NotNull SavingsAccount savingsAccount, @NotNull String transactionCode, - SavingsAccountTransactionType transactionType) { - String routingCode = getRoutingCode(); - for (SavingsAccountTransaction transaction : savingsAccount.getTransactions()) { - if (transactionType != null && !transactionType.getValue().equals(transaction.getTypeOf())) { - continue; - } - - PaymentDetail detail = transaction.getPaymentDetail(); - if (detail != null && routingCode.equals(detail.getRoutingCode()) && transactionCode.equals(detail.getReceiptNumber())) { - return transaction; - } - } - return null; + private SavingsAccountTransaction findTransaction(SavingsAccount savingsAccount, String transactionCode, Integer transactionTypeValue) { + return savingsAccount.getTransactions().stream().filter(t -> transactionTypeValue.equals(t.getTypeOf())).filter(t -> { + PaymentDetail detail = t.getPaymentDetail(); + return detail != null && getRoutingCode().equals(detail.getRoutingCode()) && transactionCode.equals(detail.getReceiptNumber()); + }).findFirst().orElse(null); } public InteropIdentifier findIdentifier(@NotNull InteropIdentifierType idType, @NotNull String idValue, String subIdOrType) { - return identifierRepository.findOne(where(idTypeEqual(idType)).and(idValueEqual(idValue)).and(subIdOrTypeEqual(subIdOrType))) - .orElseThrow(() -> new UnsupportedOperationException( - "Account not found for identifier " + idType + "/" + idValue + (subIdOrType == null ? "" : ("/" + subIdOrType)))); - } - - public static Specification idTypeEqual(@NotNull InteropIdentifierType idType) { - return (Root root, CriteriaQuery query, CriteriaBuilder cb) -> cb.and(cb.equal(root.get("type"), idType)); - } - - public static Specification idValueEqual(@NotNull String idValue) { - return (Root root, CriteriaQuery query, CriteriaBuilder cb) -> cb.and(cb.equal(root.get("value"), idValue)); - } - - public static Specification subIdOrTypeEqual(String subIdOrType) { - return (Root root, CriteriaQuery query, CriteriaBuilder cb) -> { - Path path = root.get("subValueOrType"); - return cb.and(subIdOrType == null ? cb.isNull(path) : cb.equal(path, subIdOrType)); - }; + return identifierRepository.findOneByTypeAndValueAndSubType(idType, idValue, subIdOrType); } private AppUser getLoginUser() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index 27b2d9b77..681a56cfc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -829,7 +829,7 @@ public class SavingsAccount extends AbstractPersistableCustom { } else { Money overdraftAmount = Money.zero(this.currency); Money transactionAmount = Money.zero(this.currency); - if (transaction.isCredit()) { + if (transaction.isCredit() || transaction.isAmountRelease()) { if (runningBalance.isLessThanZero()) { Money diffAmount = transaction.getAmount(this.currency).plus(runningBalance); if (diffAmount.isGreaterThanZero()) { @@ -839,7 +839,7 @@ public class SavingsAccount extends AbstractPersistableCustom { } } transactionAmount = transactionAmount.plus(transaction.getAmount(this.currency)); - } else if (transaction.isDebit()) { + } else if (transaction.isDebit() || transaction.isAmountOnHold()) { if (runningBalance.isLessThanZero()) { overdraftAmount = transaction.getAmount(this.currency); } @@ -1038,15 +1038,17 @@ public class SavingsAccount extends AbstractPersistableCustom { } validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_WITHDRAWAL, transactionDTO.getTransactionDate()); + if (applyWithdrawFee) { + // auto pay withdrawal fee + payWithdrawalFee(transactionDTO.getTransactionAmount(), transactionDTO.getTransactionDate(), transactionDTO.getAppUser()); + } + final Money transactionAmountMoney = Money.of(this.currency, transactionDTO.getTransactionAmount()); final SavingsAccountTransaction transaction = SavingsAccountTransaction.withdrawal(this, office(), transactionDTO.getPaymentDetail(), transactionDTO.getTransactionDate(), transactionAmountMoney, transactionDTO.getCreatedDate(), transactionDTO.getAppUser()); addTransaction(transaction); - if (applyWithdrawFee) { - // auto pay withdrawal fee - payWithdrawalFee(transactionDTO.getTransactionAmount(), transactionDTO.getTransactionDate(), transactionDTO.getAppUser()); - } + if (this.sub_status.equals(SavingsAccountSubStatusEnum.INACTIVE.getValue()) || this.sub_status.equals(SavingsAccountSubStatusEnum.DORMANT.getValue())) { this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue(); @@ -3271,7 +3273,7 @@ public class SavingsAccount extends AbstractPersistableCustom { this.savingsOnHoldAmount = getSavingsHoldAmount().add(amount); } - public void releaseAmount(BigDecimal amount) { + public void releaseOnHoldAmount(BigDecimal amount) { this.savingsOnHoldAmount = getSavingsHoldAmount().subtract(amount); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index 8b3afbcf9..9e10b001e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -1618,7 +1618,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi .validateReleaseAmountAndAssembleForm(holdTransaction, submittedBy); final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId); checkClientOrGroupActive(account); - account.releaseAmount(transaction.getAmount()); + account.releaseOnHoldAmount(transaction.getAmount()); this.savingsAccountTransactionRepository.save(transaction); holdTransaction.updateReleaseId(transaction.getId()); diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V352__interop_init.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V352__interop_init.sql index e5c1c8bf9..7ffe66b74 100644 --- a/fineract-provider/src/main/resources/sql/migrations/core_db/V352__interop_init.sql +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V352__interop_init.sql @@ -60,6 +60,9 @@ VALUES ('interop', 'READ_INTERTRANSFER', 'INTERTRANSFER', 'READ', 0); INSERT INTO m_permission (`grouping`, code, entity_name, action_name, can_maker_checker) VALUES ('interop', 'PREPARE_INTERTRANSFER', 'INTERTRANSFER', 'PREPARE', 0); +INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) +VALUES ('interop', 'RELEASE_INTERTRANSFER', 'INTERTRANSFER', 'RELEASE', 0); + INSERT INTO m_permission (`grouping`, code, entity_name, action_name, can_maker_checker) VALUES ('interop', 'CREATE_INTERID', 'INTERID', 'CREATE', 0); INSERT INTO m_permission (`grouping`, code, entity_name, action_name, can_maker_checker)