Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* 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.commands.data;

import java.time.OffsetDateTime;
import lombok.Builder;
import lombok.Getter;

/**
* Lightweight DTO representing a single pending maker-checker entry for a resource.
*/
@Getter
@Builder
public class PendingMakerCheckerData {

/** The maker-checker command source id (m_portfolio_command_source.id) */
private final Long id;

/** e.g. "APPROVE", "DISBURSE", "CREATE", "ACTIVATE" */
private final String actionName;

/** e.g. "LOAN", "CLIENT", "SAVINGSACCOUNT" */
private final String entityName;

/** Human-readable label, e.g. "APPROVE_LOAN" */
private final String permissionCode;

/** Username of the maker who submitted this command */
private final String makerUsername;

/** When the maker submitted this command */
private final OffsetDateTime madeOnDate;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.apache.fineract.commands.domain;

import java.time.OffsetDateTime;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
Expand All @@ -29,6 +30,15 @@ public interface CommandSourceRepository extends JpaRepository<CommandSource, Lo

CommandSource findByActionNameAndEntityNameAndIdempotencyKey(String actionName, String entityName, String idempotencyKey);

@Query("select distinct c from CommandSource c join fetch c.maker where c.loanId = :loanId and c.status = :status order by c.madeOnDate desc")
List<CommandSource> findPendingByLoanId(@Param("loanId") Long loanId, @Param("status") Integer status);

@Query("select distinct c from CommandSource c join fetch c.maker where c.clientId = :clientId and c.status = :status order by c.madeOnDate desc")
List<CommandSource> findPendingByClientId(@Param("clientId") Long clientId, @Param("status") Integer status);

@Query("select distinct c from CommandSource c join fetch c.maker where c.savingsId = :savingsId and c.status = :status order by c.madeOnDate desc")
List<CommandSource> findPendingBySavingsId(@Param("savingsId") Long savingsId, @Param("status") Integer status);

@Modifying(flushAutomatically = true)
@Query("delete from CommandSource c where c.status = :status and c.madeOnDate is not null and c.madeOnDate <= :dateForPurgeCriteria")
void deleteOlderEventsWithStatus(@Param("status") Integer status, @Param("dateForPurgeCriteria") OffsetDateTime dateForPurgeCriteria);
Expand Down
Original file line number Diff line number Diff line change
@@ -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.commands.service;

import java.util.List;
import org.apache.fineract.commands.data.PendingMakerCheckerData;

public interface MakerCheckerReadService {

List<PendingMakerCheckerData> retrievePendingByLoanId(Long loanId);

List<PendingMakerCheckerData> retrievePendingByClientId(Long clientId);

List<PendingMakerCheckerData> retrievePendingBySavingsId(Long savingsId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* 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.commands.service;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.data.PendingMakerCheckerData;
import org.apache.fineract.commands.domain.CommandProcessingResultType;
import org.apache.fineract.commands.domain.CommandSource;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MakerCheckerReadServiceImpl implements MakerCheckerReadService {

private final CommandSourceRepository commandSourceRepository;

@Override
public List<PendingMakerCheckerData> retrievePendingByLoanId(final Long loanId) {
return commandSourceRepository.findPendingByLoanId(loanId, CommandProcessingResultType.AWAITING_APPROVAL.getValue()).stream()
.map(this::toData).toList();
}

@Override
public List<PendingMakerCheckerData> retrievePendingByClientId(final Long clientId) {
return commandSourceRepository.findPendingByClientId(clientId, CommandProcessingResultType.AWAITING_APPROVAL.getValue()).stream()
.map(this::toData).toList();
}

@Override
public List<PendingMakerCheckerData> retrievePendingBySavingsId(final Long savingsId) {
return commandSourceRepository.findPendingBySavingsId(savingsId, CommandProcessingResultType.AWAITING_APPROVAL.getValue()).stream()
.map(this::toData).toList();
}

private PendingMakerCheckerData toData(final CommandSource cs) {
final String makerUsername = cs.getMaker() != null ? cs.getMaker().getUsername() : null;
return PendingMakerCheckerData.builder().id(cs.getId()).actionName(cs.getActionName()).entityName(cs.getEntityName())
.permissionCode(cs.getPermissionCode()).makerUsername(makerUsername).madeOnDate(cs.getMadeOnDate()).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.apache.commons.lang3.builder.CompareToBuilder;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.fineract.commands.data.PendingMakerCheckerData;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
Expand Down Expand Up @@ -115,6 +116,8 @@ public final class ClientData implements Comparable<ClientData>, Serializable {

private List<DatatableData> datatables;

private List<PendingMakerCheckerData> pendingMakerCheckerApprovals;

// import fields
private transient Integer rowIndex;
private String dateFormat;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import lombok.Setter;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.fineract.commands.data.PendingMakerCheckerData;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.jersey.serializer.legacy.JsonLocalDateArrayFormat;
import org.apache.fineract.infrastructure.dataqueries.data.DatatableData;
Expand Down Expand Up @@ -95,6 +96,8 @@ public final class SavingsAccountData implements Serializable {
private final Integer daysToDormancy;
private final Integer daysToEscheat;
private final BigDecimal savingsAmountOnHold;

private List<PendingMakerCheckerData> pendingMakerCheckerApprovals;
// associations
private final SavingsAccountSummaryData summary;
@SuppressWarnings("unused")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/**
* 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.commands.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Collections;
import java.util.List;
import org.apache.fineract.commands.data.PendingMakerCheckerData;
import org.apache.fineract.commands.domain.CommandProcessingResultType;
import org.apache.fineract.commands.domain.CommandSource;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.useradministration.domain.AppUser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class MakerCheckerReadServiceImplTest {

@Mock
private CommandSourceRepository commandSourceRepository;

@InjectMocks
private MakerCheckerReadServiceImpl service;

private static final Integer AWAITING_STATUS = CommandProcessingResultType.AWAITING_APPROVAL.getValue();
private static final OffsetDateTime NOW = OffsetDateTime.now(ZoneOffset.UTC);

private CommandSource buildCommandSource(String action, String entity, String username) {
final AppUser maker = mock(AppUser.class);
when(maker.getUsername()).thenReturn(username);

return CommandSource.builder().actionName(action).entityName(entity).maker(maker).madeOnDate(NOW)
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()).sanitized(false).build();
}

@Test
void retrievePendingByLoanId_withPendingCommand_returnsMappedData() {
final Long loanId = 101L;
final CommandSource cs = buildCommandSource("APPROVE", "LOAN", "maker01");
when(commandSourceRepository.findPendingByLoanId(loanId, AWAITING_STATUS)).thenReturn(List.of(cs));

final List<PendingMakerCheckerData> result = service.retrievePendingByLoanId(loanId);

assertThat(result).hasSize(1);
assertThat(result.get(0).getActionName()).isEqualTo("APPROVE");
assertThat(result.get(0).getEntityName()).isEqualTo("LOAN");
assertThat(result.get(0).getPermissionCode()).isEqualTo("APPROVE_LOAN"); // computed: action + "_" + entity
assertThat(result.get(0).getMakerUsername()).isEqualTo("maker01");
assertThat(result.get(0).getMadeOnDate()).isEqualTo(NOW);
verify(commandSourceRepository).findPendingByLoanId(loanId, AWAITING_STATUS);
}

@Test
void retrievePendingByLoanId_withNoPendingCommands_returnsEmptyList() {
final Long loanId = 102L;
when(commandSourceRepository.findPendingByLoanId(loanId, AWAITING_STATUS)).thenReturn(Collections.emptyList());

final List<PendingMakerCheckerData> result = service.retrievePendingByLoanId(loanId);

assertThat(result).isEmpty();
}

@Test
void retrievePendingByLoanId_withMultiplePendingCommands_returnsAllMapped() {
final Long loanId = 103L;
final CommandSource cs1 = buildCommandSource("APPROVE", "LOAN", "maker01");
final CommandSource cs2 = buildCommandSource("DISBURSE", "LOAN", "maker02");
when(commandSourceRepository.findPendingByLoanId(loanId, AWAITING_STATUS)).thenReturn(List.of(cs1, cs2));

final List<PendingMakerCheckerData> result = service.retrievePendingByLoanId(loanId);

assertThat(result).hasSize(2);
assertThat(result.get(0).getActionName()).isEqualTo("APPROVE");
assertThat(result.get(1).getActionName()).isEqualTo("DISBURSE");
}

@Test
void retrievePendingByLoanId_withNullMaker_returnsMakerUsernameNull() {
final Long loanId = 104L;
final CommandSource cs = CommandSource.builder().actionName("APPROVE").entityName("LOAN").maker(null).madeOnDate(NOW)
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()).sanitized(false).build();
when(commandSourceRepository.findPendingByLoanId(loanId, AWAITING_STATUS)).thenReturn(List.of(cs));

final List<PendingMakerCheckerData> result = service.retrievePendingByLoanId(loanId);

assertThat(result).hasSize(1);
assertThat(result.get(0).getMakerUsername()).isNull();
}

@Test
void retrievePendingByClientId_withPendingCommand_returnsMappedData() {
final Long clientId = 201L;
final CommandSource cs = buildCommandSource("ACTIVATE", "CLIENT", "maker03");
when(commandSourceRepository.findPendingByClientId(clientId, AWAITING_STATUS)).thenReturn(List.of(cs));

final List<PendingMakerCheckerData> result = service.retrievePendingByClientId(clientId);

assertThat(result).hasSize(1);
assertThat(result.get(0).getActionName()).isEqualTo("ACTIVATE");
assertThat(result.get(0).getEntityName()).isEqualTo("CLIENT");
assertThat(result.get(0).getPermissionCode()).isEqualTo("ACTIVATE_CLIENT");
assertThat(result.get(0).getMakerUsername()).isEqualTo("maker03");
verify(commandSourceRepository).findPendingByClientId(clientId, AWAITING_STATUS);
}

@Test
void retrievePendingByClientId_withNoPendingCommands_returnsEmptyList() {
final Long clientId = 202L;
when(commandSourceRepository.findPendingByClientId(clientId, AWAITING_STATUS)).thenReturn(Collections.emptyList());

final List<PendingMakerCheckerData> result = service.retrievePendingByClientId(clientId);

assertThat(result).isEmpty();
}

@Test
void retrievePendingBySavingsId_withPendingCommand_returnsMappedData() {
final Long savingsId = 301L;
final CommandSource cs = buildCommandSource("APPROVE", "SAVINGSACCOUNT", "maker04");
when(commandSourceRepository.findPendingBySavingsId(savingsId, AWAITING_STATUS)).thenReturn(List.of(cs));

final List<PendingMakerCheckerData> result = service.retrievePendingBySavingsId(savingsId);

assertThat(result).hasSize(1);
assertThat(result.get(0).getActionName()).isEqualTo("APPROVE");
assertThat(result.get(0).getEntityName()).isEqualTo("SAVINGSACCOUNT");
assertThat(result.get(0).getPermissionCode()).isEqualTo("APPROVE_SAVINGSACCOUNT");
assertThat(result.get(0).getMakerUsername()).isEqualTo("maker04");
verify(commandSourceRepository).findPendingBySavingsId(savingsId, AWAITING_STATUS);
}

@Test
void retrievePendingBySavingsId_withNoPendingCommands_returnsEmptyList() {
final Long savingsId = 302L;
when(commandSourceRepository.findPendingBySavingsId(savingsId, AWAITING_STATUS)).thenReturn(Collections.emptyList());

final List<PendingMakerCheckerData> result = service.retrievePendingBySavingsId(savingsId);

assertThat(result).isEmpty();
}

@Test
void retrievePendingBySavingsId_coversFixedDeposit_returnsMappedData() {
final Long fdId = 401L;
final CommandSource cs = buildCommandSource("ACTIVATE", "FIXEDDEPOSITACCOUNT", "maker05");
when(commandSourceRepository.findPendingBySavingsId(fdId, AWAITING_STATUS)).thenReturn(List.of(cs));

final List<PendingMakerCheckerData> result = service.retrievePendingBySavingsId(fdId);

assertThat(result).hasSize(1);
assertThat(result.get(0).getEntityName()).isEqualTo("FIXEDDEPOSITACCOUNT");
assertThat(result.get(0).getPermissionCode()).isEqualTo("ACTIVATE_FIXEDDEPOSITACCOUNT");
assertThat(result.get(0).getMakerUsername()).isEqualTo("maker05");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.fineract.commands.data.PendingMakerCheckerData;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.data.StringEnumOptionData;
Expand Down Expand Up @@ -295,6 +296,8 @@ public class LoanAccountData {
private StringEnumOptionData buyDownFeeIncomeType;
private Boolean merchantBuyDownFee;

private List<PendingMakerCheckerData> pendingMakerCheckerApprovals;

public static LoanAccountData importInstanceIndividual(EnumOptionData loanTypeEnumOption, Long clientId, Long productId,
Long loanOfficerId, LocalDate submittedOnDate, Long fundId, BigDecimal principal, Integer numberOfRepayments,
Integer repaymentEvery, EnumOptionData repaidEveryFrequencyEnums, Integer loanTermFrequency,
Expand Down
Loading