Skip to content

Commit

Permalink
Job Accrual for Savings
Browse files Browse the repository at this point in the history
  • Loading branch information
Jose Alberto Hernandez committed Sep 13, 2023
1 parent a4b6fe8 commit 178808f
Show file tree
Hide file tree
Showing 15 changed files with 296 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1257,6 +1257,14 @@ public CommandWrapperBuilder excuteAccrualAccounting() {
return this;
}

public CommandWrapperBuilder excuteAccrualAccountingForSavings() {
this.actionName = "EXECUTE";
this.entityName = "PERIODICACCRUALACCOUNTINGFORSAVINGS";
this.entityId = null;
this.href = "/accrualaccounting";
return this;
}

public CommandWrapperBuilder createGLAccount() {
this.actionName = "CREATE";
this.entityName = "GLACCOUNT";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ public enum JobName {
LOAN_DELINQUENCY_CLASSIFICATION("Loan Delinquency Classification"), //
SEND_ASYNCHRONOUS_EVENTS("Send Asynchronous Events"), //
PURGE_EXTERNAL_EVENTS("Purge External Events"), //
PURGE_PROCESSED_COMMANDS("Purge Processed Commands");
PURGE_PROCESSED_COMMANDS("Purge Processed Commands"), //
ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS("Add Accrual Transactions For Savings");

private final String name;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
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.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
import org.springframework.stereotype.Component;

@Path("/v1/runaccruals")
Expand All @@ -54,12 +56,16 @@ public class AccrualAccountingApiResource {
@Operation(summary = "Executes Periodic Accrual Accounting", method = "POST", description = "Mandatory Fields\n" + "\n" + "tillDate\n")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = AccrualAccountingApiResourceSwagger.PostRunaccrualsRequest.class)))
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
public String executePeriodicAccrualAccounting(@Parameter(hidden = true) final String jsonRequestBody) {

final CommandWrapper commandRequest = new CommandWrapperBuilder().excuteAccrualAccounting().withJson(jsonRequestBody).build();
public String executePeriodicAccrualAccounting(@QueryParam("product") @Parameter(description = "product") final String productParam,
@Parameter(hidden = true) final String jsonRequestBody) {

CommandWrapper commandRequest = null;
if (productParam == null || CommandParameterUtil.is(productParam, "loans")) {
commandRequest = new CommandWrapperBuilder().excuteAccrualAccounting().withJson(jsonRequestBody).build();
} else if (CommandParameterUtil.is(productParam, "savings")) {
commandRequest = new CommandWrapperBuilder().excuteAccrualAccountingForSavings().withJson(jsonRequestBody).build();
}
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);

return this.apiJsonSerializerService.serialize(result);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* 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.accounting.accrual.handler;

import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.accrual.service.AccrualAccountingWritePlatformService;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@CommandType(entity = "PERIODICACCRUALACCOUNTINGFORSAVINGS", action = "EXECUTE")
@RequiredArgsConstructor
public class ExecutePeriodicAccrualForSavingsCommandHandler implements NewCommandSourceHandler {

private final AccrualAccountingWritePlatformService writePlatformService;

@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.executeLoansPeriodicAccrual(command);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ else if (savingsTransactionDTO.getTransactionType().isInterestPosting() && savin
transactionId, transactionDate, overdraftAmount, isReversal);
if (isPositive) {
this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(),
AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(),
AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId,
transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1768,12 +1768,14 @@ public Map<String, Object> deriveAccountingBridgeData(final String currencyCode,
trans = getTransactions();
}

final Set<Long> accrualChargeIds = product.accrualChargeIds();

// Adding new transactions to the array
for (final SavingsAccountTransaction transaction : trans) {
if (transaction.isReversed() && !existingReversedTransactionIds.contains(transaction.getId())) {
newSavingsTransactions.add(transaction.toMapData(currencyCode));
newSavingsTransactions.add(transaction.toMapData(currencyCode, accrualChargeIds));
} else if (!existingTransactionIds.contains(transaction.getId())) {
newSavingsTransactions.add(transaction.toMapData(currencyCode));
newSavingsTransactions.add(transaction.toMapData(currencyCode, accrualChargeIds));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ public boolean hasNotAmount(final Money amountToCheck) {
return transactionAmount.isNotEqualTo(amountToCheck);
}

public Map<String, Object> toMapData(final String currencyCode) {
public Map<String, Object> toMapData(final String currencyCode, final Set<Long> accrualChargeIds) {
final Map<String, Object> thisTransactionData = new LinkedHashMap<>();

final SavingsAccountTransactionEnumData transactionType = SavingsEnumerations.transactionType(this.typeOf);
Expand All @@ -556,7 +556,9 @@ public Map<String, Object> toMapData(final String currencyCode) {
final List<Map<String, Object>> savingsChargesPaidData = new ArrayList<>();
for (final SavingsAccountChargePaidBy chargePaidBy : this.savingsAccountChargesPaid) {
final Map<String, Object> savingChargePaidData = new LinkedHashMap<>();
savingChargePaidData.put("chargeId", chargePaidBy.getSavingsAccountCharge().getCharge().getId());
final Long chargeId = chargePaidBy.getSavingsAccountCharge().getCharge().getId();
savingChargePaidData.put("chargeId", chargeId);
savingChargePaidData.put("isAccrued", accrualChargeIds.contains(chargeId));
savingChargePaidData.put("isPenalty", chargePaidBy.getSavingsAccountCharge().getCharge().isPenalty());
savingChargePaidData.put("savingsChargeId", chargePaidBy.getSavingsAccountCharge().getId());
savingChargePaidData.put("amount", chargePaidBy.getAmount());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,12 @@ public Set<Charge> accrualCharges() {
return this.accrualCharges;
}

public Set<Long> accrualChargeIds() {
Set<Long> accrualChargeIds = new HashSet<>();
this.accrualCharges.stream().map(charge -> accrualChargeIds.add(charge.getId()));
return accrualChargeIds;
}

public InterestRateChart applicableChart(@SuppressWarnings("unused") final LocalDate target) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* 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.savings.jobs.addaccrualtransactionforsavings;

import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class AddAccrualTransactionForSavingsConfig {

@Autowired
private JobRepository jobRepository;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService;
@Autowired
private SavingsAccountWritePlatformService savingsAccountWritePlatformService;

@Bean
protected Step addAccrualTransactionForSavingsStep() {
return new StepBuilder(JobName.ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS.name(), jobRepository)
.tasklet(addAccrualTransactionForSavingsTasklet(), transactionManager).build();
}

@Bean
public Job addAccrualTransactionForSavingsJob() {
return new JobBuilder(JobName.ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS.name(), jobRepository).start(addAccrualTransactionForSavingsStep())
.incrementer(new RunIdIncrementer()).build();
}

@Bean
public AddAccrualTransactionForSavingsTasklet addAccrualTransactionForSavingsTasklet() {
return new AddAccrualTransactionForSavingsTasklet(savingsAccountChargeReadPlatformService, savingsAccountWritePlatformService);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* 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.savings.jobs.addaccrualtransactionforsavings;

import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.savings.data.SavingsAccountAnnualFeeData;
import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;

@Slf4j
@RequiredArgsConstructor
public class AddAccrualTransactionForSavingsTasklet implements Tasklet {

private final SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService;
private final SavingsAccountWritePlatformService savingsAccountWritePlatformService;

@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
final Collection<SavingsAccountAnnualFeeData> annualFeeData = savingsAccountChargeReadPlatformService
.retrieveChargesWithAnnualFeeDue();

for (final SavingsAccountAnnualFeeData savingsAccountReference : annualFeeData) {
try {
savingsAccountWritePlatformService.applyAnnualFee(savingsAccountReference.getId(), savingsAccountReference.getAccountId());
} catch (final PlatformApiDataValidationException e) {
final List<ApiParameterError> errors = e.getErrors();
for (final ApiParameterError error : errors) {
log.error("Add Accrual Transaction failed for account: {} with message {}", savingsAccountReference.getAccountNo(), error);
}
} catch (final Exception ex) {
log.error("Add Accrual Transaction failed for account: {}", savingsAccountReference.getAccountNo(), ex);
}
}

log.debug("{}: Records affected by addAccrualTransactionForSavings: {}", ThreadLocalContextUtil.getTenant().getName(),
annualFeeData.size());
return RepeatStatus.FINISHED;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,18 @@ public CommandProcessingResult addSavingsAccountCharge(final JsonCommand command
savingsAccount.addCharge(fmt, savingsAccountCharge, chargeDefinition);
this.savingsAccountChargeRepository.save(savingsAccountCharge);
this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount);

final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus();
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateSavingsTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds);
}

postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill);

return new CommandProcessingResultBuilder() //
.withEntityId(savingsAccountCharge.getId()) //
.withOfficeId(savingsAccount.officeId()) //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,4 @@
<include file="parts/0122_add_batch_job_execution_params_index.xml" relativeToChangelogFile="true" />
<include file="parts/0123_add_is_down_payment_to_repayment_schedule.xml" relativeToChangelogFile="true" />
<include file="parts/0124_transaction_summary_with_asset_owner_report_typo_fix_3.xml" relativeToChangelogFile="true" />
<include file="parts/0125_add_savings_product_charge_for_accrual.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?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.3.xsd">
<changeSet author="fineract" id="1">
<insert tableName="job">
<column name="name" value="Add Accrual Transactions For Savings"/>
<column name="display_name" value="Add Accrual Transactions For Savings"/>
<column name="cron_expression" value="0 1 0 1/1 * ? *"/>
<column name="create_time" valueDate="${current_datetime}"/>
<column name="task_priority" valueNumeric="5"/>
<column name="group_name"/>
<column name="previous_run_start_time"/>
<column name="job_key" value="Add Accrual Transactions For Savings _ DEFAULT"/>
<column name="initializing_errorlog"/>
<column name="is_active" valueBoolean="false"/>
<column name="currently_running" valueBoolean="false"/>
<column name="updates_allowed" valueBoolean="true"/>
<column name="scheduler_group" valueNumeric="0"/>
<column name="is_misfired" valueBoolean="false"/>
<column name="node_id" valueNumeric="1"/>
<column name="is_mismatched_job" valueBoolean="true"/>
</insert>
</changeSet>
</databaseChangeLog>
Loading

0 comments on commit 178808f

Please sign in to comment.