Merge pull request #619 from vishwasbabu/mifosplatform-1.10.3

handle overpayments and refunds in accounting
This commit is contained in:
Vishwas Babu A J
2013-10-04 01:05:49 -07:00
16 changed files with 192 additions and 53 deletions
@@ -0,0 +1,16 @@
/***Set defaults for income from Penalties for existing Saving Products**/
ALTER TABLE `m_loan_transaction`
ADD COLUMN `overpayment_portion_derived` DECIMAL(19,6) NULL DEFAULT NULL AFTER `penalty_charges_portion_derived`;
/**Add dummy liability account if organization already has a loan product with accounting enabled**/
INSERT INTO `acc_gl_account` (`name`, `hierarchy`, `gl_code`, `classification_enum`,`description`)
select 'Loan Overpayments (Temp)', '.', '22000-Temp', 2,'Temporary account to track Loan overpayments Liabilities'
FROM m_product_loan WHERE accounting_type != 1
limit 1;
/**Map a liability account for every loan which has accounting enabled**/
INSERT INTO `acc_product_mapping` (`gl_account_id`,`product_id`,`product_type`,`financial_account_type`)
select (select id from acc_gl_account where classification_enum=2 and account_usage=1 LIMIT 1), mapping.product_id, mapping.product_type, 11
from acc_product_mapping mapping
where mapping.financial_account_type = 2 and mapping.product_type=1;
@@ -13,7 +13,7 @@ public class AccountingConstants {
/*** Accounting placeholders for cash based accounting for loan products ***/
public static enum CASH_ACCOUNTS_FOR_LOAN {
FUND_SOURCE(1), LOAN_PORTFOLIO(2), INTEREST_ON_LOANS(3), INCOME_FROM_FEES(4), INCOME_FROM_PENALTIES(5), LOSSES_WRITTEN_OFF(6), TRANSFERS_SUSPENSE(
10);
10), OVERPAYMENT(11);
private final Integer value;
@@ -46,7 +46,7 @@ public class AccountingConstants {
/*** Accounting placeholders for accrual based accounting for loan products ***/
public static enum ACCRUAL_ACCOUNTS_FOR_LOAN {
FUND_SOURCE(1), LOAN_PORTFOLIO(2), INTEREST_ON_LOANS(3), INCOME_FROM_FEES(4), INCOME_FROM_PENALTIES(5), LOSSES_WRITTEN_OFF(6), INTEREST_RECEIVABLE(
7), FEES_RECEIVABLE(8), PENALTIES_RECEIVABLE(9), TRANSFERS_SUSPENSE(10);
7), FEES_RECEIVABLE(8), PENALTIES_RECEIVABLE(9), TRANSFERS_SUSPENSE(10), OVERPAYMENT(11);
private final Integer value;
@@ -83,12 +83,12 @@ public class AccountingConstants {
***/
public static enum LOAN_PRODUCT_ACCOUNTING_PARAMS {
FUND_SOURCE("fundSourceAccountId"), LOAN_PORTFOLIO("loanPortfolioAccountId"), INTEREST_ON_LOANS("interestOnLoanAccountId"), INCOME_FROM_FEES(
"incomeFromFeeAccountId"), INCOME_FROM_PENALTIES("incomeFromPenaltyAccountId"), LOSSES_WRITTEN_OFF("writeOffAccountId"), INTEREST_RECEIVABLE(
"receivableInterestAccountId"), FEES_RECEIVABLE("receivableFeeAccountId"), PENALTIES_RECEIVABLE(
"receivablePenaltyAccountId"), TRANSFERS_SUSPENSE("transfersInSuspenseAccountId"), PAYMENT_CHANNEL_FUND_SOURCE_MAPPING(
"paymentChannelToFundSourceMappings"), PAYMENT_TYPE("paymentTypeId"), FEE_INCOME_ACCOUNT_MAPPING(
"feeToIncomeAccountMappings"), PENALTY_INCOME_ACCOUNT_MAPPING("penaltyToIncomeAccountMappings"), CHARGE_ID("chargeId"), INCOME_ACCOUNT_ID(
"incomeAccountId");
"incomeFromFeeAccountId"), INCOME_FROM_PENALTIES("incomeFromPenaltyAccountId"), LOSSES_WRITTEN_OFF("writeOffAccountId"), OVERPAYMENT(
"overpaymentLiabilityAccountId"), INTEREST_RECEIVABLE("receivableInterestAccountId"), FEES_RECEIVABLE(
"receivableFeeAccountId"), PENALTIES_RECEIVABLE("receivablePenaltyAccountId"), TRANSFERS_SUSPENSE(
"transfersInSuspenseAccountId"), PAYMENT_CHANNEL_FUND_SOURCE_MAPPING("paymentChannelToFundSourceMappings"), PAYMENT_TYPE(
"paymentTypeId"), FEE_INCOME_ACCOUNT_MAPPING("feeToIncomeAccountMappings"), PENALTY_INCOME_ACCOUNT_MAPPING(
"penaltyToIncomeAccountMappings"), CHARGE_ID("chargeId"), INCOME_ACCOUNT_ID("incomeAccountId");
private final String value;
@@ -69,6 +69,13 @@ public class AccountingDropdownReadPlatformServiceImpl implements AccountingDrop
expenseAccountOptions = null;
}
accountOptions.put("expenseAccountOptions", expenseAccountOptions);
List<GLAccountData> liabilityAccountOptions = this.accountReadPlatformService
.retrieveAllEnabledDetailGLAccounts(GLAccountType.LIABILITY);
if (liabilityAccountOptions.isEmpty()) {
liabilityAccountOptions = null;
}
accountOptions.put("liabilityAccountOptions", liabilityAccountOptions);
return accountOptions;
}
@@ -27,6 +27,7 @@ public class LoanTransactionDTO {
private final BigDecimal interest;
private final BigDecimal fees;
private final BigDecimal penalties;
private final BigDecimal overPayment;
/*** Boolean values determines if the transaction is reversed ***/
private final boolean reversed;
@@ -37,8 +38,8 @@ public class LoanTransactionDTO {
public LoanTransactionDTO(final Long officeId, final Long paymentTypeId, final String transactionId, final Date transactionDate,
final LoanTransactionEnumData transactionType, final BigDecimal amount, final BigDecimal principal, final BigDecimal interest,
final BigDecimal fees, final BigDecimal penalties, final boolean reversed, final List<ChargePaymentDTO> feePayments,
final List<ChargePaymentDTO> penaltyPayments) {
final BigDecimal fees, final BigDecimal penalties, final BigDecimal overPayment, final boolean reversed,
final List<ChargePaymentDTO> feePayments, final List<ChargePaymentDTO> penaltyPayments) {
this.paymentTypeId = paymentTypeId;
this.transactionId = transactionId;
this.transactionDate = transactionDate;
@@ -51,6 +52,7 @@ public class LoanTransactionDTO {
this.transactionType = transactionType;
this.feePayments = feePayments;
this.penaltyPayments = penaltyPayments;
this.overPayment = overPayment;
this.officeId = officeId;
}
@@ -90,6 +92,10 @@ public class LoanTransactionDTO {
return this.penalties;
}
public BigDecimal getOverPayment() {
return this.overPayment;
}
public boolean isReversed() {
return this.reversed;
}
@@ -32,6 +32,7 @@ import org.mifosplatform.accounting.journalentry.exception.JournalEntryInvalidEx
import org.mifosplatform.accounting.producttoaccountmapping.domain.PortfolioProductType;
import org.mifosplatform.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
import org.mifosplatform.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
import org.mifosplatform.accounting.producttoaccountmapping.exception.ProductToGLAccountMappingNotFoundException;
import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException;
import org.mifosplatform.organisation.monetary.data.CurrencyData;
import org.mifosplatform.organisation.office.domain.Office;
@@ -81,11 +82,12 @@ public class AccountingProcessorHelper {
final BigDecimal interest = (BigDecimal) map.get("interestPortion");
final BigDecimal fees = (BigDecimal) map.get("feeChargesPortion");
final BigDecimal penalties = (BigDecimal) map.get("penaltyChargesPortion");
final BigDecimal overPayments = (BigDecimal) map.get("overPaymentPortion");
final boolean reversed = (Boolean) map.get("reversed");
final Long paymentTypeId = (Long) map.get("paymentTypeId");
final List<ChargePaymentDTO> feePayments = new ArrayList<ChargePaymentDTO>();
final List<ChargePaymentDTO> penaltyPayments = new ArrayList<ChargePaymentDTO>();
final List<ChargePaymentDTO> feePaymentDetails = new ArrayList<ChargePaymentDTO>();
final List<ChargePaymentDTO> penaltyPaymentDetails = new ArrayList<ChargePaymentDTO>();
// extract charge payment details (if exists)
if (map.containsKey("loanChargesPaid")) {
@SuppressWarnings("unchecked")
@@ -97,15 +99,16 @@ public class AccountingProcessorHelper {
final BigDecimal chargeAmountPaid = (BigDecimal) loanChargePaid.get("amount");
final ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, loanChargeId, chargeAmountPaid);
if (isPenalty) {
penaltyPayments.add(chargePaymentDTO);
penaltyPaymentDetails.add(chargePaymentDTO);
} else {
feePayments.add(chargePaymentDTO);
feePaymentDetails.add(chargePaymentDTO);
}
}
}
final LoanTransactionDTO transaction = new LoanTransactionDTO(transactionOfficeId, paymentTypeId, transactionId,
transactionDate, transactionType, amount, principal, interest, fees, penalties, reversed, feePayments, penaltyPayments);
transactionDate, transactionType, amount, principal, interest, fees, penalties, overPayments, reversed,
feePaymentDetails, penaltyPaymentDetails);
newLoanTransactions.add(transaction);
@@ -513,6 +516,9 @@ public class AccountingProcessorHelper {
}
}
if (accountMapping == null) { throw new ProductToGLAccountMappingNotFoundException(PortfolioProductType.LOAN, loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.OVERPAYMENT.toString()); }
return accountMapping.getGlAccount();
}
@@ -55,6 +55,11 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
|| loanTransactionDTO.getTransactionType().isChargePayment()) {
createJournalEntriesForRepayments(loanDTO, loanTransactionDTO, office);
}
/** Logic for Refunds of Overpayments **/
else if (loanTransactionDTO.getTransactionType().isRefund()) {
createJournalEntriesForRefund(loanDTO, loanTransactionDTO, office);
}
/***
* Only principal write off affects cash based accounting (interest
* and fee write off need not be considered). Debit losses written
@@ -103,6 +108,35 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
isReversal);
}
/**
* Debit loan Portfolio and credit Fund source for a Disbursement <br/>
*
* All debits are turned into credits and vice versa in case of disbursement
* reversals
*
*
* @param loanDTO
* @param loanTransactionDTO
* @param office
*/
private void createJournalEntriesForRefund(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office) {
// loan properties
final Long loanProductId = loanDTO.getLoanProductId();
final Long loanId = loanDTO.getLoanId();
final String currencyCode = loanDTO.getCurrencyCode();
// transaction properties
final String transactionId = loanTransactionDTO.getTransactionId();
final Date transactionDate = loanTransactionDTO.getTransactionDate();
final BigDecimal refundAmount = loanTransactionDTO.getAmount();
final boolean isReversal = loanTransactionDTO.isReversed();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
this.helper.createCashBasedJournalEntriesAndReversalsForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.OVERPAYMENT,
CASH_ACCOUNTS_FOR_LOAN.FUND_SOURCE, loanProductId, paymentTypeId, loanId, transactionId, transactionDate, refundAmount,
isReversal);
}
/**
* Create a single Debit to fund source and multiple credits if applicable
* (loan portfolio for principal repayments, Interest on loans for interest
@@ -125,6 +159,7 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
final BigDecimal interestAmount = loanTransactionDTO.getInterest();
final BigDecimal feesAmount = loanTransactionDTO.getFees();
final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment();
final boolean isReversal = loanTransactionDTO.isReversed();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
@@ -156,6 +191,12 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
penaltiesAmount, isReversal, loanTransactionDTO.getPenaltyPayments());
}
if (overPaymentAmount != null && !(overPaymentAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(overPaymentAmount);
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.OVERPAYMENT, loanProductId,
paymentTypeId, loanId, transactionId, transactionDate, overPaymentAmount, isReversal);
}
/*** create a single debit entry (or reversal) for the entire amount **/
this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.FUND_SOURCE, loanProductId,
paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, isReversal);
@@ -15,7 +15,7 @@ import org.mifosplatform.infrastructure.core.exception.AbstractPlatformResourceN
public class ProductToGLAccountMappingNotFoundException extends AbstractPlatformResourceNotFoundException {
public ProductToGLAccountMappingNotFoundException(final PortfolioProductType type, final Long productId, final String accountType) {
super("error.msg.productTyAccountMapping.not.found", "Mapping for product of type" + type.toString() + " with Id " + productId
super("error.msg.productToAccountMapping.not.found", "Mapping for product of type " + type.toString() + " with Id " + productId
+ " does not exist for an account of type " + accountType, type.toString(), productId, accountType);
}
}
@@ -107,6 +107,11 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer {
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue()).value(writeOffAccountId)
.notNull().integerGreaterThanZero();
final Long overpaymentAccountId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(), element);
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue()).value(overpaymentAccountId)
.notNull().integerGreaterThanZero();
final Long transfersInSuspenseAccountId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.TRANSFERS_SUSPENSE.getValue(), element);
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.TRANSFERS_SUSPENSE.getValue())
@@ -52,6 +52,11 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.EXPENSE, PortfolioProductType.LOAN);
}
public void saveLoanToLiabilityAccountMapping(final JsonElement element, final String paramName, final Long productId,
final int placeHolderTypeId) {
saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.LIABILITY, PortfolioProductType.LOAN);
}
/*** Set of abstractions for merging Savings Products to GL Account Mappings ***/
public void mergeLoanToAssetAccountMappingChanges(final JsonElement element, final String paramName, final Long productId,
final int accountTypeId, final String accountTypeName, final Map<String, Object> changes) {
@@ -71,6 +76,12 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
PortfolioProductType.LOAN);
}
public void mergeLoanToLiabilityAccountMappingChanges(final JsonElement element, final String paramName, final Long productId,
final int accountTypeId, final String accountTypeName, final Map<String, Object> changes) {
mergeProductToAccountMappingChanges(element, paramName, productId, accountTypeId, accountTypeName, changes,
GLAccountType.LIABILITY, PortfolioProductType.LOAN);
}
/*** Abstractions for payments channel related to loan products ***/
public void savePaymentChannelToFundSourceMappings(final JsonCommand command, final JsonElement element, final Long productId,
@@ -112,6 +123,8 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_PENALTIES.getValue(), element);
final Long writeOffAccountId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(), element);
final Long overPaymentAccountId = this.fromApiJsonHelper.extractLongNamed(LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(),
element);
final Long transfersInSuspenseAccountId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.TRANSFERS_SUSPENSE.getValue(), element);
@@ -132,19 +145,13 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_FEES.getValue(), incomeFromFeeId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_PENALTIES.getValue(), incomeFromPenaltyId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(), writeOffAccountId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(), overPaymentAccountId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.TRANSFERS_SUSPENSE.getValue(), transfersInSuspenseAccountId);
break;
//$FALL-THROUGH$
case ACCRUAL_BASED:
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.FUND_SOURCE.getValue(), fundAccountId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.LOAN_PORTFOLIO.getValue(), loanPortfolioAccountId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.INTEREST_ON_LOANS.getValue(), incomeFromInterestId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_FEES.getValue(), incomeFromFeeId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_PENALTIES.getValue(), incomeFromPenaltyId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(), writeOffAccountId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.INTEREST_RECEIVABLE.getValue(), receivableInterestAccountId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.FEES_RECEIVABLE.getValue(), receivableFeeAccountId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.PENALTIES_RECEIVABLE.getValue(), receivablePenaltyAccountId);
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.TRANSFERS_SUSPENSE.getValue(), transfersInSuspenseAccountId);
break;
}
@@ -187,6 +194,10 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
mergeLoanToExpenseAccountMappingChanges(element, LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(),
loanProductId, CASH_ACCOUNTS_FOR_LOAN.LOSSES_WRITTEN_OFF.getValue(),
CASH_ACCOUNTS_FOR_LOAN.LOSSES_WRITTEN_OFF.toString(), changes);
// liabilities
mergeLoanToLiabilityAccountMappingChanges(element, LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(), loanProductId,
CASH_ACCOUNTS_FOR_LOAN.OVERPAYMENT.getValue(), CASH_ACCOUNTS_FOR_LOAN.OVERPAYMENT.toString(), changes);
break;
case ACCRUAL_BASED:
// assets (including receivables)
@@ -221,6 +232,10 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
mergeLoanToExpenseAccountMappingChanges(element, LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(),
loanProductId, ACCRUAL_ACCOUNTS_FOR_LOAN.LOSSES_WRITTEN_OFF.getValue(),
ACCRUAL_ACCOUNTS_FOR_LOAN.LOSSES_WRITTEN_OFF.toString(), changes);
// liabilities
mergeLoanToLiabilityAccountMappingChanges(element, LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(), loanProductId,
CASH_ACCOUNTS_FOR_LOAN.OVERPAYMENT.getValue(), CASH_ACCOUNTS_FOR_LOAN.OVERPAYMENT.toString(), changes);
break;
}
}
@@ -106,6 +106,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
accountMappingDetails.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.TRANSFERS_SUSPENSE.getValue(), glAccountId);
} else if (glAccountForLoan.equals(CASH_ACCOUNTS_FOR_LOAN.LOSSES_WRITTEN_OFF)) {
accountMappingDetails.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(), glAccountId);
} else if (glAccountForLoan.equals(CASH_ACCOUNTS_FOR_LOAN.OVERPAYMENT)) {
accountMappingDetails.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(), glAccountId);
}
}
} else if (AccountingRuleType.ACCRUAL_BASED.getValue().equals(accountingType)) {
@@ -126,6 +128,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
accountMappingDetails.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.INTEREST_ON_LOANS.getValue(), glAccountId);
} else if (glAccountForLoan.equals(ACCRUAL_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO)) {
accountMappingDetails.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.LOAN_PORTFOLIO.getValue(), glAccountId);
} else if (glAccountForLoan.equals(ACCRUAL_ACCOUNTS_FOR_LOAN.OVERPAYMENT)) {
accountMappingDetails.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(), glAccountId);
} else if (glAccountForLoan.equals(ACCRUAL_ACCOUNTS_FOR_LOAN.TRANSFERS_SUSPENSE)) {
accountMappingDetails.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.TRANSFERS_SUSPENSE.getValue(), glAccountId);
} else if (glAccountForLoan.equals(ACCRUAL_ACCOUNTS_FOR_LOAN.LOSSES_WRITTEN_OFF)) {
@@ -79,9 +79,11 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.getValue());
// expenses
this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element,
LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(), loanProductId,
CASH_ACCOUNTS_FOR_LOAN.LOSSES_WRITTEN_OFF.getValue());
this.loanProductToGLAccountMappingHelper
.saveLoanToLiabilityAccountMapping(element, LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(), loanProductId,
CASH_ACCOUNTS_FOR_LOAN.OVERPAYMENT.getValue());
// liabilities
// advanced accounting mappings
this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
@@ -125,6 +127,11 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(), loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.LOSSES_WRITTEN_OFF.getValue());
// liabilities
this.loanProductToGLAccountMappingHelper.saveLoanToLiabilityAccountMapping(element,
LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(), loanProductId,
ACCRUAL_ACCOUNTS_FOR_LOAN.OVERPAYMENT.getValue());
// advanced accounting mappings
this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
@@ -35,6 +35,7 @@ public class LoanTransactionData {
private final BigDecimal interestPortion;
private final BigDecimal feeChargesPortion;
private final BigDecimal penaltyChargesPortion;
private final BigDecimal overpaymentPortion;
private final String externalId;
private final AccountTransferData transfer;
@@ -46,23 +47,24 @@ public class LoanTransactionData {
return new LoanTransactionData(loanTransactionData.id, loanTransactionData.officeId, loanTransactionData.officeName,
loanTransactionData.type, loanTransactionData.paymentDetailData, loanTransactionData.currency, loanTransactionData.date,
loanTransactionData.amount, loanTransactionData.principalPortion, loanTransactionData.interestPortion,
loanTransactionData.feeChargesPortion, loanTransactionData.penaltyChargesPortion, paymentTypeOptions,
loanTransactionData.externalId, loanTransactionData.transfer);
loanTransactionData.feeChargesPortion, loanTransactionData.penaltyChargesPortion, loanTransactionData.overpaymentPortion,
paymentTypeOptions, loanTransactionData.externalId, loanTransactionData.transfer);
}
public LoanTransactionData(final Long id, final Long officeId, final String officeName, final LoanTransactionEnumData transactionType,
final PaymentDetailData paymentDetailData, final CurrencyData currency, final LocalDate date, final BigDecimal amount,
final BigDecimal principalPortion, final BigDecimal interestPortion, final BigDecimal feeChargesPortion,
final BigDecimal penaltyChargesPortion, final String externalId, final AccountTransferData transfer) {
this(id, officeId, officeName, transactionType, paymentDetailData, currency, date, amount, principalPortion, interestPortion,
feeChargesPortion, penaltyChargesPortion, null, externalId, transfer);
}
public LoanTransactionData(final Long id, final Long officeId, final String officeName, final LoanTransactionEnumData transactionType,
final PaymentDetailData paymentDetailData, final CurrencyData currency, final LocalDate date, final BigDecimal amount,
final BigDecimal principalPortion, final BigDecimal interestPortion, final BigDecimal feeChargesPortion,
final BigDecimal penaltyChargesPortion, final Collection<CodeValueData> paymentTypeOptions, final String externalId,
final BigDecimal penaltyChargesPortion, final BigDecimal overpaymentPortion, final String externalId,
final AccountTransferData transfer) {
this(id, officeId, officeName, transactionType, paymentDetailData, currency, date, amount, principalPortion, interestPortion,
feeChargesPortion, penaltyChargesPortion, overpaymentPortion, null, externalId, transfer);
}
public LoanTransactionData(final Long id, final Long officeId, final String officeName, final LoanTransactionEnumData transactionType,
final PaymentDetailData paymentDetailData, final CurrencyData currency, final LocalDate date, final BigDecimal amount,
final BigDecimal principalPortion, final BigDecimal interestPortion, final BigDecimal feeChargesPortion,
final BigDecimal penaltyChargesPortion, final BigDecimal overpaymentPortion,
final Collection<CodeValueData> paymentTypeOptions, final String externalId, final AccountTransferData transfer) {
this.id = id;
this.officeId = officeId;
this.officeName = officeName;
@@ -78,6 +80,7 @@ public class LoanTransactionData {
this.paymentTypeOptions = paymentTypeOptions;
this.externalId = externalId;
this.transfer = transfer;
this.overpaymentPortion = overpaymentPortion;
}
public LocalDate dateOf() {
@@ -82,6 +82,9 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
@Column(name = "penalty_charges_portion_derived", scale = 6, precision = 19, nullable = true)
private BigDecimal penaltyChargesPortion;
@Column(name = "overpayment_portion_derived", scale = 6, precision = 19, nullable = true)
private BigDecimal overPaymentPortion;
@Column(name = "is_reversed", nullable = false)
private boolean reversed;
@@ -133,21 +136,21 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
return new LoanTransaction(loan, office, LoanTransactionType.INITIATE_TRANSFER.getValue(), transferDate.toDateMidnight().toDate(),
loan.getSummary().getTotalOutstanding(), loan.getSummary().getTotalPrincipalOutstanding(), loan.getSummary()
.getTotalInterestOutstanding(), loan.getSummary().getTotalFeeChargesOutstanding(), loan.getSummary()
.getTotalPenaltyChargesOutstanding(), false, null);
.getTotalPenaltyChargesOutstanding(), null, false, null);
}
public static LoanTransaction approveTransfer(final Office office, final Loan loan, final LocalDate transferDate) {
return new LoanTransaction(loan, office, LoanTransactionType.APPROVE_TRANSFER.getValue(), transferDate.toDateMidnight().toDate(),
loan.getSummary().getTotalOutstanding(), loan.getSummary().getTotalPrincipalOutstanding(), loan.getSummary()
.getTotalInterestOutstanding(), loan.getSummary().getTotalFeeChargesOutstanding(), loan.getSummary()
.getTotalPenaltyChargesOutstanding(), false, null);
.getTotalPenaltyChargesOutstanding(), null, false, null);
}
public static LoanTransaction withdrawTransfer(final Office office, final Loan loan, final LocalDate transferDate) {
return new LoanTransaction(loan, office, LoanTransactionType.WITHDRAW_TRANSFER.getValue(), transferDate.toDateMidnight().toDate(),
loan.getSummary().getTotalOutstanding(), loan.getSummary().getTotalPrincipalOutstanding(), loan.getSummary()
.getTotalInterestOutstanding(), loan.getSummary().getTotalFeeChargesOutstanding(), loan.getSummary()
.getTotalPenaltyChargesOutstanding(), false, null);
.getTotalPenaltyChargesOutstanding(), null, false, null);
}
public static LoanTransaction refund(final Office office, final Money amount, final PaymentDetail paymentDetail,
@@ -158,8 +161,8 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
public static LoanTransaction copyTransactionProperties(final LoanTransaction loanTransaction) {
return new LoanTransaction(loanTransaction.loan, loanTransaction.office, loanTransaction.typeOf, loanTransaction.dateOf,
loanTransaction.amount, loanTransaction.principalPortion, loanTransaction.interestPortion,
loanTransaction.feeChargesPortion, loanTransaction.penaltyChargesPortion, loanTransaction.reversed,
loanTransaction.paymentDetail);
loanTransaction.feeChargesPortion, loanTransaction.penaltyChargesPortion, loanTransaction.overPaymentPortion,
loanTransaction.reversed, loanTransaction.paymentDetail);
}
public static LoanTransaction applyLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate,
@@ -176,13 +179,15 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
&& (loanTransaction.getPrincipalPortion(currency).isEqualTo(newLoanTransaction.getPrincipalPortion(currency)))
&& (loanTransaction.getInterestPortion(currency).isEqualTo(newLoanTransaction.getInterestPortion(currency)))
&& (loanTransaction.getFeeChargesPortion(currency).isEqualTo(newLoanTransaction.getFeeChargesPortion(currency)))
&& (loanTransaction.getPenaltyChargesPortion(currency).isEqualTo(newLoanTransaction.getPenaltyChargesPortion(currency)))) { return true; }
&& (loanTransaction.getPenaltyChargesPortion(currency).isEqualTo(newLoanTransaction.getPenaltyChargesPortion(currency)))
&& (loanTransaction.getOverPaymentPortion(currency).isEqualTo(newLoanTransaction.getOverPaymentPortion(currency)))) { return true; }
return false;
}
private LoanTransaction(final Loan loan, final Office office, final Integer typeOf, final Date dateOf, final BigDecimal amount,
final BigDecimal principalPortion, final BigDecimal interestPortion, final BigDecimal feeChargesPortion,
final BigDecimal penaltyChargesPortion, final boolean reversed, final PaymentDetail paymentDetail) {
final BigDecimal penaltyChargesPortion, final BigDecimal overPaymentPortion, final boolean reversed,
final PaymentDetail paymentDetail) {
super();
this.loan = loan;
this.typeOf = typeOf;
@@ -192,6 +197,7 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
this.interestPortion = interestPortion;
this.feeChargesPortion = feeChargesPortion;
this.penaltyChargesPortion = penaltyChargesPortion;
this.overPaymentPortion = overPaymentPortion;
this.reversed = reversed;
this.paymentDetail = paymentDetail;
this.office = office;
@@ -274,6 +280,11 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
.plus(getPenaltyChargesPortion(currency)).getAmount();
}
public void updateOverPayments(final Money overPayment) {
final MonetaryCurrency currency = overPayment.getCurrency();
this.overPaymentPortion = defaultToNullIfZero(getOverPaymentPortion(currency).plus(overPayment).getAmount());
}
public Money getPrincipalPortion(final MonetaryCurrency currency) {
return Money.of(currency, this.principalPortion);
}
@@ -294,6 +305,10 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
return Money.of(currency, this.penaltyChargesPortion);
}
public Money getOverPaymentPortion(final MonetaryCurrency currency) {
return Money.of(currency, this.overPaymentPortion);
}
public Money getAmount(final MonetaryCurrency currency) {
return Money.of(currency, this.amount);
}
@@ -425,7 +440,7 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
}
return new LoanTransactionData(getId(), this.office.getId(), this.office.getName(), transactionType, paymentDetailData,
currencyData, getTransactionDate(), this.amount, this.principalPortion, this.interestPortion, this.feeChargesPortion,
this.penaltyChargesPortion, this.externalId, transfer);
this.penaltyChargesPortion, this.overPaymentPortion, this.externalId, transfer);
}
public Map<String, Object> toMapData(final CurrencyData currencyData) {
@@ -444,6 +459,7 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
thisTransactionData.put("interestPortion", this.interestPortion);
thisTransactionData.put("feeChargesPortion", this.feeChargesPortion);
thisTransactionData.put("penaltyChargesPortion", this.penaltyChargesPortion);
thisTransactionData.put("overPaymentPortion", this.overPaymentPortion);
if (this.paymentDetail != null) {
thisTransactionData.put("paymentTypeId", this.paymentDetail.getPaymentType().getId());
@@ -186,6 +186,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
if (transactionAmountUnprocessed.isGreaterThanZero()) {
onLoanOverpayment(loanTransaction, transactionAmountUnprocessed);
loanTransaction.updateOverPayments(transactionAmountUnprocessed);
}
}
@@ -330,7 +330,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final Collection<CodeValueData> paymentOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(PaymentDetailConstants.paymentTypeCodeName);
return new LoanTransactionData(null, null, null, transactionType, null, currencyData, earliestUnpaidInstallmentDate,
possibleNextRepaymentAmount.getAmount(), null, null, null, null, paymentOptions, null, null);
possibleNextRepaymentAmount.getAmount(), null, null, null, null, null, paymentOptions, null, null);
}
@Override
@@ -354,7 +354,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final BigDecimal amount = waiveOfInterest.getAmount(currency).getAmount();
return new LoanTransactionData(null, null, null, transactionType, null, currencyData, waiveOfInterest.getTransactionDate(), amount,
null, null, null, null, null, null);
null, null, null, null, null, null, null);
}
@Override
@@ -364,7 +364,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.WRITEOFF);
return new LoanTransactionData(null, null, null, transactionType, null, null, DateUtils.getLocalDateOfTenant(), null, null, null,
null, null, null, null);
null, null, null, null, null);
}
@Override
@@ -375,7 +375,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final Collection<CodeValueData> paymentOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(PaymentDetailConstants.paymentTypeCodeName);
return new LoanTransactionData(null, null, null, transactionType, null, null, loan.getExpectedDisbursedOnLocalDate(), null, null,
null, null, null, paymentOptions, null, null);
null, null, null, null, paymentOptions, null, null);
}
@Override
@@ -861,6 +861,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
return " tr.id as id, tr.transaction_type_enum as transactionType, tr.transaction_date as `date`, tr.amount as total, "
+ " tr.principal_portion_derived as principal, tr.interest_portion_derived as interest, "
+ " tr.fee_charges_portion_derived as fees, tr.penalty_charges_portion_derived as penalties, "
+ " tr.overpayment_portion_derived as overpayment, "
+ " pd.payment_type_cv_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, "
+ " pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, "
+ " l.currency_code as currencyCode, l.currency_digits as currencyDigits, l.currency_multiplesof as inMultiplesOf, rc.`name` as currencyName, "
@@ -920,6 +921,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final BigDecimal interestPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interest");
final BigDecimal feeChargesPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "fees");
final BigDecimal penaltyChargesPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penalties");
final BigDecimal overPaymentPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "overpayment");
final String externalId = rs.getString("externalId");
AccountTransferData transfer = null;
@@ -944,7 +946,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
}
return new LoanTransactionData(id, officeId, officeName, transactionType, paymentDetailData, currencyData, date, totalAmount,
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, externalId, transfer);
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, overPaymentPortion, externalId, transfer);
}
}
@@ -48,8 +48,8 @@ public final class LoanProductDataValidator {
LOAN_PRODUCT_ACCOUNTING_PARAMS.FUND_SOURCE.getValue(), LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_FEES.getValue(),
LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_FROM_PENALTIES.getValue(), LOAN_PRODUCT_ACCOUNTING_PARAMS.INTEREST_ON_LOANS.getValue(),
LOAN_PRODUCT_ACCOUNTING_PARAMS.INTEREST_RECEIVABLE.getValue(), LOAN_PRODUCT_ACCOUNTING_PARAMS.LOAN_PORTFOLIO.getValue(),
LOAN_PRODUCT_ACCOUNTING_PARAMS.TRANSFERS_SUSPENSE.getValue(), LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(),
LOAN_PRODUCT_ACCOUNTING_PARAMS.PENALTIES_RECEIVABLE.getValue(),
LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(), LOAN_PRODUCT_ACCOUNTING_PARAMS.TRANSFERS_SUSPENSE.getValue(),
LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue(), LOAN_PRODUCT_ACCOUNTING_PARAMS.PENALTIES_RECEIVABLE.getValue(),
LOAN_PRODUCT_ACCOUNTING_PARAMS.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(),
LOAN_PRODUCT_ACCOUNTING_PARAMS.FEE_INCOME_ACCOUNT_MAPPING.getValue(),
LOAN_PRODUCT_ACCOUNTING_PARAMS.PENALTY_INCOME_ACCOUNT_MAPPING.getValue()));
@@ -282,6 +282,11 @@ public final class LoanProductDataValidator {
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue()).value(writeOffAccountId)
.notNull().integerGreaterThanZero();
final Long overpaymentAccountId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(), element);
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue()).value(overpaymentAccountId)
.notNull().integerGreaterThanZero();
validatePaymentChannelFundSourceMappings(baseDataValidator, element);
validateChargeToIncomeAccountMappings(baseDataValidator, element);
@@ -520,6 +525,11 @@ public final class LoanProductDataValidator {
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.LOSSES_WRITTEN_OFF.getValue()).value(writeOffAccountId)
.ignoreIfNull().integerGreaterThanZero();
final Long overpaymentAccountId = this.fromApiJsonHelper.extractLongNamed(LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue(),
element);
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.OVERPAYMENT.getValue()).value(overpaymentAccountId)
.ignoreIfNull().integerGreaterThanZero();
final Long receivableInterestAccountId = this.fromApiJsonHelper.extractLongNamed(
LOAN_PRODUCT_ACCOUNTING_PARAMS.INTEREST_RECEIVABLE.getValue(), element);
baseDataValidator.reset().parameter(LOAN_PRODUCT_ACCOUNTING_PARAMS.INTEREST_RECEIVABLE.getValue())