FINERACT-2332: SavingCOB infrastructure

This commit is contained in:
Jose Alberto Hernandez
2026-02-12 14:02:18 +01:00
committed by Aleksandar Vidakovic
parent 356dd07ca0
commit 8d560a1f7c
23 changed files with 795 additions and 13 deletions
@@ -0,0 +1,27 @@
{
"name": "SavingsAccountStayedLockedDataV1",
"namespace": "org.apache.fineract.avro.savings.v1",
"type": "record",
"fields": [
{
"name": "id",
"type": "long"
},
{
"default": null,
"name": "externalId",
"type": [
"null",
"string"
]
},
{
"default": null,
"name": "accountNo",
"type": [
"null",
"string"
]
}
]
}
@@ -0,0 +1,12 @@
{
"name": "SavingsAccountsStayedLockedDataV1",
"namespace": "org.apache.fineract.avro.savings.v1",
"type": "record",
"fields": [{
"name": "savingsAccounts",
"type": {
"type": "array",
"items": "org.apache.fineract.avro.savings.v1.SavingsAccountStayedLockedDataV1"
}
}]
}
@@ -0,0 +1,117 @@
/**
* 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.cob.savings;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.cob.data.COBIdAndExternalIdAndAccountNo;
import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate;
import org.apache.fineract.cob.data.COBParameter;
import org.apache.fineract.cob.data.COBPartition;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class RetrieveSavingsIdServiceImpl implements RetrieveSavingsIdService {
private static final Collection<Integer> NON_CLOSED_SAVINGS_STATUSES = new ArrayList<>(
Arrays.asList(SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.getValue(), SavingsAccountStatusType.APPROVED.getValue(),
SavingsAccountStatusType.ACTIVE.getValue(), SavingsAccountStatusType.TRANSFER_IN_PROGRESS.getValue(),
SavingsAccountStatusType.TRANSFER_ON_HOLD.getValue()));
private final SavingsAccountRepository savingsAccountRepository;
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Override
public List<COBPartition> retrieveSavingsCOBPartitions(Long numberOfDays, LocalDate businessDate, boolean isCatchUp,
int partitionSize) {
String sql = """
select min(id) as min, max(id) as max, page, count(id) as count from
(select floor(((row_number() over(order by id))-1) / :pageSize) as page, t.* from
(select id from m_savings_account where status_enum in (:statusIds) and
""";
if (isCatchUp) {
sql = sql + " last_closed_business_date = :businessDate ";
} else {
sql = sql + " (last_closed_business_date = :businessDate or last_closed_business_date is null) ";
}
sql = sql + " order by id) t) t2 group by page order by page";
MapSqlParameterSource parameters = new MapSqlParameterSource();
parameters.addValue("pageSize", partitionSize);
parameters.addValue("statusIds", NON_CLOSED_SAVINGS_STATUSES);
parameters.addValue("businessDate", businessDate.minusDays(numberOfDays));
return namedParameterJdbcTemplate.query(sql.toString(), parameters, RetrieveSavingsIdServiceImpl::mapRow);
}
private static COBPartition mapRow(ResultSet rs, int rowNum) throws SQLException {
return new COBPartition(rs.getLong("min"), rs.getLong("max"), rs.getLong("page"), rs.getLong("count"));
}
@Override
public List<COBIdAndLastClosedBusinessDate> retrieveSavingsIdsBehindDate(LocalDate businessDate, List<Long> savingsIds) {
return savingsAccountRepository.findAllSavingsIdsBehindDate(businessDate, savingsIds);
}
@Override
public List<COBIdAndLastClosedBusinessDate> retrieveSavingsIdsBehindDateOrNull(LocalDate businessDate, List<Long> savingsIds) {
return savingsAccountRepository.findAllSavingsIdsBehindDateOrNull(businessDate, savingsIds);
}
@Override
public List<COBIdAndLastClosedBusinessDate> retrieveSavingsIdsOldestCobProcessed(LocalDate businessDate) {
return savingsAccountRepository.findAllSavingsIdsOldestCobProcessed();
}
@Override
public List<Long> retrieveAllNonClosedSavingsByLastClosedBusinessDateAndMinAndMaxSavingsId(COBParameter savingsCOBParameter,
boolean isCatchUp) {
LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)
.minusDays(SavingsCOBConstant.NUMBER_OF_DAYS_BEHIND);
if (isCatchUp) {
return savingsAccountRepository.findAllSavingsByLastClosedBusinessDateNotNullAndMinAndMaxSavingsIdAndStatuses(
savingsCOBParameter.getMinAccountId(), savingsCOBParameter.getMaxAccountId(), cobBusinessDate,
NON_CLOSED_SAVINGS_STATUSES);
} else {
return savingsAccountRepository.findAllSavingsByLastClosedBusinessDateAndMinAndMaxSavingsIdAndStatuses(
savingsCOBParameter.getMinAccountId(), savingsCOBParameter.getMaxAccountId(), cobBusinessDate,
NON_CLOSED_SAVINGS_STATUSES);
}
}
@Override
public List<COBIdAndExternalIdAndAccountNo> findAllStayedLockedByCobBusinessDate(LocalDate cobBusinessDate) {
// This will be implemented when we add the query to join with SavingsAccountLock
// For now, return empty list as the lock table doesn't exist yet
return List.of();
}
}
@@ -0,0 +1,34 @@
/**
* 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.infrastructure.event.external.service.serialization.mapper.savings;
import org.apache.fineract.avro.savings.v1.SavingsAccountStayedLockedDataV1;
import org.apache.fineract.avro.savings.v1.SavingsAccountsStayedLockedDataV1;
import org.apache.fineract.cob.data.COBIdAndExternalIdAndAccountNo;
import org.apache.fineract.cob.savings.SavingsAccountsStayedLockedData;
import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.AvroMapperConfig;
import org.mapstruct.Mapper;
@Mapper(config = AvroMapperConfig.class)
public interface SavingsAccountsStayedLockedDataMapper {
SavingsAccountsStayedLockedDataV1 map(SavingsAccountsStayedLockedData savingsAccountsStayedLockedData);
SavingsAccountStayedLockedDataV1 map(COBIdAndExternalIdAndAccountNo cobIdAndExternalIdAndAccountNo);
}
@@ -0,0 +1,54 @@
/**
* 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.infrastructure.event.external.service.serialization.serializer.savings;
import lombok.RequiredArgsConstructor;
import org.apache.avro.generic.GenericContainer;
import org.apache.fineract.avro.generator.ByteBufferSerializable;
import org.apache.fineract.avro.savings.v1.SavingsAccountsStayedLockedDataV1;
import org.apache.fineract.cob.savings.SavingsAccountsStayedLockedBusinessEvent;
import org.apache.fineract.cob.savings.SavingsAccountsStayedLockedData;
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.savings.SavingsAccountsStayedLockedDataMapper;
import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class SavingsAccountsStayedLockedBusinessEventSerializer implements BusinessEventSerializer {
private final SavingsAccountsStayedLockedDataMapper mapper;
@Override
public <T> boolean canSerialize(BusinessEvent<T> event) {
return event instanceof SavingsAccountsStayedLockedBusinessEvent;
}
@Override
public <T> ByteBufferSerializable toAvroDTO(BusinessEvent<T> rawEvent) {
SavingsAccountsStayedLockedBusinessEvent event = (SavingsAccountsStayedLockedBusinessEvent) rawEvent;
SavingsAccountsStayedLockedData savingsAccounts = event.get();
return mapper.map(savingsAccounts);
}
@Override
public Class<? extends GenericContainer> getSupportedSchema() {
return SavingsAccountsStayedLockedDataV1.class;
}
}
@@ -112,7 +112,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "LoanApprovedAmountChangedBusinessEvent");
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "LoanApprovedAmountChangedBusinessEvent",
"SavingsAccountsStayedLockedBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -149,6 +150,7 @@ public class ExternalEventConfigurationValidationServiceTest {
// then
String expectedMessage = "No external events configured";
String actualMessage = exceptionThrown.getMessage();
assertTrue(actualMessage.contains(expectedMessage));
@@ -206,7 +208,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "LoanApprovedAmountChangedBusinessEvent");
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "LoanApprovedAmountChangedBusinessEvent",
"SavingsAccountsStayedLockedBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -0,0 +1,44 @@
/**
* 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.cob.savings;
import java.time.LocalDate;
import java.util.List;
import org.apache.fineract.cob.data.COBIdAndExternalIdAndAccountNo;
import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate;
import org.apache.fineract.cob.data.COBParameter;
import org.apache.fineract.cob.data.COBPartition;
import org.springframework.data.repository.query.Param;
public interface RetrieveSavingsIdService {
List<COBPartition> retrieveSavingsCOBPartitions(Long numberOfDays, LocalDate businessDate, boolean isCatchUp, int partitionSize);
List<COBIdAndLastClosedBusinessDate> retrieveSavingsIdsBehindDate(LocalDate businessDate, List<Long> savingsIds);
List<COBIdAndLastClosedBusinessDate> retrieveSavingsIdsBehindDateOrNull(LocalDate businessDate, List<Long> savingsIds);
List<COBIdAndLastClosedBusinessDate> retrieveSavingsIdsOldestCobProcessed(LocalDate businessDate);
List<Long> retrieveAllNonClosedSavingsByLastClosedBusinessDateAndMinAndMaxSavingsId(COBParameter savingsCOBParameter,
boolean isCatchUp);
List<COBIdAndExternalIdAndAccountNo> findAllStayedLockedByCobBusinessDate(@Param("cobBusinessDate") LocalDate cobBusinessDate);
}
@@ -0,0 +1,80 @@
/**
* 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.cob.savings;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity
@Table(name = "m_savings_account_locks")
@NoArgsConstructor
@Getter
public class SavingsAccountLock {
@Id
@Column(name = "savings_id", nullable = false)
private Long savingsId;
@Version
@Column(name = "version")
private Long version;
@Enumerated(EnumType.STRING)
@Column(name = "lock_owner", nullable = false)
private SavingsLockOwner lockOwner;
@Column(name = "lock_placed_on", nullable = false)
private OffsetDateTime lockPlacedOn;
@Column(name = "error")
private String error;
@Column(name = "stacktrace")
private String stacktrace;
@Column(name = "lock_placed_on_cob_business_date")
private LocalDate lockPlacedOnCobBusinessDate;
public SavingsAccountLock(Long savingsId, SavingsLockOwner lockOwner, LocalDate lockPlacedOnCobBusinessDate) {
this.savingsId = savingsId;
this.lockOwner = lockOwner;
this.lockPlacedOn = DateUtils.getAuditOffsetDateTime();
this.lockPlacedOnCobBusinessDate = lockPlacedOnCobBusinessDate;
}
public void setError(String errorMessage, String stacktrace) {
this.error = errorMessage;
this.stacktrace = stacktrace;
}
public void setNewLockOwner(SavingsLockOwner newLockOwner) {
this.lockOwner = newLockOwner;
this.lockPlacedOn = DateUtils.getAuditOffsetDateTime();
}
}
@@ -0,0 +1,50 @@
/**
* 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.cob.savings;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
public interface SavingsAccountLockRepository
extends JpaRepository<SavingsAccountLock, Long>, JpaSpecificationExecutor<SavingsAccountLock> {
Optional<SavingsAccountLock> findBySavingsIdAndLockOwner(Long savingsId, SavingsLockOwner lockOwner);
void deleteBySavingsIdInAndLockOwner(List<Long> savingsIds, SavingsLockOwner lockOwner);
List<SavingsAccountLock> findAllBySavingsIdIn(List<Long> savingsIds);
boolean existsBySavingsIdAndLockOwner(Long savingsId, SavingsLockOwner lockOwner);
boolean existsBySavingsIdAndLockOwnerAndErrorIsNotNull(Long savingsId, SavingsLockOwner lockOwner);
@Query("""
delete from SavingsAccountLock lck where lck.lockPlacedOnCobBusinessDate is not null and lck.error is not null and
lck.lockOwner in (org.apache.fineract.cob.savings.SavingsLockOwner.SAVINGS_COB_CHUNK_PROCESSING,
org.apache.fineract.cob.savings.SavingsLockOwner.SAVINGS_INLINE_COB_PROCESSING)
""")
@Modifying(flushAutomatically = true)
void removeLockByOwner();
List<SavingsAccountLock> findAllBySavingsIdInAndLockOwner(List<Long> savingsIds, SavingsLockOwner lockOwner);
}
@@ -0,0 +1,46 @@
/**
* 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.cob.savings;
import org.apache.fineract.infrastructure.event.business.domain.AbstractBusinessEvent;
public class SavingsAccountsStayedLockedBusinessEvent extends AbstractBusinessEvent<SavingsAccountsStayedLockedData> {
private static final String CATEGORY = "Savings COB";
private static final String TYPE = "SavingsAccountsStayedLockedBusinessEvent";
public SavingsAccountsStayedLockedBusinessEvent(SavingsAccountsStayedLockedData value) {
super(value);
}
@Override
public String getType() {
return TYPE;
}
@Override
public String getCategory() {
return CATEGORY;
}
@Override
public Long getAggregateRootId() {
return null;
}
}
@@ -0,0 +1,31 @@
/**
* 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.cob.savings;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.fineract.cob.data.COBIdAndExternalIdAndAccountNo;
@Getter
@AllArgsConstructor
public class SavingsAccountsStayedLockedData {
private List<COBIdAndExternalIdAndAccountNo> savingsAccounts;
}
@@ -0,0 +1,26 @@
/**
* 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.cob.savings;
import org.apache.fineract.cob.COBBusinessStep;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
public interface SavingsCOBBusinessStep extends COBBusinessStep<SavingsAccount> {
}
@@ -0,0 +1,40 @@
/**
* 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.cob.savings;
import org.apache.fineract.cob.COBConstant;
public final class SavingsCOBConstant extends COBConstant {
public static final String JOB_NAME = "SAVINGS_COB";
public static final String JOB_HUMAN_READABLE_NAME = "Savings COB";
public static final String SAVINGS_COB_JOB_NAME = "SAVINGS_CLOSE_OF_BUSINESS";
public static final String SAVINGS_COB_PARAMETER = "savingsCobParameter";
public static final String SAVINGS_COB_WORKER_STEP = "savingsCOBWorkerStep";
public static final String INLINE_SAVINGS_COB_JOB_NAME = "INLINE_SAVINGS_COB";
public static final String SAVINGS_IDS_PARAMETER_NAME = "SavingsIds";
public static final String SAVINGS_COB_PARTITIONER_STEP = "Savings COB partition - Step";
public static final String PARTITION_KEY = "partition";
private SavingsCOBConstant() {
}
}
@@ -0,0 +1,26 @@
/**
* 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.cob.savings;
public class SavingsLockCannotBeAppliedException extends Exception {
public SavingsLockCannotBeAppliedException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,24 @@
/**
* 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.cob.savings;
public enum SavingsLockOwner {
SAVINGS_COB_CHUNK_PROCESSING, //
SAVINGS_INLINE_COB_PROCESSING; //
}
@@ -0,0 +1,36 @@
/**
* 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.cob.savings;
import java.util.List;
public interface SavingsLockingService {
void upgradeLock(List<Long> accountsToLock, SavingsLockOwner lockOwner);
void deleteBySavingsIdInAndLockOwner(List<Long> savingsIds, SavingsLockOwner lockOwner);
List<SavingsAccountLock> findAllBySavingsIdIn(List<Long> savingsIds);
SavingsAccountLock findBySavingsIdAndLockOwner(Long savingsId, SavingsLockOwner lockOwner);
List<SavingsAccountLock> findAllBySavingsIdInAndLockOwner(List<Long> savingsIds, SavingsLockOwner lockOwner);
void applyLock(List<Long> savingsIds, SavingsLockOwner lockOwner);
}
@@ -0,0 +1,33 @@
/**
* 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.cob.savings;
public class SavingsReadException extends Exception {
private final Long id;
public SavingsReadException(Long id, Throwable t) {
super(String.format("Savings account is in already locked state! savingsId: %d", id), t);
this.id = id;
}
public Long getId() {
return id;
}
}
@@ -341,6 +341,9 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
@Column(name = "accrued_till_date")
private LocalDate accruedTillDate;
@Column(name = "last_closed_business_date")
private LocalDate lastClosedBusinessDate;
@Column(name = "total_savings_amount_on_hold", scale = 6, precision = 19, nullable = true)
private BigDecimal savingsOnHoldAmount;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true, fetch = FetchType.LAZY)
@@ -3855,6 +3858,14 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
this.accruedTillDate = accruedTillDate;
}
public LocalDate getLastClosedBusinessDate() {
return this.lastClosedBusinessDate;
}
public void setLastClosedBusinessDate(LocalDate lastClosedBusinessDate) {
this.lastClosedBusinessDate = lastClosedBusinessDate;
}
public List<SavingsAccountTransactionDetailsForPostingPeriod> toSavingsAccountTransactionDetailsForPostingPeriodList(
List<SavingsAccountTransaction> transactions) {
return transactions.stream()
@@ -20,7 +20,9 @@ package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.LockModeType;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.portfolio.savings.data.SavingsAccrualData;
import org.springframework.data.domain.Page;
@@ -97,4 +99,52 @@ public interface SavingsAccountRepository extends JpaRepository<SavingsAccount,
@Query("SELECT sa.id FROM SavingsAccount sa WHERE sa.status = :status")
List<Long> findSavingsAccountIdsByStatusId(Integer status);
// COB related queries
@Query("""
SELECT sa.id FROM SavingsAccount sa
WHERE sa.id BETWEEN :minSavingsId AND :maxSavingsId
AND sa.status IN :savingsStatuses
AND (:cobBusinessDate = sa.lastClosedBusinessDate OR sa.lastClosedBusinessDate IS NULL)
""")
List<Long> findAllSavingsByLastClosedBusinessDateAndMinAndMaxSavingsIdAndStatuses(@Param("minSavingsId") Long minSavingsId,
@Param("maxSavingsId") Long maxSavingsId, @Param("cobBusinessDate") LocalDate cobBusinessDate,
@Param("savingsStatuses") Collection<Integer> savingsStatuses);
@Query("""
SELECT sa.id FROM SavingsAccount sa
WHERE sa.id BETWEEN :minSavingsId AND :maxSavingsId
AND sa.status IN :savingsStatuses
AND sa.lastClosedBusinessDate = :cobBusinessDate
""")
List<Long> findAllSavingsByLastClosedBusinessDateNotNullAndMinAndMaxSavingsIdAndStatuses(@Param("minSavingsId") Long minSavingsId,
@Param("maxSavingsId") Long maxSavingsId, @Param("cobBusinessDate") LocalDate cobBusinessDate,
@Param("savingsStatuses") Collection<Integer> savingsStatuses);
@Query("""
SELECT sa.id, sa.lastClosedBusinessDate
FROM SavingsAccount sa
WHERE sa.id IN :savingsIds
AND (sa.lastClosedBusinessDate < :businessDate OR sa.lastClosedBusinessDate IS NULL)
""")
List<COBIdAndLastClosedBusinessDate> findAllSavingsIdsBehindDateOrNull(@Param("businessDate") LocalDate businessDate,
@Param("savingsIds") List<Long> savingsIds);
@Query("""
SELECT sa.id, sa.lastClosedBusinessDate
FROM SavingsAccount sa
WHERE sa.id IN :savingsIds
AND sa.lastClosedBusinessDate < :businessDate
""")
List<COBIdAndLastClosedBusinessDate> findAllSavingsIdsBehindDate(@Param("businessDate") LocalDate businessDate,
@Param("savingsIds") List<Long> savingsIds);
@Query("""
SELECT sa.id, sa.lastClosedBusinessDate
FROM SavingsAccount sa
WHERE sa.status IN (100, 200, 300, 303, 304)
AND sa.lastClosedBusinessDate IS NOT NULL
ORDER BY sa.lastClosedBusinessDate ASC
""")
List<COBIdAndLastClosedBusinessDate> findAllSavingsIdsOldestCobProcessed();
}
@@ -20,15 +20,14 @@ package org.apache.fineract.portfolio.savings.domain;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.data.SavingsAccrualData;
import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.query.Param;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -44,19 +43,11 @@ import org.springframework.transaction.annotation.Transactional;
* </p>
*/
@Service
@RequiredArgsConstructor
public class SavingsAccountRepositoryWrapper {
private final SavingsAccountRepository repository;
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
private final JdbcTemplate jdbcTemplate;
@Autowired
public SavingsAccountRepositoryWrapper(final SavingsAccountRepository repository,
final SavingsAccountTransactionRepository savingsAccountTransactionRepository, final JdbcTemplate jdbcTemplate) {
this.repository = repository;
this.savingsAccountTransactionRepository = savingsAccountTransactionRepository;
this.jdbcTemplate = jdbcTemplate;
}
@Transactional(readOnly = true)
public SavingsAccount findOneWithNotFoundDetection(final Long savingsId) {
@@ -26,4 +26,5 @@
<include file="parts/2001_add_savings_accrual_job.xml" relativeToChangelogFile="true" />
<include file="parts/2002_add_savings_accrual_permission.xml" relativeToChangelogFile="true" />
<include file="parts/2003_add_accrued_till_date_to_savings_account.xml" relativeToChangelogFile="true" />
<include file="parts/2004_add_savings_account_cob_infrastructure.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,41 @@
<?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="m_external_event_configuration">
<column name="type" value="SavingsAccountsStayedLockedBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet author="fineract" id="2">
<addColumn tableName="m_savings_account">
<column name="last_closed_business_date" type="DATE"/>
</addColumn>
</changeSet>
<changeSet author="fineract" id="3">
<createIndex indexName="FK_savings_account_last_closed_business_date" tableName="m_savings_account">
<column name="last_closed_business_date"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -662,6 +662,11 @@ public class ExternalEventConfigurationHelper {
loanApprovedAmountChangedBusinessEvent.put("enabled", false);
defaults.add(loanApprovedAmountChangedBusinessEvent);
Map<String, Object> savingsAccountsStayedLockedBusinessEvent = new HashMap<>();
savingsAccountsStayedLockedBusinessEvent.put("type", "SavingsAccountsStayedLockedBusinessEvent");
savingsAccountsStayedLockedBusinessEvent.put("enabled", false);
defaults.add(savingsAccountsStayedLockedBusinessEvent);
return defaults;
}