Merge self-hosted/feature/account-charges into feature/Backend-DSC-MFA
This commit is contained in:
+8
@@ -23,6 +23,7 @@ public enum PortfolioAccountType {
|
|||||||
INVALID(0, "accountType.invalid"), //
|
INVALID(0, "accountType.invalid"), //
|
||||||
LOAN(1, "accountType.loan"), //
|
LOAN(1, "accountType.loan"), //
|
||||||
SAVINGS(2, "accountType.savings"), //
|
SAVINGS(2, "accountType.savings"), //
|
||||||
|
CURRENT(3, "accountType.current"), //
|
||||||
;
|
;
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
@@ -52,6 +53,9 @@ public enum PortfolioAccountType {
|
|||||||
case 2:
|
case 2:
|
||||||
enumType = SAVINGS;
|
enumType = SAVINGS;
|
||||||
break;
|
break;
|
||||||
|
case 3:
|
||||||
|
enumType = CURRENT;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
enumType = INVALID;
|
enumType = INVALID;
|
||||||
}
|
}
|
||||||
@@ -68,4 +72,8 @@ public enum PortfolioAccountType {
|
|||||||
public boolean isLoanAccount() {
|
public boolean isLoanAccount() {
|
||||||
return this.equals(LOAN);
|
return this.equals(LOAN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isCurrentAccount() {
|
||||||
|
return this.equals(CURRENT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -120,6 +120,8 @@ public final class DepositsApiConstants {
|
|||||||
public static final String principalAmountParamName = "principalAmount";
|
public static final String principalAmountParamName = "principalAmount";
|
||||||
public static final String annualInterestRateParamName = "annualInterestRate";
|
public static final String annualInterestRateParamName = "annualInterestRate";
|
||||||
public static final String interestPostingPeriodInMonthsParamName = "interestPostingPeriodInMonths";
|
public static final String interestPostingPeriodInMonthsParamName = "interestPostingPeriodInMonths";
|
||||||
|
public static final String isSeniorCitizenParamName = "isSeniorCitizen";
|
||||||
|
public static final String seniorCitizenInterestRateParamName = "seniorCitizenInterestRate";
|
||||||
public static final String tenureInMonthsParamName = "tenureInMonths";
|
public static final String tenureInMonthsParamName = "tenureInMonths";
|
||||||
public static final String interestCompoundingPeriodInMonthsParamName = "interestCompoundingPeriodInMonths";
|
public static final String interestCompoundingPeriodInMonthsParamName = "interestCompoundingPeriodInMonths";
|
||||||
|
|
||||||
@@ -544,6 +546,8 @@ public final class DepositsApiConstants {
|
|||||||
fixedDepositRequestData.add(SavingsApiConstants.jointHoldersParamName);
|
fixedDepositRequestData.add(SavingsApiConstants.jointHoldersParamName);
|
||||||
fixedDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName);
|
fixedDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName);
|
||||||
fixedDepositRequestData.add(SavingsApiConstants.nominationFacilityParamName);
|
fixedDepositRequestData.add(SavingsApiConstants.nominationFacilityParamName);
|
||||||
|
fixedDepositRequestData.add(isSeniorCitizenParamName);
|
||||||
|
fixedDepositRequestData.add(seniorCitizenInterestRateParamName);
|
||||||
return fixedDepositRequestData;
|
return fixedDepositRequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,6 +616,8 @@ public final class DepositsApiConstants {
|
|||||||
recurringDepositRequestData.add(SavingsApiConstants.balanceSwapOptionParamName);
|
recurringDepositRequestData.add(SavingsApiConstants.balanceSwapOptionParamName);
|
||||||
recurringDepositRequestData.add(SavingsApiConstants.msmeParamName);
|
recurringDepositRequestData.add(SavingsApiConstants.msmeParamName);
|
||||||
recurringDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName);
|
recurringDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName);
|
||||||
|
recurringDepositRequestData.add(isSeniorCitizenParamName);
|
||||||
|
recurringDepositRequestData.add(seniorCitizenInterestRateParamName);
|
||||||
return recurringDepositRequestData;
|
return recurringDepositRequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
@@ -600,6 +600,27 @@ public class AccountingProcessorHelper {
|
|||||||
createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount);
|
createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a direct transfer journal entry between two savings-type accounts (savings or current),
|
||||||
|
* each with their own product GL mapping and entity ID so the correct account number appears per entry.
|
||||||
|
*
|
||||||
|
* @param debitProductId product ID whose SAVINGS_CONTROL GL is debited
|
||||||
|
* @param debitEntityId entity (account) ID shown on the debit entry
|
||||||
|
* @param creditProductId product ID whose SAVINGS_CONTROL GL is credited
|
||||||
|
* @param creditEntityId entity (account) ID shown on the credit entry
|
||||||
|
*/
|
||||||
|
public void createDirectTransferJournalEntriesForSavings(final Office office, final String currencyCode,
|
||||||
|
final Long debitProductId, final Long debitEntityId,
|
||||||
|
final Long creditProductId, final Long creditEntityId,
|
||||||
|
final String transactionId, final LocalDate transactionDate, final BigDecimal amount) {
|
||||||
|
final GLAccount debitAccount = getLinkedGLAccountForSavingsProduct(debitProductId,
|
||||||
|
CashAccountsForSavings.SAVINGS_CONTROL.getValue(), null);
|
||||||
|
final GLAccount creditAccount = getLinkedGLAccountForSavingsProduct(creditProductId,
|
||||||
|
CashAccountsForSavings.SAVINGS_CONTROL.getValue(), null);
|
||||||
|
createDebitJournalEntryForSavings(office, currencyCode, debitAccount, debitEntityId, transactionId, transactionDate, amount);
|
||||||
|
createCreditJournalEntryForSavings(office, currencyCode, creditAccount, creditEntityId, transactionId, transactionDate, amount);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience method that creates a pair of related Debits and Credits for Cash Based accounting.
|
* Convenience method that creates a pair of related Debits and Credits for Cash Based accounting.
|
||||||
*
|
*
|
||||||
|
|||||||
+3
-4
@@ -106,8 +106,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
|
|||||||
.append(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ")
|
.append(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ")
|
||||||
.append(" eao.external_id as externalAssetOwner, ")
|
.append(" eao.external_id as externalAssetOwner, ")
|
||||||
.append(" CASE ")
|
.append(" CASE ")
|
||||||
.append(" WHEN journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id LIKE 'CA%' THEN ca.account_no ")
|
.append(" WHEN journalEntry.entity_type_enum = 2 THEN COALESCE(ca.account_no, sa.account_no) ")
|
||||||
.append(" WHEN journalEntry.entity_type_enum = 2 THEN sa.account_no ")
|
|
||||||
.append(" ELSE NULL ")
|
.append(" ELSE NULL ")
|
||||||
.append(" END as savingsAccountNo ");
|
.append(" END as savingsAccountNo ");
|
||||||
if (associationParametersData.isRunningBalanceRequired()) {
|
if (associationParametersData.isRunningBalanceRequired()) {
|
||||||
@@ -130,8 +129,8 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
|
|||||||
.append(" join m_currency curr on curr.code = journalEntry.currency_code ")
|
.append(" join m_currency curr on curr.code = journalEntry.currency_code ")
|
||||||
.append(" left join m_external_asset_owner_journal_entry_mapping eajem on eajem.journal_entry_id = journalEntry.id ")
|
.append(" left join m_external_asset_owner_journal_entry_mapping eajem on eajem.journal_entry_id = journalEntry.id ")
|
||||||
.append(" left join m_external_asset_owner eao on eao.id = eajem.owner_id ")
|
.append(" left join m_external_asset_owner eao on eao.id = eajem.owner_id ")
|
||||||
.append(" left join m_savings_account sa on sa.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id NOT LIKE 'CA%' ")
|
.append(" left join m_savings_account sa on sa.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ")
|
||||||
.append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id LIKE 'CA%' ");
|
.append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ");
|
||||||
if (associationParametersData.isTransactionDetailsRequired()) {
|
if (associationParametersData.isTransactionDetailsRequired()) {
|
||||||
sb.append(" left join m_loan_transaction as lt on journalEntry.loan_transaction_id = lt.id ")
|
sb.append(" left join m_loan_transaction as lt on journalEntry.loan_transaction_id = lt.id ")
|
||||||
.append(" left join m_savings_account_transaction as st on journalEntry.savings_transaction_id = st.id ")
|
.append(" left join m_savings_account_transaction as st on journalEntry.savings_transaction_id = st.id ")
|
||||||
|
|||||||
+3
-3
@@ -207,10 +207,10 @@ public class StandingInstructionApiResource {
|
|||||||
if (associationParameters.contains("template")) {
|
if (associationParameters.contains("template")) {
|
||||||
final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate(
|
final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate(
|
||||||
standingInstructionData.getFromClient().getOfficeId(), standingInstructionData.getFromClient().getId(),
|
standingInstructionData.getFromClient().getOfficeId(), standingInstructionData.getFromClient().getId(),
|
||||||
standingInstructionData.getFromAccount().getId(), standingInstructionData.getFromAccountType().getValue(),
|
standingInstructionData.getFromAccount().getId(), standingInstructionData.fromAccountTypeEnum().getValue(),
|
||||||
standingInstructionData.getToClient().getOfficeId(), standingInstructionData.getToClient().getId(),
|
standingInstructionData.getToClient().getOfficeId(), standingInstructionData.getToClient().getId(),
|
||||||
standingInstructionData.getToAccount().getId(), standingInstructionData.getToAccountType().getValue(),
|
standingInstructionData.getToAccount().getId(), standingInstructionData.toAccountTypeEnum().getValue(),
|
||||||
standingInstructionData.getTransferType().getValue());
|
standingInstructionData.transferTypeEnum().getValue());
|
||||||
standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData);
|
standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -59,7 +59,7 @@ public class AccountTransfersDetailDataValidator {
|
|||||||
|
|
||||||
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
|
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
|
||||||
baseDataValidator.reset().parameter(fromAccountTypeParamName).value(fromAccountType).notNull()
|
baseDataValidator.reset().parameter(fromAccountTypeParamName).value(fromAccountType).notNull()
|
||||||
.isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2));
|
.isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3));
|
||||||
|
|
||||||
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
|
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
|
||||||
baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero();
|
baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero();
|
||||||
@@ -72,7 +72,7 @@ public class AccountTransfersDetailDataValidator {
|
|||||||
|
|
||||||
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
|
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
|
||||||
baseDataValidator.reset().parameter(toAccountTypeParamName).value(toAccountType).notNull().isOneOfTheseValues(Integer.valueOf(1),
|
baseDataValidator.reset().parameter(toAccountTypeParamName).value(toAccountType).notNull().isOneOfTheseValues(Integer.valueOf(1),
|
||||||
Integer.valueOf(2));
|
Integer.valueOf(2), Integer.valueOf(3));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+40
-10
@@ -48,20 +48,27 @@ public final class StandingInstructionData {
|
|||||||
private final Long accountDetailId;
|
private final Long accountDetailId;
|
||||||
@Getter
|
@Getter
|
||||||
private final String name;
|
private final String name;
|
||||||
|
@Getter
|
||||||
private final OfficeData fromOffice;
|
private final OfficeData fromOffice;
|
||||||
@Getter
|
@Getter
|
||||||
private final ClientData fromClient;
|
private final ClientData fromClient;
|
||||||
|
@Getter
|
||||||
private final EnumOptionData fromAccountType;
|
private final EnumOptionData fromAccountType;
|
||||||
@Getter
|
@Getter
|
||||||
private final PortfolioAccountData fromAccount;
|
private final PortfolioAccountData fromAccount;
|
||||||
|
@Getter
|
||||||
private final OfficeData toOffice;
|
private final OfficeData toOffice;
|
||||||
@Getter
|
@Getter
|
||||||
private final ClientData toClient;
|
private final ClientData toClient;
|
||||||
|
@Getter
|
||||||
private final EnumOptionData toAccountType;
|
private final EnumOptionData toAccountType;
|
||||||
@Getter
|
@Getter
|
||||||
private final PortfolioAccountData toAccount;
|
private final PortfolioAccountData toAccount;
|
||||||
|
@Getter
|
||||||
private final EnumOptionData transferType;
|
private final EnumOptionData transferType;
|
||||||
|
@Getter
|
||||||
private final EnumOptionData priority;
|
private final EnumOptionData priority;
|
||||||
|
@Getter
|
||||||
private final EnumOptionData instructionType;
|
private final EnumOptionData instructionType;
|
||||||
@Getter
|
@Getter
|
||||||
private final EnumOptionData status;
|
private final EnumOptionData status;
|
||||||
@@ -69,27 +76,46 @@ public final class StandingInstructionData {
|
|||||||
private final BigDecimal amount;
|
private final BigDecimal amount;
|
||||||
@Getter
|
@Getter
|
||||||
private final LocalDate validFrom;
|
private final LocalDate validFrom;
|
||||||
|
@Getter
|
||||||
private final LocalDate validTill;
|
private final LocalDate validTill;
|
||||||
|
@Getter
|
||||||
private final EnumOptionData recurrenceType;
|
private final EnumOptionData recurrenceType;
|
||||||
|
@Getter
|
||||||
private final EnumOptionData recurrenceFrequency;
|
private final EnumOptionData recurrenceFrequency;
|
||||||
@Getter
|
@Getter
|
||||||
private final Integer recurrenceInterval;
|
private final Integer recurrenceInterval;
|
||||||
|
@Getter
|
||||||
private final MonthDay recurrenceOnMonthDay;
|
private final MonthDay recurrenceOnMonthDay;
|
||||||
|
@Getter
|
||||||
private final Page<AccountTransferData> transactions;
|
private final Page<AccountTransferData> transactions;
|
||||||
|
|
||||||
|
@Getter
|
||||||
private final Collection<OfficeData> fromOfficeOptions;
|
private final Collection<OfficeData> fromOfficeOptions;
|
||||||
|
@Getter
|
||||||
private final Collection<ClientData> fromClientOptions;
|
private final Collection<ClientData> fromClientOptions;
|
||||||
|
@Getter
|
||||||
private final Collection<EnumOptionData> fromAccountTypeOptions;
|
private final Collection<EnumOptionData> fromAccountTypeOptions;
|
||||||
|
@Getter
|
||||||
private final Collection<PortfolioAccountData> fromAccountOptions;
|
private final Collection<PortfolioAccountData> fromAccountOptions;
|
||||||
|
@Getter
|
||||||
private final Collection<OfficeData> toOfficeOptions;
|
private final Collection<OfficeData> toOfficeOptions;
|
||||||
|
@Getter
|
||||||
private final Collection<ClientData> toClientOptions;
|
private final Collection<ClientData> toClientOptions;
|
||||||
|
@Getter
|
||||||
private final Collection<EnumOptionData> toAccountTypeOptions;
|
private final Collection<EnumOptionData> toAccountTypeOptions;
|
||||||
|
@Getter
|
||||||
private final Collection<PortfolioAccountData> toAccountOptions;
|
private final Collection<PortfolioAccountData> toAccountOptions;
|
||||||
|
@Getter
|
||||||
private final Collection<EnumOptionData> transferTypeOptions;
|
private final Collection<EnumOptionData> transferTypeOptions;
|
||||||
|
@Getter
|
||||||
private final Collection<EnumOptionData> statusOptions;
|
private final Collection<EnumOptionData> statusOptions;
|
||||||
|
@Getter
|
||||||
private final Collection<EnumOptionData> instructionTypeOptions;
|
private final Collection<EnumOptionData> instructionTypeOptions;
|
||||||
|
@Getter
|
||||||
private final Collection<EnumOptionData> priorityOptions;
|
private final Collection<EnumOptionData> priorityOptions;
|
||||||
|
@Getter
|
||||||
private final Collection<EnumOptionData> recurrenceTypeOptions;
|
private final Collection<EnumOptionData> recurrenceTypeOptions;
|
||||||
|
@Getter
|
||||||
private final Collection<EnumOptionData> recurrenceFrequencyOptions;
|
private final Collection<EnumOptionData> recurrenceFrequencyOptions;
|
||||||
|
|
||||||
public static StandingInstructionData template(final Collection<OfficeData> fromOfficeOptions,
|
public static StandingInstructionData template(final Collection<OfficeData> fromOfficeOptions,
|
||||||
@@ -282,28 +308,32 @@ public final class StandingInstructionData {
|
|||||||
instructionData.recurrenceTypeOptions, instructionData.recurrenceFrequencyOptions);
|
instructionData.recurrenceTypeOptions, instructionData.recurrenceFrequencyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public StandingInstructionType getInstructionType() {
|
/**
|
||||||
|
* Returns the domain enum for instruction type — used internally by business logic.
|
||||||
|
* NOTE: renamed from getInstructionType() to avoid overriding Lombok @Getter (which must
|
||||||
|
* return EnumOptionData so Jackson serializes it as an object, not a plain string).
|
||||||
|
*/
|
||||||
|
public StandingInstructionType instructionTypeEnum() {
|
||||||
return Optional.ofNullable(this.instructionType).map(e -> StandingInstructionType.fromInt(e.getId().intValue())).orElse(null);
|
return Optional.ofNullable(this.instructionType).map(e -> StandingInstructionType.fromInt(e.getId().intValue())).orElse(null);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountTransferRecurrenceType getRecurrenceType() {
|
public AccountTransferRecurrenceType recurrenceTypeEnum() {
|
||||||
return Optional.ofNullable(this.recurrenceType).map(e -> AccountTransferRecurrenceType.fromInt(e.getId().intValue())).orElse(null);
|
return Optional.ofNullable(this.recurrenceType).map(e -> AccountTransferRecurrenceType.fromInt(e.getId().intValue())).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PeriodFrequencyType getRecurrenceFrequency() {
|
public PeriodFrequencyType recurrenceFrequencyEnum() {
|
||||||
return Optional.ofNullable(this.recurrenceFrequency).map(e -> PeriodFrequencyType.fromInt(e.getId().intValue())).orElse(null);
|
return Optional.ofNullable(this.recurrenceFrequency).map(e -> PeriodFrequencyType.fromInt(e.getId().intValue())).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PortfolioAccountType getFromAccountType() {
|
public PortfolioAccountType fromAccountTypeEnum() {
|
||||||
return Optional.ofNullable(this.fromAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null);
|
return Optional.ofNullable(this.fromAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PortfolioAccountType getToAccountType() {
|
public PortfolioAccountType toAccountTypeEnum() {
|
||||||
return Optional.ofNullable(this.toAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null);
|
return Optional.ofNullable(this.toAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountTransferType getTransferType() {
|
public AccountTransferType transferTypeEnum() {
|
||||||
return Optional.ofNullable(this.transferType).map(e -> AccountTransferType.fromInt(e.getId().intValue())).orElse(null);
|
return Optional.ofNullable(this.transferType).map(e -> AccountTransferType.fromInt(e.getId().intValue())).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,10 +347,10 @@ public final class StandingInstructionData {
|
|||||||
|
|
||||||
public Integer toTransferType() {
|
public Integer toTransferType() {
|
||||||
Integer transferType = null;
|
Integer transferType = null;
|
||||||
AccountTransferType accountTransferType = getTransferType();
|
AccountTransferType accountTransferType = transferTypeEnum();
|
||||||
if (accountTransferType.isChargePayment()) {
|
if (accountTransferType != null && accountTransferType.isChargePayment()) {
|
||||||
transferType = LoanTransactionType.CHARGE_PAYMENT.getValue();
|
transferType = LoanTransactionType.CHARGE_PAYMENT.getValue();
|
||||||
} else if (accountTransferType.isLoanRepayment()) {
|
} else if (accountTransferType != null && accountTransferType.isLoanRepayment()) {
|
||||||
transferType = LoanTransactionType.REPAYMENT.getValue();
|
transferType = LoanTransactionType.REPAYMENT.getValue();
|
||||||
}
|
}
|
||||||
return transferType;
|
return transferType;
|
||||||
|
|||||||
+4
-2
@@ -171,7 +171,8 @@ public class StandingInstructionDataValidator {
|
|||||||
baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull();
|
baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull();
|
||||||
|
|
||||||
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
|
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
|
||||||
if (toAccountType != null && PortfolioAccountType.fromInt(toAccountType).isSavingsAccount()) {
|
if (toAccountType != null && (PortfolioAccountType.fromInt(toAccountType).isSavingsAccount()
|
||||||
|
|| PortfolioAccountType.fromInt(toAccountType).isCurrentAccount())) {
|
||||||
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
|
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
|
||||||
.notNull().inMinMaxRange(1, 1);
|
.notNull().inMinMaxRange(1, 1);
|
||||||
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
|
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
|
||||||
@@ -192,7 +193,8 @@ public class StandingInstructionDataValidator {
|
|||||||
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) {
|
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) {
|
||||||
errorCode = "not.account.transfer";
|
errorCode = "not.account.transfer";
|
||||||
} else if (accountTransferType.isLoanRepayment()
|
} else if (accountTransferType.isLoanRepayment()
|
||||||
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isSavingsAccount())) {
|
&& (fromPortfolioAccountType.isLoanAccount()
|
||||||
|
|| (toPortfolioAccountType.isSavingsAccount() || toPortfolioAccountType.isCurrentAccount()))) {
|
||||||
errorCode = "not.loan.repayment";
|
errorCode = "not.loan.repayment";
|
||||||
}
|
}
|
||||||
if (errorCode != null) {
|
if (errorCode != null) {
|
||||||
|
|||||||
+101
-1
@@ -34,6 +34,8 @@ import org.apache.fineract.organisation.office.domain.Office;
|
|||||||
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
|
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
|
||||||
import org.apache.fineract.portfolio.client.domain.Client;
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||||
|
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
|
||||||
|
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||||
@@ -49,16 +51,19 @@ public class AccountTransferDetailAssembler {
|
|||||||
private final SavingsAccountAssembler savingsAccountAssembler;
|
private final SavingsAccountAssembler savingsAccountAssembler;
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
private final LoanAssembler loanAccountAssembler;
|
private final LoanAssembler loanAccountAssembler;
|
||||||
|
private final CurrentAccountRepository currentAccountRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository,
|
public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository,
|
||||||
final OfficeRepositoryWrapper officeRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler,
|
final OfficeRepositoryWrapper officeRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler,
|
||||||
final FromJsonHelper fromApiJsonHelper, final LoanAssembler loanAccountAssembler) {
|
final FromJsonHelper fromApiJsonHelper, final LoanAssembler loanAccountAssembler,
|
||||||
|
final CurrentAccountRepository currentAccountRepository) {
|
||||||
this.clientRepository = clientRepository;
|
this.clientRepository = clientRepository;
|
||||||
this.officeRepositoryWrapper = officeRepositoryWrapper;
|
this.officeRepositoryWrapper = officeRepositoryWrapper;
|
||||||
this.savingsAccountAssembler = savingsAccountAssembler;
|
this.savingsAccountAssembler = savingsAccountAssembler;
|
||||||
this.fromApiJsonHelper = fromApiJsonHelper;
|
this.fromApiJsonHelper = fromApiJsonHelper;
|
||||||
this.loanAccountAssembler = loanAccountAssembler;
|
this.loanAccountAssembler = loanAccountAssembler;
|
||||||
|
this.currentAccountRepository = currentAccountRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
|
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
|
||||||
@@ -211,4 +216,99 @@ public class AccountTransferDetailAssembler {
|
|||||||
return AccountTransferDetails.loanToLoanTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toLoanAccount,
|
return AccountTransferDetails.loanToLoanTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toLoanAccount,
|
||||||
transferType);
|
transferType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Current account assembly methods ---
|
||||||
|
|
||||||
|
public AccountTransferDetails assembleCurrentToCurrentTransfer(final JsonCommand command) {
|
||||||
|
final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
|
||||||
|
final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId));
|
||||||
|
|
||||||
|
final Long toCurrentAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
|
||||||
|
final CurrentAccount toCurrentAccount = currentAccountRepository.findById(toCurrentAccountId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("To current account not found: " + toCurrentAccountId));
|
||||||
|
|
||||||
|
final JsonElement element = command.parsedJson();
|
||||||
|
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
|
||||||
|
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
|
||||||
|
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
|
||||||
|
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
|
||||||
|
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
|
||||||
|
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
|
||||||
|
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
|
||||||
|
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
|
||||||
|
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
|
||||||
|
|
||||||
|
return AccountTransferDetails.currentToCurrentTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient,
|
||||||
|
toCurrentAccount, transfertype);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountTransferDetails assembleCurrentToSavingsTransfer(final JsonCommand command) {
|
||||||
|
final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
|
||||||
|
final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId));
|
||||||
|
|
||||||
|
final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
|
||||||
|
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, false);
|
||||||
|
|
||||||
|
final JsonElement element = command.parsedJson();
|
||||||
|
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
|
||||||
|
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
|
||||||
|
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
|
||||||
|
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
|
||||||
|
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
|
||||||
|
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
|
||||||
|
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
|
||||||
|
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
|
||||||
|
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
|
||||||
|
|
||||||
|
return AccountTransferDetails.currentToSavingsTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient,
|
||||||
|
toSavingsAccount, transfertype);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountTransferDetails assembleSavingsToCurrentTransfer(final JsonCommand command) {
|
||||||
|
final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName);
|
||||||
|
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false);
|
||||||
|
|
||||||
|
final Long toCurrentAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
|
||||||
|
final CurrentAccount toCurrentAccount = currentAccountRepository.findById(toCurrentAccountId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("To current account not found: " + toCurrentAccountId));
|
||||||
|
|
||||||
|
final JsonElement element = command.parsedJson();
|
||||||
|
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
|
||||||
|
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
|
||||||
|
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
|
||||||
|
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
|
||||||
|
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
|
||||||
|
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
|
||||||
|
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
|
||||||
|
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
|
||||||
|
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
|
||||||
|
|
||||||
|
return AccountTransferDetails.savingsToCurrentTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
|
||||||
|
toCurrentAccount, transfertype);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountTransferDetails assembleCurrentToLoanTransfer(final JsonCommand command) {
|
||||||
|
final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
|
||||||
|
final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId));
|
||||||
|
|
||||||
|
final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
|
||||||
|
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId);
|
||||||
|
|
||||||
|
final JsonElement element = command.parsedJson();
|
||||||
|
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
|
||||||
|
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
|
||||||
|
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
|
||||||
|
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
|
||||||
|
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
|
||||||
|
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
|
||||||
|
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
|
||||||
|
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
|
||||||
|
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
|
||||||
|
|
||||||
|
return AccountTransferDetails.currentToLoanTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient,
|
||||||
|
toLoanAccount, transfertype);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-10
@@ -32,6 +32,7 @@ import java.util.List;
|
|||||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
import org.apache.fineract.organisation.office.domain.Office;
|
import org.apache.fineract.organisation.office.domain.Office;
|
||||||
import org.apache.fineract.portfolio.client.domain.Client;
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||||
|
|
||||||
@@ -71,6 +72,14 @@ public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
|
|||||||
@JoinColumn(name = "from_loan_account_id", nullable = true)
|
@JoinColumn(name = "from_loan_account_id", nullable = true)
|
||||||
private Loan fromLoanAccount;
|
private Loan fromLoanAccount;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "from_current_account_id", nullable = true)
|
||||||
|
private CurrentAccount fromCurrentAccount;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "to_current_account_id", nullable = true)
|
||||||
|
private CurrentAccount toCurrentAccount;
|
||||||
|
|
||||||
@Column(name = "transfer_type")
|
@Column(name = "transfer_type")
|
||||||
private Integer transferType;
|
private Integer transferType;
|
||||||
|
|
||||||
@@ -84,21 +93,21 @@ public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
|
|||||||
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
|
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
|
||||||
Integer transferType) {
|
Integer transferType) {
|
||||||
|
|
||||||
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, toSavingsAccount, null,
|
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, toSavingsAccount,
|
||||||
transferType, null);
|
null, null, transferType, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient,
|
public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient,
|
||||||
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final Loan toLoanAccount,
|
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final Loan toLoanAccount,
|
||||||
Integer transferType) {
|
Integer transferType) {
|
||||||
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, null, toLoanAccount,
|
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, null, toLoanAccount,
|
||||||
transferType, null);
|
null, transferType, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AccountTransferDetails loanTosavingsTransfer(final Office fromOffice, final Client fromClient, final Loan fromLoanAccount,
|
public static AccountTransferDetails loanTosavingsTransfer(final Office fromOffice, final Client fromClient, final Loan fromLoanAccount,
|
||||||
final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, Integer transferType) {
|
final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, Integer transferType) {
|
||||||
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, toOffice, toClient, toSavingsAccount, null,
|
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, null, toOffice, toClient, toSavingsAccount, null,
|
||||||
transferType, null);
|
null, transferType, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected AccountTransferDetails() {
|
protected AccountTransferDetails() {
|
||||||
@@ -106,17 +115,21 @@ public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private AccountTransferDetails(final Office fromOffice, final Client fromClient, final SavingsAccount fromSavingsAccount,
|
private AccountTransferDetails(final Office fromOffice, final Client fromClient, final SavingsAccount fromSavingsAccount,
|
||||||
final Loan fromLoanAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
|
final Loan fromLoanAccount, final CurrentAccount fromCurrentAccount,
|
||||||
final Loan toLoanAccount, final Integer transferType,
|
final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
|
||||||
|
final Loan toLoanAccount, final CurrentAccount toCurrentAccount,
|
||||||
|
final Integer transferType,
|
||||||
final AccountTransferStandingInstruction accountTransferStandingInstruction) {
|
final AccountTransferStandingInstruction accountTransferStandingInstruction) {
|
||||||
this.fromOffice = fromOffice;
|
this.fromOffice = fromOffice;
|
||||||
this.fromClient = fromClient;
|
this.fromClient = fromClient;
|
||||||
this.fromSavingsAccount = fromSavingsAccount;
|
this.fromSavingsAccount = fromSavingsAccount;
|
||||||
this.fromLoanAccount = fromLoanAccount;
|
this.fromLoanAccount = fromLoanAccount;
|
||||||
|
this.fromCurrentAccount = fromCurrentAccount;
|
||||||
this.toOffice = toOffice;
|
this.toOffice = toOffice;
|
||||||
this.toClient = toClient;
|
this.toClient = toClient;
|
||||||
this.toSavingsAccount = toSavingsAccount;
|
this.toSavingsAccount = toSavingsAccount;
|
||||||
this.toLoanAccount = toLoanAccount;
|
this.toLoanAccount = toLoanAccount;
|
||||||
|
this.toCurrentAccount = toCurrentAccount;
|
||||||
this.transferType = transferType;
|
this.transferType = transferType;
|
||||||
this.accountTransferStandingInstruction = accountTransferStandingInstruction;
|
this.accountTransferStandingInstruction = accountTransferStandingInstruction;
|
||||||
}
|
}
|
||||||
@@ -145,6 +158,14 @@ public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
|
|||||||
return this.fromLoanAccount;
|
return this.fromLoanAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CurrentAccount fromCurrentAccount() {
|
||||||
|
return this.fromCurrentAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CurrentAccount toCurrentAccount() {
|
||||||
|
return this.toCurrentAccount;
|
||||||
|
}
|
||||||
|
|
||||||
public AccountTransferStandingInstruction accountTransferStandingInstruction() {
|
public AccountTransferStandingInstruction accountTransferStandingInstruction() {
|
||||||
return this.accountTransferStandingInstruction;
|
return this.accountTransferStandingInstruction;
|
||||||
}
|
}
|
||||||
@@ -155,8 +176,38 @@ public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
|
|||||||
|
|
||||||
public static AccountTransferDetails loanToLoanTransfer(Office fromOffice, Client fromClient, Loan fromLoanAccount, Office toOffice,
|
public static AccountTransferDetails loanToLoanTransfer(Office fromOffice, Client fromClient, Loan fromLoanAccount, Office toOffice,
|
||||||
Client toClient, Loan toLoanAccount, Integer transferType) {
|
Client toClient, Loan toLoanAccount, Integer transferType) {
|
||||||
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, toOffice, toClient, null, toLoanAccount,
|
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, null, toOffice, toClient, null, toLoanAccount,
|
||||||
transferType, null);
|
null, transferType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Current account factory methods ---
|
||||||
|
|
||||||
|
public static AccountTransferDetails currentToCurrentTransfer(final Office fromOffice, final Client fromClient,
|
||||||
|
final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient,
|
||||||
|
final CurrentAccount toCurrentAccount, Integer transferType) {
|
||||||
|
return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, null, null,
|
||||||
|
toCurrentAccount, transferType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccountTransferDetails currentToSavingsTransfer(final Office fromOffice, final Client fromClient,
|
||||||
|
final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient,
|
||||||
|
final SavingsAccount toSavingsAccount, Integer transferType) {
|
||||||
|
return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, toSavingsAccount,
|
||||||
|
null, null, transferType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccountTransferDetails savingsToCurrentTransfer(final Office fromOffice, final Client fromClient,
|
||||||
|
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient,
|
||||||
|
final CurrentAccount toCurrentAccount, Integer transferType) {
|
||||||
|
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, null, null,
|
||||||
|
toCurrentAccount, transferType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccountTransferDetails currentToLoanTransfer(final Office fromOffice, final Client fromClient,
|
||||||
|
final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient,
|
||||||
|
final Loan toLoanAccount, Integer transferType) {
|
||||||
|
return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, null, toLoanAccount,
|
||||||
|
null, transferType, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<AccountTransferTransaction> getAccountTransferTransactions() {
|
public List<AccountTransferTransaction> getAccountTransferTransactions() {
|
||||||
|
|||||||
+15
-3
@@ -249,7 +249,9 @@ public class AccountTransferStandingInstruction extends AbstractPersistableCusto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.accountTransferDetails.toSavingsAccount() != null) {
|
// For savings-to-savings or current-to-savings: enforce Fixed instruction + Periodic recurrence
|
||||||
|
if (this.accountTransferDetails.toSavingsAccount() != null
|
||||||
|
|| this.accountTransferDetails.toCurrentAccount() != null) {
|
||||||
baseDataValidator.reset().parameter(instructionTypeParamName).value(this.instructionType).notNull().inMinMaxRange(1, 1);
|
baseDataValidator.reset().parameter(instructionTypeParamName).value(this.instructionType).notNull().inMinMaxRange(1, 1);
|
||||||
baseDataValidator.reset().parameter(recurrenceTypeParamName).value(this.recurrenceType).notNull().inMinMaxRange(1, 1);
|
baseDataValidator.reset().parameter(recurrenceTypeParamName).value(this.recurrenceType).notNull().inMinMaxRange(1, 1);
|
||||||
}
|
}
|
||||||
@@ -258,12 +260,22 @@ public class AccountTransferStandingInstruction extends AbstractPersistableCusto
|
|||||||
baseDataValidator.reset().parameter(amountParamName).value(this.amount).notNull();
|
baseDataValidator.reset().parameter(amountParamName).value(this.amount).notNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Account Transfer: from must be savings OR current; to must be savings OR current
|
||||||
|
boolean fromAccountValid = this.accountTransferDetails.fromSavingsAccount() != null
|
||||||
|
|| this.accountTransferDetails.fromCurrentAccount() != null;
|
||||||
|
boolean toAccountValid = this.accountTransferDetails.toSavingsAccount() != null
|
||||||
|
|| this.accountTransferDetails.toCurrentAccount() != null;
|
||||||
|
|
||||||
|
// Loan Repayment: from must be savings OR current; to must be loan
|
||||||
|
boolean fromAccountValidForLoan = this.accountTransferDetails.fromSavingsAccount() != null
|
||||||
|
|| this.accountTransferDetails.fromCurrentAccount() != null;
|
||||||
|
|
||||||
String errorCode = null;
|
String errorCode = null;
|
||||||
if (this.accountTransferDetails.transferType().isAccountTransfer()
|
if (this.accountTransferDetails.transferType().isAccountTransfer()
|
||||||
&& (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toSavingsAccount() == null)) {
|
&& !(fromAccountValid && toAccountValid)) {
|
||||||
errorCode = "not.account.transfer";
|
errorCode = "not.account.transfer";
|
||||||
} else if (this.accountTransferDetails.transferType().isLoanRepayment()
|
} else if (this.accountTransferDetails.transferType().isLoanRepayment()
|
||||||
&& (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toLoanAccount() == null)) {
|
&& !(fromAccountValidForLoan && this.accountTransferDetails.toLoanAccount() != null)) {
|
||||||
errorCode = "not.loan.repayment";
|
errorCode = "not.loan.repayment";
|
||||||
}
|
}
|
||||||
if (errorCode != null) {
|
if (errorCode != null) {
|
||||||
|
|||||||
+52
-2
@@ -35,6 +35,9 @@ import java.time.LocalDate;
|
|||||||
import java.time.MonthDay;
|
import java.time.MonthDay;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
|
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
|
||||||
|
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -61,8 +64,13 @@ public class StandingInstructionAssembler {
|
|||||||
BigDecimal amount = null;
|
BigDecimal amount = null;
|
||||||
final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName);
|
final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName);
|
||||||
if (transferAmount != null) {
|
if (transferAmount != null) {
|
||||||
final Money monetaryAmount = Money.of(accountTransferDetails.fromSavingsAccount().getCurrency(), transferAmount);
|
final MonetaryCurrency currency = resolveCurrency(accountTransferDetails);
|
||||||
amount = monetaryAmount.getAmount();
|
if (currency != null) {
|
||||||
|
final Money monetaryAmount = Money.of(currency, transferAmount);
|
||||||
|
amount = monetaryAmount.getAmount();
|
||||||
|
} else {
|
||||||
|
amount = transferAmount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final Integer status = command.integerValueOfParameterNamed(statusParamName);
|
final Integer status = command.integerValueOfParameterNamed(statusParamName);
|
||||||
final Integer priority = command.integerValueOfParameterNamed(priorityParamName);
|
final Integer priority = command.integerValueOfParameterNamed(priorityParamName);
|
||||||
@@ -78,6 +86,22 @@ public class StandingInstructionAssembler {
|
|||||||
accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction);
|
accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the currency from the "from" account — savings, loan (null), or current account.
|
||||||
|
*/
|
||||||
|
private MonetaryCurrency resolveCurrency(final AccountTransferDetails details) {
|
||||||
|
final SavingsAccount fromSavings = details.fromSavingsAccount();
|
||||||
|
if (fromSavings != null) {
|
||||||
|
return fromSavings.getCurrency();
|
||||||
|
}
|
||||||
|
final CurrentAccount fromCurrent = details.fromCurrentAccount();
|
||||||
|
if (fromCurrent != null) {
|
||||||
|
return new MonetaryCurrency(fromCurrent.getCurrencyCode(), fromCurrent.getCurrencyDigits(),
|
||||||
|
fromCurrent.getCurrencyMultiplesOf());
|
||||||
|
}
|
||||||
|
return null; // loan accounts don't have a direct currency on AccountTransferDetails
|
||||||
|
}
|
||||||
|
|
||||||
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
|
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
|
||||||
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToLoanTransfer(command);
|
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToLoanTransfer(command);
|
||||||
assembleStandingInstruction(command, accountTransferDetails);
|
assembleStandingInstruction(command, accountTransferDetails);
|
||||||
@@ -90,4 +114,30 @@ public class StandingInstructionAssembler {
|
|||||||
return accountTransferDetails;
|
return accountTransferDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Current account methods ---
|
||||||
|
|
||||||
|
public AccountTransferDetails assembleCurrentToCurrentTransfer(final JsonCommand command) {
|
||||||
|
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToCurrentTransfer(command);
|
||||||
|
assembleStandingInstruction(command, accountTransferDetails);
|
||||||
|
return accountTransferDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountTransferDetails assembleCurrentToSavingsTransfer(final JsonCommand command) {
|
||||||
|
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToSavingsTransfer(command);
|
||||||
|
assembleStandingInstruction(command, accountTransferDetails);
|
||||||
|
return accountTransferDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountTransferDetails assembleSavingsToCurrentTransfer(final JsonCommand command) {
|
||||||
|
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToCurrentTransfer(command);
|
||||||
|
assembleStandingInstruction(command, accountTransferDetails);
|
||||||
|
return accountTransferDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountTransferDetails assembleCurrentToLoanTransfer(final JsonCommand command) {
|
||||||
|
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToLoanTransfer(command);
|
||||||
|
assembleStandingInstruction(command, accountTransferDetails);
|
||||||
|
return accountTransferDetails;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.account.jobs.executestandinginstructions;
|
|||||||
|
|
||||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
|
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
|
||||||
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
||||||
|
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
|
||||||
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
|
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
|
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
|
||||||
import org.springframework.batch.core.Job;
|
import org.springframework.batch.core.Job;
|
||||||
@@ -49,6 +50,8 @@ public class ExecuteStandingInstructionsConfig {
|
|||||||
private DatabaseSpecificSQLGenerator sqlGenerator;
|
private DatabaseSpecificSQLGenerator sqlGenerator;
|
||||||
@Autowired
|
@Autowired
|
||||||
private AccountTransfersWritePlatformService accountTransfersWritePlatformService;
|
private AccountTransfersWritePlatformService accountTransfersWritePlatformService;
|
||||||
|
@Autowired
|
||||||
|
private AccountTransferDetailRepository accountTransferDetailRepository;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
protected Step executeStandingInstructionsStep() {
|
protected Step executeStandingInstructionsStep() {
|
||||||
@@ -65,6 +68,6 @@ public class ExecuteStandingInstructionsConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() {
|
public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() {
|
||||||
return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator,
|
return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator,
|
||||||
accountTransfersWritePlatformService);
|
accountTransfersWritePlatformService, accountTransferDetailRepository);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-7
@@ -34,6 +34,8 @@ import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
|
|||||||
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
|
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
|
||||||
import org.apache.fineract.portfolio.account.data.StandingInstructionData;
|
import org.apache.fineract.portfolio.account.data.StandingInstructionData;
|
||||||
import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData;
|
import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData;
|
||||||
|
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
|
||||||
|
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
|
||||||
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
|
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
|
||||||
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
|
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
|
||||||
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
|
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
|
||||||
@@ -58,6 +60,7 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet {
|
|||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
private final DatabaseSpecificSQLGenerator sqlGenerator;
|
private final DatabaseSpecificSQLGenerator sqlGenerator;
|
||||||
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
|
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
|
||||||
|
private final AccountTransferDetailRepository accountTransferDetailRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
|
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
|
||||||
@@ -66,12 +69,12 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet {
|
|||||||
List<Throwable> errors = new ArrayList<>();
|
List<Throwable> errors = new ArrayList<>();
|
||||||
for (StandingInstructionData data : instructionData) {
|
for (StandingInstructionData data : instructionData) {
|
||||||
boolean isDueForTransfer = false;
|
boolean isDueForTransfer = false;
|
||||||
AccountTransferRecurrenceType recurrenceType = data.getRecurrenceType();
|
AccountTransferRecurrenceType recurrenceType = data.recurrenceTypeEnum();
|
||||||
StandingInstructionType instructionType = data.getInstructionType();
|
StandingInstructionType instructionType = data.instructionTypeEnum();
|
||||||
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
|
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
|
||||||
if (recurrenceType.isPeriodicRecurrence()) {
|
if (recurrenceType.isPeriodicRecurrence()) {
|
||||||
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
|
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
|
||||||
PeriodFrequencyType frequencyType = data.getRecurrenceFrequency();
|
PeriodFrequencyType frequencyType = data.recurrenceFrequencyEnum();
|
||||||
LocalDate startDate = data.getValidFrom();
|
LocalDate startDate = data.getValidFrom();
|
||||||
if (frequencyType.isMonthly()) {
|
if (frequencyType.isMonthly()) {
|
||||||
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay());
|
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay());
|
||||||
@@ -89,11 +92,11 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet {
|
|||||||
|
|
||||||
}
|
}
|
||||||
BigDecimal transactionAmount = data.getAmount();
|
BigDecimal transactionAmount = data.getAmount();
|
||||||
if (data.getToAccountType().isLoanAccount()
|
if (data.toAccountTypeEnum() != null && data.toAccountTypeEnum().isLoanAccount()
|
||||||
&& (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) {
|
&& (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) {
|
||||||
StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService
|
StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService
|
||||||
.retriveLoanDuesData(data.getToAccount().getId());
|
.retriveLoanDuesData(data.getToAccount().getId());
|
||||||
if (data.getInstructionType().isDuesAmoutTransfer()) {
|
if (data.instructionTypeEnum().isDuesAmoutTransfer()) {
|
||||||
transactionAmount = standingInstructionDuesData.totalDueAmount();
|
transactionAmount = standingInstructionDuesData.totalDueAmount();
|
||||||
}
|
}
|
||||||
if (recurrenceType.isDuesRecurrence()) {
|
if (recurrenceType.isDuesRecurrence()) {
|
||||||
@@ -105,10 +108,15 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet {
|
|||||||
final SavingsAccount fromSavingsAccount = null;
|
final SavingsAccount fromSavingsAccount = null;
|
||||||
final boolean isRegularTransaction = true;
|
final boolean isRegularTransaction = true;
|
||||||
final boolean isExceptionForBalanceCheck = false;
|
final boolean isExceptionForBalanceCheck = false;
|
||||||
|
// Load existing AccountTransferDetails so transactions are linked to
|
||||||
|
// this standing instruction and appear in its transaction history
|
||||||
|
final AccountTransferDetails existingDetails = data.getAccountDetailId() != null
|
||||||
|
? accountTransferDetailRepository.findById(data.getAccountDetailId()).orElse(null)
|
||||||
|
: null;
|
||||||
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount,
|
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount,
|
||||||
data.getFromAccountType(), data.getToAccountType(), data.getFromAccount().getId(), data.getToAccount().getId(),
|
data.fromAccountTypeEnum(), data.toAccountTypeEnum(), data.getFromAccount().getId(), data.getToAccount().getId(),
|
||||||
data.getName() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null,
|
data.getName() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null,
|
||||||
data.getTransferType().getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount,
|
data.transferTypeEnum().getValue(), existingDetails, null, ExternalId.empty(), null, null, fromSavingsAccount,
|
||||||
isRegularTransaction, isExceptionForBalanceCheck);
|
isRegularTransaction, isExceptionForBalanceCheck);
|
||||||
final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId());
|
final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId());
|
||||||
|
|
||||||
|
|||||||
+4
@@ -52,6 +52,10 @@ public final class AccountTransferEnumerations {
|
|||||||
optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(),
|
optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(),
|
||||||
PortfolioAccountType.SAVINGS.getCode(), "Savings Account");
|
PortfolioAccountType.SAVINGS.getCode(), "Savings Account");
|
||||||
break;
|
break;
|
||||||
|
case CURRENT:
|
||||||
|
optionData = new EnumOptionData(PortfolioAccountType.CURRENT.getValue().longValue(),
|
||||||
|
PortfolioAccountType.CURRENT.getCode(), "Current Account");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+304
@@ -34,6 +34,9 @@ import java.util.Collection;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings;
|
||||||
|
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
|
||||||
|
import org.apache.fineract.accounting.journalentry.service.AccountingProcessorHelper;
|
||||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||||
@@ -42,6 +45,8 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild
|
|||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||||
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
import org.apache.fineract.portfolio.account.PortfolioAccountType;
|
import org.apache.fineract.portfolio.account.PortfolioAccountType;
|
||||||
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
|
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
|
||||||
import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator;
|
import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator;
|
||||||
@@ -52,6 +57,10 @@ import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
|
|||||||
import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction;
|
import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction;
|
||||||
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
|
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
|
||||||
import org.apache.fineract.portfolio.account.exception.DifferentCurrenciesException;
|
import org.apache.fineract.portfolio.account.exception.DifferentCurrenciesException;
|
||||||
|
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
|
||||||
|
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
|
||||||
|
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction;
|
||||||
|
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
|
||||||
@@ -88,6 +97,9 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
private final ConfigurationDomainService configurationDomainService;
|
private final ConfigurationDomainService configurationDomainService;
|
||||||
private final ExternalIdFactory externalIdFactory;
|
private final ExternalIdFactory externalIdFactory;
|
||||||
private final FineractProperties fineractProperties;
|
private final FineractProperties fineractProperties;
|
||||||
|
private final CurrentAccountRepository currentAccountRepository;
|
||||||
|
private final CurrentAccountTransactionRepository currentAccountTransactionRepository;
|
||||||
|
private final AccountingProcessorHelper accountingProcessorHelper;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -448,6 +460,144 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
gsim.setParentDeposit(newBalance);
|
gsim.setParentDeposit(newBalance);
|
||||||
gsimRepository.save(gsim);
|
gsimRepository.save(gsim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if (isCurrentToSavingsAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
|
||||||
|
// --- Current Account → Savings Account ---
|
||||||
|
final CurrentAccount fromCurrentAccount = currentAccountRepository
|
||||||
|
.findById(accountTransferDTO.getFromAccountId())
|
||||||
|
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
|
||||||
|
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getFromAccountId()));
|
||||||
|
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler
|
||||||
|
.assembleFrom(accountTransferDTO.getToAccountId(), backdatedTxnsAllowedTill);
|
||||||
|
|
||||||
|
final CurrentAccountTransaction withdrawal = performCurrentWithdrawal(fromCurrentAccount,
|
||||||
|
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getTransactionDate(),
|
||||||
|
toSavingsAccount.productId(), toSavingsAccount.getId());
|
||||||
|
|
||||||
|
// skipAccountingPostings=true: current account withdrawal leg (C-txn) already posts
|
||||||
|
// DR CURRENT ACCOUNT / CR SAVING INDIVIDUAL directly — each with its own account number.
|
||||||
|
// Savings deposit GL entries would produce a self-cancelling DR+CR on SAVING INDIVIDUAL — suppress them.
|
||||||
|
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount,
|
||||||
|
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(),
|
||||||
|
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
|
||||||
|
isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill, true);
|
||||||
|
|
||||||
|
if (accountTransferDetails == null) {
|
||||||
|
accountTransferDetails = AccountTransferDetails.currentToSavingsTransfer(
|
||||||
|
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
|
||||||
|
toSavingsAccount.office(), toSavingsAccount.getClient(), toSavingsAccount,
|
||||||
|
accountTransferDTO.getTransferType());
|
||||||
|
}
|
||||||
|
final Money amount = Money.of(toSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount());
|
||||||
|
accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
|
||||||
|
accountTransferDetails, null, deposit, accountTransferDTO.getTransactionDate(), amount,
|
||||||
|
accountTransferDTO.getDescription()));
|
||||||
|
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
|
||||||
|
transferTransactionId = accountTransferDetails.getId();
|
||||||
|
|
||||||
|
} else if (isSavingsToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
|
||||||
|
// --- Savings Account → Current Account ---
|
||||||
|
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler
|
||||||
|
.assembleFrom(accountTransferDTO.getFromAccountId(), backdatedTxnsAllowedTill);
|
||||||
|
final CurrentAccount toCurrentAccount = currentAccountRepository
|
||||||
|
.findById(accountTransferDTO.getToAccountId())
|
||||||
|
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
|
||||||
|
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId()));
|
||||||
|
|
||||||
|
// skipAccountingPostings=true: current account deposit leg (C-txn) posts
|
||||||
|
// DR SAVING INDIVIDUAL / CR CURRENT ACCOUNT directly. Savings withdrawal GL entries
|
||||||
|
// would produce a self-cancelling DR+CR on the same SAVING INDIVIDUAL GL — suppress them.
|
||||||
|
final SavingsTransactionBooleanValues txnBooleans = new SavingsTransactionBooleanValues(isAccountTransfer,
|
||||||
|
isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), false,
|
||||||
|
accountTransferDTO.isExceptionForBalanceCheck(), true);
|
||||||
|
final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount,
|
||||||
|
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(),
|
||||||
|
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
|
||||||
|
txnBooleans, backdatedTxnsAllowedTill);
|
||||||
|
|
||||||
|
performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
||||||
|
accountTransferDTO.getTransactionDate(),
|
||||||
|
fromSavingsAccount.productId(), fromSavingsAccount.getId());
|
||||||
|
|
||||||
|
if (accountTransferDetails == null) {
|
||||||
|
accountTransferDetails = AccountTransferDetails.savingsToCurrentTransfer(
|
||||||
|
fromSavingsAccount.office(), fromSavingsAccount.getClient(), fromSavingsAccount,
|
||||||
|
toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount,
|
||||||
|
accountTransferDTO.getTransferType());
|
||||||
|
}
|
||||||
|
final Money amount = Money.of(fromSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount());
|
||||||
|
accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
|
||||||
|
accountTransferDetails, withdrawal, null, accountTransferDTO.getTransactionDate(), amount,
|
||||||
|
accountTransferDTO.getDescription()));
|
||||||
|
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
|
||||||
|
transferTransactionId = accountTransferDetails.getId();
|
||||||
|
|
||||||
|
} else if (isCurrentToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
|
||||||
|
// --- Current Account → Current Account ---
|
||||||
|
final CurrentAccount fromCurrentAccount = currentAccountRepository
|
||||||
|
.findById(accountTransferDTO.getFromAccountId())
|
||||||
|
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
|
||||||
|
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getFromAccountId()));
|
||||||
|
final CurrentAccount toCurrentAccount = currentAccountRepository
|
||||||
|
.findById(accountTransferDTO.getToAccountId())
|
||||||
|
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
|
||||||
|
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId()));
|
||||||
|
|
||||||
|
performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
||||||
|
accountTransferDTO.getTransactionDate(),
|
||||||
|
toCurrentAccount.getProductId(), toCurrentAccount.getId());
|
||||||
|
performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
||||||
|
accountTransferDTO.getTransactionDate(),
|
||||||
|
fromCurrentAccount.getProductId(), fromCurrentAccount.getId());
|
||||||
|
|
||||||
|
if (accountTransferDetails == null) {
|
||||||
|
accountTransferDetails = AccountTransferDetails.currentToCurrentTransfer(
|
||||||
|
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
|
||||||
|
toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount,
|
||||||
|
accountTransferDTO.getTransferType());
|
||||||
|
}
|
||||||
|
final MonetaryCurrency currency = new MonetaryCurrency(fromCurrentAccount.getCurrencyCode(),
|
||||||
|
fromCurrentAccount.getCurrencyDigits(), fromCurrentAccount.getCurrencyMultiplesOf());
|
||||||
|
final Money amount = Money.of(currency, accountTransferDTO.getTransactionAmount());
|
||||||
|
accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
|
||||||
|
accountTransferDetails, null, null, accountTransferDTO.getTransactionDate(), amount,
|
||||||
|
accountTransferDTO.getDescription()));
|
||||||
|
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
|
||||||
|
transferTransactionId = accountTransferDetails.getId();
|
||||||
|
|
||||||
|
} else if (isCurrentToLoanAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
|
||||||
|
// --- Current Account → Loan Account ---
|
||||||
|
final CurrentAccount fromCurrentAccount = currentAccountRepository
|
||||||
|
.findById(accountTransferDTO.getFromAccountId())
|
||||||
|
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
|
||||||
|
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getFromAccountId()));
|
||||||
|
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId());
|
||||||
|
|
||||||
|
performCurrentWithdrawalForLoan(fromCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
||||||
|
accountTransferDTO.getTransactionDate());
|
||||||
|
|
||||||
|
ExternalId externalId = externalIdFactory.create(accountTransferDTO.getTxnExternalId().getValue());
|
||||||
|
final boolean isRecoveryRepayment = false;
|
||||||
|
final Boolean isHolidayValidationDone = false;
|
||||||
|
final String chargeRefundChargeType = null;
|
||||||
|
LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT,
|
||||||
|
toLoanAccount, accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(),
|
||||||
|
accountTransferDTO.getPaymentDetail(), null, externalId, isRecoveryRepayment,
|
||||||
|
chargeRefundChargeType, isAccountTransfer, null, isHolidayValidationDone);
|
||||||
|
|
||||||
|
final AccountTransferDetails newDetails = AccountTransferDetails.currentToLoanTransfer(
|
||||||
|
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
|
||||||
|
toLoanAccount.getOffice(), toLoanAccount.client(), toLoanAccount,
|
||||||
|
accountTransferDTO.getTransferType());
|
||||||
|
final MonetaryCurrency currency = new MonetaryCurrency(fromCurrentAccount.getCurrencyCode(),
|
||||||
|
fromCurrentAccount.getCurrencyDigits(), fromCurrentAccount.getCurrencyMultiplesOf());
|
||||||
|
final Money amount = Money.of(currency, accountTransferDTO.getTransactionAmount());
|
||||||
|
newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToLoanTransfer(
|
||||||
|
newDetails, null, loanTransaction, accountTransferDTO.getTransactionDate(), amount,
|
||||||
|
accountTransferDTO.getDescription()));
|
||||||
|
this.accountTransferDetailRepository.saveAndFlush(newDetails);
|
||||||
|
transferTransactionId = newDetails.getId();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.accounttransfer.loan.to.loan.not.supported",
|
throw new GeneralPlatformDomainRuleException("error.msg.accounttransfer.loan.to.loan.not.supported",
|
||||||
"Account transfer from loan to another loan is not supported");
|
"Account transfer from loan to another loan is not supported");
|
||||||
@@ -505,6 +655,160 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount();
|
return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isCurrentToSavingsAccountTransfer(final PortfolioAccountType fromAccountType,
|
||||||
|
final PortfolioAccountType toAccountType) {
|
||||||
|
return fromAccountType.isCurrentAccount() && toAccountType.isSavingsAccount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSavingsToCurrentAccountTransfer(final PortfolioAccountType fromAccountType,
|
||||||
|
final PortfolioAccountType toAccountType) {
|
||||||
|
return fromAccountType.isSavingsAccount() && toAccountType.isCurrentAccount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCurrentToCurrentAccountTransfer(final PortfolioAccountType fromAccountType,
|
||||||
|
final PortfolioAccountType toAccountType) {
|
||||||
|
return fromAccountType.isCurrentAccount() && toAccountType.isCurrentAccount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCurrentToLoanAccountTransfer(final PortfolioAccountType fromAccountType,
|
||||||
|
final PortfolioAccountType toAccountType) {
|
||||||
|
return fromAccountType.isCurrentAccount() && toAccountType.isLoanAccount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Withdraw for Current → Loan transfers. Loan domain service handles the loan-side GL,
|
||||||
|
* so current account side uses LIABILITY_TRANSFER as the counterpart (classic approach).
|
||||||
|
*/
|
||||||
|
private CurrentAccountTransaction performCurrentWithdrawalForLoan(final CurrentAccount account, final BigDecimal amount,
|
||||||
|
final LocalDate txnDate) {
|
||||||
|
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
|
||||||
|
final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO;
|
||||||
|
final BigDecimal available = balance.subtract(onHold);
|
||||||
|
if (!account.isAllowOverdraft() && amount.compareTo(available) > 0) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.current.account.insufficient.balance",
|
||||||
|
"Insufficient balance in current account " + account.getId()
|
||||||
|
+ ". Available: " + available + ", Requested: " + amount, account.getId());
|
||||||
|
}
|
||||||
|
final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, account.getOffice(), amount, txnDate, null);
|
||||||
|
final BigDecimal newBalance = balance.subtract(amount);
|
||||||
|
txn.setRunningBalanceDerived(newBalance);
|
||||||
|
currentAccountTransactionRepository.saveAndFlush(txn);
|
||||||
|
account.setAccountBalanceDerived(newBalance);
|
||||||
|
account.setTotalWithdrawalsDerived(
|
||||||
|
(account.getTotalWithdrawalsDerived() != null ? account.getTotalWithdrawalsDerived() : BigDecimal.ZERO).add(amount));
|
||||||
|
currentAccountRepository.save(account);
|
||||||
|
if (account.getProductId() != null && txn.getId() != null) {
|
||||||
|
final String currentTxnId = "CA" + txn.getId();
|
||||||
|
accountingProcessorHelper.createCashBasedJournalEntriesAndReversalsForSavings(account.getOffice(),
|
||||||
|
account.getCurrencyCode(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(),
|
||||||
|
FinancialActivity.LIABILITY_TRANSFER.getValue(), account.getProductId(), null,
|
||||||
|
account.getId(), currentTxnId, txnDate, amount, false);
|
||||||
|
}
|
||||||
|
return txn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Withdraw amount from a current account and save the transaction.
|
||||||
|
* Throws {@link GeneralPlatformDomainRuleException} if balance is insufficient.
|
||||||
|
*/
|
||||||
|
private CurrentAccountTransaction performCurrentWithdrawal(final CurrentAccount account, final BigDecimal amount,
|
||||||
|
final LocalDate txnDate, final Long counterpartProductId, final Long counterpartEntityId) {
|
||||||
|
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
|
||||||
|
final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO;
|
||||||
|
final BigDecimal available = balance.subtract(onHold);
|
||||||
|
if (!account.isAllowOverdraft() && amount.compareTo(available) > 0) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.current.account.insufficient.balance",
|
||||||
|
"Insufficient balance in current account " + account.getId()
|
||||||
|
+ ". Available: " + available + ", Requested: " + amount, account.getId());
|
||||||
|
}
|
||||||
|
final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, account.getOffice(), amount, txnDate, null);
|
||||||
|
final BigDecimal newBalance = balance.subtract(amount);
|
||||||
|
txn.setRunningBalanceDerived(newBalance);
|
||||||
|
currentAccountTransactionRepository.saveAndFlush(txn);
|
||||||
|
account.setAccountBalanceDerived(newBalance);
|
||||||
|
account.setTotalWithdrawalsDerived(
|
||||||
|
(account.getTotalWithdrawalsDerived() != null ? account.getTotalWithdrawalsDerived() : BigDecimal.ZERO).add(amount));
|
||||||
|
currentAccountRepository.save(account);
|
||||||
|
// Post journal entries: DR current account GL, CR counterpart GL — each with correct entity/account number
|
||||||
|
postCurrentAccountTransferJournalEntries(account, txn, amount, txnDate, true, counterpartProductId, counterpartEntityId);
|
||||||
|
return txn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deposit amount into a current account and save the transaction.
|
||||||
|
*/
|
||||||
|
private CurrentAccountTransaction performCurrentDeposit(final CurrentAccount account, final BigDecimal amount,
|
||||||
|
final LocalDate txnDate, final Long counterpartProductId, final Long counterpartEntityId) {
|
||||||
|
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
|
||||||
|
final CurrentAccountTransaction txn = CurrentAccountTransaction.deposit(account, account.getOffice(), amount, txnDate, null);
|
||||||
|
final BigDecimal newBalance = balance.add(amount);
|
||||||
|
txn.setRunningBalanceDerived(newBalance);
|
||||||
|
currentAccountTransactionRepository.saveAndFlush(txn);
|
||||||
|
account.setAccountBalanceDerived(newBalance);
|
||||||
|
account.setTotalDepositsDerived(
|
||||||
|
(account.getTotalDepositsDerived() != null ? account.getTotalDepositsDerived() : BigDecimal.ZERO).add(amount));
|
||||||
|
currentAccountRepository.save(account);
|
||||||
|
// Post journal entries: DR counterpart GL, CR current account GL — each with correct entity/account number
|
||||||
|
postCurrentAccountTransferJournalEntries(account, txn, amount, txnDate, false, counterpartProductId, counterpartEntityId);
|
||||||
|
return txn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts double-entry journal entries for a current account leg of an account transfer.
|
||||||
|
* <p>
|
||||||
|
* Current Product GL mappings are stored under {@code PortfolioProductType.SAVING}, so the
|
||||||
|
* savings JE helper resolves them correctly.
|
||||||
|
* <p>
|
||||||
|
* Transaction id is prefixed with "C" (e.g. {@code C57}) so the entries are NOT grouped with
|
||||||
|
* savings entries that share the same numeric id (those use prefix "S"). The helper only
|
||||||
|
* appends its own "S" prefix when the id is numeric; passing "C57" keeps it as-is.
|
||||||
|
*
|
||||||
|
* @param isWithdrawal {@code true} for the from-leg (DR control / CR liability transfer);
|
||||||
|
* {@code false} for the to-leg (DR liability transfer / CR control)
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Posts direct transfer journal entries between a current account and a counterpart savings/current account.
|
||||||
|
* Each entry carries the correct entity ID so the correct account number appears on each GL line.
|
||||||
|
*
|
||||||
|
* For withdrawal (current → savings):
|
||||||
|
* DR: current product GL (entity = current account)
|
||||||
|
* CR: counterpart product GL (entity = savings/current account)
|
||||||
|
*
|
||||||
|
* For deposit (savings → current):
|
||||||
|
* DR: counterpart product GL (entity = savings/current account)
|
||||||
|
* CR: current product GL (entity = current account)
|
||||||
|
*
|
||||||
|
* @param counterpartProductId savings/current product ID of the other account
|
||||||
|
* @param counterpartEntityId savings/current account ID of the other account
|
||||||
|
*/
|
||||||
|
private void postCurrentAccountTransferJournalEntries(final CurrentAccount account, final CurrentAccountTransaction txn,
|
||||||
|
final BigDecimal amount, final LocalDate txnDate, final boolean isWithdrawal,
|
||||||
|
final Long counterpartProductId, final Long counterpartEntityId) {
|
||||||
|
if (account.getProductId() == null || txn.getId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Prefix "CA" so:
|
||||||
|
// 1. The helper does NOT re-prefix with "S" (avoids txn id collision with savings "S" prefix)
|
||||||
|
// 2. JournalEntryReadPlatformServiceImpl joins m_current_account (transaction_id LIKE 'CA%')
|
||||||
|
// so the correct current account number appears on the DR/CR entry.
|
||||||
|
final String currentTxnId = "CA" + txn.getId();
|
||||||
|
if (isWithdrawal) {
|
||||||
|
// DR: current account GL CR: counterpart (savings/current) GL
|
||||||
|
accountingProcessorHelper.createDirectTransferJournalEntriesForSavings(
|
||||||
|
account.getOffice(), account.getCurrencyCode(),
|
||||||
|
account.getProductId(), account.getId(),
|
||||||
|
counterpartProductId, counterpartEntityId,
|
||||||
|
currentTxnId, txnDate, amount);
|
||||||
|
} else {
|
||||||
|
// DR: counterpart (savings/current) GL CR: current account GL
|
||||||
|
accountingProcessorHelper.createDirectTransferJournalEntriesForSavings(
|
||||||
|
account.getOffice(), account.getCurrencyCode(),
|
||||||
|
counterpartProductId, counterpartEntityId,
|
||||||
|
account.getProductId(), account.getId(),
|
||||||
|
currentTxnId, txnDate, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public CommandProcessingResult refundByTransfer(JsonCommand command) {
|
public CommandProcessingResult refundByTransfer(JsonCommand command) {
|
||||||
|
|||||||
+86
@@ -43,12 +43,14 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount
|
|||||||
private final PortfolioSavingsAccountMapper savingsAccountMapper;
|
private final PortfolioSavingsAccountMapper savingsAccountMapper;
|
||||||
private final PortfolioLoanAccountMapper loanAccountMapper;
|
private final PortfolioLoanAccountMapper loanAccountMapper;
|
||||||
private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper;
|
private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper;
|
||||||
|
private final PortfolioCurrentAccountMapper currentAccountMapper;
|
||||||
|
|
||||||
public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) {
|
public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||||
this.jdbcTemplate = jdbcTemplate;
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
this.savingsAccountMapper = new PortfolioSavingsAccountMapper();
|
this.savingsAccountMapper = new PortfolioSavingsAccountMapper();
|
||||||
this.loanAccountMapper = new PortfolioLoanAccountMapper();
|
this.loanAccountMapper = new PortfolioLoanAccountMapper();
|
||||||
this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper(sqlGenerator);
|
this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper(sqlGenerator);
|
||||||
|
this.currentAccountMapper = new PortfolioCurrentAccountMapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -86,6 +88,15 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount
|
|||||||
|
|
||||||
accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams);
|
accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams);
|
||||||
break;
|
break;
|
||||||
|
case CURRENT:
|
||||||
|
sql = "select " + this.currentAccountMapper.schema() + " where ca.id = ?";
|
||||||
|
if (currencyCode != null) {
|
||||||
|
sql += " and ca.currency_code = ?";
|
||||||
|
sqlParams = new Object[] { accountId, currencyCode };
|
||||||
|
}
|
||||||
|
|
||||||
|
accountData = this.jdbcTemplate.queryForObject(sql, this.currentAccountMapper, sqlParams);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} catch (final EmptyResultDataAccessException e) {
|
} catch (final EmptyResultDataAccessException e) {
|
||||||
throw new AccountTransferNotFoundException(accountId, e);
|
throw new AccountTransferNotFoundException(accountId, e);
|
||||||
@@ -157,6 +168,23 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount
|
|||||||
|
|
||||||
accounts = this.jdbcTemplate.query(sql, this.savingsAccountMapper, sqlParams.toArray()); // NOSONAR
|
accounts = this.jdbcTemplate.query(sql, this.savingsAccountMapper, sqlParams.toArray()); // NOSONAR
|
||||||
break;
|
break;
|
||||||
|
case CURRENT:
|
||||||
|
sql = "select " + this.currentAccountMapper.schema() + " where ";
|
||||||
|
if (portfolioAccountDTO.getClientId() != null) {
|
||||||
|
sql += " ca.client_id = ? and ca.status_enum in (?) ";
|
||||||
|
sqlParams.add(portfolioAccountDTO.getClientId());
|
||||||
|
sqlParams.add(defaultAccountStatus);
|
||||||
|
} else {
|
||||||
|
sql += " ca.status_enum in (?) ";
|
||||||
|
sqlParams.add(defaultAccountStatus);
|
||||||
|
}
|
||||||
|
if (portfolioAccountDTO.getCurrencyCode() != null) {
|
||||||
|
sql += " and ca.currency_code = ?";
|
||||||
|
sqlParams.add(portfolioAccountDTO.getCurrencyCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts = this.jdbcTemplate.query(sql, this.currentAccountMapper, sqlParams.toArray()); // NOSONAR
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return accounts;
|
return accounts;
|
||||||
@@ -393,4 +421,62 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount
|
|||||||
|
|
||||||
return accountData;
|
return accountData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class PortfolioCurrentAccountMapper implements RowMapper<PortfolioAccountData> {
|
||||||
|
|
||||||
|
private final String schemaSql;
|
||||||
|
|
||||||
|
PortfolioCurrentAccountMapper() {
|
||||||
|
final StringBuilder sqlBuilder = new StringBuilder(400);
|
||||||
|
sqlBuilder.append("ca.id as id, ca.account_no as accountNo, ca.external_id as externalId, ");
|
||||||
|
sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
|
||||||
|
sqlBuilder.append("null as groupId, null as groupName, ");
|
||||||
|
sqlBuilder.append("cp.id as productId, cp.name as productName, ");
|
||||||
|
sqlBuilder.append("null as fieldOfficerId, null as fieldOfficerName, ");
|
||||||
|
sqlBuilder.append("ca.currency_code as currencyCode, ca.currency_digits as currencyDigits, ");
|
||||||
|
sqlBuilder.append("ca.currency_multiplesof as inMultiplesOf, ");
|
||||||
|
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
|
||||||
|
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
|
||||||
|
sqlBuilder.append("from m_current_account ca ");
|
||||||
|
sqlBuilder.append("join m_current_product cp ON ca.product_id = cp.id ");
|
||||||
|
sqlBuilder.append("join m_currency curr on curr.code = ca.currency_code ");
|
||||||
|
sqlBuilder.append("left join m_client c ON c.id = ca.client_id ");
|
||||||
|
|
||||||
|
this.schemaSql = sqlBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String schema() {
|
||||||
|
return this.schemaSql;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
|
||||||
|
final Long id = rs.getLong("id");
|
||||||
|
final String accountNo = rs.getString("accountNo");
|
||||||
|
final String externalId = rs.getString("externalId");
|
||||||
|
|
||||||
|
final Long groupId = JdbcSupport.getLong(rs, "groupId");
|
||||||
|
final String groupName = rs.getString("groupName");
|
||||||
|
final Long clientId = JdbcSupport.getLong(rs, "clientId");
|
||||||
|
final String clientName = rs.getString("clientName");
|
||||||
|
|
||||||
|
final Long productId = rs.getLong("productId");
|
||||||
|
final String productName = rs.getString("productName");
|
||||||
|
|
||||||
|
final Long fieldOfficerId = JdbcSupport.getLong(rs, "fieldOfficerId");
|
||||||
|
final String fieldOfficerName = rs.getString("fieldOfficerName");
|
||||||
|
|
||||||
|
final String currencyCode = rs.getString("currencyCode");
|
||||||
|
final String currencyName = rs.getString("currencyName");
|
||||||
|
final String currencyNameCode = rs.getString("currencyNameCode");
|
||||||
|
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
|
||||||
|
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
|
||||||
|
final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
|
||||||
|
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf,
|
||||||
|
currencyDisplaySymbol, currencyNameCode);
|
||||||
|
|
||||||
|
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
|
||||||
|
fieldOfficerId, fieldOfficerName, currency);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-6
@@ -109,20 +109,21 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
|
|||||||
|
|
||||||
final EnumOptionData loanAccountType = accountType(PortfolioAccountType.LOAN);
|
final EnumOptionData loanAccountType = accountType(PortfolioAccountType.LOAN);
|
||||||
final EnumOptionData savingsAccountType = accountType(PortfolioAccountType.SAVINGS);
|
final EnumOptionData savingsAccountType = accountType(PortfolioAccountType.SAVINGS);
|
||||||
|
final EnumOptionData currentAccountType = accountType(PortfolioAccountType.CURRENT);
|
||||||
|
|
||||||
final Integer mostRelevantFromAccountType = fromAccountType;
|
final Integer mostRelevantFromAccountType = fromAccountType;
|
||||||
Collection<EnumOptionData> fromAccountTypeOptions = null;
|
Collection<EnumOptionData> fromAccountTypeOptions = null;
|
||||||
Collection<EnumOptionData> toAccountTypeOptions = null;
|
Collection<EnumOptionData> toAccountTypeOptions = null;
|
||||||
|
|
||||||
if (accountTransferType.isAccountTransfer()) {
|
if (accountTransferType.isAccountTransfer()) {
|
||||||
fromAccountTypeOptions = Arrays.asList(savingsAccountType);
|
fromAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType);
|
||||||
toAccountTypeOptions = Arrays.asList(savingsAccountType);
|
toAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType);
|
||||||
} else if (accountTransferType.isLoanRepayment()) {
|
} else if (accountTransferType.isLoanRepayment()) {
|
||||||
fromAccountTypeOptions = Arrays.asList(savingsAccountType);
|
fromAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType);
|
||||||
toAccountTypeOptions = Arrays.asList(loanAccountType);
|
toAccountTypeOptions = Arrays.asList(loanAccountType);
|
||||||
} else {
|
} else {
|
||||||
fromAccountTypeOptions = Arrays.asList(savingsAccountType, loanAccountType);
|
fromAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType, loanAccountType);
|
||||||
toAccountTypeOptions = Arrays.asList(loanAccountType, savingsAccountType);
|
toAccountTypeOptions = Arrays.asList(loanAccountType, savingsAccountType, currentAccountType);
|
||||||
}
|
}
|
||||||
final Integer mostRelevantToAccountType = toAccountType;
|
final Integer mostRelevantToAccountType = toAccountType;
|
||||||
|
|
||||||
@@ -152,6 +153,8 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
|
|||||||
Integer accountType;
|
Integer accountType;
|
||||||
if (mostRelevantFromAccountType == 1) {
|
if (mostRelevantFromAccountType == 1) {
|
||||||
accountType = PortfolioAccountType.LOAN.getValue();
|
accountType = PortfolioAccountType.LOAN.getValue();
|
||||||
|
} else if (mostRelevantFromAccountType == 3) {
|
||||||
|
accountType = PortfolioAccountType.CURRENT.getValue();
|
||||||
} else {
|
} else {
|
||||||
accountType = PortfolioAccountType.SAVINGS.getValue();
|
accountType = PortfolioAccountType.SAVINGS.getValue();
|
||||||
}
|
}
|
||||||
@@ -301,6 +304,9 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
|
|||||||
} else if (accountType.isLoanAccount()) {
|
} else if (accountType.isLoanAccount()) {
|
||||||
sqlBuilder.append(" fromloanacc.id=? ");
|
sqlBuilder.append(" fromloanacc.id=? ");
|
||||||
paramObj.add(standingInstructionDTO.fromAccount());
|
paramObj.add(standingInstructionDTO.fromAccount());
|
||||||
|
} else if (accountType.isCurrentAccount()) {
|
||||||
|
sqlBuilder.append(" fromcuracc.id=? ");
|
||||||
|
paramObj.add(standingInstructionDTO.fromAccount());
|
||||||
}
|
}
|
||||||
addAndCaluse = true;
|
addAndCaluse = true;
|
||||||
}
|
}
|
||||||
@@ -387,7 +393,11 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
|
|||||||
sqlBuilder.append("tosavacc.id as toSavingsAccountId, tosavacc.account_no as toSavingsAccountNo,");
|
sqlBuilder.append("tosavacc.id as toSavingsAccountId, tosavacc.account_no as toSavingsAccountNo,");
|
||||||
sqlBuilder.append("tosp.id as toProductId, tosp.name as toProductName, ");
|
sqlBuilder.append("tosp.id as toProductId, tosp.name as toProductName, ");
|
||||||
sqlBuilder.append("toloanacc.id as toLoanAccountId, toloanacc.account_no as toLoanAccountNo, ");
|
sqlBuilder.append("toloanacc.id as toLoanAccountId, toloanacc.account_no as toLoanAccountNo, ");
|
||||||
sqlBuilder.append("tolp.id as toLoanProductId, tolp.name as toLoanProductName ");
|
sqlBuilder.append("tolp.id as toLoanProductId, tolp.name as toLoanProductName, ");
|
||||||
|
sqlBuilder.append("fromcuracc.id as fromCurrentAccountId, fromcuracc.account_no as fromCurrentAccountNo, ");
|
||||||
|
sqlBuilder.append("fromcp.id as fromCurrentProductId, fromcp.name as fromCurrentProductName, ");
|
||||||
|
sqlBuilder.append("tocuracc.id as toCurrentAccountId, tocuracc.account_no as toCurrentAccountNo, ");
|
||||||
|
sqlBuilder.append("tocp.id as toCurrentProductId, tocp.name as toCurrentProductName ");
|
||||||
sqlBuilder.append(" FROM m_account_transfer_standing_instructions atsi ");
|
sqlBuilder.append(" FROM m_account_transfer_standing_instructions atsi ");
|
||||||
sqlBuilder.append("join m_account_transfer_details atd on atd.id = atsi.account_transfer_details_id ");
|
sqlBuilder.append("join m_account_transfer_details atd on atd.id = atsi.account_transfer_details_id ");
|
||||||
sqlBuilder.append("join m_office fromoff on fromoff.id = atd.from_office_id ");
|
sqlBuilder.append("join m_office fromoff on fromoff.id = atd.from_office_id ");
|
||||||
@@ -402,6 +412,10 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
|
|||||||
sqlBuilder.append("left join m_savings_product tosp ON tosavacc.product_id = tosp.id ");
|
sqlBuilder.append("left join m_savings_product tosp ON tosavacc.product_id = tosp.id ");
|
||||||
sqlBuilder.append("left join m_loan toloanacc on toloanacc.id = atd.to_loan_account_id ");
|
sqlBuilder.append("left join m_loan toloanacc on toloanacc.id = atd.to_loan_account_id ");
|
||||||
sqlBuilder.append("left join m_product_loan tolp ON toloanacc.product_id = tolp.id ");
|
sqlBuilder.append("left join m_product_loan tolp ON toloanacc.product_id = tolp.id ");
|
||||||
|
sqlBuilder.append("left join m_current_account fromcuracc on fromcuracc.id = atd.from_current_account_id ");
|
||||||
|
sqlBuilder.append("left join m_current_product fromcp ON fromcuracc.product_id = fromcp.id ");
|
||||||
|
sqlBuilder.append("left join m_current_account tocuracc on tocuracc.id = atd.to_current_account_id ");
|
||||||
|
sqlBuilder.append("left join m_current_product tocp ON tocuracc.product_id = tocp.id ");
|
||||||
|
|
||||||
this.schemaSql = sqlBuilder.toString();
|
this.schemaSql = sqlBuilder.toString();
|
||||||
}
|
}
|
||||||
@@ -489,6 +503,16 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
|
|||||||
fromAccount = new PortfolioAccountData(fromLoanAccountId, fromLoanAccountNo, null, null, null, null, null,
|
fromAccount = new PortfolioAccountData(fromLoanAccountId, fromLoanAccountNo, null, null, null, null, null,
|
||||||
fromLoanProductId, fromLoanProductName, null, null, null);
|
fromLoanProductId, fromLoanProductName, null, null, null);
|
||||||
fromAccountType = accountType(PortfolioAccountType.LOAN);
|
fromAccountType = accountType(PortfolioAccountType.LOAN);
|
||||||
|
} else {
|
||||||
|
final Long fromCurrentAccountId = JdbcSupport.getLong(rs, "fromCurrentAccountId");
|
||||||
|
if (fromCurrentAccountId != null) {
|
||||||
|
final String fromCurrentAccountNo = rs.getString("fromCurrentAccountNo");
|
||||||
|
final Long fromCurrentProductId = JdbcSupport.getLong(rs, "fromCurrentProductId");
|
||||||
|
final String fromCurrentProductName = rs.getString("fromCurrentProductName");
|
||||||
|
fromAccount = new PortfolioAccountData(fromCurrentAccountId, fromCurrentAccountNo, null, null, null, null, null,
|
||||||
|
fromCurrentProductId, fromCurrentProductName, null, null, null);
|
||||||
|
fromAccountType = accountType(PortfolioAccountType.CURRENT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PortfolioAccountData toAccount = null;
|
PortfolioAccountData toAccount = null;
|
||||||
@@ -510,6 +534,16 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
|
|||||||
toAccount = new PortfolioAccountData(toLoanAccountId, toLoanAccountNo, null, null, null, null, null, toLoanProductId,
|
toAccount = new PortfolioAccountData(toLoanAccountId, toLoanAccountNo, null, null, null, null, null, toLoanProductId,
|
||||||
toLoanProductName, null, null, null);
|
toLoanProductName, null, null, null);
|
||||||
toAccountType = accountType(PortfolioAccountType.LOAN);
|
toAccountType = accountType(PortfolioAccountType.LOAN);
|
||||||
|
} else {
|
||||||
|
final Long toCurrentAccountId = JdbcSupport.getLong(rs, "toCurrentAccountId");
|
||||||
|
if (toCurrentAccountId != null) {
|
||||||
|
final String toCurrentAccountNo = rs.getString("toCurrentAccountNo");
|
||||||
|
final Long toCurrentProductId = JdbcSupport.getLong(rs, "toCurrentProductId");
|
||||||
|
final String toCurrentProductName = rs.getString("toCurrentProductName");
|
||||||
|
toAccount = new PortfolioAccountData(toCurrentAccountId, toCurrentAccountNo, null, null, null, null, null,
|
||||||
|
toCurrentProductId, toCurrentProductName, null, null, null);
|
||||||
|
toAccountType = accountType(PortfolioAccountType.CURRENT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return StandingInstructionData.instance(id, accountDetailId, name, fromOffice, toOffice, fromClient, toClient, fromAccountType,
|
return StandingInstructionData.instance(id, accountDetailId, name, fromOffice, toOffice, fromClient, toClient, fromAccountType,
|
||||||
|
|||||||
+36
-2
@@ -82,11 +82,25 @@ public class StandingInstructionWritePlatformServiceImpl implements StandingInst
|
|||||||
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
|
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
|
||||||
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
|
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
|
||||||
} else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) {
|
} else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) {
|
||||||
|
|
||||||
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command);
|
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command);
|
||||||
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
|
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
|
||||||
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
|
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
|
||||||
|
} else if (isCurrentToCurrentAccountTransfer(fromAccountType, toAccountType)) {
|
||||||
|
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToCurrentTransfer(command);
|
||||||
|
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
|
||||||
|
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
|
||||||
|
} else if (isCurrentToSavingsAccountTransfer(fromAccountType, toAccountType)) {
|
||||||
|
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToSavingsTransfer(command);
|
||||||
|
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
|
||||||
|
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
|
||||||
|
} else if (isSavingsToCurrentAccountTransfer(fromAccountType, toAccountType)) {
|
||||||
|
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToCurrentTransfer(command);
|
||||||
|
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
|
||||||
|
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
|
||||||
|
} else if (isCurrentToLoanAccountTransfer(fromAccountType, toAccountType)) {
|
||||||
|
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToLoanTransfer(command);
|
||||||
|
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
|
||||||
|
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
|
||||||
}
|
}
|
||||||
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||||
final Throwable throwable = dve.getMostSpecificCause();
|
final Throwable throwable = dve.getMostSpecificCause();
|
||||||
@@ -122,6 +136,26 @@ public class StandingInstructionWritePlatformServiceImpl implements StandingInst
|
|||||||
return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount();
|
return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isCurrentToCurrentAccountTransfer(final PortfolioAccountType fromAccountType,
|
||||||
|
final PortfolioAccountType toAccountType) {
|
||||||
|
return fromAccountType.isCurrentAccount() && toAccountType.isCurrentAccount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCurrentToSavingsAccountTransfer(final PortfolioAccountType fromAccountType,
|
||||||
|
final PortfolioAccountType toAccountType) {
|
||||||
|
return fromAccountType.isCurrentAccount() && toAccountType.isSavingsAccount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSavingsToCurrentAccountTransfer(final PortfolioAccountType fromAccountType,
|
||||||
|
final PortfolioAccountType toAccountType) {
|
||||||
|
return fromAccountType.isSavingsAccount() && toAccountType.isCurrentAccount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCurrentToLoanAccountTransfer(final PortfolioAccountType fromAccountType,
|
||||||
|
final PortfolioAccountType toAccountType) {
|
||||||
|
return fromAccountType.isCurrentAccount() && toAccountType.isLoanAccount();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult update(final Long id, final JsonCommand command) {
|
public CommandProcessingResult update(final Long id, final JsonCommand command) {
|
||||||
this.standingInstructionDataValidator.validateForUpdate(command);
|
this.standingInstructionDataValidator.validateForUpdate(command);
|
||||||
|
|||||||
+8
-2
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.account.starter;
|
package org.apache.fineract.portfolio.account.starter;
|
||||||
|
|
||||||
|
import org.apache.fineract.accounting.journalentry.service.AccountingProcessorHelper;
|
||||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||||
@@ -26,6 +27,8 @@ import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecific
|
|||||||
import org.apache.fineract.infrastructure.security.service.SqlValidator;
|
import org.apache.fineract.infrastructure.security.service.SqlValidator;
|
||||||
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
|
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
|
||||||
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
|
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
|
||||||
|
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
|
||||||
|
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository;
|
||||||
import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator;
|
import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator;
|
||||||
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
|
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
|
||||||
import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler;
|
import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler;
|
||||||
@@ -91,11 +94,14 @@ public class AccountConfiguration {
|
|||||||
LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService,
|
LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService,
|
||||||
AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService,
|
AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService,
|
||||||
GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
|
GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
|
||||||
FineractProperties fineractProperties) {
|
FineractProperties fineractProperties, CurrentAccountRepository currentAccountRepository,
|
||||||
|
CurrentAccountTransactionRepository currentAccountTransactionRepository,
|
||||||
|
AccountingProcessorHelper accountingProcessorHelper) {
|
||||||
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler,
|
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler,
|
||||||
accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler,
|
accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler,
|
||||||
loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService,
|
loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService,
|
||||||
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties);
|
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties,
|
||||||
|
currentAccountRepository, currentAccountTransactionRepository, accountingProcessorHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+276
@@ -0,0 +1,276 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashexchange.api;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.cashexchange.domain.CashExchange;
|
||||||
|
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
|
||||||
|
import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeDetail;
|
||||||
|
import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeDetailRepository;
|
||||||
|
import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeRepository;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Path("/v1/cashexchange")
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
public class CashExchangeApiResource {
|
||||||
|
|
||||||
|
private final CashExchangeRepository repository;
|
||||||
|
private final CashExchangeDetailRepository detailRepository;
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Transactional
|
||||||
|
public String create(String body) {
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
|
||||||
|
final Long accountId = json.has("accountId") && !json.get("accountId").isJsonNull() ? json.get("accountId").getAsLong() : null;
|
||||||
|
final String accountNo = json.has("accountNo") && !json.get("accountNo").isJsonNull() ? json.get("accountNo").getAsString()
|
||||||
|
: null;
|
||||||
|
final Long clientId = json.has("clientId") && !json.get("clientId").isJsonNull() ? json.get("clientId").getAsLong() : null;
|
||||||
|
final LocalDate exchangeDate = json.has("exchangeDate") && !json.get("exchangeDate").isJsonNull()
|
||||||
|
? LocalDate.parse(json.get("exchangeDate").getAsString())
|
||||||
|
: null;
|
||||||
|
final String note = json.has("note") && !json.get("note").isJsonNull() ? json.get("note").getAsString() : null;
|
||||||
|
final String customerIdProof = json.has("customerIdProof") && !json.get("customerIdProof").isJsonNull()
|
||||||
|
? json.get("customerIdProof").getAsString() : null;
|
||||||
|
|
||||||
|
final JsonArray received = json.has("received") && json.get("received").isJsonArray() ? json.getAsJsonArray("received")
|
||||||
|
: new JsonArray();
|
||||||
|
final JsonArray given = json.has("given") && json.get("given").isJsonArray() ? json.getAsJsonArray("given") : new JsonArray();
|
||||||
|
|
||||||
|
final BigDecimal receivedTotal = sumTotal(received);
|
||||||
|
final BigDecimal givenTotal = sumTotal(given);
|
||||||
|
|
||||||
|
if (!hasPositiveCount(received) || !hasPositiveCount(given)) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashexchange.empty",
|
||||||
|
"Both received and given denominations are required.");
|
||||||
|
}
|
||||||
|
if (receivedTotal.compareTo(givenTotal) != 0) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashexchange.mismatch",
|
||||||
|
"Received total (" + receivedTotal + ") must equal given total (" + givenTotal + ").");
|
||||||
|
}
|
||||||
|
if (receivedTotal.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashexchange.empty",
|
||||||
|
"Both received and given denominations are required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
CashExchange header = new CashExchange();
|
||||||
|
header.setAccountId(accountId);
|
||||||
|
header.setAccountNo(accountNo);
|
||||||
|
header.setClientId(clientId);
|
||||||
|
header.setExchangeAmount(receivedTotal);
|
||||||
|
header.setExchangeDate(exchangeDate);
|
||||||
|
header.setNote(note);
|
||||||
|
header.setCustomerIdProof(customerIdProof);
|
||||||
|
header.setIsReversed(false);
|
||||||
|
header.setCreatedAt(LocalDateTime.now());
|
||||||
|
try {
|
||||||
|
AppUser user = this.context.authenticatedUser();
|
||||||
|
if (user != null) {
|
||||||
|
header.setPerformedBy(user.getId());
|
||||||
|
header.setPerformedByUsername(user.getUsername());
|
||||||
|
// Auto-detect cashier linked to this user
|
||||||
|
try {
|
||||||
|
Long cashierId = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id "
|
||||||
|
+ "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?",
|
||||||
|
Long.class, user.getId());
|
||||||
|
header.setCashierId(cashierId);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// resolving the current user must never block recording the exchange
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that cashier has enough of each denomination being GIVEN to the customer
|
||||||
|
if (header.getCashierId() != null) {
|
||||||
|
final java.util.Map<Long, Long> stock = denominationBreakdownWriter.getCashierNetPosition(header.getCashierId());
|
||||||
|
final java.util.List<String> shortages = new java.util.ArrayList<>();
|
||||||
|
for (JsonElement el : given) {
|
||||||
|
JsonObject item = el.getAsJsonObject();
|
||||||
|
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
|
||||||
|
if (count <= 0) continue;
|
||||||
|
if (!item.has("denominationId") || item.get("denominationId").isJsonNull()) continue;
|
||||||
|
long denomId = item.get("denominationId").getAsLong();
|
||||||
|
long available = stock.getOrDefault(denomId, 0L);
|
||||||
|
if (count > available) {
|
||||||
|
int denomValue = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
|
||||||
|
? item.get("denominationValue").getAsInt() : 0;
|
||||||
|
shortages.add("₹" + denomValue + " (need " + count + ", available " + available + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!shortages.isEmpty()) {
|
||||||
|
throw new PlatformDataIntegrityException(
|
||||||
|
"error.msg.cashier.denomination.insufficient",
|
||||||
|
"Insufficient denomination stock for exchange — cannot give: " + String.join("; ", shortages));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CashExchange saved = repository.saveAndFlush(header);
|
||||||
|
|
||||||
|
saveDetails(saved.getId(), "RECEIVED", received);
|
||||||
|
saveDetails(saved.getId(), "GIVEN", given);
|
||||||
|
|
||||||
|
JsonObject result = new JsonObject();
|
||||||
|
result.addProperty("resourceId", saved.getId());
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{id}/reverse")
|
||||||
|
public String reverse(@PathParam("id") Long id, String body) {
|
||||||
|
CashExchange exchange = repository.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Cash exchange not found: " + id));
|
||||||
|
if (Boolean.TRUE.equals(exchange.getIsReversed())) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashexchange.already.reversed",
|
||||||
|
"Cash exchange " + id + " is already reversed.");
|
||||||
|
}
|
||||||
|
String reason = null;
|
||||||
|
try {
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
reason = json.has("reason") && !json.get("reason").isJsonNull() ? json.get("reason").getAsString() : null;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
exchange.setIsReversed(true);
|
||||||
|
exchange.setReversedAt(LocalDateTime.now());
|
||||||
|
exchange.setReverseReason(reason);
|
||||||
|
try {
|
||||||
|
AppUser user = this.context.authenticatedUser();
|
||||||
|
if (user != null) exchange.setReversedBy(user.getId());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
repository.save(exchange);
|
||||||
|
JsonObject result = new JsonObject();
|
||||||
|
result.addProperty("resourceId", id);
|
||||||
|
result.addProperty("reversed", true);
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public String getAll(@QueryParam("accountId") Long accountId) {
|
||||||
|
List<CashExchange> list = accountId != null ? repository.findByAccountIdOrderByCreatedAtDesc(accountId)
|
||||||
|
: repository.findAllByOrderByCreatedAtDesc();
|
||||||
|
return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{id}")
|
||||||
|
public String getOne(@PathParam("id") Long id) {
|
||||||
|
CashExchange header = repository.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Cash exchange not found: " + id));
|
||||||
|
Map<String, Object> map = toMap(header);
|
||||||
|
List<Map<String, Object>> details = detailRepository.findByExchangeId(id).stream().map(this::detailToMap)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
map.put("details", details);
|
||||||
|
return gson.toJson(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal sumTotal(JsonArray items) {
|
||||||
|
BigDecimal total = BigDecimal.ZERO;
|
||||||
|
for (JsonElement el : items) {
|
||||||
|
JsonObject item = el.getAsJsonObject();
|
||||||
|
int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
|
||||||
|
? item.get("denominationValue").getAsInt()
|
||||||
|
: 0;
|
||||||
|
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
|
||||||
|
if (count > 0) {
|
||||||
|
total = total.add(BigDecimal.valueOf((long) value * count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasPositiveCount(JsonArray items) {
|
||||||
|
for (JsonElement el : items) {
|
||||||
|
JsonObject item = el.getAsJsonObject();
|
||||||
|
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
|
||||||
|
if (count > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveDetails(Long exchangeId, String direction, JsonArray items) {
|
||||||
|
for (JsonElement el : items) {
|
||||||
|
JsonObject item = el.getAsJsonObject();
|
||||||
|
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
|
||||||
|
if (count <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
|
||||||
|
? item.get("denominationValue").getAsInt()
|
||||||
|
: 0;
|
||||||
|
Long denominationId = item.has("denominationId") && !item.get("denominationId").isJsonNull()
|
||||||
|
? item.get("denominationId").getAsLong()
|
||||||
|
: null;
|
||||||
|
CashExchangeDetail detail = new CashExchangeDetail();
|
||||||
|
detail.setExchangeId(exchangeId);
|
||||||
|
detail.setDirection(direction);
|
||||||
|
detail.setDenominationId(denominationId);
|
||||||
|
detail.setDenominationValue(value);
|
||||||
|
detail.setCount(count);
|
||||||
|
detail.setTotalAmount(BigDecimal.valueOf((long) value * count));
|
||||||
|
detailRepository.save(detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toMap(CashExchange e) {
|
||||||
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
|
map.put("id", e.getId());
|
||||||
|
map.put("cashierId", e.getCashierId());
|
||||||
|
map.put("accountId", e.getAccountId());
|
||||||
|
map.put("accountNo", e.getAccountNo());
|
||||||
|
map.put("clientId", e.getClientId());
|
||||||
|
map.put("customerIdProof", e.getCustomerIdProof());
|
||||||
|
map.put("exchangeAmount", e.getExchangeAmount());
|
||||||
|
map.put("exchangeDate", e.getExchangeDate() != null ? e.getExchangeDate().toString() : null);
|
||||||
|
map.put("note", e.getNote());
|
||||||
|
map.put("performedBy", e.getPerformedBy());
|
||||||
|
map.put("performedByUsername", e.getPerformedByUsername());
|
||||||
|
map.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null);
|
||||||
|
map.put("isReversed", Boolean.TRUE.equals(e.getIsReversed()));
|
||||||
|
map.put("reversedBy", e.getReversedBy());
|
||||||
|
map.put("reversedAt", e.getReversedAt() != null ? e.getReversedAt().toString() : null);
|
||||||
|
map.put("reverseReason", e.getReverseReason());
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> detailToMap(CashExchangeDetail d) {
|
||||||
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
|
map.put("id", d.getId());
|
||||||
|
map.put("direction", d.getDirection());
|
||||||
|
map.put("denominationId", d.getDenominationId());
|
||||||
|
map.put("denominationValue", d.getDenominationValue());
|
||||||
|
map.put("count", d.getCount());
|
||||||
|
map.put("totalAmount", d.getTotalAmount());
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.cashexchange.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_cash_exchange")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CashExchange {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "account_id")
|
||||||
|
private Long accountId;
|
||||||
|
|
||||||
|
@Column(name = "account_no", length = 50)
|
||||||
|
private String accountNo;
|
||||||
|
|
||||||
|
@Column(name = "client_id")
|
||||||
|
private Long clientId;
|
||||||
|
|
||||||
|
@Column(name = "exchange_amount", nullable = false)
|
||||||
|
private BigDecimal exchangeAmount;
|
||||||
|
|
||||||
|
@Column(name = "exchange_date")
|
||||||
|
private LocalDate exchangeDate;
|
||||||
|
|
||||||
|
@Column(name = "note", length = 500)
|
||||||
|
private String note;
|
||||||
|
|
||||||
|
@Column(name = "cashier_id")
|
||||||
|
private Long cashierId;
|
||||||
|
|
||||||
|
@Column(name = "customer_id_proof", length = 50)
|
||||||
|
private String customerIdProof;
|
||||||
|
|
||||||
|
@Column(name = "performed_by")
|
||||||
|
private Long performedBy;
|
||||||
|
|
||||||
|
@Column(name = "performed_by_username", length = 100)
|
||||||
|
private String performedByUsername;
|
||||||
|
|
||||||
|
@Column(name = "created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "is_reversed")
|
||||||
|
private Boolean isReversed;
|
||||||
|
|
||||||
|
@Column(name = "reversed_by")
|
||||||
|
private Long reversedBy;
|
||||||
|
|
||||||
|
@Column(name = "reversed_at")
|
||||||
|
private LocalDateTime reversedAt;
|
||||||
|
|
||||||
|
@Column(name = "reverse_reason", length = 500)
|
||||||
|
private String reverseReason;
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.cashexchange.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_cash_exchange_detail")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CashExchangeDetail {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "exchange_id", nullable = false)
|
||||||
|
private Long exchangeId;
|
||||||
|
|
||||||
|
@Column(name = "direction", nullable = false, length = 10)
|
||||||
|
private String direction;
|
||||||
|
|
||||||
|
@Column(name = "denomination_id")
|
||||||
|
private Long denominationId;
|
||||||
|
|
||||||
|
@Column(name = "denomination_value", nullable = false)
|
||||||
|
private Integer denominationValue;
|
||||||
|
|
||||||
|
@Column(name = "count", nullable = false)
|
||||||
|
private Integer count;
|
||||||
|
|
||||||
|
@Column(name = "total_amount", nullable = false)
|
||||||
|
private BigDecimal totalAmount;
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashexchange.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface CashExchangeDetailRepository extends JpaRepository<CashExchangeDetail, Long> {
|
||||||
|
List<CashExchangeDetail> findByExchangeId(Long exchangeId);
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashexchange.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface CashExchangeRepository extends JpaRepository<CashExchange, Long> {
|
||||||
|
List<CashExchange> findAllByOrderByCreatedAtDesc();
|
||||||
|
List<CashExchange> findByAccountIdOrderByCreatedAtDesc(Long accountId);
|
||||||
|
}
|
||||||
+224
@@ -0,0 +1,224 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashintercashier.api;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashier;
|
||||||
|
import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashierDetail;
|
||||||
|
import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashierDetailRepository;
|
||||||
|
import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashierRepository;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Path("/v1/cashintercashier")
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
public class CashInterCashierApiResource {
|
||||||
|
|
||||||
|
private final CashInterCashierRepository repository;
|
||||||
|
private final CashInterCashierDetailRepository detailRepository;
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Transactional
|
||||||
|
public String create(String body) {
|
||||||
|
context.authenticatedUser().validateHasPermissionTo("CREATE_CASHINTERCASHIER");
|
||||||
|
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
|
||||||
|
final Long tellerId = json.has("tellerId") && !json.get("tellerId").isJsonNull()
|
||||||
|
? json.get("tellerId").getAsLong() : null;
|
||||||
|
final Long fromCashierId = json.has("fromCashierId") && !json.get("fromCashierId").isJsonNull()
|
||||||
|
? json.get("fromCashierId").getAsLong() : null;
|
||||||
|
final Long toCashierId = json.has("toCashierId") && !json.get("toCashierId").isJsonNull()
|
||||||
|
? json.get("toCashierId").getAsLong() : null;
|
||||||
|
|
||||||
|
if (tellerId == null || fromCashierId == null || toCashierId == null) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashintercashier.parties.required",
|
||||||
|
"tellerId, fromCashierId and toCashierId are required.");
|
||||||
|
}
|
||||||
|
if (fromCashierId.equals(toCashierId)) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashintercashier.same.cashier",
|
||||||
|
"From-cashier and To-cashier cannot be the same.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull()
|
||||||
|
? LocalDate.parse(json.get("transferDate").getAsString()) : LocalDate.now();
|
||||||
|
final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull()
|
||||||
|
? json.get("fromParty").getAsString() : null;
|
||||||
|
final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull()
|
||||||
|
? json.get("toParty").getAsString() : null;
|
||||||
|
final String note = json.has("note") && !json.get("note").isJsonNull()
|
||||||
|
? json.get("note").getAsString() : null;
|
||||||
|
|
||||||
|
final JsonArray denominations = json.has("denominations") && json.get("denominations").isJsonArray()
|
||||||
|
? json.getAsJsonArray("denominations") : new JsonArray();
|
||||||
|
|
||||||
|
if (!hasPositiveCount(denominations)) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashintercashier.empty",
|
||||||
|
"At least one denomination with a positive count is required.");
|
||||||
|
}
|
||||||
|
final BigDecimal total = sumTotal(denominations);
|
||||||
|
|
||||||
|
CashInterCashier header = new CashInterCashier();
|
||||||
|
header.setTransferDate(transferDate);
|
||||||
|
header.setTellerId(tellerId);
|
||||||
|
header.setFromCashierId(fromCashierId);
|
||||||
|
header.setToCashierId(toCashierId);
|
||||||
|
header.setFromParty(fromParty);
|
||||||
|
header.setToParty(toParty);
|
||||||
|
header.setTransferAmount(total);
|
||||||
|
header.setNote(note);
|
||||||
|
header.setCreatedAt(LocalDateTime.now());
|
||||||
|
header.setIsReversed(false);
|
||||||
|
try {
|
||||||
|
AppUser user = this.context.authenticatedUser();
|
||||||
|
header.setPerformedBy(user.getId());
|
||||||
|
header.setPerformedByUsername(user.getUsername());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
CashInterCashier saved = repository.saveAndFlush(header);
|
||||||
|
saveDetails(saved.getId(), denominations);
|
||||||
|
|
||||||
|
JsonObject result = new JsonObject();
|
||||||
|
result.addProperty("resourceId", saved.getId());
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{id}/reverse")
|
||||||
|
@Transactional
|
||||||
|
public String reverse(@PathParam("id") Long id, String body) {
|
||||||
|
context.authenticatedUser().validateHasPermissionTo("REVERSE_CASHINTERCASHIER");
|
||||||
|
|
||||||
|
CashInterCashier record = repository.findById(id)
|
||||||
|
.orElseThrow(() -> new PlatformDataIntegrityException("error.msg.cashintercashier.not.found",
|
||||||
|
"Inter-cashier transfer not found: " + id));
|
||||||
|
if (Boolean.TRUE.equals(record.getIsReversed())) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashintercashier.already.reversed",
|
||||||
|
"Inter-cashier transfer " + id + " is already reversed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String reason = null;
|
||||||
|
try {
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
reason = json.has("reason") ? json.get("reason").getAsString() : null;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
AppUser user = null;
|
||||||
|
try { user = this.context.authenticatedUser(); } catch (Exception ignored) {}
|
||||||
|
|
||||||
|
record.setIsReversed(true);
|
||||||
|
record.setReversedAt(LocalDateTime.now());
|
||||||
|
record.setReverseReason(reason);
|
||||||
|
if (user != null) record.setReversedBy(user.getId());
|
||||||
|
repository.save(record);
|
||||||
|
|
||||||
|
JsonObject result = new JsonObject();
|
||||||
|
result.addProperty("resourceId", id);
|
||||||
|
result.addProperty("reversed", true);
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public String getAll() {
|
||||||
|
return gson.toJson(repository.findAllByOrderByCreatedAtDesc().stream()
|
||||||
|
.map(this::toMap).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{id}")
|
||||||
|
public String getOne(@PathParam("id") Long id) {
|
||||||
|
CashInterCashier header = repository.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Inter-cashier transfer not found: " + id));
|
||||||
|
Map<String, Object> map = toMap(header);
|
||||||
|
map.put("details", detailRepository.findByInterCashierId(id).stream()
|
||||||
|
.map(this::detailToMap).collect(Collectors.toList()));
|
||||||
|
return gson.toJson(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal sumTotal(JsonArray items) {
|
||||||
|
BigDecimal total = BigDecimal.ZERO;
|
||||||
|
for (JsonElement el : items) {
|
||||||
|
JsonObject item = el.getAsJsonObject();
|
||||||
|
int value = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0;
|
||||||
|
int count = item.has("count") ? item.get("count").getAsInt() : 0;
|
||||||
|
if (count > 0) total = total.add(BigDecimal.valueOf((long) value * count));
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasPositiveCount(JsonArray items) {
|
||||||
|
for (JsonElement el : items) {
|
||||||
|
if (el.getAsJsonObject().get("count").getAsInt() > 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveDetails(Long interCashierId, JsonArray items) {
|
||||||
|
for (JsonElement el : items) {
|
||||||
|
JsonObject item = el.getAsJsonObject();
|
||||||
|
int count = item.has("count") ? item.get("count").getAsInt() : 0;
|
||||||
|
if (count <= 0) continue;
|
||||||
|
int value = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0;
|
||||||
|
Long denomId = item.has("denominationId") && !item.get("denominationId").isJsonNull()
|
||||||
|
? item.get("denominationId").getAsLong() : null;
|
||||||
|
CashInterCashierDetail d = new CashInterCashierDetail();
|
||||||
|
d.setInterCashierId(interCashierId);
|
||||||
|
d.setDenominationId(denomId);
|
||||||
|
d.setDenominationValue(value);
|
||||||
|
d.setCount(count);
|
||||||
|
d.setTotalAmount(BigDecimal.valueOf((long) value * count));
|
||||||
|
detailRepository.save(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toMap(CashInterCashier e) {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("id", e.getId());
|
||||||
|
m.put("transferDate", e.getTransferDate() != null ? e.getTransferDate().toString() : null);
|
||||||
|
m.put("tellerId", e.getTellerId());
|
||||||
|
m.put("fromCashierId", e.getFromCashierId());
|
||||||
|
m.put("toCashierId", e.getToCashierId());
|
||||||
|
m.put("fromParty", e.getFromParty());
|
||||||
|
m.put("toParty", e.getToParty());
|
||||||
|
m.put("transferAmount", e.getTransferAmount());
|
||||||
|
m.put("note", e.getNote());
|
||||||
|
m.put("performedByUsername", e.getPerformedByUsername());
|
||||||
|
m.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null);
|
||||||
|
m.put("isReversed", e.getIsReversed());
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> detailToMap(CashInterCashierDetail d) {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("id", d.getId());
|
||||||
|
m.put("denominationId", d.getDenominationId());
|
||||||
|
m.put("denominationValue", d.getDenominationValue());
|
||||||
|
m.put("count", d.getCount());
|
||||||
|
m.put("totalAmount", d.getTotalAmount());
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashintercashier.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_cash_inter_cashier")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CashInterCashier {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "transfer_date")
|
||||||
|
private LocalDate transferDate;
|
||||||
|
|
||||||
|
@Column(name = "teller_id")
|
||||||
|
private Long tellerId;
|
||||||
|
|
||||||
|
@Column(name = "from_cashier_id")
|
||||||
|
private Long fromCashierId;
|
||||||
|
|
||||||
|
@Column(name = "to_cashier_id")
|
||||||
|
private Long toCashierId;
|
||||||
|
|
||||||
|
@Column(name = "from_party", length = 100)
|
||||||
|
private String fromParty;
|
||||||
|
|
||||||
|
@Column(name = "to_party", length = 100)
|
||||||
|
private String toParty;
|
||||||
|
|
||||||
|
@Column(name = "transfer_amount", nullable = false)
|
||||||
|
private BigDecimal transferAmount;
|
||||||
|
|
||||||
|
@Column(name = "note", length = 500)
|
||||||
|
private String note;
|
||||||
|
|
||||||
|
@Column(name = "performed_by")
|
||||||
|
private Long performedBy;
|
||||||
|
|
||||||
|
@Column(name = "performed_by_username", length = 100)
|
||||||
|
private String performedByUsername;
|
||||||
|
|
||||||
|
@Column(name = "created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "is_reversed")
|
||||||
|
private Boolean isReversed = false;
|
||||||
|
|
||||||
|
@Column(name = "reversed_by")
|
||||||
|
private Long reversedBy;
|
||||||
|
|
||||||
|
@Column(name = "reversed_at")
|
||||||
|
private LocalDateTime reversedAt;
|
||||||
|
|
||||||
|
@Column(name = "reverse_reason", length = 500)
|
||||||
|
private String reverseReason;
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashintercashier.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_cash_inter_cashier_detail")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CashInterCashierDetail {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "inter_cashier_id", nullable = false)
|
||||||
|
private Long interCashierId;
|
||||||
|
|
||||||
|
@Column(name = "denomination_id")
|
||||||
|
private Long denominationId;
|
||||||
|
|
||||||
|
@Column(name = "denomination_value", nullable = false)
|
||||||
|
private Integer denominationValue;
|
||||||
|
|
||||||
|
@Column(name = "count", nullable = false)
|
||||||
|
private Integer count;
|
||||||
|
|
||||||
|
@Column(name = "total_amount", nullable = false)
|
||||||
|
private BigDecimal totalAmount;
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashintercashier.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface CashInterCashierDetailRepository extends JpaRepository<CashInterCashierDetail, Long> {
|
||||||
|
List<CashInterCashierDetail> findByInterCashierId(Long interCashierId);
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashintercashier.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface CashInterCashierRepository extends JpaRepository<CashInterCashier, Long> {
|
||||||
|
List<CashInterCashier> findAllByOrderByCreatedAtDesc();
|
||||||
|
List<CashInterCashier> findByTellerIdOrderByCreatedAtDesc(Long tellerId);
|
||||||
|
}
|
||||||
+387
@@ -0,0 +1,387 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashreconciliation.api;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.cashreconciliation.domain.CashReconciliation;
|
||||||
|
import org.apache.fineract.portfolio.cashreconciliation.domain.CashReconciliationDetail;
|
||||||
|
import org.apache.fineract.portfolio.cashreconciliation.domain.CashReconciliationDetailRepository;
|
||||||
|
import org.apache.fineract.portfolio.cashreconciliation.domain.CashReconciliationRepository;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Path("/v1/cashreconciliation")
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
public class CashReconciliationApiResource {
|
||||||
|
|
||||||
|
private final CashReconciliationRepository repository;
|
||||||
|
private final CashReconciliationDetailRepository detailRepository;
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /cashreconciliation/expected?cashierId=X&asOfDate=yyyy-MM-dd
|
||||||
|
* Returns denomination-wise expected balance for a cashier.
|
||||||
|
* Computation: allocations (m_cash_transfer_detail where to_cashier_id=X)
|
||||||
|
* + inter-cashier-in (m_cash_inter_cashier_detail where to_cashier_id=X)
|
||||||
|
* - settlements (m_cash_settlement_detail where from_cashier_id=X)
|
||||||
|
* - inter-cashier-out (m_cash_inter_cashier_detail where from_cashier_id=X)
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("expected")
|
||||||
|
public String getExpected(@QueryParam("cashierId") Long cashierId, @QueryParam("asOfDate") String asOfDate) {
|
||||||
|
context.authenticatedUser().validateHasPermissionTo("READ_CASHRECONCILIATION");
|
||||||
|
if (cashierId == null) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required",
|
||||||
|
"cashierId query param is required.");
|
||||||
|
}
|
||||||
|
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
|
||||||
|
|
||||||
|
// In: vault → cashier allocations — GROUP BY denomination_id+kind to keep coin and note separate
|
||||||
|
final String sqlIn1 = "SELECT ctd.denomination_id, ctd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ctd.count) AS total_count "
|
||||||
|
+ "FROM m_cash_transfer_detail ctd "
|
||||||
|
+ "JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = ctd.denomination_id "
|
||||||
|
+ "WHERE ct.to_cashier_id = ? AND (ct.is_reversed IS NULL OR ct.is_reversed = false) "
|
||||||
|
+ "AND ct.transfer_date <= CAST(? AS DATE) "
|
||||||
|
+ "GROUP BY ctd.denomination_id, ctd.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// In: inter-cashier in
|
||||||
|
final String sqlIn2 = "SELECT icd.denomination_id, icd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(icd.count) AS total_count "
|
||||||
|
+ "FROM m_cash_inter_cashier_detail icd "
|
||||||
|
+ "JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = icd.denomination_id "
|
||||||
|
+ "WHERE ic.to_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false) "
|
||||||
|
+ "AND ic.transfer_date <= CAST(? AS DATE) "
|
||||||
|
+ "GROUP BY icd.denomination_id, icd.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// In: customer deposits received by this cashier
|
||||||
|
final String sqlIn3 = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(std.count) AS total_count "
|
||||||
|
+ "FROM m_savings_txn_denomination std "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id "
|
||||||
|
+ "WHERE std.txn_type = 'DEPOSIT' "
|
||||||
|
+ "AND std.txn_date <= CAST(? AS DATE) "
|
||||||
|
+ "AND std.officer_id IN ("
|
||||||
|
+ " SELECT au.id FROM m_appuser au "
|
||||||
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) "
|
||||||
|
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// Out: settlements to vault
|
||||||
|
final String sqlOut1 = "SELECT csd.denomination_id, csd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(csd.count) AS total_count "
|
||||||
|
+ "FROM m_cash_settlement_detail csd "
|
||||||
|
+ "JOIN m_cash_settlement cs ON cs.id = csd.settlement_id "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = csd.denomination_id "
|
||||||
|
+ "WHERE cs.from_cashier_id = ? AND (cs.is_reversed IS NULL OR cs.is_reversed = false) "
|
||||||
|
+ "AND cs.settlement_date <= CAST(? AS DATE) "
|
||||||
|
+ "GROUP BY csd.denomination_id, csd.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// Out: inter-cashier out
|
||||||
|
final String sqlOut2 = "SELECT icd.denomination_id, icd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(icd.count) AS total_count "
|
||||||
|
+ "FROM m_cash_inter_cashier_detail icd "
|
||||||
|
+ "JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = icd.denomination_id "
|
||||||
|
+ "WHERE ic.from_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false) "
|
||||||
|
+ "AND ic.transfer_date <= CAST(? AS DATE) "
|
||||||
|
+ "GROUP BY icd.denomination_id, icd.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// Out: customer withdrawals paid by this cashier
|
||||||
|
final String sqlOut3 = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(std.count) AS total_count "
|
||||||
|
+ "FROM m_savings_txn_denomination std "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id "
|
||||||
|
+ "WHERE std.txn_type = 'WITHDRAWAL' "
|
||||||
|
+ "AND std.txn_date <= CAST(? AS DATE) "
|
||||||
|
+ "AND std.officer_id IN ("
|
||||||
|
+ " SELECT au.id FROM m_appuser au "
|
||||||
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) "
|
||||||
|
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// In: cash exchange — customer gave denomination TO cashier (RECEIVED direction)
|
||||||
|
final String sqlIn4 = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ced.count) AS total_count "
|
||||||
|
+ "FROM m_cash_exchange_detail ced "
|
||||||
|
+ "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id "
|
||||||
|
+ "WHERE ce.cashier_id = ? AND ced.direction = 'RECEIVED' "
|
||||||
|
+ "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) "
|
||||||
|
+ "AND ce.exchange_date <= CAST(? AS DATE) "
|
||||||
|
+ "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// Out: cash exchange — cashier gave denomination TO customer (GIVEN direction)
|
||||||
|
final String sqlOut4 = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ced.count) AS total_count "
|
||||||
|
+ "FROM m_cash_exchange_detail ced "
|
||||||
|
+ "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id "
|
||||||
|
+ "WHERE ce.cashier_id = ? AND ced.direction = 'GIVEN' "
|
||||||
|
+ "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) "
|
||||||
|
+ "AND ce.exchange_date <= CAST(? AS DATE) "
|
||||||
|
+ "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// Balance map: denomination_id → long[3] {denomValue, kindFlag (0=note/1=coin), netCount}
|
||||||
|
Map<Long, long[]> balance = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlIn1, DENOM_ROW, cashierId, dateFilter), 1);
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlIn2, DENOM_ROW, cashierId, dateFilter), 1);
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlIn3, DENOM_ROW, dateFilter, cashierId), 1);
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlIn4, DENOM_ROW, cashierId, dateFilter), 1);
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlOut1, DENOM_ROW, cashierId, dateFilter), -1);
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlOut2, DENOM_ROW, cashierId, dateFilter), -1);
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlOut3, DENOM_ROW, dateFilter, cashierId), -1);
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlOut4, DENOM_ROW, cashierId, dateFilter), -1);
|
||||||
|
|
||||||
|
List<Map<String, Object>> rows = new ArrayList<>();
|
||||||
|
balance.entrySet().stream()
|
||||||
|
.sorted((a, b) -> Long.compare(b.getValue()[0], a.getValue()[0]))
|
||||||
|
.forEach(e -> {
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("denominationId", e.getKey());
|
||||||
|
row.put("denominationValue", (int) e.getValue()[0]);
|
||||||
|
row.put("kind", e.getValue()[1] == 1L ? "coin" : "note");
|
||||||
|
row.put("expectedCount", e.getValue()[2]);
|
||||||
|
row.put("expectedAmount", BigDecimal.valueOf(e.getValue()[0] * e.getValue()[2]));
|
||||||
|
rows.add(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
BigDecimal grandTotal = rows.stream()
|
||||||
|
.map(r -> (BigDecimal) r.get("expectedAmount"))
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
JsonObject result = new JsonObject();
|
||||||
|
result.addProperty("cashierId", cashierId);
|
||||||
|
result.addProperty("asOfDate", dateFilter);
|
||||||
|
result.addProperty("expectedTotal", grandTotal);
|
||||||
|
result.add("denominations", gson.toJsonTree(rows));
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /cashreconciliation/expected/me
|
||||||
|
* Same as /expected but auto-detects cashierId from the authenticated user.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("expected/me")
|
||||||
|
public String getExpectedForMe(@QueryParam("asOfDate") String asOfDate) {
|
||||||
|
context.authenticatedUser().validateHasPermissionTo("READ_CASHRECONCILIATION");
|
||||||
|
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
|
||||||
|
Long appUserId;
|
||||||
|
try {
|
||||||
|
appUserId = context.authenticatedUser().getId();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashreconciliation.not.authenticated", "Not authenticated.");
|
||||||
|
}
|
||||||
|
Long cashierId;
|
||||||
|
try {
|
||||||
|
cashierId = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?",
|
||||||
|
Long.class, appUserId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Map<String, Object> empty = new LinkedHashMap<>();
|
||||||
|
empty.put("cashierId", null);
|
||||||
|
empty.put("asOfDate", dateFilter);
|
||||||
|
empty.put("expectedTotal", java.math.BigDecimal.ZERO);
|
||||||
|
empty.put("denominations", new ArrayList<>());
|
||||||
|
return gson.toJson(empty);
|
||||||
|
}
|
||||||
|
// Delegate to the existing /expected logic
|
||||||
|
return getExpected(cashierId, dateFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /cashreconciliation/check?cashierId=X&date=yyyy-MM-dd
|
||||||
|
* Returns whether reconciliation has been done today for a cashier.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("check")
|
||||||
|
public String check(@QueryParam("cashierId") Long cashierId, @QueryParam("date") String date) {
|
||||||
|
context.authenticatedUser().validateHasPermissionTo("READ_CASHRECONCILIATION");
|
||||||
|
if (cashierId == null) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required",
|
||||||
|
"cashierId query param is required.");
|
||||||
|
}
|
||||||
|
final String reconDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
|
||||||
|
Integer count = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_cash_reconciliation WHERE cashier_id = ? AND recon_date = CAST(? AS DATE)",
|
||||||
|
Integer.class, cashierId, reconDate);
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("cashierId", cashierId);
|
||||||
|
result.put("date", reconDate);
|
||||||
|
result.put("reconciled", count != null && count > 0);
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Transactional
|
||||||
|
public String create(String body) {
|
||||||
|
context.authenticatedUser().validateHasPermissionTo("CREATE_CASHRECONCILIATION");
|
||||||
|
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
|
||||||
|
final Long cashierId = json.has("cashierId") ? json.get("cashierId").getAsLong() : null;
|
||||||
|
if (cashierId == null) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required",
|
||||||
|
"cashierId is required.");
|
||||||
|
}
|
||||||
|
final LocalDate reconDate = json.has("reconDate") && !json.get("reconDate").isJsonNull()
|
||||||
|
? LocalDate.parse(json.get("reconDate").getAsString()) : LocalDate.now();
|
||||||
|
final String cashierName = json.has("cashierName") && !json.get("cashierName").isJsonNull()
|
||||||
|
? json.get("cashierName").getAsString() : null;
|
||||||
|
final Long tellerId = json.has("tellerId") && !json.get("tellerId").isJsonNull()
|
||||||
|
? json.get("tellerId").getAsLong() : null;
|
||||||
|
final String note = json.has("note") && !json.get("note").isJsonNull()
|
||||||
|
? json.get("note").getAsString() : null;
|
||||||
|
|
||||||
|
final JsonArray denominations = json.has("denominations") && json.get("denominations").isJsonArray()
|
||||||
|
? json.getAsJsonArray("denominations") : new JsonArray();
|
||||||
|
|
||||||
|
BigDecimal expectedTotal = BigDecimal.ZERO;
|
||||||
|
BigDecimal physicalTotal = BigDecimal.ZERO;
|
||||||
|
List<CashReconciliationDetail> details = new ArrayList<>();
|
||||||
|
|
||||||
|
for (JsonElement el : denominations) {
|
||||||
|
JsonObject item = el.getAsJsonObject();
|
||||||
|
int denomValue = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0;
|
||||||
|
int expectedCount = item.has("expectedCount") ? item.get("expectedCount").getAsInt() : 0;
|
||||||
|
int physicalCount = item.has("physicalCount") ? item.get("physicalCount").getAsInt() : 0;
|
||||||
|
int varianceCount = physicalCount - expectedCount;
|
||||||
|
BigDecimal varianceAmount = BigDecimal.valueOf((long) denomValue * varianceCount);
|
||||||
|
expectedTotal = expectedTotal.add(BigDecimal.valueOf((long) denomValue * expectedCount));
|
||||||
|
physicalTotal = physicalTotal.add(BigDecimal.valueOf((long) denomValue * physicalCount));
|
||||||
|
|
||||||
|
Long denomId = item.has("denominationId") && !item.get("denominationId").isJsonNull()
|
||||||
|
? item.get("denominationId").getAsLong() : null;
|
||||||
|
|
||||||
|
CashReconciliationDetail d = new CashReconciliationDetail();
|
||||||
|
d.setDenominationId(denomId);
|
||||||
|
d.setDenominationValue(denomValue);
|
||||||
|
d.setExpectedCount(expectedCount);
|
||||||
|
d.setPhysicalCount(physicalCount);
|
||||||
|
d.setVarianceCount(varianceCount);
|
||||||
|
d.setVarianceAmount(varianceAmount);
|
||||||
|
details.add(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal variance = physicalTotal.subtract(expectedTotal);
|
||||||
|
String status = variance.compareTo(BigDecimal.ZERO) == 0 ? "OK"
|
||||||
|
: variance.compareTo(BigDecimal.ZERO) > 0 ? "EXCESS" : "SHORT";
|
||||||
|
|
||||||
|
CashReconciliation header = new CashReconciliation();
|
||||||
|
header.setReconDate(reconDate);
|
||||||
|
header.setCashierId(cashierId);
|
||||||
|
header.setCashierName(cashierName);
|
||||||
|
header.setTellerId(tellerId);
|
||||||
|
header.setExpectedAmount(expectedTotal);
|
||||||
|
header.setPhysicalAmount(physicalTotal);
|
||||||
|
header.setVariance(variance);
|
||||||
|
header.setStatus(status);
|
||||||
|
header.setNote(note);
|
||||||
|
header.setCreatedAt(LocalDateTime.now());
|
||||||
|
try {
|
||||||
|
AppUser user = this.context.authenticatedUser();
|
||||||
|
header.setPerformedBy(user.getId());
|
||||||
|
header.setPerformedByUsername(user.getUsername());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
CashReconciliation saved = repository.saveAndFlush(header);
|
||||||
|
|
||||||
|
for (CashReconciliationDetail d : details) {
|
||||||
|
d.setReconciliationId(saved.getId());
|
||||||
|
detailRepository.save(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject result = new JsonObject();
|
||||||
|
result.addProperty("resourceId", saved.getId());
|
||||||
|
result.addProperty("status", status);
|
||||||
|
result.addProperty("variance", variance);
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public String getAll() {
|
||||||
|
return gson.toJson(repository.findAllByOrderByCreatedAtDesc().stream()
|
||||||
|
.map(this::toMap).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{id}")
|
||||||
|
public String getOne(@PathParam("id") Long id) {
|
||||||
|
CashReconciliation header = repository.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Cash reconciliation not found: " + id));
|
||||||
|
Map<String, Object> map = toMap(header);
|
||||||
|
map.put("details", detailRepository.findByReconciliationId(id).stream()
|
||||||
|
.map(this::detailToMap).collect(Collectors.toList()));
|
||||||
|
return gson.toJson(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row mapper: Object[]{Long denomId, Integer denomValue, String kind, Long count}
|
||||||
|
private static final org.springframework.jdbc.core.RowMapper<Object[]> DENOM_ROW =
|
||||||
|
(rs, r) -> new Object[] {
|
||||||
|
rs.getLong("denomination_id"),
|
||||||
|
rs.getInt("denomination_value"),
|
||||||
|
rs.getString("kind"),
|
||||||
|
rs.getLong("total_count")
|
||||||
|
};
|
||||||
|
|
||||||
|
private void addToBalance(Map<Long, long[]> balance, List<Object[]> rows, int sign) {
|
||||||
|
for (Object[] row : rows) {
|
||||||
|
Long denomId = (Long) row[0];
|
||||||
|
int denomValue = (Integer) row[1];
|
||||||
|
long kindFlag = "coin".equalsIgnoreCase((String) row[2]) ? 1L : 0L;
|
||||||
|
long count = (Long) row[3];
|
||||||
|
long[] entry = balance.computeIfAbsent(denomId, k -> new long[] { denomValue, kindFlag, 0L });
|
||||||
|
entry[2] += count * sign;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toMap(CashReconciliation e) {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("id", e.getId());
|
||||||
|
m.put("reconDate", e.getReconDate() != null ? e.getReconDate().toString() : null);
|
||||||
|
m.put("cashierId", e.getCashierId());
|
||||||
|
m.put("cashierName", e.getCashierName());
|
||||||
|
m.put("tellerId", e.getTellerId());
|
||||||
|
m.put("expectedAmount", e.getExpectedAmount());
|
||||||
|
m.put("physicalAmount", e.getPhysicalAmount());
|
||||||
|
m.put("variance", e.getVariance());
|
||||||
|
m.put("status", e.getStatus());
|
||||||
|
m.put("note", e.getNote());
|
||||||
|
m.put("performedByUsername", e.getPerformedByUsername());
|
||||||
|
m.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> detailToMap(CashReconciliationDetail d) {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("id", d.getId());
|
||||||
|
m.put("denominationId", d.getDenominationId());
|
||||||
|
m.put("denominationValue", d.getDenominationValue());
|
||||||
|
m.put("expectedCount", d.getExpectedCount());
|
||||||
|
m.put("physicalCount", d.getPhysicalCount());
|
||||||
|
m.put("varianceCount", d.getVarianceCount());
|
||||||
|
m.put("varianceAmount", d.getVarianceAmount());
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashreconciliation.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_cash_reconciliation")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CashReconciliation {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "recon_date", nullable = false)
|
||||||
|
private LocalDate reconDate;
|
||||||
|
|
||||||
|
@Column(name = "cashier_id", nullable = false)
|
||||||
|
private Long cashierId;
|
||||||
|
|
||||||
|
@Column(name = "cashier_name", length = 150)
|
||||||
|
private String cashierName;
|
||||||
|
|
||||||
|
@Column(name = "teller_id")
|
||||||
|
private Long tellerId;
|
||||||
|
|
||||||
|
@Column(name = "expected_amount")
|
||||||
|
private BigDecimal expectedAmount = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
@Column(name = "physical_amount")
|
||||||
|
private BigDecimal physicalAmount = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
@Column(name = "variance")
|
||||||
|
private BigDecimal variance = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
@Column(name = "status", length = 10)
|
||||||
|
private String status = "OK";
|
||||||
|
|
||||||
|
@Column(name = "note", length = 500)
|
||||||
|
private String note;
|
||||||
|
|
||||||
|
@Column(name = "performed_by")
|
||||||
|
private Long performedBy;
|
||||||
|
|
||||||
|
@Column(name = "performed_by_username", length = 100)
|
||||||
|
private String performedByUsername;
|
||||||
|
|
||||||
|
@Column(name = "created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashreconciliation.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_cash_reconciliation_detail")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CashReconciliationDetail {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "reconciliation_id", nullable = false)
|
||||||
|
private Long reconciliationId;
|
||||||
|
|
||||||
|
@Column(name = "denomination_id")
|
||||||
|
private Long denominationId;
|
||||||
|
|
||||||
|
@Column(name = "denomination_value", nullable = false)
|
||||||
|
private Integer denominationValue;
|
||||||
|
|
||||||
|
@Column(name = "expected_count")
|
||||||
|
private Integer expectedCount = 0;
|
||||||
|
|
||||||
|
@Column(name = "physical_count")
|
||||||
|
private Integer physicalCount = 0;
|
||||||
|
|
||||||
|
@Column(name = "variance_count")
|
||||||
|
private Integer varianceCount = 0;
|
||||||
|
|
||||||
|
@Column(name = "variance_amount")
|
||||||
|
private BigDecimal varianceAmount = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashreconciliation.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface CashReconciliationDetailRepository extends JpaRepository<CashReconciliationDetail, Long> {
|
||||||
|
List<CashReconciliationDetail> findByReconciliationId(Long reconciliationId);
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashreconciliation.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface CashReconciliationRepository extends JpaRepository<CashReconciliation, Long> {
|
||||||
|
List<CashReconciliation> findAllByOrderByCreatedAtDesc();
|
||||||
|
List<CashReconciliation> findByCashierIdOrderByReconDateDesc(Long cashierId);
|
||||||
|
}
|
||||||
+437
@@ -0,0 +1,437 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashreport.api;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/cashreports")
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
public class CashReportApiResource {
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("closing")
|
||||||
|
public String closing(@QueryParam("date") String date) {
|
||||||
|
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
|
||||||
|
|
||||||
|
// Single query — conditional aggregation for deposit, withdrawal, net per denomination
|
||||||
|
final String sql = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, "
|
||||||
|
+ "SUM(CASE WHEN std.txn_type = 'DEPOSIT' THEN std.count ELSE 0 END) AS deposit_count, "
|
||||||
|
+ "SUM(CASE WHEN std.txn_type = 'DEPOSIT' THEN std.total_amount ELSE 0 END) AS deposit_amount, "
|
||||||
|
+ "SUM(CASE WHEN std.txn_type = 'WITHDRAWAL' THEN std.count ELSE 0 END) AS withdrawal_count, "
|
||||||
|
+ "SUM(CASE WHEN std.txn_type = 'WITHDRAWAL' THEN std.total_amount ELSE 0 END) AS withdrawal_amount "
|
||||||
|
+ "FROM m_savings_txn_denomination std "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id "
|
||||||
|
+ "WHERE std.txn_date = CAST(? AS DATE) "
|
||||||
|
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind "
|
||||||
|
+ "ORDER BY std.denomination_value DESC, dm.kind";
|
||||||
|
|
||||||
|
final List<Map<String, Object>> rows = jdbcTemplate.query(sql, (rs, rowNum) -> {
|
||||||
|
long depositCount = rs.getLong("deposit_count");
|
||||||
|
long withdrawalCount = rs.getLong("withdrawal_count");
|
||||||
|
BigDecimal depositAmount = rs.getBigDecimal("deposit_amount");
|
||||||
|
BigDecimal withdrawalAmount = rs.getBigDecimal("withdrawal_amount");
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("denominationId", rs.getLong("denomination_id"));
|
||||||
|
row.put("denominationValue", rs.getInt("denomination_value"));
|
||||||
|
row.put("kind", rs.getString("kind"));
|
||||||
|
row.put("depositCount", depositCount);
|
||||||
|
row.put("depositAmount", depositAmount);
|
||||||
|
row.put("withdrawalCount", withdrawalCount);
|
||||||
|
row.put("withdrawalAmount", withdrawalAmount);
|
||||||
|
row.put("netCount", depositCount - withdrawalCount);
|
||||||
|
row.put("netAmount", depositAmount.subtract(withdrawalAmount));
|
||||||
|
return row;
|
||||||
|
}, txnDate);
|
||||||
|
|
||||||
|
BigDecimal depositTotal = BigDecimal.ZERO;
|
||||||
|
BigDecimal withdrawalTotal = BigDecimal.ZERO;
|
||||||
|
for (Map<String, Object> row : rows) {
|
||||||
|
depositTotal = depositTotal.add((BigDecimal) row.get("depositAmount"));
|
||||||
|
withdrawalTotal = withdrawalTotal.add((BigDecimal) row.get("withdrawalAmount"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cash exchange rows — separate section (denomination swap, net monetary = 0)
|
||||||
|
final String exchangeSql = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, "
|
||||||
|
+ "SUM(CASE WHEN ced.direction = 'RECEIVED' THEN ced.count ELSE 0 END) AS received_count, "
|
||||||
|
+ "SUM(CASE WHEN ced.direction = 'RECEIVED' THEN ced.total_amount ELSE 0 END) AS received_amount, "
|
||||||
|
+ "SUM(CASE WHEN ced.direction = 'GIVEN' THEN ced.count ELSE 0 END) AS given_count, "
|
||||||
|
+ "SUM(CASE WHEN ced.direction = 'GIVEN' THEN ced.total_amount ELSE 0 END) AS given_amount "
|
||||||
|
+ "FROM m_cash_exchange_detail ced "
|
||||||
|
+ "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id "
|
||||||
|
+ "WHERE ce.exchange_date = CAST(? AS DATE) "
|
||||||
|
+ "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) "
|
||||||
|
+ "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind "
|
||||||
|
+ "ORDER BY ced.denomination_value DESC, dm.kind";
|
||||||
|
|
||||||
|
final List<Map<String, Object>> exchangeRows = jdbcTemplate.query(exchangeSql, (rs, rowNum) -> {
|
||||||
|
long receivedCount = rs.getLong("received_count");
|
||||||
|
long givenCount = rs.getLong("given_count");
|
||||||
|
BigDecimal receivedAmount = rs.getBigDecimal("received_amount");
|
||||||
|
BigDecimal givenAmount = rs.getBigDecimal("given_amount");
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("denominationId", rs.getLong("denomination_id"));
|
||||||
|
row.put("denominationValue", rs.getInt("denomination_value"));
|
||||||
|
row.put("kind", rs.getString("kind"));
|
||||||
|
row.put("receivedCount", receivedCount);
|
||||||
|
row.put("receivedAmount", receivedAmount);
|
||||||
|
row.put("givenCount", givenCount);
|
||||||
|
row.put("givenAmount", givenAmount);
|
||||||
|
row.put("netCount", receivedCount - givenCount);
|
||||||
|
row.put("netAmount", receivedAmount.subtract(givenAmount));
|
||||||
|
return row;
|
||||||
|
}, txnDate);
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("date", txnDate);
|
||||||
|
result.put("rows", rows);
|
||||||
|
result.put("depositTotal", depositTotal);
|
||||||
|
result.put("withdrawalTotal", withdrawalTotal);
|
||||||
|
result.put("netTotal", depositTotal.subtract(withdrawalTotal));
|
||||||
|
result.put("exchangeRows", exchangeRows);
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("excess")
|
||||||
|
public String excess(@QueryParam("threshold") BigDecimal threshold, @QueryParam("fromDate") String fromDate,
|
||||||
|
@QueryParam("toDate") String toDate) {
|
||||||
|
final BigDecimal effectiveThreshold = threshold != null ? threshold : BigDecimal.valueOf(50000);
|
||||||
|
final boolean hasDateRange = fromDate != null && !fromDate.isBlank() && toDate != null && !toDate.isBlank();
|
||||||
|
|
||||||
|
// Savings / Current / FD / RD transactions
|
||||||
|
final StringBuilder sql = new StringBuilder();
|
||||||
|
sql.append("SELECT txn_id, MAX(account_no) account_no, MAX(client_id) client_id, MAX(cif_no) cif_no, ");
|
||||||
|
sql.append("MAX(txn_date) txn_date, SUM(total_amount) total_amount FROM m_savings_txn_denomination ");
|
||||||
|
final List<Object> args = new ArrayList<>();
|
||||||
|
if (hasDateRange) {
|
||||||
|
sql.append("WHERE txn_date BETWEEN CAST(? AS DATE) AND CAST(? AS DATE) ");
|
||||||
|
args.add(fromDate);
|
||||||
|
args.add(toDate);
|
||||||
|
}
|
||||||
|
sql.append("GROUP BY txn_id HAVING SUM(total_amount) >= ? ORDER BY total_amount DESC");
|
||||||
|
args.add(effectiveThreshold);
|
||||||
|
|
||||||
|
final List<Map<String, Object>> savingsRows = jdbcTemplate.query(sql.toString(), (rs, rowNum) -> {
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("type", "SAVINGS_TXN");
|
||||||
|
row.put("txnId", rs.getLong("txn_id"));
|
||||||
|
row.put("accountNo", rs.getString("account_no"));
|
||||||
|
row.put("clientId", rs.getObject("client_id"));
|
||||||
|
row.put("cifNo", rs.getString("cif_no"));
|
||||||
|
final java.sql.Date d = rs.getDate("txn_date");
|
||||||
|
row.put("txnDate", d != null ? d.toString() : null);
|
||||||
|
row.put("totalAmount", rs.getBigDecimal("total_amount"));
|
||||||
|
row.put("idProof", null);
|
||||||
|
return row;
|
||||||
|
}, args.toArray());
|
||||||
|
|
||||||
|
// Cash exchange transactions
|
||||||
|
final StringBuilder exSql = new StringBuilder();
|
||||||
|
exSql.append("SELECT id, account_no, client_id, exchange_date, exchange_amount, customer_id_proof, performed_by_username ");
|
||||||
|
exSql.append("FROM m_cash_exchange ");
|
||||||
|
exSql.append("WHERE (is_reversed IS NULL OR is_reversed = false) AND exchange_amount >= ? ");
|
||||||
|
final List<Object> exArgs = new ArrayList<>();
|
||||||
|
exArgs.add(effectiveThreshold);
|
||||||
|
if (hasDateRange) {
|
||||||
|
exSql.append("AND exchange_date BETWEEN CAST(? AS DATE) AND CAST(? AS DATE) ");
|
||||||
|
exArgs.add(fromDate);
|
||||||
|
exArgs.add(toDate);
|
||||||
|
}
|
||||||
|
exSql.append("ORDER BY exchange_amount DESC");
|
||||||
|
|
||||||
|
final List<Map<String, Object>> exchangeRows = jdbcTemplate.query(exSql.toString(), (rs, rowNum) -> {
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("type", "EXCHANGE");
|
||||||
|
row.put("txnId", rs.getLong("id"));
|
||||||
|
row.put("accountNo", rs.getString("account_no"));
|
||||||
|
row.put("clientId", rs.getObject("client_id"));
|
||||||
|
row.put("cifNo", null);
|
||||||
|
final java.sql.Date d = rs.getDate("exchange_date");
|
||||||
|
row.put("txnDate", d != null ? d.toString() : null);
|
||||||
|
row.put("totalAmount", rs.getBigDecimal("exchange_amount"));
|
||||||
|
row.put("idProof", rs.getString("customer_id_proof"));
|
||||||
|
return row;
|
||||||
|
}, exArgs.toArray());
|
||||||
|
|
||||||
|
// Merge and sort by amount DESC
|
||||||
|
final List<Map<String, Object>> combined = new ArrayList<>();
|
||||||
|
combined.addAll(savingsRows);
|
||||||
|
combined.addAll(exchangeRows);
|
||||||
|
combined.sort((a, b) -> ((BigDecimal) b.get("totalAmount")).compareTo((BigDecimal) a.get("totalAmount")));
|
||||||
|
|
||||||
|
return gson.toJson(combined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /cashreports/cashier-position?cashierId=X&asOfDate=yyyy-MM-dd
|
||||||
|
* Returns denomination-wise net position per cashier grouped by denomination_id+kind.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("cashier-position")
|
||||||
|
public String cashierPosition(@QueryParam("cashierId") Long cashierId,
|
||||||
|
@QueryParam("asOfDate") String asOfDate) {
|
||||||
|
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
|
||||||
|
|
||||||
|
if (cashierId != null) {
|
||||||
|
return gson.toJson(buildPositionForCashier(cashierId, dateFilter));
|
||||||
|
}
|
||||||
|
|
||||||
|
final String idSql = "SELECT DISTINCT cashier_id FROM ("
|
||||||
|
+ " SELECT to_cashier_id AS cashier_id FROM m_cash_transfer WHERE to_cashier_id IS NOT NULL "
|
||||||
|
+ " UNION SELECT from_cashier_id FROM m_cash_settlement WHERE from_cashier_id IS NOT NULL "
|
||||||
|
+ " UNION SELECT from_cashier_id FROM m_cash_inter_cashier WHERE from_cashier_id IS NOT NULL "
|
||||||
|
+ " UNION SELECT to_cashier_id FROM m_cash_inter_cashier WHERE to_cashier_id IS NOT NULL "
|
||||||
|
+ " UNION SELECT cashier_id FROM m_cash_exchange WHERE cashier_id IS NOT NULL "
|
||||||
|
+ ") ids";
|
||||||
|
|
||||||
|
List<Long> cashierIds = jdbcTemplate.queryForList(idSql, Long.class);
|
||||||
|
List<Map<String, Object>> allPositions = new ArrayList<>();
|
||||||
|
for (Long cid : cashierIds) {
|
||||||
|
allPositions.add(buildPositionForCashier(cid, dateFilter));
|
||||||
|
}
|
||||||
|
return gson.toJson(allPositions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("cashier-position/me")
|
||||||
|
public String cashierPositionMe(@QueryParam("asOfDate") String asOfDate) {
|
||||||
|
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
|
||||||
|
Long appUserId;
|
||||||
|
try {
|
||||||
|
appUserId = context.authenticatedUser().getId();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Map<String, Object> err = new LinkedHashMap<>();
|
||||||
|
err.put("error", "Not authenticated");
|
||||||
|
return gson.toJson(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Long cashierId;
|
||||||
|
try {
|
||||||
|
cashierId = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?",
|
||||||
|
Long.class, appUserId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Map<String, Object> empty = new LinkedHashMap<>();
|
||||||
|
empty.put("cashierId", null);
|
||||||
|
empty.put("asOfDate", dateFilter);
|
||||||
|
empty.put("totalAmount", BigDecimal.ZERO);
|
||||||
|
empty.put("denominations", new ArrayList<>());
|
||||||
|
return gson.toJson(empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return gson.toJson(buildPositionForCashier(cashierId, dateFilter));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /cashreports/cashier-morning?cashierId=X&date=YYYY-MM-DD
|
||||||
|
* Returns denomination-wise cash received by the cashier from vault on the given date
|
||||||
|
* (today's morning opening allocation from m_cash_transfer only, non-reversed).
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("cashier-morning")
|
||||||
|
public String cashierMorning(@QueryParam("cashierId") Long cashierId,
|
||||||
|
@QueryParam("date") String date) {
|
||||||
|
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
|
||||||
|
if (cashierId == null) {
|
||||||
|
Map<String, Object> empty = new LinkedHashMap<>();
|
||||||
|
empty.put("cashierId", null);
|
||||||
|
empty.put("date", txnDate);
|
||||||
|
empty.put("totalMorning", BigDecimal.ZERO);
|
||||||
|
empty.put("denominations", new ArrayList<>());
|
||||||
|
return gson.toJson(empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String sql = "SELECT ctd.denomination_id, ctd.denomination_value, "
|
||||||
|
+ "COALESCE(dm.kind, 'note') AS kind, "
|
||||||
|
+ "SUM(ctd.count) AS total_count, "
|
||||||
|
+ "SUM(ctd.total_amount) AS total_amount "
|
||||||
|
+ "FROM m_cash_transfer_detail ctd "
|
||||||
|
+ "JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = ctd.denomination_id "
|
||||||
|
+ "WHERE ct.to_cashier_id = ? "
|
||||||
|
+ "AND ct.transfer_date = CAST(? AS DATE) "
|
||||||
|
+ "AND (ct.is_reversed IS NULL OR ct.is_reversed = false) "
|
||||||
|
+ "GROUP BY ctd.denomination_id, ctd.denomination_value, dm.kind "
|
||||||
|
+ "ORDER BY ctd.denomination_value DESC";
|
||||||
|
|
||||||
|
List<Map<String, Object>> rows = jdbcTemplate.query(sql, (rs, rowNum) -> {
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("denominationId", rs.getLong("denomination_id"));
|
||||||
|
row.put("denominationValue", rs.getInt("denomination_value"));
|
||||||
|
row.put("kind", rs.getString("kind"));
|
||||||
|
row.put("morningCount", rs.getLong("total_count"));
|
||||||
|
row.put("morningAmount", rs.getBigDecimal("total_amount"));
|
||||||
|
return row;
|
||||||
|
}, cashierId, txnDate);
|
||||||
|
|
||||||
|
BigDecimal total = rows.stream()
|
||||||
|
.map(r -> (BigDecimal) r.get("morningAmount"))
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("cashierId", cashierId);
|
||||||
|
result.put("date", txnDate);
|
||||||
|
result.put("totalMorning", total);
|
||||||
|
result.put("denominations", rows);
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildPositionForCashier(Long cashierId, String dateFilter) {
|
||||||
|
// Vault → Cashier allocations — GROUP BY denomination_id+kind to keep coin and note separate
|
||||||
|
final String sqlIn1 = "SELECT ctd.denomination_id, ctd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ctd.count) AS total_count "
|
||||||
|
+ "FROM m_cash_transfer_detail ctd JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = ctd.denomination_id "
|
||||||
|
+ "WHERE ct.to_cashier_id = ? AND (ct.is_reversed IS NULL OR ct.is_reversed = false) "
|
||||||
|
+ "AND ct.transfer_date <= CAST(? AS DATE) "
|
||||||
|
+ "GROUP BY ctd.denomination_id, ctd.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// Inter-cashier IN
|
||||||
|
final String sqlIn2 = "SELECT icd.denomination_id, icd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(icd.count) AS total_count "
|
||||||
|
+ "FROM m_cash_inter_cashier_detail icd JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = icd.denomination_id "
|
||||||
|
+ "WHERE ic.to_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false) "
|
||||||
|
+ "AND ic.transfer_date <= CAST(? AS DATE) "
|
||||||
|
+ "GROUP BY icd.denomination_id, icd.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// Customer deposits
|
||||||
|
final String sqlIn3 = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(std.count) AS total_count "
|
||||||
|
+ "FROM m_savings_txn_denomination std "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id "
|
||||||
|
+ "WHERE std.txn_type = 'DEPOSIT' AND std.txn_date <= CAST(? AS DATE) "
|
||||||
|
+ "AND std.officer_id IN ("
|
||||||
|
+ " SELECT au.id FROM m_appuser au "
|
||||||
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) "
|
||||||
|
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// Settlements back to vault
|
||||||
|
final String sqlOut1 = "SELECT csd.denomination_id, csd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(csd.count) AS total_count "
|
||||||
|
+ "FROM m_cash_settlement_detail csd JOIN m_cash_settlement cs ON cs.id = csd.settlement_id "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = csd.denomination_id "
|
||||||
|
+ "WHERE cs.from_cashier_id = ? AND (cs.is_reversed IS NULL OR cs.is_reversed = false) "
|
||||||
|
+ "AND cs.settlement_date <= CAST(? AS DATE) "
|
||||||
|
+ "GROUP BY csd.denomination_id, csd.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// Inter-cashier OUT
|
||||||
|
final String sqlOut2 = "SELECT icd.denomination_id, icd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(icd.count) AS total_count "
|
||||||
|
+ "FROM m_cash_inter_cashier_detail icd JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = icd.denomination_id "
|
||||||
|
+ "WHERE ic.from_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false) "
|
||||||
|
+ "AND ic.transfer_date <= CAST(? AS DATE) "
|
||||||
|
+ "GROUP BY icd.denomination_id, icd.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// Customer withdrawals
|
||||||
|
final String sqlOut3 = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(std.count) AS total_count "
|
||||||
|
+ "FROM m_savings_txn_denomination std "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id "
|
||||||
|
+ "WHERE std.txn_type = 'WITHDRAWAL' AND std.txn_date <= CAST(? AS DATE) "
|
||||||
|
+ "AND std.officer_id IN ("
|
||||||
|
+ " SELECT au.id FROM m_appuser au "
|
||||||
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) "
|
||||||
|
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// Exchange IN: customer gave denomination to cashier (RECEIVED direction)
|
||||||
|
final String sqlIn4 = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ced.count) AS total_count "
|
||||||
|
+ "FROM m_cash_exchange_detail ced "
|
||||||
|
+ "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id "
|
||||||
|
+ "WHERE ce.cashier_id = ? AND ced.direction = 'RECEIVED' "
|
||||||
|
+ "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) "
|
||||||
|
+ "AND ce.exchange_date <= CAST(? AS DATE) "
|
||||||
|
+ "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// Exchange OUT: cashier gave denomination to customer (GIVEN direction)
|
||||||
|
final String sqlOut4 = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ced.count) AS total_count "
|
||||||
|
+ "FROM m_cash_exchange_detail ced "
|
||||||
|
+ "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id "
|
||||||
|
+ "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id "
|
||||||
|
+ "WHERE ce.cashier_id = ? AND ced.direction = 'GIVEN' "
|
||||||
|
+ "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) "
|
||||||
|
+ "AND ce.exchange_date <= CAST(? AS DATE) "
|
||||||
|
+ "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind";
|
||||||
|
|
||||||
|
// Balance map: denomination_id → long[3] {denomValue, kindFlag (0=note/1=coin), netCount}
|
||||||
|
Map<Long, long[]> balance = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlIn1, DENOM_ROW, cashierId, dateFilter), 1);
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlIn2, DENOM_ROW, cashierId, dateFilter), 1);
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlIn3, DENOM_ROW, dateFilter, cashierId), 1);
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlIn4, DENOM_ROW, cashierId, dateFilter), 1);
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlOut1, DENOM_ROW, cashierId, dateFilter), -1);
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlOut2, DENOM_ROW, cashierId, dateFilter), -1);
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlOut3, DENOM_ROW, dateFilter, cashierId), -1);
|
||||||
|
addToBalance(balance, jdbcTemplate.query(sqlOut4, DENOM_ROW, cashierId, dateFilter), -1);
|
||||||
|
|
||||||
|
List<Map<String, Object>> rows = new ArrayList<>();
|
||||||
|
balance.entrySet().stream()
|
||||||
|
.filter(e -> e.getValue()[2] != 0)
|
||||||
|
.sorted((a, b) -> Long.compare(b.getValue()[0], a.getValue()[0]))
|
||||||
|
.forEach(e -> {
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("denominationId", e.getKey());
|
||||||
|
row.put("denominationValue", (int) e.getValue()[0]);
|
||||||
|
row.put("kind", e.getValue()[1] == 1L ? "coin" : "note");
|
||||||
|
row.put("count", e.getValue()[2]);
|
||||||
|
row.put("amount", BigDecimal.valueOf(e.getValue()[0] * e.getValue()[2]));
|
||||||
|
rows.add(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
BigDecimal totalAmount = rows.stream()
|
||||||
|
.map(r -> (BigDecimal) r.get("amount"))
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
String cashierName = null;
|
||||||
|
try {
|
||||||
|
cashierName = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT ms.display_name FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id WHERE mc.id = ?",
|
||||||
|
String.class, cashierId);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("cashierId", cashierId);
|
||||||
|
result.put("cashierName", cashierName);
|
||||||
|
result.put("asOfDate", dateFilter);
|
||||||
|
result.put("totalAmount", totalAmount);
|
||||||
|
result.put("denominations", rows);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row mapper: returns Object[]{Long denomId, Integer denomValue, String kind, Long count}
|
||||||
|
private static final org.springframework.jdbc.core.RowMapper<Object[]> DENOM_ROW =
|
||||||
|
(rs, r) -> new Object[] {
|
||||||
|
rs.getLong("denomination_id"),
|
||||||
|
rs.getInt("denomination_value"),
|
||||||
|
rs.getString("kind"),
|
||||||
|
rs.getLong("total_count")
|
||||||
|
};
|
||||||
|
|
||||||
|
private void addToBalance(Map<Long, long[]> balance, List<Object[]> rows, int sign) {
|
||||||
|
for (Object[] row : rows) {
|
||||||
|
Long denomId = (Long) row[0];
|
||||||
|
int denomValue = (Integer) row[1];
|
||||||
|
long kindFlag = "coin".equalsIgnoreCase((String) row[2]) ? 1L : 0L;
|
||||||
|
long count = (Long) row[3];
|
||||||
|
long[] entry = balance.computeIfAbsent(denomId, k -> new long[] { denomValue, kindFlag, 0L });
|
||||||
|
entry[2] += count * sign;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+309
@@ -0,0 +1,309 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashsettlement.api;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlement;
|
||||||
|
import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlementDetail;
|
||||||
|
import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlementDetailRepository;
|
||||||
|
import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlementRepository;
|
||||||
|
import org.apache.fineract.portfolio.teller.service.GlDispatchService;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
|
|
||||||
|
@Path("/v1/cashsettlement")
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
public class CashSettlementApiResource {
|
||||||
|
|
||||||
|
private final CashSettlementRepository repository;
|
||||||
|
private final CashSettlementDetailRepository detailRepository;
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final GlDispatchService glDispatchService;
|
||||||
|
private final PlatformTransactionManager transactionManager;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
@POST
|
||||||
|
public String create(String body) {
|
||||||
|
context.authenticatedUser().validateHasPermissionTo("CREATE_CASHSETTLEMENT");
|
||||||
|
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
|
||||||
|
final Long fromCashierId = json.has("fromCashierId") && !json.get("fromCashierId").isJsonNull()
|
||||||
|
? json.get("fromCashierId").getAsLong() : null;
|
||||||
|
final Long toTellerId = json.has("toTellerId") && !json.get("toTellerId").isJsonNull()
|
||||||
|
? json.get("toTellerId").getAsLong() : null;
|
||||||
|
|
||||||
|
if (fromCashierId == null || toTellerId == null) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashsettlement.parties.required",
|
||||||
|
"fromCashierId and toTellerId are required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final LocalDate settlementDate = json.has("settlementDate") && !json.get("settlementDate").isJsonNull()
|
||||||
|
? LocalDate.parse(json.get("settlementDate").getAsString()) : LocalDate.now();
|
||||||
|
final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull()
|
||||||
|
? json.get("fromParty").getAsString() : null;
|
||||||
|
final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull()
|
||||||
|
? json.get("toParty").getAsString() : null;
|
||||||
|
final String note = json.has("note") && !json.get("note").isJsonNull()
|
||||||
|
? json.get("note").getAsString() : null;
|
||||||
|
final String currencyCode = json.has("currencyCode") && !json.get("currencyCode").isJsonNull()
|
||||||
|
? json.get("currencyCode").getAsString() : "INR";
|
||||||
|
|
||||||
|
// Require reconciliation before settlement
|
||||||
|
Integer reconCount = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_cash_reconciliation WHERE cashier_id = ? AND recon_date = CAST(? AS DATE)",
|
||||||
|
Integer.class, fromCashierId, settlementDate.toString());
|
||||||
|
if (reconCount == null || reconCount == 0) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashsettlement.reconciliation.required",
|
||||||
|
"Reconciliation must be completed before settlement for cashier " + fromCashierId + " on " + settlementDate + ". Please complete today's reconciliation first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent duplicate settlement for same cashier+date
|
||||||
|
Integer existingCount = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_cash_settlement WHERE from_cashier_id = ? AND settlement_date = CAST(? AS DATE) AND (is_reversed IS NULL OR is_reversed = false)",
|
||||||
|
Integer.class, fromCashierId, settlementDate.toString());
|
||||||
|
if (existingCount != null && existingCount > 0) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashsettlement.duplicate",
|
||||||
|
"A settlement for cashier " + fromCashierId + " on " + settlementDate + " already exists. Reverse the existing settlement first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final JsonArray denominations = json.has("denominations") && json.get("denominations").isJsonArray()
|
||||||
|
? json.getAsJsonArray("denominations") : new JsonArray();
|
||||||
|
|
||||||
|
if (!hasPositiveCount(denominations)) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashsettlement.empty",
|
||||||
|
"At least one denomination with a positive count is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final BigDecimal total = sumTotal(denominations);
|
||||||
|
|
||||||
|
CashSettlement header = new CashSettlement();
|
||||||
|
header.setSettlementDate(settlementDate);
|
||||||
|
header.setFromCashierId(fromCashierId);
|
||||||
|
header.setToTellerId(toTellerId);
|
||||||
|
header.setFromParty(fromParty);
|
||||||
|
header.setToParty(toParty);
|
||||||
|
header.setSettlementAmount(total);
|
||||||
|
header.setNote(note);
|
||||||
|
header.setCreatedAt(LocalDateTime.now());
|
||||||
|
header.setIsReversed(false);
|
||||||
|
try {
|
||||||
|
AppUser user = this.context.authenticatedUser();
|
||||||
|
header.setPerformedBy(user.getId());
|
||||||
|
header.setPerformedByUsername(user.getUsername());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
// Commit header + details first, then GL dispatch separately to avoid
|
||||||
|
// EclipseLink flushing pending detail rows inside Fineract's GL transaction.
|
||||||
|
final Long savedId = new TransactionTemplate(transactionManager).execute(status -> {
|
||||||
|
CashSettlement saved = repository.saveAndFlush(header);
|
||||||
|
saveDetails(saved.getId(), denominations);
|
||||||
|
return saved.getId();
|
||||||
|
});
|
||||||
|
|
||||||
|
String glWarning = null;
|
||||||
|
try {
|
||||||
|
// Use cashier's own teller_id from m_cashiers for GL dispatch so the
|
||||||
|
// Fineract cashier-teller link is respected regardless of which vault teller
|
||||||
|
// the user submitted cash to.
|
||||||
|
Long glTellerId = toTellerId;
|
||||||
|
try {
|
||||||
|
Long resolved = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, fromCashierId);
|
||||||
|
if (resolved != null) glTellerId = resolved;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
glDispatchService.settleCashFromCashier(glTellerId, fromCashierId, total,
|
||||||
|
currencyCode, settlementDate.format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||||
|
note != null ? note : "");
|
||||||
|
} catch (Exception e) {
|
||||||
|
glWarning = "Settlement saved but GL entry failed: " + e.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject result = new JsonObject();
|
||||||
|
result.addProperty("resourceId", savedId);
|
||||||
|
if (glWarning != null) result.addProperty("glWarning", glWarning);
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{id}/reverse")
|
||||||
|
@Transactional
|
||||||
|
public String reverse(@PathParam("id") Long id, String body) {
|
||||||
|
context.authenticatedUser().validateHasPermissionTo("REVERSE_CASHSETTLEMENT");
|
||||||
|
|
||||||
|
CashSettlement settlement = repository.findById(id)
|
||||||
|
.orElseThrow(() -> new PlatformDataIntegrityException("error.msg.cashsettlement.not.found",
|
||||||
|
"Settlement not found: " + id));
|
||||||
|
if (Boolean.TRUE.equals(settlement.getIsReversed())) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashsettlement.already.reversed",
|
||||||
|
"Settlement " + id + " is already reversed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String reason = null;
|
||||||
|
try {
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
reason = json.has("reason") ? json.get("reason").getAsString() : null;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
AppUser user = null;
|
||||||
|
try { user = this.context.authenticatedUser(); } catch (Exception ignored) {}
|
||||||
|
|
||||||
|
settlement.setIsReversed(true);
|
||||||
|
settlement.setReversedAt(LocalDateTime.now());
|
||||||
|
settlement.setReverseReason(reason);
|
||||||
|
if (user != null) settlement.setReversedBy(user.getId());
|
||||||
|
repository.save(settlement);
|
||||||
|
|
||||||
|
if (settlement.getToTellerId() != null && settlement.getFromCashierId() != null) {
|
||||||
|
try {
|
||||||
|
Long glTellerId = settlement.getToTellerId();
|
||||||
|
try {
|
||||||
|
Long resolved = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, settlement.getFromCashierId());
|
||||||
|
if (resolved != null) glTellerId = resolved;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
glDispatchService.allocateCashToCashier(glTellerId,
|
||||||
|
settlement.getFromCashierId(), settlement.getSettlementAmount(), "INR",
|
||||||
|
LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||||
|
"Reversal of cash settlement #" + id + (reason != null ? ": " + reason : ""));
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject result = new JsonObject();
|
||||||
|
result.addProperty("resourceId", id);
|
||||||
|
result.addProperty("reversed", true);
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("check")
|
||||||
|
public String check(@QueryParam("cashierId") Long cashierId, @QueryParam("date") String date) {
|
||||||
|
context.authenticatedUser().validateHasPermissionTo("READ_CASHSETTLEMENT");
|
||||||
|
if (cashierId == null) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashsettlement.cashier.required",
|
||||||
|
"cashierId query param is required.");
|
||||||
|
}
|
||||||
|
final String settlementDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
|
||||||
|
Integer count = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_cash_settlement WHERE from_cashier_id = ? AND settlement_date = CAST(? AS DATE) AND (is_reversed IS NULL OR is_reversed = false)",
|
||||||
|
Integer.class, cashierId, settlementDate);
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("cashierId", cashierId);
|
||||||
|
result.put("date", settlementDate);
|
||||||
|
result.put("alreadySettled", count != null && count > 0);
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public String getAll() {
|
||||||
|
List<CashSettlement> list = repository.findAllByOrderByCreatedAtDesc();
|
||||||
|
return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{id}")
|
||||||
|
public String getOne(@PathParam("id") Long id) {
|
||||||
|
CashSettlement header = repository.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Cash settlement not found: " + id));
|
||||||
|
Map<String, Object> map = toMap(header);
|
||||||
|
List<Map<String, Object>> details = detailRepository.findBySettlementId(id).stream()
|
||||||
|
.map(this::detailToMap).collect(Collectors.toList());
|
||||||
|
map.put("details", details);
|
||||||
|
return gson.toJson(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal sumTotal(JsonArray items) {
|
||||||
|
BigDecimal total = BigDecimal.ZERO;
|
||||||
|
for (JsonElement el : items) {
|
||||||
|
JsonObject item = el.getAsJsonObject();
|
||||||
|
int value = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0;
|
||||||
|
int count = item.has("count") ? item.get("count").getAsInt() : 0;
|
||||||
|
if (count > 0) total = total.add(BigDecimal.valueOf((long) value * count));
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasPositiveCount(JsonArray items) {
|
||||||
|
for (JsonElement el : items) {
|
||||||
|
if (el.getAsJsonObject().get("count").getAsInt() > 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveDetails(Long settlementId, JsonArray items) {
|
||||||
|
for (JsonElement el : items) {
|
||||||
|
JsonObject item = el.getAsJsonObject();
|
||||||
|
int count = item.has("count") ? item.get("count").getAsInt() : 0;
|
||||||
|
if (count <= 0) continue;
|
||||||
|
int value = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0;
|
||||||
|
Long denomId = item.has("denominationId") && !item.get("denominationId").isJsonNull()
|
||||||
|
? item.get("denominationId").getAsLong() : null;
|
||||||
|
CashSettlementDetail d = new CashSettlementDetail();
|
||||||
|
d.setSettlementId(settlementId);
|
||||||
|
d.setDenominationId(denomId);
|
||||||
|
d.setDenominationValue(value);
|
||||||
|
d.setCount(count);
|
||||||
|
d.setTotalAmount(BigDecimal.valueOf((long) value * count));
|
||||||
|
detailRepository.save(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toMap(CashSettlement e) {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("id", e.getId());
|
||||||
|
m.put("settlementDate", e.getSettlementDate() != null ? e.getSettlementDate().toString() : null);
|
||||||
|
m.put("fromCashierId", e.getFromCashierId());
|
||||||
|
m.put("toTellerId", e.getToTellerId());
|
||||||
|
m.put("fromParty", e.getFromParty());
|
||||||
|
m.put("toParty", e.getToParty());
|
||||||
|
m.put("settlementAmount", e.getSettlementAmount());
|
||||||
|
m.put("note", e.getNote());
|
||||||
|
m.put("performedBy", e.getPerformedBy());
|
||||||
|
m.put("performedByUsername", e.getPerformedByUsername());
|
||||||
|
m.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null);
|
||||||
|
m.put("isReversed", e.getIsReversed());
|
||||||
|
m.put("reversedBy", e.getReversedBy());
|
||||||
|
m.put("reversedAt", e.getReversedAt() != null ? e.getReversedAt().toString() : null);
|
||||||
|
m.put("reverseReason", e.getReverseReason());
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> detailToMap(CashSettlementDetail d) {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("id", d.getId());
|
||||||
|
m.put("denominationId", d.getDenominationId());
|
||||||
|
m.put("denominationValue", d.getDenominationValue());
|
||||||
|
m.put("count", d.getCount());
|
||||||
|
m.put("totalAmount", d.getTotalAmount());
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashsettlement.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_cash_settlement")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CashSettlement {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "settlement_date")
|
||||||
|
private LocalDate settlementDate;
|
||||||
|
|
||||||
|
@Column(name = "from_cashier_id")
|
||||||
|
private Long fromCashierId;
|
||||||
|
|
||||||
|
@Column(name = "to_teller_id")
|
||||||
|
private Long toTellerId;
|
||||||
|
|
||||||
|
@Column(name = "from_party", length = 100)
|
||||||
|
private String fromParty;
|
||||||
|
|
||||||
|
@Column(name = "to_party", length = 100)
|
||||||
|
private String toParty;
|
||||||
|
|
||||||
|
@Column(name = "settlement_amount", nullable = false)
|
||||||
|
private BigDecimal settlementAmount;
|
||||||
|
|
||||||
|
@Column(name = "note", length = 500)
|
||||||
|
private String note;
|
||||||
|
|
||||||
|
@Column(name = "performed_by")
|
||||||
|
private Long performedBy;
|
||||||
|
|
||||||
|
@Column(name = "performed_by_username", length = 100)
|
||||||
|
private String performedByUsername;
|
||||||
|
|
||||||
|
@Column(name = "created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "is_reversed")
|
||||||
|
private Boolean isReversed = false;
|
||||||
|
|
||||||
|
@Column(name = "reversed_by")
|
||||||
|
private Long reversedBy;
|
||||||
|
|
||||||
|
@Column(name = "reversed_at")
|
||||||
|
private LocalDateTime reversedAt;
|
||||||
|
|
||||||
|
@Column(name = "reverse_reason", length = 500)
|
||||||
|
private String reverseReason;
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashsettlement.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_cash_settlement_detail")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CashSettlementDetail {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "settlement_id", nullable = false)
|
||||||
|
private Long settlementId;
|
||||||
|
|
||||||
|
@Column(name = "denomination_id")
|
||||||
|
private Long denominationId;
|
||||||
|
|
||||||
|
@Column(name = "denomination_value", nullable = false)
|
||||||
|
private Integer denominationValue;
|
||||||
|
|
||||||
|
@Column(name = "count", nullable = false)
|
||||||
|
private Integer count;
|
||||||
|
|
||||||
|
@Column(name = "total_amount", nullable = false)
|
||||||
|
private BigDecimal totalAmount;
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashsettlement.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface CashSettlementDetailRepository extends JpaRepository<CashSettlementDetail, Long> {
|
||||||
|
List<CashSettlementDetail> findBySettlementId(Long settlementId);
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashsettlement.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface CashSettlementRepository extends JpaRepository<CashSettlement, Long> {
|
||||||
|
List<CashSettlement> findAllByOrderByCreatedAtDesc();
|
||||||
|
List<CashSettlement> findByFromCashierIdOrderByCreatedAtDesc(Long cashierId);
|
||||||
|
}
|
||||||
+298
@@ -0,0 +1,298 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashtransfer.api;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
|
import org.apache.fineract.portfolio.teller.service.GlDispatchService;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.cashtransfer.domain.CashTransfer;
|
||||||
|
import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferDetail;
|
||||||
|
import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferDetailRepository;
|
||||||
|
import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferRepository;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
|
|
||||||
|
@Path("/v1/cashtransfer")
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
public class CashTransferApiResource {
|
||||||
|
|
||||||
|
private final CashTransferRepository repository;
|
||||||
|
private final CashTransferDetailRepository detailRepository;
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final GlDispatchService glDispatchService;
|
||||||
|
private final PlatformTransactionManager transactionManager;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
@POST
|
||||||
|
public String create(String body) {
|
||||||
|
this.context.authenticatedUser().validateHasPermissionTo("CREATE_CASHTRANSFER");
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
|
||||||
|
final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull()
|
||||||
|
? LocalDate.parse(json.get("transferDate").getAsString())
|
||||||
|
: null;
|
||||||
|
final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull() ? json.get("fromParty").getAsString()
|
||||||
|
: null;
|
||||||
|
final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull() ? json.get("toParty").getAsString() : null;
|
||||||
|
final String note = json.has("note") && !json.get("note").isJsonNull() ? json.get("note").getAsString() : null;
|
||||||
|
|
||||||
|
final Long fromTellerId = json.has("fromTellerId") && !json.get("fromTellerId").isJsonNull()
|
||||||
|
? json.get("fromTellerId").getAsLong()
|
||||||
|
: null;
|
||||||
|
final Long toCashierId = json.has("toCashierId") && !json.get("toCashierId").isJsonNull()
|
||||||
|
? json.get("toCashierId").getAsLong()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (fromTellerId == null || toCashierId == null) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashtransfer.parties.required",
|
||||||
|
"fromTellerId and toCashierId are required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final String currencyCode = json.has("currencyCode") && !json.get("currencyCode").isJsonNull()
|
||||||
|
? json.get("currencyCode").getAsString()
|
||||||
|
: "INR";
|
||||||
|
|
||||||
|
final JsonArray denominations = json.has("denominations") && json.get("denominations").isJsonArray()
|
||||||
|
? json.getAsJsonArray("denominations")
|
||||||
|
: new JsonArray();
|
||||||
|
|
||||||
|
if (!hasPositiveCount(denominations)) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashtransfer.empty",
|
||||||
|
"At least one denomination with a positive count is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final BigDecimal total = sumTotal(denominations);
|
||||||
|
if (total.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashtransfer.empty",
|
||||||
|
"At least one denomination with a positive count is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
CashTransfer header = new CashTransfer();
|
||||||
|
header.setTransferDate(transferDate);
|
||||||
|
header.setFromParty(fromParty);
|
||||||
|
header.setToParty(toParty);
|
||||||
|
header.setFromTellerId(fromTellerId);
|
||||||
|
header.setToCashierId(toCashierId);
|
||||||
|
header.setTransferAmount(total);
|
||||||
|
header.setNote(note);
|
||||||
|
header.setCreatedAt(LocalDateTime.now());
|
||||||
|
try {
|
||||||
|
AppUser user = this.context.authenticatedUser();
|
||||||
|
if (user != null) {
|
||||||
|
header.setPerformedBy(user.getId());
|
||||||
|
header.setPerformedByUsername(user.getUsername());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// resolving the current user must never block recording the transfer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save header + details in one committed transaction, then GL dispatch separately.
|
||||||
|
// This prevents EclipseLink from flushing pending detail rows inside Fineract's
|
||||||
|
// ALLOCATECASHTOCASHIER transaction, which caused null transfer_id constraint errors.
|
||||||
|
final Long savedId = new TransactionTemplate(transactionManager).execute(status -> {
|
||||||
|
CashTransfer saved = repository.saveAndFlush(header);
|
||||||
|
saveDetails(saved.getId(), denominations);
|
||||||
|
return saved.getId();
|
||||||
|
});
|
||||||
|
|
||||||
|
String glWarning = null;
|
||||||
|
try {
|
||||||
|
// Use the cashier's own teller_id from m_cashiers for GL dispatch.
|
||||||
|
// Fineract's allocateCashToCashier requires cashier and teller to be linked —
|
||||||
|
// the user-selected fromTellerId is the business vault but may differ from the
|
||||||
|
// cashier's registered teller_id. Always resolve from DB to avoid GL failures.
|
||||||
|
Long glTellerId = fromTellerId;
|
||||||
|
try {
|
||||||
|
Long resolved = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, toCashierId);
|
||||||
|
if (resolved != null) glTellerId = resolved;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
final LocalDate txnDate = transferDate != null ? transferDate : LocalDate.now();
|
||||||
|
glDispatchService.allocateCashToCashier(glTellerId, toCashierId, total,
|
||||||
|
currencyCode, txnDate.format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||||
|
note != null ? note : "");
|
||||||
|
} catch (Exception e) {
|
||||||
|
glWarning = "Transfer saved but GL entry failed: " + e.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject result = new JsonObject();
|
||||||
|
result.addProperty("resourceId", savedId);
|
||||||
|
if (glWarning != null) result.addProperty("glWarning", glWarning);
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{id}/reverse")
|
||||||
|
@Transactional
|
||||||
|
public String reverse(@PathParam("id") Long id, String body) {
|
||||||
|
this.context.authenticatedUser().validateHasPermissionTo("REVERSE_CASHTRANSFER");
|
||||||
|
|
||||||
|
CashTransfer transfer = repository.findById(id)
|
||||||
|
.orElseThrow(() -> new PlatformDataIntegrityException("error.msg.cashtransfer.not.found",
|
||||||
|
"Cash transfer not found: " + id));
|
||||||
|
if (Boolean.TRUE.equals(transfer.getIsReversed())) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.cashtransfer.already.reversed",
|
||||||
|
"Cash transfer " + id + " is already reversed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String reason = null;
|
||||||
|
try {
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
reason = json.has("reason") ? json.get("reason").getAsString() : null;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
AppUser user = this.context.authenticatedUser();
|
||||||
|
transfer.setReversedBy(user.getId());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
transfer.setIsReversed(true);
|
||||||
|
transfer.setReversedAt(java.time.LocalDateTime.now());
|
||||||
|
transfer.setReverseReason(reason);
|
||||||
|
repository.save(transfer);
|
||||||
|
|
||||||
|
if (transfer.getFromTellerId() != null && transfer.getToCashierId() != null) {
|
||||||
|
try {
|
||||||
|
Long glTellerId = transfer.getFromTellerId();
|
||||||
|
try {
|
||||||
|
Long resolved = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, transfer.getToCashierId());
|
||||||
|
if (resolved != null) glTellerId = resolved;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
glDispatchService.settleCashFromCashier(glTellerId,
|
||||||
|
transfer.getToCashierId(), transfer.getTransferAmount(), "INR",
|
||||||
|
java.time.LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||||
|
"Reversal of cash transfer #" + id + (reason != null ? ": " + reason : ""));
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject result = new JsonObject();
|
||||||
|
result.addProperty("resourceId", id);
|
||||||
|
result.addProperty("reversed", true);
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public String getAll() {
|
||||||
|
List<CashTransfer> list = repository.findAllByOrderByCreatedAtDesc();
|
||||||
|
return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{id}")
|
||||||
|
public String getOne(@PathParam("id") Long id) {
|
||||||
|
CashTransfer header = repository.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Cash transfer not found: " + id));
|
||||||
|
Map<String, Object> map = toMap(header);
|
||||||
|
List<Map<String, Object>> details = detailRepository.findByTransferId(id).stream().map(this::detailToMap)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
map.put("details", details);
|
||||||
|
return gson.toJson(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal sumTotal(JsonArray items) {
|
||||||
|
BigDecimal total = BigDecimal.ZERO;
|
||||||
|
for (JsonElement el : items) {
|
||||||
|
JsonObject item = el.getAsJsonObject();
|
||||||
|
int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
|
||||||
|
? item.get("denominationValue").getAsInt()
|
||||||
|
: 0;
|
||||||
|
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
|
||||||
|
if (count > 0) {
|
||||||
|
total = total.add(BigDecimal.valueOf((long) value * count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasPositiveCount(JsonArray items) {
|
||||||
|
for (JsonElement el : items) {
|
||||||
|
JsonObject item = el.getAsJsonObject();
|
||||||
|
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
|
||||||
|
if (count > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveDetails(Long transferId, JsonArray items) {
|
||||||
|
for (JsonElement el : items) {
|
||||||
|
JsonObject item = el.getAsJsonObject();
|
||||||
|
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
|
||||||
|
if (count <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
|
||||||
|
? item.get("denominationValue").getAsInt()
|
||||||
|
: 0;
|
||||||
|
Long denominationId = item.has("denominationId") && !item.get("denominationId").isJsonNull()
|
||||||
|
? item.get("denominationId").getAsLong()
|
||||||
|
: null;
|
||||||
|
CashTransferDetail detail = new CashTransferDetail();
|
||||||
|
detail.setTransferId(transferId);
|
||||||
|
detail.setDenominationId(denominationId);
|
||||||
|
detail.setDenominationValue(value);
|
||||||
|
detail.setCount(count);
|
||||||
|
detail.setTotalAmount(BigDecimal.valueOf((long) value * count));
|
||||||
|
detailRepository.save(detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toMap(CashTransfer e) {
|
||||||
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
|
map.put("id", e.getId());
|
||||||
|
map.put("transferDate", e.getTransferDate() != null ? e.getTransferDate().toString() : null);
|
||||||
|
map.put("fromParty", e.getFromParty());
|
||||||
|
map.put("toParty", e.getToParty());
|
||||||
|
map.put("fromTellerId", e.getFromTellerId());
|
||||||
|
map.put("toCashierId", e.getToCashierId());
|
||||||
|
map.put("transferAmount", e.getTransferAmount());
|
||||||
|
map.put("note", e.getNote());
|
||||||
|
map.put("performedBy", e.getPerformedBy());
|
||||||
|
map.put("performedByUsername", e.getPerformedByUsername());
|
||||||
|
map.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null);
|
||||||
|
map.put("isReversed", Boolean.TRUE.equals(e.getIsReversed()));
|
||||||
|
map.put("reversedBy", e.getReversedBy());
|
||||||
|
map.put("reversedAt", e.getReversedAt() != null ? e.getReversedAt().toString() : null);
|
||||||
|
map.put("reverseReason", e.getReverseReason());
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> detailToMap(CashTransferDetail d) {
|
||||||
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
|
map.put("id", d.getId());
|
||||||
|
map.put("denominationId", d.getDenominationId());
|
||||||
|
map.put("denominationValue", d.getDenominationValue());
|
||||||
|
map.put("count", d.getCount());
|
||||||
|
map.put("totalAmount", d.getTotalAmount());
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.cashtransfer.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_cash_transfer")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CashTransfer {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "transfer_date")
|
||||||
|
private LocalDate transferDate;
|
||||||
|
|
||||||
|
@Column(name = "from_party", length = 100)
|
||||||
|
private String fromParty;
|
||||||
|
|
||||||
|
@Column(name = "to_party", length = 100)
|
||||||
|
private String toParty;
|
||||||
|
|
||||||
|
@Column(name = "transfer_amount", nullable = false)
|
||||||
|
private BigDecimal transferAmount;
|
||||||
|
|
||||||
|
@Column(name = "note", length = 500)
|
||||||
|
private String note;
|
||||||
|
|
||||||
|
@Column(name = "performed_by")
|
||||||
|
private Long performedBy;
|
||||||
|
|
||||||
|
@Column(name = "performed_by_username", length = 100)
|
||||||
|
private String performedByUsername;
|
||||||
|
|
||||||
|
@Column(name = "created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "from_teller_id")
|
||||||
|
private Long fromTellerId;
|
||||||
|
|
||||||
|
@Column(name = "to_cashier_id")
|
||||||
|
private Long toCashierId;
|
||||||
|
|
||||||
|
@Column(name = "is_reversed")
|
||||||
|
private Boolean isReversed = false;
|
||||||
|
|
||||||
|
@Column(name = "reversed_by")
|
||||||
|
private Long reversedBy;
|
||||||
|
|
||||||
|
@Column(name = "reversed_at")
|
||||||
|
private java.time.LocalDateTime reversedAt;
|
||||||
|
|
||||||
|
@Column(name = "reverse_reason", length = 500)
|
||||||
|
private String reverseReason;
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.cashtransfer.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_cash_transfer_detail")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CashTransferDetail {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "transfer_id", nullable = false)
|
||||||
|
private Long transferId;
|
||||||
|
|
||||||
|
@Column(name = "denomination_id")
|
||||||
|
private Long denominationId;
|
||||||
|
|
||||||
|
@Column(name = "denomination_value", nullable = false)
|
||||||
|
private Integer denominationValue;
|
||||||
|
|
||||||
|
@Column(name = "count", nullable = false)
|
||||||
|
private Integer count;
|
||||||
|
|
||||||
|
@Column(name = "total_amount", nullable = false)
|
||||||
|
private BigDecimal totalAmount;
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashtransfer.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface CashTransferDetailRepository extends JpaRepository<CashTransferDetail, Long> {
|
||||||
|
List<CashTransferDetail> findByTransferId(Long transferId);
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package org.apache.fineract.portfolio.cashtransfer.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface CashTransferRepository extends JpaRepository<CashTransfer, Long> {
|
||||||
|
List<CashTransfer> findAllByOrderByCreatedAtDesc();
|
||||||
|
}
|
||||||
+232
@@ -0,0 +1,232 @@
|
|||||||
|
package org.apache.fineract.portfolio.denominationmaster.api;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.DELETE;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.PUT;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMaster;
|
||||||
|
import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterAudit;
|
||||||
|
import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterAuditRepository;
|
||||||
|
import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterRepository;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/denominationmaster")
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
public class DenominationMasterApiResource {
|
||||||
|
|
||||||
|
private final DenominationMasterRepository repository;
|
||||||
|
private final DenominationMasterAuditRepository auditRepository;
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public String getAll() {
|
||||||
|
List<DenominationMaster> list = repository.findByDeletedFalseOrderByKindDescDenominationDesc();
|
||||||
|
return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("active")
|
||||||
|
public String getActive() {
|
||||||
|
List<DenominationMaster> list = repository.findByIsActiveTrueAndDeletedFalseOrderByKindDescDenominationDesc();
|
||||||
|
return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("deleted")
|
||||||
|
public String getDeleted() {
|
||||||
|
List<DenominationMaster> list = repository.findByDeletedTrueOrderByKindDescDenominationDesc();
|
||||||
|
return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("audit")
|
||||||
|
public String getAllAudit() {
|
||||||
|
List<DenominationMasterAudit> list = auditRepository.findAllByOrderByPerformedOnDesc();
|
||||||
|
return gson.toJson(list.stream().map(this::auditToMap).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{id}/audit")
|
||||||
|
public String getAuditForDenomination(@PathParam("id") Long id) {
|
||||||
|
List<DenominationMasterAudit> list = auditRepository.findByDenominationIdOrderByPerformedOnDesc(id);
|
||||||
|
return gson.toJson(list.stream().map(this::auditToMap).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
public String create(String body) {
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
final int denomination = json.get("denomination").getAsInt();
|
||||||
|
final String kind = json.get("kind").getAsString();
|
||||||
|
if (repository.existsByDenominationAndKindAndDeletedFalse(denomination, kind)) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.denomination.duplicate",
|
||||||
|
"A " + kind + " of denomination " + denomination + " already exists.", "denomination", denomination);
|
||||||
|
}
|
||||||
|
DenominationMaster dm = DenominationMaster.create(denomination, kind,
|
||||||
|
json.has("description") ? json.get("description").getAsString() : null,
|
||||||
|
json.has("isActive") ? json.get("isActive").getAsBoolean() : true,
|
||||||
|
json.has("introducedOn") ? LocalDate.parse(json.get("introducedOn").getAsString()) : null, null);
|
||||||
|
DenominationMaster saved = repository.save(dm);
|
||||||
|
writeAudit("CREATE", saved, null);
|
||||||
|
JsonObject result = new JsonObject();
|
||||||
|
result.addProperty("resourceId", saved.getId());
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("{id}")
|
||||||
|
public String update(@PathParam("id") Long id, @QueryParam("command") String command, String body) {
|
||||||
|
DenominationMaster dm = repository.findById(id).orElseThrow(() -> new RuntimeException("Denomination not found: " + id));
|
||||||
|
|
||||||
|
if ("activate".equals(command)) {
|
||||||
|
dm.setIsActive(true);
|
||||||
|
dm.setDiscontinuedOn(null);
|
||||||
|
repository.save(dm);
|
||||||
|
writeAudit("ACTIVATE", dm, null);
|
||||||
|
} else if ("deactivate".equals(command)) {
|
||||||
|
dm.setIsActive(false);
|
||||||
|
dm.setDiscontinuedOn(LocalDate.now());
|
||||||
|
repository.save(dm);
|
||||||
|
writeAudit("DEACTIVATE", dm, null);
|
||||||
|
} else if ("restore".equals(command)) {
|
||||||
|
if (repository.existsByDenominationAndKindAndDeletedFalseAndIdNot(dm.getDenomination(), dm.getKind(), id)) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.denomination.duplicate.restore",
|
||||||
|
"Cannot restore: a " + dm.getKind() + " of denomination " + dm.getDenomination() + " already exists.",
|
||||||
|
"denomination", dm.getDenomination());
|
||||||
|
}
|
||||||
|
dm.setDeleted(false);
|
||||||
|
repository.save(dm);
|
||||||
|
writeAudit("RESTORE", dm, null);
|
||||||
|
} else {
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
final int effectiveDenomination = json.has("denomination") ? json.get("denomination").getAsInt() : dm.getDenomination();
|
||||||
|
final String effectiveKind = json.has("kind") ? json.get("kind").getAsString() : dm.getKind();
|
||||||
|
if (repository.existsByDenominationAndKindAndDeletedFalseAndIdNot(effectiveDenomination, effectiveKind, id)) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.denomination.duplicate",
|
||||||
|
"A " + effectiveKind + " of denomination " + effectiveDenomination + " already exists.", "denomination",
|
||||||
|
effectiveDenomination);
|
||||||
|
}
|
||||||
|
List<String> changes = new ArrayList<>();
|
||||||
|
if (json.has("denomination")) {
|
||||||
|
int newVal = json.get("denomination").getAsInt();
|
||||||
|
if (!java.util.Objects.equals(dm.getDenomination(), newVal)) {
|
||||||
|
changes.add("denomination: " + dm.getDenomination() + " -> " + newVal);
|
||||||
|
}
|
||||||
|
dm.setDenomination(newVal);
|
||||||
|
}
|
||||||
|
if (json.has("kind")) {
|
||||||
|
String newVal = json.get("kind").getAsString();
|
||||||
|
if (!java.util.Objects.equals(dm.getKind(), newVal)) {
|
||||||
|
changes.add("kind: " + dm.getKind() + " -> " + newVal);
|
||||||
|
}
|
||||||
|
dm.setKind(newVal);
|
||||||
|
}
|
||||||
|
if (json.has("description")) {
|
||||||
|
String newVal = json.get("description").getAsString();
|
||||||
|
if (!java.util.Objects.equals(dm.getDescription(), newVal)) {
|
||||||
|
changes.add("description: " + dm.getDescription() + " -> " + newVal);
|
||||||
|
}
|
||||||
|
dm.setDescription(newVal);
|
||||||
|
}
|
||||||
|
if (json.has("isActive")) {
|
||||||
|
boolean newVal = json.get("isActive").getAsBoolean();
|
||||||
|
if (!java.util.Objects.equals(dm.getIsActive(), newVal)) {
|
||||||
|
changes.add("isActive: " + dm.getIsActive() + " -> " + newVal);
|
||||||
|
}
|
||||||
|
dm.setIsActive(newVal);
|
||||||
|
}
|
||||||
|
if (json.has("introducedOn")) {
|
||||||
|
LocalDate newVal = LocalDate.parse(json.get("introducedOn").getAsString());
|
||||||
|
if (!java.util.Objects.equals(dm.getIntroducedOn(), newVal)) {
|
||||||
|
changes.add("introducedOn: " + dm.getIntroducedOn() + " -> " + newVal);
|
||||||
|
}
|
||||||
|
dm.setIntroducedOn(newVal);
|
||||||
|
}
|
||||||
|
repository.save(dm);
|
||||||
|
writeAudit("UPDATE", dm, changes.isEmpty() ? null : String.join("; ", changes));
|
||||||
|
}
|
||||||
|
JsonObject result = new JsonObject();
|
||||||
|
result.addProperty("resourceId", id);
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("{id}")
|
||||||
|
public String delete(@PathParam("id") Long id) {
|
||||||
|
DenominationMaster dm = repository.findById(id).orElseThrow(() -> new RuntimeException("Denomination not found: " + id));
|
||||||
|
// soft delete: flag the row instead of removing it, so it can be restored and history is preserved
|
||||||
|
dm.setDeleted(true);
|
||||||
|
dm.setIsActive(false);
|
||||||
|
repository.save(dm);
|
||||||
|
writeAudit("DELETE", dm, null);
|
||||||
|
JsonObject result = new JsonObject();
|
||||||
|
result.addProperty("resourceId", id);
|
||||||
|
return gson.toJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeAudit(final String action, final DenominationMaster dm, final String changes) {
|
||||||
|
Long userId = null;
|
||||||
|
String username = null;
|
||||||
|
try {
|
||||||
|
AppUser user = this.context.authenticatedUser();
|
||||||
|
if (user != null) {
|
||||||
|
userId = user.getId();
|
||||||
|
username = user.getUsername();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// audit should never block the main operation if the user cannot be resolved
|
||||||
|
}
|
||||||
|
auditRepository.save(DenominationMasterAudit.of(action, dm, changes, userId, username));
|
||||||
|
}
|
||||||
|
|
||||||
|
private java.util.Map<String, Object> toMap(DenominationMaster dm) {
|
||||||
|
java.util.Map<String, Object> map = new java.util.LinkedHashMap<>();
|
||||||
|
map.put("id", dm.getId());
|
||||||
|
map.put("denomination", dm.getDenomination());
|
||||||
|
map.put("kind", dm.getKind());
|
||||||
|
map.put("description", dm.getDescription());
|
||||||
|
map.put("isActive", dm.getIsActive());
|
||||||
|
map.put("introducedOn", dm.getIntroducedOn() != null ? dm.getIntroducedOn().toString() : null);
|
||||||
|
map.put("discontinuedOn", dm.getDiscontinuedOn() != null ? dm.getDiscontinuedOn().toString() : null);
|
||||||
|
map.put("deleted", dm.getDeleted());
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private java.util.Map<String, Object> auditToMap(DenominationMasterAudit a) {
|
||||||
|
java.util.Map<String, Object> map = new java.util.LinkedHashMap<>();
|
||||||
|
map.put("id", a.getId());
|
||||||
|
map.put("denominationId", a.getDenominationId());
|
||||||
|
map.put("action", a.getAction());
|
||||||
|
map.put("denomination", a.getDenomination());
|
||||||
|
map.put("kind", a.getKind());
|
||||||
|
map.put("description", a.getDescription());
|
||||||
|
map.put("isActive", a.getIsActive());
|
||||||
|
map.put("introducedOn", a.getIntroducedOn() != null ? a.getIntroducedOn().toString() : null);
|
||||||
|
map.put("discontinuedOn", a.getDiscontinuedOn() != null ? a.getDiscontinuedOn().toString() : null);
|
||||||
|
map.put("changes", a.getChanges());
|
||||||
|
map.put("performedBy", a.getPerformedBy());
|
||||||
|
map.put("performedByUsername", a.getPerformedByUsername());
|
||||||
|
map.put("performedOn", a.getPerformedOn() != null ? a.getPerformedOn().toString() : null);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.denominationmaster.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_denomination_master")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class DenominationMaster {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "denomination", nullable = false)
|
||||||
|
private Integer denomination;
|
||||||
|
|
||||||
|
@Column(name = "kind", nullable = false, length = 10)
|
||||||
|
private String kind;
|
||||||
|
|
||||||
|
@Column(name = "description", length = 100)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "is_active", nullable = false)
|
||||||
|
private Boolean isActive = true;
|
||||||
|
|
||||||
|
@Column(name = "introduced_on")
|
||||||
|
private LocalDate introducedOn;
|
||||||
|
|
||||||
|
@Column(name = "discontinued_on")
|
||||||
|
private LocalDate discontinuedOn;
|
||||||
|
|
||||||
|
@Column(name = "deleted", nullable = false)
|
||||||
|
private Boolean deleted = false;
|
||||||
|
|
||||||
|
public static DenominationMaster create(Integer denomination, String kind, String description,
|
||||||
|
Boolean isActive, LocalDate introducedOn, LocalDate discontinuedOn) {
|
||||||
|
DenominationMaster dm = new DenominationMaster();
|
||||||
|
dm.denomination = denomination;
|
||||||
|
dm.kind = kind;
|
||||||
|
dm.description = description;
|
||||||
|
dm.isActive = isActive != null ? isActive : true;
|
||||||
|
dm.introducedOn = introducedOn;
|
||||||
|
dm.discontinuedOn = discontinuedOn;
|
||||||
|
dm.deleted = false;
|
||||||
|
return dm;
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.denominationmaster.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_denomination_master_audit")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class DenominationMasterAudit {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "denomination_id")
|
||||||
|
private Long denominationId;
|
||||||
|
|
||||||
|
@Column(name = "action", nullable = false, length = 20)
|
||||||
|
private String action;
|
||||||
|
|
||||||
|
@Column(name = "denomination")
|
||||||
|
private Integer denomination;
|
||||||
|
|
||||||
|
@Column(name = "kind", length = 10)
|
||||||
|
private String kind;
|
||||||
|
|
||||||
|
@Column(name = "description", length = 100)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "is_active")
|
||||||
|
private Boolean isActive;
|
||||||
|
|
||||||
|
@Column(name = "introduced_on")
|
||||||
|
private LocalDate introducedOn;
|
||||||
|
|
||||||
|
@Column(name = "discontinued_on")
|
||||||
|
private LocalDate discontinuedOn;
|
||||||
|
|
||||||
|
@Column(name = "changes", length = 1000)
|
||||||
|
private String changes;
|
||||||
|
|
||||||
|
@Column(name = "performed_by")
|
||||||
|
private Long performedBy;
|
||||||
|
|
||||||
|
@Column(name = "performed_by_username", length = 100)
|
||||||
|
private String performedByUsername;
|
||||||
|
|
||||||
|
@Column(name = "performed_on", nullable = false)
|
||||||
|
private LocalDateTime performedOn;
|
||||||
|
|
||||||
|
public static DenominationMasterAudit of(final String action, final DenominationMaster dm, final String changes,
|
||||||
|
final Long performedBy, final String performedByUsername) {
|
||||||
|
final DenominationMasterAudit a = new DenominationMasterAudit();
|
||||||
|
a.action = action;
|
||||||
|
a.denominationId = dm.getId();
|
||||||
|
a.denomination = dm.getDenomination();
|
||||||
|
a.kind = dm.getKind();
|
||||||
|
a.description = dm.getDescription();
|
||||||
|
a.isActive = dm.getIsActive();
|
||||||
|
a.introducedOn = dm.getIntroducedOn();
|
||||||
|
a.discontinuedOn = dm.getDiscontinuedOn();
|
||||||
|
a.changes = changes;
|
||||||
|
a.performedBy = performedBy;
|
||||||
|
a.performedByUsername = performedByUsername;
|
||||||
|
a.performedOn = LocalDateTime.now();
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.denominationmaster.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface DenominationMasterAuditRepository extends JpaRepository<DenominationMasterAudit, Long> {
|
||||||
|
|
||||||
|
List<DenominationMasterAudit> findByDenominationIdOrderByPerformedOnDesc(Long denominationId);
|
||||||
|
|
||||||
|
List<DenominationMasterAudit> findAllByOrderByPerformedOnDesc();
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package org.apache.fineract.portfolio.denominationmaster.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface DenominationMasterRepository extends JpaRepository<DenominationMaster, Long> {
|
||||||
|
List<DenominationMaster> findByDeletedFalseOrderByKindDescDenominationDesc();
|
||||||
|
List<DenominationMaster> findByIsActiveTrueAndDeletedFalseOrderByKindDescDenominationDesc();
|
||||||
|
List<DenominationMaster> findByDeletedTrueOrderByKindDescDenominationDesc();
|
||||||
|
|
||||||
|
boolean existsByDenominationAndKindAndDeletedFalse(Integer denomination, String kind);
|
||||||
|
boolean existsByDenominationAndKindAndDeletedFalseAndIdNot(Integer denomination, String kind, Long id);
|
||||||
|
}
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* 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.denominationmaster.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_savings_txn_denomination")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SavingsTxnDenomination {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "txn_id", nullable = false)
|
||||||
|
private Long txnId;
|
||||||
|
|
||||||
|
@Column(name = "savings_account_id", nullable = false)
|
||||||
|
private Long savingsAccountId;
|
||||||
|
|
||||||
|
@Column(name = "account_no", length = 50)
|
||||||
|
private String accountNo;
|
||||||
|
|
||||||
|
@Column(name = "client_id")
|
||||||
|
private Long clientId;
|
||||||
|
|
||||||
|
@Column(name = "cif_no", length = 50)
|
||||||
|
private String cifNo;
|
||||||
|
|
||||||
|
@Column(name = "denomination_id", nullable = false)
|
||||||
|
private Long denominationId;
|
||||||
|
|
||||||
|
@Column(name = "denomination_value", nullable = false)
|
||||||
|
private Integer denominationValue;
|
||||||
|
|
||||||
|
@Column(name = "count", nullable = false)
|
||||||
|
private Integer count;
|
||||||
|
|
||||||
|
@Column(name = "total_amount", nullable = false, precision = 19, scale = 6)
|
||||||
|
private BigDecimal totalAmount;
|
||||||
|
|
||||||
|
@Column(name = "txn_date")
|
||||||
|
private LocalDate txnDate;
|
||||||
|
|
||||||
|
@Column(name = "created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
/** "DEPOSIT" or "WITHDRAWAL" — identifies the direction of the cash flow. */
|
||||||
|
@Column(name = "txn_type", nullable = false, length = 20)
|
||||||
|
private String txnType;
|
||||||
|
|
||||||
|
/** ID of the logged-in teller / officer who performed the transaction. */
|
||||||
|
@Column(name = "officer_id")
|
||||||
|
private Long officerId;
|
||||||
|
|
||||||
|
/** Username of the teller / officer who performed the transaction. */
|
||||||
|
@Column(name = "officer_username", length = 100)
|
||||||
|
private String officerUsername;
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.denominationmaster.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface SavingsTxnDenominationRepository extends JpaRepository<SavingsTxnDenomination, Long> {
|
||||||
|
List<SavingsTxnDenomination> findByTxnId(Long txnId);
|
||||||
|
List<SavingsTxnDenomination> findBySavingsAccountId(Long savingsAccountId);
|
||||||
|
}
|
||||||
+349
@@ -0,0 +1,349 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.denominationmaster.service;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenomination;
|
||||||
|
import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenominationRepository;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and persists the per-transaction cash denomination breakdown sent by the UI
|
||||||
|
* on both deposits and withdrawals.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DenominationBreakdownWriter {
|
||||||
|
|
||||||
|
private final SavingsTxnDenominationRepository repository;
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
// ── Public API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full validation of the denomination breakdown:
|
||||||
|
* <ol>
|
||||||
|
* <li>Each item must have a non-null {@code denominationId}.</li>
|
||||||
|
* <li>Each item's {@code count} must be > 0.</li>
|
||||||
|
* <li>Each item's {@code denominationValue} (or {@code denomination}) must be > 0.</li>
|
||||||
|
* <li>The grand total (sum of value × count) must equal {@code expectedAmount}.</li>
|
||||||
|
* </ol>
|
||||||
|
* No-op when no breakdown is provided.
|
||||||
|
*
|
||||||
|
* @param command the JSON command containing the denominationBreakdown array
|
||||||
|
* @param expectedAmount the transaction amount that the breakdown must sum to
|
||||||
|
* @param txnType "DEPOSIT" or "WITHDRAWAL" — used in error messages
|
||||||
|
*/
|
||||||
|
public void validateBreakdown(final JsonCommand command, final BigDecimal expectedAmount, final String txnType) {
|
||||||
|
if (command == null || !command.parameterExists("denominationBreakdown")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final JsonArray breakdown = command.arrayOfParameterNamed("denominationBreakdown");
|
||||||
|
if (breakdown == null || breakdown.size() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder validator = new DataValidatorBuilder(errors).resource("denominationBreakdown");
|
||||||
|
BigDecimal computedTotal = BigDecimal.ZERO;
|
||||||
|
final String label = (txnType != null && !txnType.isBlank()) ? txnType.toUpperCase() : "TRANSACTION";
|
||||||
|
|
||||||
|
for (int i = 0; i < breakdown.size(); i++) {
|
||||||
|
final JsonObject obj = breakdown.get(i).getAsJsonObject();
|
||||||
|
final String itemPrefix = "denominationBreakdown[" + i + "]";
|
||||||
|
|
||||||
|
// 1. denominationId is required
|
||||||
|
if (!obj.has("denominationId") || obj.get("denominationId").isJsonNull()) {
|
||||||
|
validator.reset().parameter(itemPrefix + ".denominationId")
|
||||||
|
.value(null)
|
||||||
|
.failWithCode("required", "denominationId is required for each denomination item.");
|
||||||
|
continue; // cannot compute further without ID
|
||||||
|
}
|
||||||
|
final long denominationId = obj.get("denominationId").getAsLong();
|
||||||
|
if (denominationId <= 0) {
|
||||||
|
validator.reset().parameter(itemPrefix + ".denominationId")
|
||||||
|
.value(denominationId)
|
||||||
|
.failWithCode("invalid", "denominationId must be a positive number.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. count is required and must be > 0
|
||||||
|
if (!obj.has("count") || obj.get("count").isJsonNull()) {
|
||||||
|
validator.reset().parameter(itemPrefix + ".count")
|
||||||
|
.value(null)
|
||||||
|
.failWithCode("required", "count is required for each denomination item.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final int count = obj.get("count").getAsInt();
|
||||||
|
if (count <= 0) {
|
||||||
|
// count = 0 is silently skipped (UI may send zero for unselected rows)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. denominationValue (or denomination) must be present and > 0
|
||||||
|
int denominationValue = 0;
|
||||||
|
if (obj.has("denominationValue") && !obj.get("denominationValue").isJsonNull()) {
|
||||||
|
denominationValue = obj.get("denominationValue").getAsInt();
|
||||||
|
} else if (obj.has("denomination") && !obj.get("denomination").isJsonNull()) {
|
||||||
|
denominationValue = obj.get("denomination").getAsInt();
|
||||||
|
}
|
||||||
|
if (denominationValue <= 0) {
|
||||||
|
validator.reset().parameter(itemPrefix + ".denominationValue")
|
||||||
|
.value(denominationValue)
|
||||||
|
.failWithCode("invalid",
|
||||||
|
"denominationValue must be greater than zero for denomination item " + i + ".");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Accumulate total: value × count
|
||||||
|
computedTotal = computedTotal.add(BigDecimal.valueOf((long) denominationValue * count));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw item-level validation errors first
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
|
||||||
|
"Denomination breakdown validation errors exist.", errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Grand total must match the transaction amount
|
||||||
|
if (expectedAmount != null && computedTotal.compareTo(expectedAmount) != 0) {
|
||||||
|
throw new PlatformDataIntegrityException(
|
||||||
|
"error.msg.denomination.total.mismatch",
|
||||||
|
label + " denomination breakdown total (" + computedTotal
|
||||||
|
+ ") does not match the transaction amount (" + expectedAmount
|
||||||
|
+ "). Please re-check the note/coin counts.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience wrapper kept for backward-compatibility with any callers that do not
|
||||||
|
* yet pass a {@code txnType}. Delegates to {@link #validateBreakdown}.
|
||||||
|
*/
|
||||||
|
public void validateTotal(final JsonCommand command, final BigDecimal expectedAmount) {
|
||||||
|
validateBreakdown(command, expectedAmount, "TRANSACTION");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that the cashier currently holds enough of each denomination
|
||||||
|
* in the withdrawal breakdown. Auto-detects the cashier from the authenticated
|
||||||
|
* user. If the user is not linked to a cashier (e.g. admin/back-office), the
|
||||||
|
* check is skipped silently.
|
||||||
|
*
|
||||||
|
* @param command the JSON command containing the denominationBreakdown array
|
||||||
|
*/
|
||||||
|
public void validateWithdrawalStock(final JsonCommand command) {
|
||||||
|
if (command == null || !command.parameterExists("denominationBreakdown")) return;
|
||||||
|
final JsonArray breakdown = command.arrayOfParameterNamed("denominationBreakdown");
|
||||||
|
if (breakdown == null || breakdown.size() == 0) return;
|
||||||
|
|
||||||
|
Long cashierId = null;
|
||||||
|
try {
|
||||||
|
final AppUser user = context.authenticatedUser();
|
||||||
|
cashierId = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id "
|
||||||
|
+ "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?",
|
||||||
|
Long.class, user.getId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return; // not a cashier user — skip stock check
|
||||||
|
}
|
||||||
|
if (cashierId == null) return;
|
||||||
|
|
||||||
|
final Map<Long, Long> stock = getCashierNetPosition(cashierId);
|
||||||
|
final List<String> shortages = new ArrayList<>();
|
||||||
|
|
||||||
|
for (final JsonElement el : breakdown) {
|
||||||
|
final JsonObject item = el.getAsJsonObject();
|
||||||
|
final int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
|
||||||
|
if (count <= 0) continue;
|
||||||
|
if (!item.has("denominationId") || item.get("denominationId").isJsonNull()) continue;
|
||||||
|
|
||||||
|
final long denomId = item.get("denominationId").getAsLong();
|
||||||
|
final long available = stock.getOrDefault(denomId, 0L);
|
||||||
|
if (count > available) {
|
||||||
|
final int denomValue = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
|
||||||
|
? item.get("denominationValue").getAsInt() : 0;
|
||||||
|
shortages.add("₹" + denomValue + " (need " + count + ", available " + available + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shortages.isEmpty()) {
|
||||||
|
throw new PlatformDataIntegrityException(
|
||||||
|
"error.msg.cashier.denomination.insufficient",
|
||||||
|
"Insufficient denomination stock for withdrawal: " + String.join("; ", shortages));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the cashier's current net denomination-wise stock by summing all IN
|
||||||
|
* transactions and subtracting all OUT transactions for that cashier.
|
||||||
|
*
|
||||||
|
* @param cashierId the cashier whose position to compute
|
||||||
|
* @return map of denominationId → net count available
|
||||||
|
*/
|
||||||
|
public Map<Long, Long> getCashierNetPosition(final Long cashierId) {
|
||||||
|
final String sql = "SELECT denomination_id, SUM(net_count) AS available FROM ("
|
||||||
|
+ " SELECT ctd.denomination_id, CAST(SUM(ctd.count) AS BIGINT) AS net_count"
|
||||||
|
+ " FROM m_cash_transfer_detail ctd JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id"
|
||||||
|
+ " WHERE ct.to_cashier_id = ? AND (ct.is_reversed IS NULL OR ct.is_reversed = false)"
|
||||||
|
+ " GROUP BY ctd.denomination_id"
|
||||||
|
+ " UNION ALL"
|
||||||
|
+ " SELECT icd.denomination_id, CAST(SUM(icd.count) AS BIGINT)"
|
||||||
|
+ " FROM m_cash_inter_cashier_detail icd JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id"
|
||||||
|
+ " WHERE ic.to_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false)"
|
||||||
|
+ " GROUP BY icd.denomination_id"
|
||||||
|
+ " UNION ALL"
|
||||||
|
+ " SELECT std.denomination_id, CAST(SUM(std.count) AS BIGINT)"
|
||||||
|
+ " FROM m_savings_txn_denomination std"
|
||||||
|
+ " WHERE std.txn_type = 'DEPOSIT'"
|
||||||
|
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
|
||||||
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)"
|
||||||
|
+ " GROUP BY std.denomination_id"
|
||||||
|
+ " UNION ALL"
|
||||||
|
+ " SELECT ced.denomination_id, CAST(SUM(ced.count) AS BIGINT)"
|
||||||
|
+ " FROM m_cash_exchange_detail ced JOIN m_cash_exchange ce ON ce.id = ced.exchange_id"
|
||||||
|
+ " WHERE ce.cashier_id = ? AND ced.direction = 'RECEIVED'"
|
||||||
|
+ " AND (ce.is_reversed IS NULL OR ce.is_reversed = false)"
|
||||||
|
+ " GROUP BY ced.denomination_id"
|
||||||
|
+ " UNION ALL"
|
||||||
|
+ " SELECT csd.denomination_id, CAST(-SUM(csd.count) AS BIGINT)"
|
||||||
|
+ " FROM m_cash_settlement_detail csd JOIN m_cash_settlement cs ON cs.id = csd.settlement_id"
|
||||||
|
+ " WHERE cs.from_cashier_id = ? AND (cs.is_reversed IS NULL OR cs.is_reversed = false)"
|
||||||
|
+ " GROUP BY csd.denomination_id"
|
||||||
|
+ " UNION ALL"
|
||||||
|
+ " SELECT icd.denomination_id, CAST(-SUM(icd.count) AS BIGINT)"
|
||||||
|
+ " FROM m_cash_inter_cashier_detail icd JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id"
|
||||||
|
+ " WHERE ic.from_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false)"
|
||||||
|
+ " GROUP BY icd.denomination_id"
|
||||||
|
+ " UNION ALL"
|
||||||
|
+ " SELECT std.denomination_id, CAST(-SUM(std.count) AS BIGINT)"
|
||||||
|
+ " FROM m_savings_txn_denomination std"
|
||||||
|
+ " WHERE std.txn_type = 'WITHDRAWAL'"
|
||||||
|
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
|
||||||
|
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)"
|
||||||
|
+ " GROUP BY std.denomination_id"
|
||||||
|
+ " UNION ALL"
|
||||||
|
+ " SELECT ced.denomination_id, CAST(-SUM(ced.count) AS BIGINT)"
|
||||||
|
+ " FROM m_cash_exchange_detail ced JOIN m_cash_exchange ce ON ce.id = ced.exchange_id"
|
||||||
|
+ " WHERE ce.cashier_id = ? AND ced.direction = 'GIVEN'"
|
||||||
|
+ " AND (ce.is_reversed IS NULL OR ce.is_reversed = false)"
|
||||||
|
+ " GROUP BY ced.denomination_id"
|
||||||
|
+ ") t GROUP BY denomination_id";
|
||||||
|
|
||||||
|
final Map<Long, Long> stock = new HashMap<>();
|
||||||
|
jdbcTemplate.query(sql, rs -> {
|
||||||
|
stock.put(rs.getLong("denomination_id"), rs.getLong("available"));
|
||||||
|
}, cashierId, cashierId, cashierId, cashierId, cashierId, cashierId, cashierId, cashierId);
|
||||||
|
return stock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist each valid denomination row for the given transaction.
|
||||||
|
* <p>
|
||||||
|
* Items with {@code count <= 0} are silently skipped (UI sends zero for
|
||||||
|
* unselected rows). Items missing {@code denominationId} are also skipped
|
||||||
|
* (they should have already been rejected by {@link #validateBreakdown}).
|
||||||
|
* <p>
|
||||||
|
* The logged-in officer (teller) is automatically resolved from the
|
||||||
|
* security context; if resolution fails (e.g. batch job) the officer fields
|
||||||
|
* are left {@code null}.
|
||||||
|
*
|
||||||
|
* @param command the JSON command containing the denominationBreakdown array
|
||||||
|
* @param txnId ID of the saved savings / current / RD / FD transaction
|
||||||
|
* @param savingsAccountId savings / current / RD / FD account ID
|
||||||
|
* @param accountNo account number string
|
||||||
|
* @param clientId client ID (may be null for group accounts)
|
||||||
|
* @param cifNo CIF number (may be null)
|
||||||
|
* @param txnDate transaction date
|
||||||
|
* @param txnType "DEPOSIT" or "WITHDRAWAL"
|
||||||
|
*/
|
||||||
|
public void save(final JsonCommand command, final Long txnId, final Long savingsAccountId, final String accountNo,
|
||||||
|
final Long clientId, final String cifNo, final LocalDate txnDate, final String txnType) {
|
||||||
|
|
||||||
|
if (command == null || txnId == null || !command.parameterExists("denominationBreakdown")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final JsonArray breakdown = command.arrayOfParameterNamed("denominationBreakdown");
|
||||||
|
if (breakdown == null || breakdown.size() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the officer / teller from the security context
|
||||||
|
Long officerId = null;
|
||||||
|
String officerUsername = null;
|
||||||
|
try {
|
||||||
|
final AppUser user = this.context.authenticatedUser();
|
||||||
|
if (user != null) {
|
||||||
|
officerId = user.getId();
|
||||||
|
officerUsername = user.getUsername();
|
||||||
|
}
|
||||||
|
} catch (final Exception ignored) {
|
||||||
|
// If user cannot be resolved (e.g. batch / async job), leave officer fields null
|
||||||
|
}
|
||||||
|
|
||||||
|
final String resolvedTxnType = (txnType != null && !txnType.isBlank()) ? txnType.toUpperCase() : "DEPOSIT";
|
||||||
|
|
||||||
|
for (final JsonElement entry : breakdown) {
|
||||||
|
final JsonObject obj = entry.getAsJsonObject();
|
||||||
|
|
||||||
|
// denominationId is mandatory (NOT NULL in DB)
|
||||||
|
if (!obj.has("denominationId") || obj.get("denominationId").isJsonNull() || !obj.has("count")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final int count = obj.get("count").getAsInt();
|
||||||
|
if (count <= 0) {
|
||||||
|
continue; // zero-count rows sent by UI are silently ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
final long denominationId = obj.get("denominationId").getAsLong();
|
||||||
|
|
||||||
|
// Resolve denomination face value
|
||||||
|
int denominationValue = 0;
|
||||||
|
if (obj.has("denominationValue") && !obj.get("denominationValue").isJsonNull()) {
|
||||||
|
denominationValue = obj.get("denominationValue").getAsInt();
|
||||||
|
} else if (obj.has("denomination") && !obj.get("denomination").isJsonNull()) {
|
||||||
|
denominationValue = obj.get("denomination").getAsInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
final BigDecimal lineTotal = BigDecimal.valueOf((long) denominationValue * count);
|
||||||
|
|
||||||
|
final SavingsTxnDenomination row = SavingsTxnDenomination.builder()
|
||||||
|
.txnId(txnId)
|
||||||
|
.savingsAccountId(savingsAccountId)
|
||||||
|
.accountNo(accountNo)
|
||||||
|
.clientId(clientId)
|
||||||
|
.cifNo(cifNo)
|
||||||
|
.denominationId(denominationId)
|
||||||
|
.denominationValue(denominationValue)
|
||||||
|
.count(count)
|
||||||
|
.totalAmount(lineTotal)
|
||||||
|
.txnDate(txnDate)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.txnType(resolvedTxnType)
|
||||||
|
.officerId(officerId)
|
||||||
|
.officerUsername(officerUsername)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.repository.save(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -67,7 +67,7 @@ public class DepositAccountTransactionDataValidator {
|
|||||||
routingCodeParamName, receiptNumberParamName, bankNumberParamName, DepositsApiConstants.amountParamName,
|
routingCodeParamName, receiptNumberParamName, bankNumberParamName, DepositsApiConstants.amountParamName,
|
||||||
DepositsApiConstants.accountIdParamName, DepositsApiConstants.dateParamName, DepositsApiConstants.submittedOnDateParamName,
|
DepositsApiConstants.accountIdParamName, DepositsApiConstants.dateParamName, DepositsApiConstants.submittedOnDateParamName,
|
||||||
DepositsApiConstants.lienTransaction, DepositsApiConstants.isManualTransaction, DepositsApiConstants.chargesPaidByData,
|
DepositsApiConstants.lienTransaction, DepositsApiConstants.isManualTransaction, DepositsApiConstants.chargesPaidByData,
|
||||||
DepositsApiConstants.accountNoParamName, DepositsApiConstants.noteParamName));
|
DepositsApiConstants.accountNoParamName, DepositsApiConstants.noteParamName, "denominationBreakdown"));
|
||||||
|
|
||||||
private static final Set<String> DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
private static final Set<String> DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName,
|
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName,
|
||||||
|
|||||||
+12
@@ -31,6 +31,8 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.deposit
|
|||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.expectedFirstDepositOnDateParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.expectedFirstDepositOnDateParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isCalendarInheritedParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isCalendarInheritedParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isMandatoryDepositParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isMandatoryDepositParamName;
|
||||||
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isSeniorCitizenParamName;
|
||||||
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.seniorCitizenInterestRateParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.mandatoryRecommendedDepositAmountParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.mandatoryRecommendedDepositAmountParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maturityInstructionIdParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maturityInstructionIdParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferInterestToSavingsParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferInterestToSavingsParamName;
|
||||||
@@ -230,6 +232,16 @@ public class DepositAccountAssembler {
|
|||||||
interestRate = product.nominalAnnualInterestRate();
|
interestRate = product.nominalAnnualInterestRate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Senior citizen interest rate override: use the higher rate when applicable
|
||||||
|
if (command.parameterExists(isSeniorCitizenParamName) && command.booleanPrimitiveValueOfParameterNamed(isSeniorCitizenParamName)) {
|
||||||
|
if (command.parameterExists(seniorCitizenInterestRateParamName)) {
|
||||||
|
final BigDecimal scRate = command.bigDecimalValueOfParameterNamed(seniorCitizenInterestRateParamName);
|
||||||
|
if (scRate != null && scRate.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
interestRate = scRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SavingsCompoundingInterestPeriodType interestCompoundingPeriodType = null;
|
SavingsCompoundingInterestPeriodType interestCompoundingPeriodType = null;
|
||||||
final Integer interestPeriodTypeValue = command.integerValueOfParameterNamed(interestCompoundingPeriodTypeParamName);
|
final Integer interestPeriodTypeValue = command.integerValueOfParameterNamed(interestCompoundingPeriodTypeParamName);
|
||||||
if (interestPeriodTypeValue != null) {
|
if (interestPeriodTypeValue != null) {
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ import lombok.Getter;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "m_fd_base_interest_rate_effective")
|
@Table(name = "m_cbs_base_interest_rate_effective")
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class FdBaseInterestRateEffective {
|
public class FdBaseInterestRateEffective {
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ import lombok.Getter;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "m_fd_base_interest_rate_slab_history")
|
@Table(name = "m_cbs_base_interest_rate_slab_history")
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class FdBaseInterestRateSlabHistory {
|
public class FdBaseInterestRateSlabHistory {
|
||||||
|
|||||||
+16
@@ -182,6 +182,12 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
final BigDecimal depositAmount = accountTermAndPreClosure.depositAmount();
|
final BigDecimal depositAmount = accountTermAndPreClosure.depositAmount();
|
||||||
applicableInterestRate = this.chart.getApplicableInterestRate(depositAmount, depositStartDate(), depositCloseDate, this.client);
|
applicableInterestRate = this.chart.getApplicableInterestRate(depositAmount, depositStartDate(), depositCloseDate, this.client);
|
||||||
|
|
||||||
|
// CBS: override with senior citizen rate if applicable
|
||||||
|
if (isSeniorCitizen() && getSeniorCitizenInterestRate() != null
|
||||||
|
&& getSeniorCitizenInterestRate().compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
applicableInterestRate = getSeniorCitizenInterestRate();
|
||||||
|
}
|
||||||
|
|
||||||
if (applyPreMaturePenalty) {
|
if (applyPreMaturePenalty) {
|
||||||
applicableInterestRate = applicableInterestRate.subtract(penalInterest);
|
applicableInterestRate = applicableInterestRate.subtract(penalInterest);
|
||||||
applicableInterestRate = applicableInterestRate.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : applicableInterestRate;
|
applicableInterestRate = applicableInterestRate.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : applicableInterestRate;
|
||||||
@@ -192,6 +198,16 @@ public class FixedDepositAccount extends SavingsAccount {
|
|||||||
return applicableInterestRate.divide(BigDecimal.valueOf(100L), mc);
|
return applicableInterestRate.divide(BigDecimal.valueOf(100L), mc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal getEffectiveInterestRateAsFractionAccrual(final MathContext mc, final LocalDate upToInterestCalculationDate) {
|
||||||
|
// CBS: accrual must also use senior citizen rate to stay consistent with maturity calculation
|
||||||
|
if (isSeniorCitizen() && getSeniorCitizenInterestRate() != null
|
||||||
|
&& getSeniorCitizenInterestRate().compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
return getSeniorCitizenInterestRate().divide(BigDecimal.valueOf(100L), mc);
|
||||||
|
}
|
||||||
|
return super.getEffectiveInterestRateAsFractionAccrual(mc, upToInterestCalculationDate);
|
||||||
|
}
|
||||||
|
|
||||||
public void updateMaturityDateAndAmountBeforeAccountActivation(final MathContext mc, final boolean isPreMatureClosure,
|
public void updateMaturityDateAndAmountBeforeAccountActivation(final MathContext mc, final boolean isPreMatureClosure,
|
||||||
final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) {
|
final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) {
|
||||||
List<SavingsAccountTransaction> allTransactions = new ArrayList<>();
|
List<SavingsAccountTransaction> allTransactions = new ArrayList<>();
|
||||||
|
|||||||
+16
@@ -234,6 +234,12 @@ public class RecurringDepositAccount extends SavingsAccount {
|
|||||||
if (this.chart != null) {
|
if (this.chart != null) {
|
||||||
applicableInterestRate = this.chart.getApplicableInterestRate(depositAmount, depositStartDate(), depositCloseDate, this.client);
|
applicableInterestRate = this.chart.getApplicableInterestRate(depositAmount, depositStartDate(), depositCloseDate, this.client);
|
||||||
|
|
||||||
|
// CBS: override with senior citizen rate if applicable
|
||||||
|
if (isSeniorCitizen() && getSeniorCitizenInterestRate() != null
|
||||||
|
&& getSeniorCitizenInterestRate().compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
applicableInterestRate = getSeniorCitizenInterestRate();
|
||||||
|
}
|
||||||
|
|
||||||
if (applyPreMaturePenalty) {
|
if (applyPreMaturePenalty) {
|
||||||
applicableInterestRate = applicableInterestRate.subtract(penalInterest);
|
applicableInterestRate = applicableInterestRate.subtract(penalInterest);
|
||||||
applicableInterestRate = applicableInterestRate.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : applicableInterestRate;
|
applicableInterestRate = applicableInterestRate.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : applicableInterestRate;
|
||||||
@@ -245,6 +251,16 @@ public class RecurringDepositAccount extends SavingsAccount {
|
|||||||
return applicableInterestRate.divide(BigDecimal.valueOf(100L), mc);
|
return applicableInterestRate.divide(BigDecimal.valueOf(100L), mc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal getEffectiveInterestRateAsFractionAccrual(final MathContext mc, final LocalDate upToInterestCalculationDate) {
|
||||||
|
// CBS: accrual must also use senior citizen rate to stay consistent with maturity calculation
|
||||||
|
if (isSeniorCitizen() && getSeniorCitizenInterestRate() != null
|
||||||
|
&& getSeniorCitizenInterestRate().compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
return getSeniorCitizenInterestRate().divide(BigDecimal.valueOf(100L), mc);
|
||||||
|
}
|
||||||
|
return super.getEffectiveInterestRateAsFractionAccrual(mc, upToInterestCalculationDate);
|
||||||
|
}
|
||||||
|
|
||||||
public void updateMaturityDateAndAmount(final MathContext mc, final boolean isPreMatureClosure,
|
public void updateMaturityDateAndAmount(final MathContext mc, final boolean isPreMatureClosure,
|
||||||
final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) {
|
final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) {
|
||||||
final LocalDate maturityDate = calculateMaturityDate();
|
final LocalDate maturityDate = calculateMaturityDate();
|
||||||
|
|||||||
+18
@@ -42,6 +42,8 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.specialC
|
|||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.weakerSectionParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.weakerSectionParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominationFacilityParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominationFacilityParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoDebitFacilityParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoDebitFacilityParamName;
|
||||||
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isSeniorCitizenParamName;
|
||||||
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.seniorCitizenInterestRateParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.clientIdParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.clientIdParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enforceMinRequiredBalanceParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enforceMinRequiredBalanceParamName;
|
||||||
@@ -394,6 +396,22 @@ public class SavingsAccountAssembler {
|
|||||||
account.setAutoDebitFacility(this.fromApiJsonHelper.extractBooleanNamed(autoDebitFacilityParamName, element));
|
account.setAutoDebitFacility(this.fromApiJsonHelper.extractBooleanNamed(autoDebitFacilityParamName, element));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Senior citizen rate — runs after validateDomainRules so it is the final persisted value
|
||||||
|
if (command.parameterExists(isSeniorCitizenParamName)) {
|
||||||
|
final Boolean scFlag = this.fromApiJsonHelper.extractBooleanNamed(isSeniorCitizenParamName, element);
|
||||||
|
if (scFlag != null) {
|
||||||
|
account.setSeniorCitizen(scFlag);
|
||||||
|
if (Boolean.TRUE.equals(scFlag) && command.parameterExists(seniorCitizenInterestRateParamName)) {
|
||||||
|
final BigDecimal scRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
|
||||||
|
seniorCitizenInterestRateParamName, element);
|
||||||
|
if (scRate != null && scRate.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
account.setSeniorCitizenInterestRate(scRate);
|
||||||
|
account.updateNominalAnnualInterestRate(scRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
account.setGuardianName(this.fromApiJsonHelper.extractStringNamed(guardianNameParamName, element));
|
account.setGuardianName(this.fromApiJsonHelper.extractStringNamed(guardianNameParamName, element));
|
||||||
account.setGuardianCifNumber(this.fromApiJsonHelper.extractStringNamed(guardianCifNumberParamName, element));
|
account.setGuardianCifNumber(this.fromApiJsonHelper.extractStringNamed(guardianCifNumberParamName, element));
|
||||||
account.setGuardianRelationship(this.fromApiJsonHelper.extractStringNamed(guardianRelationshipParamName, element));
|
account.setGuardianRelationship(this.fromApiJsonHelper.extractStringNamed(guardianRelationshipParamName, element));
|
||||||
|
|||||||
+27
-4
@@ -139,8 +139,10 @@ public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainServi
|
|||||||
}
|
}
|
||||||
this.savingsAccountRepository.save(account);
|
this.savingsAccountRepository.save(account);
|
||||||
|
|
||||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, transactionBooleanValues.isAccountTransfer(),
|
if (!transactionBooleanValues.isSkipAccountingPostings()) {
|
||||||
backdatedTxnsAllowedTill);
|
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds,
|
||||||
|
transactionBooleanValues.isAccountTransfer(), backdatedTxnsAllowedTill);
|
||||||
|
}
|
||||||
|
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal));
|
businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal));
|
||||||
return withdrawal;
|
return withdrawal;
|
||||||
@@ -153,13 +155,32 @@ public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainServi
|
|||||||
final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) {
|
final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) {
|
||||||
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
|
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
|
||||||
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
|
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
|
||||||
savingsAccountTransactionType, backdatedTxnsAllowedTill);
|
savingsAccountTransactionType, backdatedTxnsAllowedTill, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
|
||||||
|
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||||
|
final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill,
|
||||||
|
final boolean skipAccountingPostings) {
|
||||||
|
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
|
||||||
|
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
|
||||||
|
savingsAccountTransactionType, backdatedTxnsAllowedTill, skipAccountingPostings);
|
||||||
}
|
}
|
||||||
|
|
||||||
private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
|
private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
|
||||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||||
final boolean isAccountTransfer, final boolean isRegularTransaction,
|
final boolean isAccountTransfer, final boolean isRegularTransaction,
|
||||||
final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) {
|
final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) {
|
||||||
|
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
|
||||||
|
savingsAccountTransactionType, backdatedTxnsAllowedTill, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
|
||||||
|
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||||
|
final boolean isAccountTransfer, final boolean isRegularTransaction,
|
||||||
|
final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill,
|
||||||
|
final boolean skipAccountingPostings) {
|
||||||
context.authenticatedUser();
|
context.authenticatedUser();
|
||||||
account.validateForAccountBlock();
|
account.validateForAccountBlock();
|
||||||
account.validateForCreditBlock();
|
account.validateForCreditBlock();
|
||||||
@@ -210,7 +231,9 @@ public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainServi
|
|||||||
|
|
||||||
this.savingsAccountRepository.saveAndFlush(account);
|
this.savingsAccountRepository.saveAndFlush(account);
|
||||||
|
|
||||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
|
if (!skipAccountingPostings) {
|
||||||
|
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
|
||||||
|
}
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit));
|
businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit));
|
||||||
return deposit;
|
return deposit;
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -43,6 +43,7 @@ import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotF
|
|||||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
|
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
|
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
|
||||||
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository;
|
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository;
|
||||||
|
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
|
|||||||
private final CurrentProductRepository currentProductRepository;
|
private final CurrentProductRepository currentProductRepository;
|
||||||
private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository;
|
private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository;
|
||||||
private final JournalEntryRepository journalEntryRepository;
|
private final JournalEntryRepository journalEntryRepository;
|
||||||
|
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -74,6 +76,7 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
|
|||||||
|
|
||||||
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||||
validatePositiveAmount(amount, accountId, "deposit");
|
validatePositiveAmount(amount, accountId, "deposit");
|
||||||
|
this.denominationBreakdownWriter.validateBreakdown(command, amount, "DEPOSIT");
|
||||||
|
|
||||||
final String refNo = command.stringValueOfParameterNamed("accountNumber");
|
final String refNo = command.stringValueOfParameterNamed("accountNumber");
|
||||||
|
|
||||||
@@ -85,6 +88,11 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
|
|||||||
|
|
||||||
currentAccountTransactionRepository.saveAndFlush(txn);
|
currentAccountTransactionRepository.saveAndFlush(txn);
|
||||||
|
|
||||||
|
this.denominationBreakdownWriter.save(command, txn.getId(), account.getId(), account.getAccountNo(),
|
||||||
|
account.getClient() != null ? account.getClient().getId() : null,
|
||||||
|
account.getClient() != null ? account.getClient().getCifNumber() : null, txnDate,
|
||||||
|
"DEPOSIT");
|
||||||
|
|
||||||
account.setAccountBalanceDerived(newBalance);
|
account.setAccountBalanceDerived(newBalance);
|
||||||
account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount));
|
account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount));
|
||||||
currentAccountRepository.save(account);
|
currentAccountRepository.save(account);
|
||||||
@@ -135,6 +143,14 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
|
|||||||
account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount));
|
account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount));
|
||||||
currentAccountRepository.save(account);
|
currentAccountRepository.save(account);
|
||||||
|
|
||||||
|
// Validate denomination stock availability then validate totals before saving breakdown
|
||||||
|
this.denominationBreakdownWriter.validateWithdrawalStock(command);
|
||||||
|
this.denominationBreakdownWriter.validateBreakdown(command, amount, "WITHDRAWAL");
|
||||||
|
this.denominationBreakdownWriter.save(command, txn.getId(), account.getId(), account.getAccountNo(),
|
||||||
|
account.getClient() != null ? account.getClient().getId() : null,
|
||||||
|
account.getClient() != null ? account.getClient().getCifNumber() : null, txnDate,
|
||||||
|
"WITHDRAWAL");
|
||||||
|
|
||||||
// Withdrawal GL: DR SAVINGS_CONTROL (liability down), CR SAVINGS_REFERENCE (cash out)
|
// Withdrawal GL: DR SAVINGS_CONTROL (liability down), CR SAVINGS_REFERENCE (cash out)
|
||||||
postTransactionGl(account, txn, JournalEntryType.CREDIT, JournalEntryType.DEBIT, "Current Account Withdrawal");
|
postTransactionGl(account, txn, JournalEntryType.CREDIT, JournalEntryType.DEBIT, "Current Account Withdrawal");
|
||||||
|
|
||||||
|
|||||||
+19
@@ -78,6 +78,7 @@ import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
|
|||||||
import org.apache.fineract.portfolio.client.domain.Client;
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
|
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
|
||||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||||
|
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
|
||||||
import org.apache.fineract.portfolio.group.domain.Group;
|
import org.apache.fineract.portfolio.group.domain.Group;
|
||||||
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
|
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
|
||||||
import org.apache.fineract.portfolio.note.domain.Note;
|
import org.apache.fineract.portfolio.note.domain.Note;
|
||||||
@@ -138,6 +139,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
private final HolidayRepositoryWrapper holidayRepository;
|
private final HolidayRepositoryWrapper holidayRepository;
|
||||||
private final WorkingDaysRepositoryWrapper workingDaysRepository;
|
private final WorkingDaysRepositoryWrapper workingDaysRepository;
|
||||||
private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository;
|
private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository;
|
||||||
|
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -360,9 +362,14 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
|
|
||||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||||
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
||||||
|
this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "DEPOSIT");
|
||||||
final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleFDDeposit(account, fmt, transactionDate,
|
final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleFDDeposit(account, fmt, transactionDate,
|
||||||
transactionAmount, paymentDetail);
|
transactionAmount, paymentDetail);
|
||||||
|
|
||||||
|
this.denominationBreakdownWriter.save(command, deposit.getId(), account.getId(), account.getAccountNumber(),
|
||||||
|
account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, transactionDate,
|
||||||
|
"DEPOSIT");
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder().withEntityId(deposit.getId()).withOfficeId(account.officeId())
|
return new CommandProcessingResultBuilder().withEntityId(deposit.getId()).withOfficeId(account.officeId())
|
||||||
.withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build();
|
.withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build();
|
||||||
|
|
||||||
@@ -417,9 +424,14 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
|
|
||||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||||
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
||||||
|
this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "DEPOSIT");
|
||||||
final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleRDDeposit(account, fmt, transactionDate,
|
final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleRDDeposit(account, fmt, transactionDate,
|
||||||
transactionAmount, paymentDetail, isRegularTransaction);
|
transactionAmount, paymentDetail, isRegularTransaction);
|
||||||
|
|
||||||
|
this.denominationBreakdownWriter.save(command, deposit.getId(), account.getId(), account.getAccountNumber(),
|
||||||
|
account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, transactionDate,
|
||||||
|
"DEPOSIT");
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
.withEntityId(deposit.getId()) //
|
.withEntityId(deposit.getId()) //
|
||||||
.withOfficeId(account.officeId()) //
|
.withOfficeId(account.officeId()) //
|
||||||
@@ -460,6 +472,13 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
final SavingsAccountTransaction withdrawal = this.depositAccountDomainService.handleWithdrawal(account, fmt, transactionDate,
|
final SavingsAccountTransaction withdrawal = this.depositAccountDomainService.handleWithdrawal(account, fmt, transactionDate,
|
||||||
transactionAmount, paymentDetail, true, isRegularTransaction);
|
transactionAmount, paymentDetail, true, isRegularTransaction);
|
||||||
|
|
||||||
|
// Validate denomination stock availability then validate totals before saving breakdown
|
||||||
|
this.denominationBreakdownWriter.validateWithdrawalStock(command);
|
||||||
|
this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "WITHDRAWAL");
|
||||||
|
this.denominationBreakdownWriter.save(command, withdrawal.getId(), account.getId(), account.getAccountNumber(),
|
||||||
|
account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, transactionDate,
|
||||||
|
"WITHDRAWAL");
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
.withEntityId(withdrawal.getId()) //
|
.withEntityId(withdrawal.getId()) //
|
||||||
.withOfficeId(account.officeId()) //
|
.withOfficeId(account.officeId()) //
|
||||||
|
|||||||
+16
-1
@@ -35,6 +35,7 @@ import io.github.resilience4j.retry.annotation.Retry;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.MathContext;
|
import java.math.MathContext;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -88,6 +89,7 @@ import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatfor
|
|||||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||||
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
|
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
|
||||||
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
|
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
|
||||||
|
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
|
||||||
import org.apache.fineract.portfolio.client.domain.Client;
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
|
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
|
||||||
import org.apache.fineract.portfolio.group.domain.Group;
|
import org.apache.fineract.portfolio.group.domain.Group;
|
||||||
@@ -167,6 +169,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
private final SavingsAccountInterestPostingService savingsAccountInterestPostingService;
|
private final SavingsAccountInterestPostingService savingsAccountInterestPostingService;
|
||||||
private final ErrorHandler errorHandler;
|
private final ErrorHandler errorHandler;
|
||||||
private final FacilityChargeAutoApplyService facilityChargeAutoApplyService;
|
private final FacilityChargeAutoApplyService facilityChargeAutoApplyService;
|
||||||
|
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -325,6 +328,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||||
|
|
||||||
this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account);
|
this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account);
|
||||||
|
this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "DEPOSIT");
|
||||||
|
|
||||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||||
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
||||||
@@ -333,6 +337,11 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate,
|
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate,
|
||||||
transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
||||||
|
|
||||||
|
// Save denomination breakdown for DEPOSIT via shared writer
|
||||||
|
this.denominationBreakdownWriter.save(command, deposit.getId(), account.getId(), account.getAccountNumber(),
|
||||||
|
account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null,
|
||||||
|
transactionDate, "DEPOSIT");
|
||||||
|
|
||||||
if (isGsim && (deposit.getId() != null)) {
|
if (isGsim && (deposit.getId() != null)) {
|
||||||
|
|
||||||
log.debug("Deposit account has been created: {} ", deposit);
|
log.debug("Deposit account has been created: {} ", deposit);
|
||||||
@@ -413,9 +422,15 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
BigDecimal currentBalance = gsim.getParentDeposit().subtract(transactionAmount);
|
BigDecimal currentBalance = gsim.getParentDeposit().subtract(transactionAmount);
|
||||||
gsim.setParentDeposit(currentBalance);
|
gsim.setParentDeposit(currentBalance);
|
||||||
gsimRepository.save(gsim);
|
gsimRepository.save(gsim);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate denomination stock availability then validate totals before saving breakdown
|
||||||
|
this.denominationBreakdownWriter.validateWithdrawalStock(command);
|
||||||
|
this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "WITHDRAWAL");
|
||||||
|
this.denominationBreakdownWriter.save(command, withdrawal.getId(), account.getId(), account.getAccountNumber(),
|
||||||
|
account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null,
|
||||||
|
transactionDate, "WITHDRAWAL");
|
||||||
|
|
||||||
final String noteText = command.stringValueOfParameterNamed("note");
|
final String noteText = command.stringValueOfParameterNamed("note");
|
||||||
if (StringUtils.isNotBlank(noteText)) {
|
if (StringUtils.isNotBlank(noteText)) {
|
||||||
final Note note = Note.savingsTransactionNote(account, withdrawal, noteText);
|
final Note note = Note.savingsTransactionNote(account, withdrawal, noteText);
|
||||||
|
|||||||
+9
-4
@@ -98,6 +98,7 @@ import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfig
|
|||||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimit;
|
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimit;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository;
|
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductRepository;
|
import org.apache.fineract.portfolio.savings.domain.SavingsProductRepository;
|
||||||
|
import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenominationRepository;
|
||||||
import org.apache.fineract.portfolio.savings.service.FacilityChargeAutoApplyService;
|
import org.apache.fineract.portfolio.savings.service.FacilityChargeAutoApplyService;
|
||||||
import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformService;
|
import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformServiceImpl;
|
import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformServiceImpl;
|
||||||
@@ -107,6 +108,7 @@ import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalc
|
|||||||
import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformServiceImpl;
|
import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService;
|
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformServiceImpl;
|
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformServiceImpl;
|
||||||
|
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
|
||||||
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService;
|
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformServiceJpaRepositoryImpl;
|
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformServiceJpaRepositoryImpl;
|
||||||
import org.apache.fineract.portfolio.savings.service.DepositApplicationProcessWritePlatformService;
|
import org.apache.fineract.portfolio.savings.service.DepositApplicationProcessWritePlatformService;
|
||||||
@@ -267,7 +269,8 @@ public class SavingsConfiguration {
|
|||||||
DepositAccountReadPlatformService depositAccountReadPlatformService, CalendarInstanceRepository calendarInstanceRepository,
|
DepositAccountReadPlatformService depositAccountReadPlatformService, CalendarInstanceRepository calendarInstanceRepository,
|
||||||
ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository,
|
ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository,
|
||||||
WorkingDaysRepositoryWrapper workingDaysRepository,
|
WorkingDaysRepositoryWrapper workingDaysRepository,
|
||||||
DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository
|
DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
|
||||||
|
DenominationBreakdownWriter denominationBreakdownWriter
|
||||||
|
|
||||||
) {
|
) {
|
||||||
return new DepositAccountWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper,
|
return new DepositAccountWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper,
|
||||||
@@ -276,7 +279,8 @@ public class SavingsConfiguration {
|
|||||||
journalEntryWritePlatformService, depositAccountDomainService, noteRepository, accountTransfersReadPlatformService,
|
journalEntryWritePlatformService, depositAccountDomainService, noteRepository, accountTransfersReadPlatformService,
|
||||||
chargeRepository, savingsAccountChargeRepository, accountAssociationsReadPlatformService,
|
chargeRepository, savingsAccountChargeRepository, accountAssociationsReadPlatformService,
|
||||||
accountTransfersWritePlatformService, depositAccountReadPlatformService, calendarInstanceRepository,
|
accountTransfersWritePlatformService, depositAccountReadPlatformService, calendarInstanceRepository,
|
||||||
configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository);
|
configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository,
|
||||||
|
denominationBreakdownWriter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -424,7 +428,8 @@ public class SavingsConfiguration {
|
|||||||
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, AppUserRepositoryWrapper appuserRepository,
|
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, AppUserRepositoryWrapper appuserRepository,
|
||||||
StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService,
|
StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService,
|
||||||
GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService,
|
GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService,
|
||||||
ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService) {
|
ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService,
|
||||||
|
DenominationBreakdownWriter denominationBreakdownWriter) {
|
||||||
return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper,
|
return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper,
|
||||||
staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator,
|
staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator,
|
||||||
savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService,
|
savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService,
|
||||||
@@ -432,7 +437,7 @@ public class SavingsConfiguration {
|
|||||||
chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService,
|
chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService,
|
||||||
depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository,
|
depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository,
|
||||||
standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService,
|
standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService,
|
||||||
errorHandler, facilityChargeAutoApplyService);
|
errorHandler, facilityChargeAutoApplyService, denominationBreakdownWriter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
package org.apache.fineract.portfolio.teller.service;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
|
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||||
|
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GlDispatchService {
|
||||||
|
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vault → Cashier GL entry. Runs in its own transaction so a failure
|
||||||
|
* never rolls back the caller's (already-committed) transfer record.
|
||||||
|
*/
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
public void allocateCashToCashier(Long tellerId, Long cashierId,
|
||||||
|
BigDecimal amount, String currencyCode, String txnDate, String notes) {
|
||||||
|
JsonObject body = new JsonObject();
|
||||||
|
body.addProperty("locale", "en");
|
||||||
|
body.addProperty("dateFormat", "yyyy-MM-dd");
|
||||||
|
body.addProperty("txnDate", txnDate);
|
||||||
|
body.addProperty("txnAmount", amount);
|
||||||
|
body.addProperty("currencyCode", currencyCode);
|
||||||
|
body.addProperty("txnNote", notes != null ? notes : "");
|
||||||
|
CommandWrapper wrapper = new CommandWrapperBuilder().withJson(gson.toJson(body))
|
||||||
|
.allocateCashToCashier(tellerId, cashierId).build();
|
||||||
|
commandsSourceWritePlatformService.logCommandSource(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cashier → Vault GL entry (settlement / transfer reversal).
|
||||||
|
*/
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
public void settleCashFromCashier(Long tellerId, Long cashierId,
|
||||||
|
BigDecimal amount, String currencyCode, String txnDate, String notes) {
|
||||||
|
JsonObject body = new JsonObject();
|
||||||
|
body.addProperty("locale", "en");
|
||||||
|
body.addProperty("dateFormat", "yyyy-MM-dd");
|
||||||
|
body.addProperty("txnDate", txnDate);
|
||||||
|
body.addProperty("txnAmount", amount);
|
||||||
|
body.addProperty("currencyCode", currencyCode);
|
||||||
|
body.addProperty("txnNote", notes != null ? notes : "");
|
||||||
|
CommandWrapper wrapper = new CommandWrapperBuilder().withJson(gson.toJson(body))
|
||||||
|
.settleCashFromCashier(tellerId, cashierId).build();
|
||||||
|
commandsSourceWritePlatformService.logCommandSource(wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -316,4 +316,15 @@
|
|||||||
<include file="parts/0297_fix_unique_nominee_per_account.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0297_fix_unique_nominee_per_account.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0298_unique_external_id_on_current_account.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0298_unique_external_id_on_current_account.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0299_seed_indian_states.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0299_seed_indian_states.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0300_rename_fd_base_interest_rate_tables_to_cbs.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0301_add_current_account_to_transfer_details.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0012_denomination_master.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0013_cash_exchange.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0014_cash_transfer.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0015_cash_transfer_add_ids.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0016_cash_settlement.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0017_cash_inter_cashier.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0018_cash_reconciliation.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0019_cash_reversal_columns.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0020_cash_permissions.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+204
@@ -0,0 +1,204 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="denomination_master_table_001" author="CBS">
|
||||||
|
<createTable tableName="m_denomination_master">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="denomination" type="INT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="kind" type="VARCHAR(10)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="description" type="VARCHAR(100)"/>
|
||||||
|
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="introduced_on" type="DATE"/>
|
||||||
|
<column name="discontinued_on" type="DATE"/>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="denomination_master_initial_data_001" author="CBS">
|
||||||
|
<insert tableName="m_denomination_master">
|
||||||
|
<column name="denomination" value="2000"/>
|
||||||
|
<column name="kind" value="note"/>
|
||||||
|
<column name="description" value="2000 Rupee Note"/>
|
||||||
|
<column name="is_active" valueBoolean="false"/>
|
||||||
|
<column name="introduced_on" value="2016-11-08"/>
|
||||||
|
<column name="discontinued_on" value="2023-09-30"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_denomination_master">
|
||||||
|
<column name="denomination" value="500"/>
|
||||||
|
<column name="kind" value="note"/>
|
||||||
|
<column name="description" value="500 Rupee Note"/>
|
||||||
|
<column name="is_active" valueBoolean="true"/>
|
||||||
|
<column name="introduced_on" value="2016-11-08"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_denomination_master">
|
||||||
|
<column name="denomination" value="200"/>
|
||||||
|
<column name="kind" value="note"/>
|
||||||
|
<column name="description" value="200 Rupee Note"/>
|
||||||
|
<column name="is_active" valueBoolean="true"/>
|
||||||
|
<column name="introduced_on" value="2017-08-25"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_denomination_master">
|
||||||
|
<column name="denomination" value="100"/>
|
||||||
|
<column name="kind" value="note"/>
|
||||||
|
<column name="description" value="100 Rupee Note"/>
|
||||||
|
<column name="is_active" valueBoolean="true"/>
|
||||||
|
<column name="introduced_on" value="2018-07-19"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_denomination_master">
|
||||||
|
<column name="denomination" value="50"/>
|
||||||
|
<column name="kind" value="note"/>
|
||||||
|
<column name="description" value="50 Rupee Note"/>
|
||||||
|
<column name="is_active" valueBoolean="true"/>
|
||||||
|
<column name="introduced_on" value="2017-08-25"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_denomination_master">
|
||||||
|
<column name="denomination" value="20"/>
|
||||||
|
<column name="kind" value="note"/>
|
||||||
|
<column name="description" value="20 Rupee Note"/>
|
||||||
|
<column name="is_active" valueBoolean="true"/>
|
||||||
|
<column name="introduced_on" value="2019-08-26"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_denomination_master">
|
||||||
|
<column name="denomination" value="10"/>
|
||||||
|
<column name="kind" value="note"/>
|
||||||
|
<column name="description" value="10 Rupee Note"/>
|
||||||
|
<column name="is_active" valueBoolean="true"/>
|
||||||
|
<column name="introduced_on" value="2018-01-01"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_denomination_master">
|
||||||
|
<column name="denomination" value="20"/>
|
||||||
|
<column name="kind" value="coin"/>
|
||||||
|
<column name="description" value="20 Rupee Coin"/>
|
||||||
|
<column name="is_active" valueBoolean="true"/>
|
||||||
|
<column name="introduced_on" value="2019-01-01"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_denomination_master">
|
||||||
|
<column name="denomination" value="10"/>
|
||||||
|
<column name="kind" value="coin"/>
|
||||||
|
<column name="description" value="10 Rupee Coin"/>
|
||||||
|
<column name="is_active" valueBoolean="true"/>
|
||||||
|
<column name="introduced_on" value="2009-01-01"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_denomination_master">
|
||||||
|
<column name="denomination" value="5"/>
|
||||||
|
<column name="kind" value="coin"/>
|
||||||
|
<column name="description" value="5 Rupee Coin"/>
|
||||||
|
<column name="is_active" valueBoolean="true"/>
|
||||||
|
<column name="introduced_on" value="2009-01-01"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_denomination_master">
|
||||||
|
<column name="denomination" value="2"/>
|
||||||
|
<column name="kind" value="coin"/>
|
||||||
|
<column name="description" value="2 Rupee Coin"/>
|
||||||
|
<column name="is_active" valueBoolean="true"/>
|
||||||
|
<column name="introduced_on" value="2009-01-01"/>
|
||||||
|
</insert>
|
||||||
|
<insert tableName="m_denomination_master">
|
||||||
|
<column name="denomination" value="1"/>
|
||||||
|
<column name="kind" value="coin"/>
|
||||||
|
<column name="description" value="1 Rupee Coin"/>
|
||||||
|
<column name="is_active" valueBoolean="true"/>
|
||||||
|
<column name="introduced_on" value="2009-01-01"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="savings_txn_denomination_table_001" author="CBS">
|
||||||
|
<createTable tableName="m_savings_txn_denomination">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="txn_id" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="savings_account_id" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="account_no" type="VARCHAR(50)"/>
|
||||||
|
<column name="client_id" type="BIGINT"/>
|
||||||
|
<column name="cif_no" type="VARCHAR(50)"/>
|
||||||
|
<column name="denomination_id" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="denomination_value" type="INT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="count" type="INT" defaultValueNumeric="0">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="total_amount" type="DECIMAL(19,6)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="txn_date" type="DATE"/>
|
||||||
|
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="denomination_master_audit_table_001" author="CBS">
|
||||||
|
<createTable tableName="m_denomination_master_audit">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="denomination_id" type="BIGINT"/>
|
||||||
|
<column name="action" type="VARCHAR(20)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="denomination" type="INT"/>
|
||||||
|
<column name="kind" type="VARCHAR(10)"/>
|
||||||
|
<column name="description" type="VARCHAR(100)"/>
|
||||||
|
<column name="is_active" type="BOOLEAN"/>
|
||||||
|
<column name="introduced_on" type="DATE"/>
|
||||||
|
<column name="discontinued_on" type="DATE"/>
|
||||||
|
<column name="changes" type="VARCHAR(1000)"/>
|
||||||
|
<column name="performed_by" type="BIGINT"/>
|
||||||
|
<column name="performed_by_username" type="VARCHAR(100)"/>
|
||||||
|
<column name="performed_on" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
<createIndex tableName="m_denomination_master_audit" indexName="idx_denom_audit_denom_id">
|
||||||
|
<column name="denomination_id"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="denomination_master_soft_delete_001" author="CBS">
|
||||||
|
<addColumn tableName="m_denomination_master">
|
||||||
|
<column name="deleted" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- =====================================================================
|
||||||
|
Migration: Add txn_type, officer_id, officer_username to
|
||||||
|
m_savings_txn_denomination.
|
||||||
|
txn_type distinguishes DEPOSIT from WITHDRAWAL.
|
||||||
|
officer_id / officer_username record who performed the transaction.
|
||||||
|
===================================================================== -->
|
||||||
|
<changeSet id="savings_txn_denomination_add_txn_type_officer_001" author="CBS">
|
||||||
|
<addColumn tableName="m_savings_txn_denomination">
|
||||||
|
<column name="txn_type" type="VARCHAR(20)" defaultValue="DEPOSIT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="officer_id" type="BIGINT"/>
|
||||||
|
<column name="officer_username" type="VARCHAR(100)"/>
|
||||||
|
</addColumn>
|
||||||
|
<createIndex tableName="m_savings_txn_denomination"
|
||||||
|
indexName="idx_savings_txn_denom_txntype">
|
||||||
|
<column name="txn_type"/>
|
||||||
|
</createIndex>
|
||||||
|
<createIndex tableName="m_savings_txn_denomination"
|
||||||
|
indexName="idx_savings_txn_denom_officer">
|
||||||
|
<column name="officer_id"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="cash_exchange_table_001" author="CBS">
|
||||||
|
<createTable tableName="m_cash_exchange">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="account_id" type="BIGINT"/>
|
||||||
|
<column name="account_no" type="VARCHAR(50)"/>
|
||||||
|
<column name="client_id" type="BIGINT"/>
|
||||||
|
<column name="exchange_amount" type="DECIMAL(19,6)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="exchange_date" type="DATE"/>
|
||||||
|
<column name="note" type="VARCHAR(500)"/>
|
||||||
|
<column name="performed_by" type="BIGINT"/>
|
||||||
|
<column name="performed_by_username" type="VARCHAR(100)"/>
|
||||||
|
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="cash_exchange_detail_table_001" author="CBS">
|
||||||
|
<createTable tableName="m_cash_exchange_detail">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="exchange_id" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="direction" type="VARCHAR(10)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="denomination_id" type="BIGINT"/>
|
||||||
|
<column name="denomination_value" type="INT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="count" type="INT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="total_amount" type="DECIMAL(19,6)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
<createIndex tableName="m_cash_exchange_detail" indexName="idx_cash_exchange_detail_eid">
|
||||||
|
<column name="exchange_id"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="cash_transfer_table_001" author="CBS">
|
||||||
|
<createTable tableName="m_cash_transfer">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="transfer_date" type="DATE"/>
|
||||||
|
<column name="from_party" type="VARCHAR(100)"/>
|
||||||
|
<column name="to_party" type="VARCHAR(100)"/>
|
||||||
|
<column name="transfer_amount" type="DECIMAL(19,6)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="note" type="VARCHAR(500)"/>
|
||||||
|
<column name="performed_by" type="BIGINT"/>
|
||||||
|
<column name="performed_by_username" type="VARCHAR(100)"/>
|
||||||
|
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="cash_transfer_detail_table_001" author="CBS">
|
||||||
|
<createTable tableName="m_cash_transfer_detail">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="transfer_id" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="denomination_id" type="BIGINT"/>
|
||||||
|
<column name="denomination_value" type="INT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="count" type="INT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="total_amount" type="DECIMAL(19,6)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
<createIndex tableName="m_cash_transfer_detail" indexName="idx_cash_transfer_detail_tid">
|
||||||
|
<column name="transfer_id"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="cash_transfer_add_party_ids_001" author="CBS">
|
||||||
|
<addColumn tableName="m_cash_transfer">
|
||||||
|
<column name="from_teller_id" type="BIGINT"/>
|
||||||
|
<column name="to_cashier_id" type="BIGINT"/>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="cash_settlement_table_001" author="CBS">
|
||||||
|
<createTable tableName="m_cash_settlement">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="settlement_date" type="DATE"/>
|
||||||
|
<column name="from_cashier_id" type="BIGINT"/>
|
||||||
|
<column name="to_teller_id" type="BIGINT"/>
|
||||||
|
<column name="from_party" type="VARCHAR(100)"/>
|
||||||
|
<column name="to_party" type="VARCHAR(100)"/>
|
||||||
|
<column name="settlement_amount" type="DECIMAL(19,6)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="note" type="VARCHAR(500)"/>
|
||||||
|
<column name="performed_by" type="BIGINT"/>
|
||||||
|
<column name="performed_by_username" type="VARCHAR(100)"/>
|
||||||
|
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
|
||||||
|
<column name="is_reversed" type="BOOLEAN" defaultValueBoolean="false"/>
|
||||||
|
<column name="reversed_by" type="BIGINT"/>
|
||||||
|
<column name="reversed_at" type="TIMESTAMP"/>
|
||||||
|
<column name="reverse_reason" type="VARCHAR(500)"/>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="cash_settlement_detail_table_001" author="CBS">
|
||||||
|
<createTable tableName="m_cash_settlement_detail">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="settlement_id" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="denomination_id" type="BIGINT"/>
|
||||||
|
<column name="denomination_value" type="INT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="count" type="INT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="total_amount" type="DECIMAL(19,6)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
<createIndex tableName="m_cash_settlement_detail" indexName="idx_cash_settlement_detail_sid">
|
||||||
|
<column name="settlement_id"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="cash_inter_cashier_table_001" author="CBS">
|
||||||
|
<createTable tableName="m_cash_inter_cashier">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="transfer_date" type="DATE"/>
|
||||||
|
<column name="teller_id" type="BIGINT"/>
|
||||||
|
<column name="from_cashier_id" type="BIGINT"/>
|
||||||
|
<column name="to_cashier_id" type="BIGINT"/>
|
||||||
|
<column name="from_party" type="VARCHAR(100)"/>
|
||||||
|
<column name="to_party" type="VARCHAR(100)"/>
|
||||||
|
<column name="transfer_amount" type="DECIMAL(19,6)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="note" type="VARCHAR(500)"/>
|
||||||
|
<column name="performed_by" type="BIGINT"/>
|
||||||
|
<column name="performed_by_username" type="VARCHAR(100)"/>
|
||||||
|
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
|
||||||
|
<column name="is_reversed" type="BOOLEAN" defaultValueBoolean="false"/>
|
||||||
|
<column name="reversed_by" type="BIGINT"/>
|
||||||
|
<column name="reversed_at" type="TIMESTAMP"/>
|
||||||
|
<column name="reverse_reason" type="VARCHAR(500)"/>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="cash_inter_cashier_detail_table_001" author="CBS">
|
||||||
|
<createTable tableName="m_cash_inter_cashier_detail">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="inter_cashier_id" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="denomination_id" type="BIGINT"/>
|
||||||
|
<column name="denomination_value" type="INT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="count" type="INT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="total_amount" type="DECIMAL(19,6)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
<createIndex tableName="m_cash_inter_cashier_detail" indexName="idx_cash_inter_cashier_detail_pid">
|
||||||
|
<column name="inter_cashier_id"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="cash_reconciliation_table_001" author="CBS">
|
||||||
|
<createTable tableName="m_cash_reconciliation">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="recon_date" type="DATE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="cashier_id" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="cashier_name" type="VARCHAR(150)"/>
|
||||||
|
<column name="teller_id" type="BIGINT"/>
|
||||||
|
<column name="expected_amount" type="DECIMAL(19,6)" defaultValueNumeric="0"/>
|
||||||
|
<column name="physical_amount" type="DECIMAL(19,6)" defaultValueNumeric="0"/>
|
||||||
|
<column name="variance" type="DECIMAL(19,6)" defaultValueNumeric="0"/>
|
||||||
|
<column name="status" type="VARCHAR(10)" defaultValue="OK"/>
|
||||||
|
<column name="note" type="VARCHAR(500)"/>
|
||||||
|
<column name="performed_by" type="BIGINT"/>
|
||||||
|
<column name="performed_by_username" type="VARCHAR(100)"/>
|
||||||
|
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
|
||||||
|
</createTable>
|
||||||
|
<createIndex tableName="m_cash_reconciliation" indexName="idx_cash_recon_cashier_date">
|
||||||
|
<column name="cashier_id"/>
|
||||||
|
<column name="recon_date"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="cash_reconciliation_detail_table_001" author="CBS">
|
||||||
|
<createTable tableName="m_cash_reconciliation_detail">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="reconciliation_id" type="BIGINT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="denomination_id" type="BIGINT"/>
|
||||||
|
<column name="denomination_value" type="INT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="expected_count" type="INT" defaultValueNumeric="0"/>
|
||||||
|
<column name="physical_count" type="INT" defaultValueNumeric="0"/>
|
||||||
|
<column name="variance_count" type="INT" defaultValueNumeric="0"/>
|
||||||
|
<column name="variance_amount" type="DECIMAL(19,6)" defaultValueNumeric="0"/>
|
||||||
|
</createTable>
|
||||||
|
<createIndex tableName="m_cash_reconciliation_detail" indexName="idx_cash_recon_detail_rid">
|
||||||
|
<column name="reconciliation_id"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="cash_transfer_reversal_cols_001" author="CBS">
|
||||||
|
<addColumn tableName="m_cash_transfer">
|
||||||
|
<column name="is_reversed" type="BOOLEAN" defaultValueBoolean="false"/>
|
||||||
|
<column name="reversed_by" type="BIGINT"/>
|
||||||
|
<column name="reversed_at" type="TIMESTAMP"/>
|
||||||
|
<column name="reverse_reason" type="VARCHAR(500)"/>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="cash_module_permissions_001" author="CBS" runOnChange="false">
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'cashier', 'CREATE_CASHTRANSFER', 'CASHTRANSFER', 'CREATE', false
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CASHTRANSFER');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'cashier', 'READ_CASHTRANSFER', 'CASHTRANSFER', 'READ', false
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CASHTRANSFER');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'cashier', 'REVERSE_CASHTRANSFER', 'CASHTRANSFER', 'REVERSE', false
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REVERSE_CASHTRANSFER');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'cashier', 'CREATE_CASHSETTLEMENT', 'CASHSETTLEMENT', 'CREATE', false
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CASHSETTLEMENT');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'cashier', 'READ_CASHSETTLEMENT', 'CASHSETTLEMENT', 'READ', false
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CASHSETTLEMENT');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'cashier', 'REVERSE_CASHSETTLEMENT', 'CASHSETTLEMENT', 'REVERSE', false
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REVERSE_CASHSETTLEMENT');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'cashier', 'CREATE_CASHINTERCASHIER', 'CASHINTERCASHIER', 'CREATE', false
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CASHINTERCASHIER');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'cashier', 'READ_CASHINTERCASHIER', 'CASHINTERCASHIER', 'READ', false
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CASHINTERCASHIER');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'cashier', 'REVERSE_CASHINTERCASHIER', 'CASHINTERCASHIER', 'REVERSE', false
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REVERSE_CASHINTERCASHIER');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'cashier', 'CREATE_CASHRECONCILIATION', 'CASHRECONCILIATION', 'CREATE', false
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CASHRECONCILIATION');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'cashier', 'READ_CASHRECONCILIATION', 'CASHRECONCILIATION', 'READ', false
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CASHRECONCILIATION');
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="0300-1" author="cbs">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<tableExists tableName="m_fd_base_interest_rate_effective"/>
|
||||||
|
</preConditions>
|
||||||
|
<renameTable oldTableName="m_fd_base_interest_rate_effective"
|
||||||
|
newTableName="m_cbs_base_interest_rate_effective"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0300-2" author="cbs">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<tableExists tableName="m_fd_base_interest_rate_slab_history"/>
|
||||||
|
</preConditions>
|
||||||
|
<renameTable oldTableName="m_fd_base_interest_rate_slab_history"
|
||||||
|
newTableName="m_cbs_base_interest_rate_slab_history"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<!-- Add from_current_account_id column to m_account_transfer_details -->
|
||||||
|
<changeSet id="0301-1" author="cbs">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<columnExists tableName="m_account_transfer_details" columnName="from_current_account_id"/>
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_account_transfer_details">
|
||||||
|
<column name="from_current_account_id" type="BIGINT">
|
||||||
|
<constraints nullable="true"
|
||||||
|
foreignKeyName="FK_atd_from_current_account"
|
||||||
|
references="m_current_account(id)"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Add to_current_account_id column to m_account_transfer_details -->
|
||||||
|
<changeSet id="0301-2" author="cbs">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<columnExists tableName="m_account_transfer_details" columnName="to_current_account_id"/>
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_account_transfer_details">
|
||||||
|
<column name="to_current_account_id" type="BIGINT">
|
||||||
|
<constraints nullable="true"
|
||||||
|
foreignKeyName="FK_atd_to_current_account"
|
||||||
|
references="m_current_account(id)"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+11
@@ -25,15 +25,22 @@ public class SavingsTransactionBooleanValues {
|
|||||||
private final boolean isApplyWithdrawFee;
|
private final boolean isApplyWithdrawFee;
|
||||||
private final boolean isInterestTransfer;
|
private final boolean isInterestTransfer;
|
||||||
private final boolean isExceptionForBalanceCheck;
|
private final boolean isExceptionForBalanceCheck;
|
||||||
|
private final boolean skipAccountingPostings;
|
||||||
|
|
||||||
public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction,
|
public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction,
|
||||||
final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck) {
|
final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck) {
|
||||||
|
this(isAccountTransfer, isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isExceptionForBalanceCheck, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction,
|
||||||
|
final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck,
|
||||||
|
final boolean skipAccountingPostings) {
|
||||||
this.isAccountTransfer = isAccountTransfer;
|
this.isAccountTransfer = isAccountTransfer;
|
||||||
this.isRegularTransaction = isRegularTransaction;
|
this.isRegularTransaction = isRegularTransaction;
|
||||||
this.isApplyWithdrawFee = isApplyWithdrawFee;
|
this.isApplyWithdrawFee = isApplyWithdrawFee;
|
||||||
this.isInterestTransfer = isInterestTransfer;
|
this.isInterestTransfer = isInterestTransfer;
|
||||||
this.isExceptionForBalanceCheck = isExceptionForBalanceCheck;
|
this.isExceptionForBalanceCheck = isExceptionForBalanceCheck;
|
||||||
|
this.skipAccountingPostings = skipAccountingPostings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAccountTransfer() {
|
public boolean isAccountTransfer() {
|
||||||
@@ -56,4 +63,8 @@ public class SavingsTransactionBooleanValues {
|
|||||||
return this.isExceptionForBalanceCheck;
|
return this.isExceptionForBalanceCheck;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSkipAccountingPostings() {
|
||||||
|
return this.skipAccountingPostings;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -59,7 +59,8 @@ public class SavingsAccountConstant extends SavingsApiConstants {
|
|||||||
dateFormatParamName, transactionDateParamName, transactionAmountParamName, paymentTypeIdParamName,
|
dateFormatParamName, transactionDateParamName, transactionAmountParamName, paymentTypeIdParamName,
|
||||||
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName, bankNumberParamName,
|
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName, bankNumberParamName,
|
||||||
retailEntriesParamName, childAccountIdParamName, noteParamName, amountParamName, dateParamName, isManualTransaction,
|
retailEntriesParamName, childAccountIdParamName, noteParamName, amountParamName, dateParamName, isManualTransaction,
|
||||||
lienTransaction, chargesPaidByData, submittedOnDateParamName, accountIdParamName, accountNoParamName));
|
lienTransaction, chargesPaidByData, submittedOnDateParamName, accountIdParamName, accountNoParamName,
|
||||||
|
"denominationBreakdown"));
|
||||||
|
|
||||||
protected static final Set<String> SAVINGS_ACCOUNT_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>(
|
protected static final Set<String> SAVINGS_ACCOUNT_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(idParamName, accountNoParamName));
|
Arrays.asList(idParamName, accountNoParamName));
|
||||||
|
|||||||
+18
@@ -3969,6 +3969,12 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
|||||||
@Column(name = "auto_debit_facility", nullable = false, columnDefinition = "boolean default false")
|
@Column(name = "auto_debit_facility", nullable = false, columnDefinition = "boolean default false")
|
||||||
private Boolean autoDebitFacility = false;
|
private Boolean autoDebitFacility = false;
|
||||||
|
|
||||||
|
@Column(name = "is_senior_citizen", nullable = false, columnDefinition = "boolean default false")
|
||||||
|
private boolean isSeniorCitizen = false;
|
||||||
|
|
||||||
|
@Column(name = "senior_citizen_interest_rate", scale = 6, precision = 19, nullable = true)
|
||||||
|
private BigDecimal seniorCitizenInterestRate;
|
||||||
|
|
||||||
|
|
||||||
@jakarta.persistence.OneToOne(cascade = jakarta.persistence.CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY)
|
@jakarta.persistence.OneToOne(cascade = jakarta.persistence.CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY)
|
||||||
private SavingsAccountFacilityConfig facilityConfig;
|
private SavingsAccountFacilityConfig facilityConfig;
|
||||||
@@ -4041,4 +4047,16 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
|||||||
|
|
||||||
public Boolean getAutoDebitFacility() { return autoDebitFacility; }
|
public Boolean getAutoDebitFacility() { return autoDebitFacility; }
|
||||||
public void setAutoDebitFacility(Boolean v) { this.autoDebitFacility = v; }
|
public void setAutoDebitFacility(Boolean v) { this.autoDebitFacility = v; }
|
||||||
|
|
||||||
|
public boolean isSeniorCitizen() { return isSeniorCitizen; }
|
||||||
|
public void setSeniorCitizen(boolean v) { this.isSeniorCitizen = v; }
|
||||||
|
|
||||||
|
public BigDecimal getSeniorCitizenInterestRate() { return seniorCitizenInterestRate; }
|
||||||
|
public void setSeniorCitizenInterestRate(BigDecimal v) { this.seniorCitizenInterestRate = v; }
|
||||||
|
|
||||||
|
public void updateNominalAnnualInterestRate(BigDecimal rate) {
|
||||||
|
if (rate != null && rate.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
this.nominalAnnualInterestRate = rate;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -38,6 +38,10 @@ public interface SavingsAccountDomainService {
|
|||||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
|
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
|
||||||
boolean backdatedTxnsAllowedTill);
|
boolean backdatedTxnsAllowedTill);
|
||||||
|
|
||||||
|
SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
||||||
|
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
|
||||||
|
boolean backdatedTxnsAllowedTill, boolean skipAccountingPostings);
|
||||||
|
|
||||||
void postJournalEntries(SavingsAccount savingsAccount, Set<Long> existingTransactionIds, Set<Long> existingReversedTransactionIds,
|
void postJournalEntries(SavingsAccount savingsAccount, Set<Long> existingTransactionIds, Set<Long> existingReversedTransactionIds,
|
||||||
boolean backdatedTxnsAllowedTill);
|
boolean backdatedTxnsAllowedTill);
|
||||||
|
|
||||||
|
|||||||
+1
@@ -37,4 +37,5 @@
|
|||||||
<include file="parts/2012_add_current_account_permissions.xml" relativeToChangelogFile="true" />
|
<include file="parts/2012_add_current_account_permissions.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/2013_add_missing_current_account_columns.xml" relativeToChangelogFile="true" />
|
<include file="parts/2013_add_missing_current_account_columns.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/2014_add_is_mandatory_to_joint_holders.xml" relativeToChangelogFile="true" />
|
<include file="parts/2014_add_is_mandatory_to_joint_holders.xml" relativeToChangelogFile="true" />
|
||||||
|
<include file="parts/2015_add_senior_citizen_to_savings_account.xml" relativeToChangelogFile="true" />
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership.
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="2015_add_senior_citizen_to_savings_account" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<tableExists tableName="m_savings_account"/>
|
||||||
|
<not><columnExists tableName="m_savings_account" columnName="is_senior_citizen"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_savings_account">
|
||||||
|
<column name="is_senior_citizen" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="senior_citizen_interest_rate" type="DECIMAL(19,6)">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
Reference in New Issue
Block a user