FINERACT-2239: New command processing - resource notes

This commit is contained in:
Aleksandar Vidakovic
2026-02-11 00:15:27 +01:00
committed by Aleksandar Vidakovic
parent 28f63cc20e
commit 02fb166b5b
39 changed files with 763 additions and 882 deletions
-2
View File
@@ -16,8 +16,6 @@
# under the License.
#
version: "3.8"
services:
fineract:
# user: "${FINERACT_USER}:${FINERACT_GROUP}"
+1 -1
View File
@@ -56,4 +56,4 @@ FINERACT_REMOTE_JOB_MESSAGE_HANDLER_SPRING_EVENTS_ENABLED=true
FINERACT_INSECURE_HTTP_CLIENT=true
SPRING_PROFILES_ACTIVE=test,diagnostics
OTEL_SERVICE_NAME=fineract
JAVA_TOOL_OPTIONS="-Xmx1G -XX:MinRAMPercentage=25 -XX:MaxRAMPercentage=80 -XX:TieredStopAtLevel=1 -XX:+UseContainerSupport -XX:+UseStringDeduplication --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED"
JAVA_TOOL_OPTIONS=" -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5000 -Xmx1G -XX:MinRAMPercentage=25 -XX:MaxRAMPercentage=80 -XX:TieredStopAtLevel=1 -XX:+UseContainerSupport -XX:+UseStringDeduplication --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED"
@@ -19,9 +19,12 @@
package com.acme.fineract.portfolio.note.service;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
import org.apache.fineract.portfolio.note.data.NoteDeleteRequest;
import org.apache.fineract.portfolio.note.data.NoteDeleteResponse;
import org.apache.fineract.portfolio.note.data.NoteUpdateRequest;
import org.apache.fineract.portfolio.note.data.NoteUpdateResponse;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -38,27 +41,17 @@ public class AcmeNoteWritePlatformService implements NoteWritePlatformService, I
}
@Override
public CommandProcessingResult createNote(JsonCommand command) {
public NoteCreateResponse createNote(NoteCreateRequest request) {
throw new UnsupportedOperationException("createNote() is not yet implemented.");
}
@Override
public void createLoanTransactionNote(Long loanTransactionId, String note) {
throw new UnsupportedOperationException("createLoanTransactionNote() is not yet implemented.");
}
@Override
public CommandProcessingResult updateNote(JsonCommand command) {
public NoteUpdateResponse updateNote(NoteUpdateRequest request) {
throw new UnsupportedOperationException("updateNote() is not yet implemented.");
}
@Override
public CommandProcessingResult deleteNote(JsonCommand command) {
public NoteDeleteResponse deleteNote(NoteDeleteRequest request) {
throw new UnsupportedOperationException("deleteNote() is not yet implemented.");
}
@Override
public void createAndPersistClientNote(Client client, JsonCommand command) {
log.warn("createAndPersistClientNote() is intentionally left empty and does nothing.");
}
}
@@ -21,7 +21,6 @@ package com.acme.fineract.portfolio.note.starter;
import static org.mockito.Mockito.mock;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSource;
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSourceServiceFactory;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
@@ -29,7 +28,6 @@ import org.apache.fineract.portfolio.group.domain.GroupRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.note.serialization.NoteCommandFromApiJsonDeserializer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -38,11 +36,6 @@ import org.springframework.jdbc.core.JdbcTemplate;
public class TestDefaultConfiguration {
// NOTE: unfortunately an abastract base class that contains all these mock functions won't work
@Bean
public FromJsonHelper fromJsonHelper() {
return mock(FromJsonHelper.class);
}
@Bean
public RoutingDataSourceServiceFactory routingDataSourceServiceFactory() {
return mock(RoutingDataSourceServiceFactory.class);
@@ -82,9 +75,4 @@ public class TestDefaultConfiguration {
public LoanTransactionRepository loanTransactionRepository() {
return mock(LoanTransactionRepository.class);
}
@Bean
public NoteCommandFromApiJsonDeserializer fromApiJsonDeserializer(FromJsonHelper fromJsonHelper) {
return new NoteCommandFromApiJsonDeserializer(fromJsonHelper);
}
}
@@ -20,7 +20,6 @@ package com.acme.fineract.portfolio.note.starter;
import static org.mockito.Mockito.mock;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSource;
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSourceServiceFactory;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
@@ -28,7 +27,6 @@ import org.apache.fineract.portfolio.group.domain.GroupRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.note.serialization.NoteCommandFromApiJsonDeserializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -37,11 +35,6 @@ import org.springframework.jdbc.core.JdbcTemplate;
public class TestOverrideConfiguration {
// NOTE: unfortunately an abastract base class that contains all these mock functions won't work
@Bean
public FromJsonHelper fromJsonHelper() {
return mock(FromJsonHelper.class);
}
@Bean
public RoutingDataSourceServiceFactory routingDataSourceServiceFactory() {
return mock(RoutingDataSourceServiceFactory.class);
@@ -81,9 +74,4 @@ public class TestOverrideConfiguration {
public LoanTransactionRepository loanTransactionRepository() {
return mock(LoanTransactionRepository.class);
}
@Bean
public NoteCommandFromApiJsonDeserializer fromApiJsonDeserializer(FromJsonHelper fromJsonHelper) {
return new NoteCommandFromApiJsonDeserializer(fromJsonHelper);
}
}
+1
View File
@@ -29,6 +29,7 @@ services:
service: fineract
ports:
- "8443:8443"
- "5000:5000"
depends_on:
db:
condition: service_healthy
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.infrastructure.cache.data;
import java.io.Serial;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -28,7 +30,10 @@ import org.apache.fineract.infrastructure.core.data.EnumOptionData;
@Data
@NoArgsConstructor
@AllArgsConstructor
public final class CacheData {
public final class CacheData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@SuppressWarnings("unused")
private EnumOptionData cacheType;
@@ -0,0 +1,48 @@
/**
* 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.note.data;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.io.Serial;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.fineract.portfolio.note.domain.NoteType;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NoteCreateRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Hidden
private Long resourceId;
@Hidden
private NoteType type;
@Size(max = 1000, message = "{org.apache.fineract.portfolio.note.note.size}")
@NotNull(message = "{org.apache.fineract.portfolio.note.note.not-null}")
private String note;
}
@@ -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.portfolio.note.data;
import java.io.Serial;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NoteCreateResponse implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long entityId;
private Long officeId;
private Long resourceId;
}
@@ -18,31 +18,37 @@
*/
package org.apache.fineract.portfolio.note.data;
import java.io.Serial;
import java.io.Serializable;
import java.time.OffsetDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
/**
* Immutable data object represent note or case information about a client, loan or loan transaction.
*/
@Getter
@Builder
public class NoteData {
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NoteData implements Serializable {
private final Long id;
private final Long clientId;
private final Long groupId;
private final Long loanId;
private final Long loanTransactionId;
private final Long depositAccountId;
private final Long savingAccountId;
private final EnumOptionData noteType;
private final String note;
private final Long createdById;
private final String createdByUsername;
private final OffsetDateTime createdOn;
private final Long updatedById;
private final String updatedByUsername;
private final OffsetDateTime updatedOn;
@Serial
private static final long serialVersionUID = 1L;
private Long id;
private Long clientId;
private Long groupId;
private Long loanId;
private Long loanTransactionId;
private Long depositAccountId;
private Long savingAccountId;
private EnumOptionData noteType;
private String note;
private Long createdById;
private String createdByUsername;
private OffsetDateTime createdOn;
private Long updatedById;
private String updatedByUsername;
private OffsetDateTime updatedOn;
}
@@ -0,0 +1,45 @@
/**
* 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.note.data;
import io.swagger.v3.oas.annotations.Hidden;
import java.io.Serial;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.fineract.portfolio.note.domain.NoteType;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NoteDeleteRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Hidden
private Long id;
@Hidden
private Long resourceId;
@Hidden
private NoteType type;
}
@@ -21,16 +21,18 @@ package org.apache.fineract.portfolio.note.data;
import java.io.Serial;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NoteRequest implements Serializable {
public class NoteDeleteResponse implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String note;
private Long resourceId;
}
@@ -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.portfolio.note.data;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.io.Serial;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.fineract.portfolio.note.domain.NoteType;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NoteUpdateRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Hidden
private Long id;
@Hidden
private Long resourceId;
@Hidden
private NoteType type;
@Size(max = 1000, message = "{org.apache.fineract.portfolio.note.note.size}")
@NotNull(message = "{org.apache.fineract.portfolio.note.note.not-null}")
private String note;
}
@@ -0,0 +1,41 @@
/**
* 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.note.data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NoteUpdateResponse implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long officeId;
private Long resourceId;
private Map<String, Object> changes;
}
@@ -18,19 +18,18 @@
*/
package org.apache.fineract.portfolio.note.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
import org.apache.fineract.portfolio.note.data.NoteDeleteRequest;
import org.apache.fineract.portfolio.note.data.NoteDeleteResponse;
import org.apache.fineract.portfolio.note.data.NoteUpdateRequest;
import org.apache.fineract.portfolio.note.data.NoteUpdateResponse;
public interface NoteWritePlatformService {
CommandProcessingResult createNote(JsonCommand command);
NoteCreateResponse createNote(NoteCreateRequest request);
void createLoanTransactionNote(Long loanTransactionId, String note);
NoteUpdateResponse updateNote(NoteUpdateRequest request);
CommandProcessingResult updateNote(JsonCommand command);
CommandProcessingResult deleteNote(JsonCommand command);
void createAndPersistClientNote(Client client, JsonCommand command);
NoteDeleteResponse deleteNote(NoteDeleteRequest request);
}
@@ -49,9 +49,11 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.repository.LoanBuyDownFeeBalanceRepository;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.portfolio.note.domain.NoteType;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@@ -63,11 +65,11 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
private final LoanTransactionRepository loanTransactionRepository;
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
private final LoanJournalEntryPoster loanJournalEntryPoster;
private final NoteWritePlatformService noteWritePlatformService;
private final ExternalIdFactory externalIdFactory;
private final LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository;
private final BusinessEventNotifierService businessEventNotifierService;
private final CodeValueRepository codeValueRepository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
@Override
@@ -108,7 +110,8 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
// Add note if provided
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
noteWritePlatformService.createLoanTransactionNote(buyDownFeeTransaction.getId(), noteText);
eventPublisher.publishEvent(NoteCreateRequest.builder().type(NoteType.LOAN_TRANSACTION)
.resourceId(buyDownFeeTransaction.getId()).note(noteText).build());
}
loanJournalEntryPoster.postJournalEntriesForLoanTransaction(buyDownFeeTransaction, false, false);
@@ -177,7 +180,8 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
// Create a note if provided
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
noteWritePlatformService.createLoanTransactionNote(savedBuyDownFeeAdjustment.getId(), noteText);
eventPublisher.publishEvent(NoteCreateRequest.builder().type(NoteType.LOAN_TRANSACTION)
.resourceId(savedBuyDownFeeAdjustment.getId()).note(noteText).build());
}
loanJournalEntryPoster.postJournalEntriesForLoanTransaction(savedBuyDownFeeAdjustment, false, false);
@@ -48,9 +48,11 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.portfolio.note.domain.NoteType;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@@ -62,7 +64,6 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
private final LoanTransactionRepository loanTransactionRepository;
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
private final LoanJournalEntryPoster journalEntryPoster;
private final NoteWritePlatformService noteWritePlatformService;
private final ExternalIdFactory externalIdFactory;
private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
@@ -71,6 +72,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
private final BusinessEventNotifierService businessEventNotifierService;
private final CodeValueRepository codeValueRepository;
private final LoanScheduleService loanScheduleService;
private final ApplicationEventPublisher eventPublisher;
@Transactional
@Override
@@ -107,7 +109,8 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
// Create a note if provided
final String noteText = command.stringValueOfParameterNamed("note");
if (noteText != null && !noteText.isEmpty()) {
noteWritePlatformService.createLoanTransactionNote(capitalizedIncomeTransaction.getId(), noteText);
eventPublisher.publishEvent(NoteCreateRequest.builder().type(NoteType.LOAN_TRANSACTION)
.resourceId(capitalizedIncomeTransaction.getId()).note(noteText).build());
}
// Create journal entries immediately for this transaction
@@ -156,7 +159,8 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
// Create a note if provided
final String noteText = command.stringValueOfParameterNamed("note");
if (noteText != null && !noteText.isEmpty()) {
noteWritePlatformService.createLoanTransactionNote(savedCapitalizedIncomeAdjustment.getId(), noteText);
eventPublisher.publishEvent(NoteCreateRequest.builder().type(NoteType.LOAN_TRANSACTION)
.resourceId(savedCapitalizedIncomeAdjustment.getId()).note(noteText).build());
}
// Create journal entries immediately for this transaction
journalEntryPoster.postJournalEntriesForLoanTransaction(savedCapitalizedIncomeAdjustment, false, false);
@@ -42,9 +42,9 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidatorImpl;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -56,15 +56,14 @@ public class ProgressiveLoanAccountConfiguration {
public CapitalizedIncomePlatformService capitalizedIncomePlatformService(ProgressiveLoanTransactionValidator loanTransactionValidator,
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster journalEntryPoster,
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory,
LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository,
ExternalIdFactory externalIdFactory, LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository,
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanBalanceService loanBalanceService,
LoanLifecycleStateMachine loanLifecycleStateMachine, BusinessEventNotifierService businessEventNotifierService,
CodeValueRepository codeValueRepository, LoanScheduleService loanScheduleService) {
CodeValueRepository codeValueRepository, LoanScheduleService loanScheduleService, ApplicationEventPublisher eventPublisher) {
return new CapitalizedIncomeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
paymentDetailWritePlatformService, journalEntryPoster, noteWritePlatformService, externalIdFactory,
capitalizedIncomeBalanceRepository, reprocessLoanTransactionsService, loanBalanceService, loanLifecycleStateMachine,
businessEventNotifierService, codeValueRepository, loanScheduleService);
paymentDetailWritePlatformService, journalEntryPoster, externalIdFactory, capitalizedIncomeBalanceRepository,
reprocessLoanTransactionsService, loanBalanceService, loanLifecycleStateMachine, businessEventNotifierService,
codeValueRepository, loanScheduleService, eventPublisher);
}
@Bean
@@ -157,6 +157,50 @@ public class SecurityConfig {
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_CURRENCY")
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/currencies"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_CURRENCY")
// notes: read
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/clients/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_CLIENTNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/loans/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_LOANNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/loanTransactions/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_LOANTRANSACTIONNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/savings/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_SAVINGNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/groups/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_GROUPNOTE")
// notes: create
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/clients/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_CLIENTNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/loans/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_LOANNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/loanTransactions/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_LOANTRANSACTIONNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/savings/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_SAVINGNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/groups/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_GROUPNOTE")
// notes: update
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/clients/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_CLIENTNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/loans/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_LOANNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/loanTransactions/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_LOANTRANSACTIONNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/savings/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_SAVINGNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/groups/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_GROUPNOTE")
// notes: delete
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/clients/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_CLIENTNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/loans/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_LOANNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/loanTransactions/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_LOANTRANSACTIONNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/savings/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_SAVINGNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/groups/*/notes"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_GROUPNOTE")
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated()
.requestMatchers(API_MATCHER.matcher("/api/*/twofactor")).fullyAuthenticated()
@@ -175,7 +175,6 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService;
import org.apache.fineract.portfolio.rate.service.RateAssembler;
@@ -184,6 +183,7 @@ import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.service.Repaym
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.service.GSIMReadPlatformService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@@ -397,12 +397,12 @@ public class LoanAccountConfiguration {
public BuyDownFeePlatformService buyDownFeePlatformService(ProgressiveLoanTransactionValidator loanTransactionValidator,
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster loanJournalEntryPoster,
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory,
LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository, BusinessEventNotifierService businessEventNotifierService,
CodeValueRepository codeValueRepository) {
ExternalIdFactory externalIdFactory, LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository,
BusinessEventNotifierService businessEventNotifierService, CodeValueRepository codeValueRepository,
ApplicationEventPublisher eventPublisher) {
return new BuyDownFeeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
paymentDetailWritePlatformService, loanJournalEntryPoster, noteWritePlatformService, externalIdFactory,
loanBuyDownFeeBalanceRepository, businessEventNotifierService, codeValueRepository);
paymentDetailWritePlatformService, loanJournalEntryPoster, externalIdFactory, loanBuyDownFeeBalanceRepository,
businessEventNotifierService, codeValueRepository, eventPublisher);
}
@Bean
@@ -19,13 +19,8 @@
package org.apache.fineract.portfolio.note.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
@@ -35,201 +30,160 @@ import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
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.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.command.core.CommandPipeline;
import org.apache.fineract.portfolio.note.command.NoteCreateCommand;
import org.apache.fineract.portfolio.note.command.NoteDeleteCommand;
import org.apache.fineract.portfolio.note.command.NoteUpdateCommand;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
import org.apache.fineract.portfolio.note.data.NoteData;
import org.apache.fineract.portfolio.note.data.NoteRequest;
import org.apache.fineract.portfolio.note.data.NoteDeleteRequest;
import org.apache.fineract.portfolio.note.data.NoteDeleteResponse;
import org.apache.fineract.portfolio.note.data.NoteUpdateRequest;
import org.apache.fineract.portfolio.note.data.NoteUpdateResponse;
import org.apache.fineract.portfolio.note.domain.NoteType;
import org.apache.fineract.portfolio.note.exception.NoteResourceNotSupportedException;
import org.apache.fineract.portfolio.note.service.NoteReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/{resourceType}/{resourceId}/notes")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Component
@Tag(name = "Notes", description = "Notes API allows to enter notes for supported resources.")
@Tag(name = "Notes", description = """
Notes API allows to enter notes for supported resources.
""")
@RequiredArgsConstructor
public class NotesApiResource {
public static final String CLIENTNOTE = "CLIENTNOTE";
public static final String LOANNOTE = "LOANNOTE";
public static final String LOANTRANSACTIONNOTE = "LOANTRANSACTIONNOTE";
public static final String SAVINGNOTE = "SAVINGNOTE";
public static final String GROUPNOTE = "GROUPNOTE";
public static final String INVALIDNOTE = "INVALIDNOTE";
private static final Set<String> NOTE_DATA_PARAMETERS = new HashSet<>(
Arrays.asList("id", "resourceId", "clientId", "groupId", "loanId", "loanTransactionId", "depositAccountId", "savingAccountId",
"noteType", "note", "createdById", "createdByUsername", "createdOn", "updatedById", "updatedByUsername", "updatedOn"));
private final PlatformSecurityContext context;
private final NoteReadPlatformService readPlatformService;
private final DefaultToApiJsonSerializer<NoteData> toApiJsonSerializer;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final CommandPipeline commandPipeline;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a Resource's description", description = "Retrieves a Resource's Notes\n\n"
+ "Note: Notes are returned in descending createOn order.\n" + "\n" + "Example Requests:\n" + "\n" + "clients/2/notes\n" + "\n"
+ "\n" + "groups/2/notes?fields=note,createdOn,createdByUsername")
public List<NoteData> retrieveNotesByResource(
@PathParam("resourceType") @Parameter(description = "resourceType") final String resourceType,
@PathParam("resourceId") @Parameter(description = "resourceId") final Long resourceId) {
@Operation(summary = "Retrieve a Resource's description", description = """
Retrieves a resource's notes
final NoteType noteType = NoteType.fromApiUrl(resourceType);
Note: results are returned in descending createOn order.
Example Requests:
- clients/2/notes
- groups/2/notes?fields=note,createdOn,createdByUsername
""")
public List<NoteData> retrieveNotesByResource(@PathParam("resourceType") final String resourceType,
@PathParam("resourceId") final Long resourceId) {
final var noteType = NoteType.fromApiUrl(resourceType);
if (noteType == null) {
throw new NoteResourceNotSupportedException(resourceType);
}
this.context.authenticatedUser().validateHasReadPermission(getResourceDetails(noteType, resourceId).entityName());
final Integer noteTypeId = noteType.getValue();
return readPlatformService.retrieveNotesByResource(resourceId, noteTypeId);
return readPlatformService.retrieveNotesByResource(resourceId, noteType.getValue());
}
@GET
@Path("{noteId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a Resource Note", description = "Retrieves a Resource Note\n\n" + "Example Requests:\n" + "\n"
+ "clients/1/notes/76\n" + "\n" + "\n" + "groups/1/notes/20\n" + "\n" + "\n"
+ "clients/1/notes/76?fields=note,createdOn,createdByUsername\n" + "\n" + "\n"
+ "groups/1/notes/20?fields=note,createdOn,createdByUsername")
public NoteData retrieveNote(@PathParam("resourceType") @Parameter(description = "resourceType") final String resourceType,
@PathParam("resourceId") @Parameter(description = "resourceId") final Long resourceId,
@PathParam("noteId") @Parameter(description = "noteId") final Long noteId) {
@Operation(summary = "Retrieve a Resource Note", description = """
Retrieves a resource Note
Example Requests:
- clients/1/notes/76
- groups/1/notes/20
- clients/1/notes/76?fields=note,createdOn,createdByUsername
- groups/1/notes/20?fields=note,createdOn,createdByUsername
""")
public NoteData retrieveNote(@PathParam("resourceType") final String resourceType, @PathParam("resourceId") final Long resourceId,
@PathParam("noteId") final Long noteId) {
final NoteType noteType = NoteType.fromApiUrl(resourceType);
if (noteType == null) {
throw new NoteResourceNotSupportedException(resourceType);
}
this.context.authenticatedUser().validateHasReadPermission(getResourceDetails(noteType, resourceId).entityName());
final Integer noteTypeId = noteType.getValue();
final NoteData note = this.readPlatformService.retrieveNote(noteId, resourceId, noteTypeId);
return note;
return readPlatformService.retrieveNote(noteId, resourceId, noteType.getValue());
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Add a Resource Note", description = "Adds a new note to a supported resource.\n\n" + "Example Requests:\n" + "\n"
+ "clients/1/notes\n" + "\n" + "\n" + "groups/1/notes")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = NoteRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = NotesApiResourceSwagger.PostResourceTypeResourceIdNotesResponse.class))) })
public CommandProcessingResult addNewNote(@PathParam("resourceType") @Parameter(description = "resourceType") final String resourceType,
@PathParam("resourceId") @Parameter(description = "resourceId") final Long resourceId,
@Parameter(hidden = true) final NoteRequest noteRequest) {
@Operation(summary = "Add a Resource Note", description = """
Adds a new note to a supported resource.
final NoteType noteType = NoteType.fromApiUrl(resourceType);
Example Requests:
if (noteType == null) {
- clients/1/notes
- groups/1/notes
""")
public NoteCreateResponse addNewNote(@PathParam("resourceType") final String resourceType,
@PathParam("resourceId") final Long resourceId, @Valid final NoteCreateRequest request) {
final var type = NoteType.fromApiUrl(resourceType);
if (type == null) {
throw new NoteResourceNotSupportedException(resourceType);
}
final CommandWrapper resourceDetails = getResourceDetails(noteType, resourceId);
final CommandWrapper commandRequest = new CommandWrapperBuilder().createNote(resourceDetails, resourceType, resourceId)
.withJson(toApiJsonSerializer.serialize(noteRequest)).build();
request.setResourceId(resourceId);
request.setType(type);
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
final var command = new NoteCreateCommand();
command.setPayload(request);
final Supplier<NoteCreateResponse> response = commandPipeline.send(command);
return response.get();
}
@PUT
@Path("{noteId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update a Resource Note", description = "Updates a Resource Note")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = NoteRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = NotesApiResourceSwagger.PutResourceTypeResourceIdNotesNoteIdResponse.class))) })
public CommandProcessingResult updateNote(@PathParam("resourceType") @Parameter(description = "resourceType") final String resourceType,
@PathParam("resourceId") @Parameter(description = "resourceId") final Long resourceId,
@PathParam("noteId") @Parameter(description = "noteId") final Long noteId,
@Parameter(hidden = true) final NoteRequest noteRequest) {
@Operation(summary = "Update a Resource Note", description = """
Updates a Resource Note
""")
public NoteUpdateResponse updateNote(@PathParam("resourceType") final String resourceType,
@PathParam("resourceId") final Long resourceId, @PathParam("noteId") final Long noteId,
@Valid final NoteUpdateRequest request) {
final var type = NoteType.fromApiUrl(resourceType);
final NoteType noteType = NoteType.fromApiUrl(resourceType);
if (noteType == null) {
if (type == null) {
throw new NoteResourceNotSupportedException(resourceType);
}
final CommandWrapper resourceDetails = getResourceDetails(noteType, resourceId);
request.setId(noteId);
request.setResourceId(resourceId);
request.setType(type);
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateNote(resourceDetails, resourceType, resourceId, noteId)
.withJson(toApiJsonSerializer.serialize(noteRequest)).build();
final var command = new NoteUpdateCommand();
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
command.setPayload(request);
final Supplier<NoteUpdateResponse> response = commandPipeline.send(command);
return response.get();
}
@DELETE
@Path("{noteId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a Resource Note", description = "Deletes a Resource Note")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = NotesApiResourceSwagger.DeleteResourceTypeResourceIdNotesNoteIdResponse.class))) })
public CommandProcessingResult deleteNote(@PathParam("resourceType") @Parameter(description = "resourceType") final String resourceType,
@PathParam("resourceId") @Parameter(description = "resourceId") final Long resourceId,
@PathParam("noteId") @Parameter(description = "noteId") final Long noteId) {
@Operation(summary = "Delete a Resource Note", description = """
Deletes a Resource Note
""")
public NoteDeleteResponse deleteNote(@PathParam("resourceType") final String resourceType,
@PathParam("resourceId") final Long resourceId, @PathParam("noteId") final Long noteId) {
final var type = NoteType.fromApiUrl(resourceType);
final NoteType noteType = NoteType.fromApiUrl(resourceType);
if (noteType == null) {
if (type == null) {
throw new NoteResourceNotSupportedException(resourceType);
}
final CommandWrapper resourceDetails = getResourceDetails(noteType, resourceId);
var request = NoteDeleteRequest.builder().id(noteId).resourceId(resourceId).type(type).build();
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteNote(resourceDetails, resourceType, resourceId, noteId)
.build();
final var command = new NoteDeleteCommand();
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
command.setPayload(request);
private CommandWrapper getResourceDetails(final NoteType type, final Long resourceId) {
CommandWrapperBuilder resourceDetails = new CommandWrapperBuilder();
String resourceNameForPermissions;
switch (type) {
case CLIENT -> {
resourceNameForPermissions = CLIENTNOTE;
resourceDetails.withClientId(resourceId);
}
case LOAN -> {
resourceNameForPermissions = LOANNOTE;
resourceDetails.withLoanId(resourceId);
}
case LOAN_TRANSACTION -> {
resourceNameForPermissions = LOANTRANSACTIONNOTE;
// updating loanId, to distinguish saving transaction note and
// loan transaction note as we are using subEntityId for both.
resourceDetails.withLoanId(resourceId);
resourceDetails.withSubEntityId(resourceId);
}
case SAVING_ACCOUNT -> {
resourceNameForPermissions = SAVINGNOTE;
resourceDetails.withSavingsId(resourceId);
}
case GROUP -> {
resourceNameForPermissions = GROUPNOTE;
resourceDetails.withGroupId(resourceId);
}
default -> resourceNameForPermissions = INVALIDNOTE;
}
return resourceDetails.withEntityName(resourceNameForPermissions).build();
final Supplier<NoteDeleteResponse> response = commandPipeline.send(command);
return response.get();
}
}
@@ -1,82 +0,0 @@
/**
* 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.note.api;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* Created by Chirag Gupta on 12/29/17.
*/
public final class NotesApiResourceSwagger {
private NotesApiResourceSwagger() {}
@Schema(description = "PostResourceTypeResourceIdNotesRequest")
public static final class PostResourceTypeResourceIdNotesRequest {
private PostResourceTypeResourceIdNotesRequest() {}
@Schema(example = "a note about the client")
public String note;
}
@Schema(description = "PostResourceTypeResourceIdNotesResponse")
public static final class PostResourceTypeResourceIdNotesResponse {
private PostResourceTypeResourceIdNotesResponse() {}
@Schema(example = "1")
public Long officeId;
@Schema(example = "1")
public Long clientId;
@Schema(example = "76")
public Long resourceId;
}
@Schema(description = "PutResourceTypeResourceIdNotesNoteIdResponse")
public static final class PutResourceTypeResourceIdNotesNoteIdResponse {
private PutResourceTypeResourceIdNotesNoteIdResponse() {}
static final class PutNotesChanges {
private PutNotesChanges() {}
@Schema(example = "a note about the client")
public String note;
}
@Schema(example = "1")
public Long officeId;
@Schema(example = "1")
public Long clientId;
@Schema(example = "76")
public Long resourceId;
public PutNotesChanges changes;
}
@Schema(description = "DeleteResourceTypeResourceIdNotesNoteIdResponse")
public static final class DeleteResourceTypeResourceIdNotesNoteIdResponse {
private DeleteResourceTypeResourceIdNotesNoteIdResponse() {}
@Schema(example = "76")
public Long resourceId;
}
}
@@ -18,16 +18,11 @@
*/
package org.apache.fineract.portfolio.note.command;
/**
* Immutable command used for create or update of notes.
*/
public class NoteCommand {
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
@SuppressWarnings("unused")
private final String note;
public NoteCommand(final String note) {
this.note = note;
}
}
@Data
@EqualsAndHashCode(callSuper = true)
public class NoteCreateCommand extends Command<NoteCreateRequest> {}
@@ -0,0 +1,28 @@
/**
* 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.note.command;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.portfolio.note.data.NoteDeleteRequest;
@Data
@EqualsAndHashCode(callSuper = true)
public class NoteDeleteCommand extends Command<NoteDeleteRequest> {}
@@ -0,0 +1,28 @@
/**
* 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.note.command;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.portfolio.note.data.NoteUpdateRequest;
@Data
@EqualsAndHashCode(callSuper = true)
public class NoteUpdateCommand extends Command<NoteUpdateRequest> {}
@@ -23,10 +23,9 @@ import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.commons.lang3.Strings;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.group.domain.Group;
@@ -74,13 +73,11 @@ public class Note extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@JoinColumn(name = "share_account_id", nullable = true)
private ShareAccount shareAccount;
public static Note clientNoteFromJson(final Client client, final JsonCommand command) {
final String note = command.stringValueOfParameterNamed("note");
public static Note clientNote(final Client client, final String note) {
return new Note(client, note);
}
public static Note groupNoteFromJson(final Group group, final JsonCommand command) {
final String note = command.stringValueOfParameterNamed("note");
public static Note groupNote(final Group group, final String note) {
return new Note(group, note);
}
@@ -164,20 +161,12 @@ public class Note extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.noteTypeId = NoteType.SHARE_ACCOUNT.getValue();
}
public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(7);
final String noteParamName = "note";
if (command.isChangeInStringParameterNamed(noteParamName, this.note)) {
final String newValue = command.stringValueOfParameterNamed(noteParamName);
actualChanges.put(noteParamName, newValue);
this.note = StringUtils.defaultIfEmpty(newValue, null);
public Map<String, Object> update(final String note) {
if (!Strings.CI.equals(note, this.note)) {
this.note = StringUtils.defaultIfEmpty(note, null);
return Map.of("note", note);
}
return actualChanges;
}
public boolean isNotAgainstClientWithIdOf(final Long clientId) {
return !this.client.identifiedBy(clientId);
return Map.of();
}
public String getNote() {
@@ -18,27 +18,34 @@
*/
package org.apache.fineract.portfolio.note.handler;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.command.core.CommandHandler;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DeleteNoteCommandHandler implements NewCommandSourceHandler {
@Slf4j
@Component
@RequiredArgsConstructor
public class NoteCreateCommandHandler implements CommandHandler<NoteCreateRequest, NoteCreateResponse> {
private final NoteWritePlatformService writePlatformService;
@Autowired
public DeleteNoteCommandHandler(final NoteWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
@Retry(name = "commandNoteCreate", fallbackMethod = "fallback")
@Override
@Transactional
public NoteCreateResponse handle(Command<NoteCreateRequest> command) {
return writePlatformService.createNote(command.getPayload());
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.deleteNote(command);
public NoteCreateResponse fallback(Command<NoteCreateRequest> command, Throwable t) {
// NOTE: fallback method needs to be in the same class
return CommandHandler.super.fallback(command, t);
}
}
@@ -18,29 +18,34 @@
*/
package org.apache.fineract.portfolio.note.handler;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.command.core.CommandHandler;
import org.apache.fineract.portfolio.note.data.NoteDeleteRequest;
import org.apache.fineract.portfolio.note.data.NoteDeleteResponse;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UpdateNoteCommandHandler implements NewCommandSourceHandler {
@Slf4j
@Component
@RequiredArgsConstructor
public class NoteDeleteCommandHandler implements CommandHandler<NoteDeleteRequest, NoteDeleteResponse> {
private final NoteWritePlatformService writePlatformService;
@Autowired
public UpdateNoteCommandHandler(final NoteWritePlatformService noteWritePlatformService) {
this.writePlatformService = noteWritePlatformService;
}
@Transactional
@Retry(name = "commandNoteDelete", fallbackMethod = "fallback")
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.updateNote(command);
@Transactional
public NoteDeleteResponse handle(Command<NoteDeleteRequest> command) {
return writePlatformService.deleteNote(command.getPayload());
}
@Override
public NoteDeleteResponse fallback(Command<NoteDeleteRequest> command, Throwable t) {
// NOTE: fallback method needs to be in the same class
return CommandHandler.super.fallback(command, t);
}
}
@@ -18,28 +18,34 @@
*/
package org.apache.fineract.portfolio.note.handler;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.command.core.CommandHandler;
import org.apache.fineract.portfolio.note.data.NoteUpdateRequest;
import org.apache.fineract.portfolio.note.data.NoteUpdateResponse;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CreateNoteCommandHandler implements NewCommandSourceHandler {
@Slf4j
@Component
@RequiredArgsConstructor
public class NoteUpdateCommandHandler implements CommandHandler<NoteUpdateRequest, NoteUpdateResponse> {
private final NoteWritePlatformService writePlatformService;
@Autowired
public CreateNoteCommandHandler(final NoteWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Retry(name = "commandNoteUpdate", fallbackMethod = "fallback")
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createNote(command);
@Transactional
public NoteUpdateResponse handle(Command<NoteUpdateRequest> command) {
return writePlatformService.updateNote(command.getPayload());
}
@Override
public NoteUpdateResponse fallback(Command<NoteUpdateRequest> command, Throwable t) {
// NOTE: fallback method needs to be in the same class
return CommandHandler.super.fallback(command, t);
}
}
@@ -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.portfolio.note.listener;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
class NoteListener {
private final NoteWritePlatformService noteWritePlatformService;
@EventListener
void onCreate(NoteCreateRequest request) {
noteWritePlatformService.createNote(request);
}
@EventListener
void onUpdate(NoteCreateRequest request) {
noteWritePlatformService.createNote(request);
}
@EventListener
void onDelete(NoteCreateRequest request) {
noteWritePlatformService.createNote(request);
}
}
@@ -1,102 +0,0 @@
/**
* 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.note.serialization;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.AbstractFromApiJsonDeserializer;
import org.apache.fineract.infrastructure.core.serialization.FromApiJsonDeserializer;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.note.command.NoteCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Implementation of {@link FromApiJsonDeserializer} for {@link NoteCommand} 's.
*/
@Component
public final class NoteCommandFromApiJsonDeserializer extends AbstractFromApiJsonDeserializer<NoteCommand> {
public static final String NOTE = "note";
/**
* The parameters supported for this command.
*/
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(List.of(NOTE));
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public NoteCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
@Override
public NoteCommand commandFromApiJson(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
final JsonElement element = this.fromApiJsonHelper.parse(json);
final String note = this.fromApiJsonHelper.extractStringNamed(NOTE, element);
return new NoteCommand(note);
}
public void validateNote(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
final JsonElement element = this.fromApiJsonHelper.parse(json);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource(NOTE);
final String note = this.fromApiJsonHelper.extractStringNamed(NOTE, element);
baseDataValidator.reset().parameter(NOTE).value(note).notBlank().notExceedingLengthOf(1000);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
}
@@ -18,30 +18,26 @@
*/
package org.apache.fineract.portfolio.note.service;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.commons.lang3.Strings;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.exception.ClientNotFoundException;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.domain.GroupRepository;
import org.apache.fineract.portfolio.group.exception.GroupNotFoundException;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
import org.apache.fineract.portfolio.note.data.NoteDeleteRequest;
import org.apache.fineract.portfolio.note.data.NoteDeleteResponse;
import org.apache.fineract.portfolio.note.data.NoteUpdateRequest;
import org.apache.fineract.portfolio.note.data.NoteUpdateResponse;
import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.note.domain.NoteType;
import org.apache.fineract.portfolio.note.exception.NoteNotFoundException;
import org.apache.fineract.portfolio.note.exception.NoteResourceNotSupportedException;
import org.apache.fineract.portfolio.note.serialization.NoteCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException;
@@ -53,419 +49,135 @@ public class NoteWritePlatformServiceJpaRepositoryImpl implements NoteWritePlatf
private final GroupRepository groupRepository;
private final LoanRepositoryWrapper loanRepository;
private final LoanTransactionRepository loanTransactionRepository;
private final NoteCommandFromApiJsonDeserializer fromApiJsonDeserializer;
private final SavingsAccountRepository savingsAccountRepository;
public NoteWritePlatformServiceJpaRepositoryImpl(final NoteRepository noteRepository, final ClientRepositoryWrapper clientRepository,
final GroupRepository groupRepository, final LoanRepositoryWrapper loanRepository,
final LoanTransactionRepository loanTransactionRepository, final NoteCommandFromApiJsonDeserializer fromApiJsonDeserializer,
final SavingsAccountRepository savingsAccountRepository) {
final LoanTransactionRepository loanTransactionRepository, final SavingsAccountRepository savingsAccountRepository) {
this.noteRepository = noteRepository;
this.clientRepository = clientRepository;
this.groupRepository = groupRepository;
this.loanRepository = loanRepository;
this.loanTransactionRepository = loanTransactionRepository;
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
this.savingsAccountRepository = savingsAccountRepository;
}
private CommandProcessingResult createClientNote(final JsonCommand command) {
final Long resourceId = command.getClientId();
final Client client = this.clientRepository.findOneWithNotFoundDetection(resourceId);
if (client == null) {
throw new ClientNotFoundException(resourceId);
}
final Note newNote = Note.clientNoteFromJson(client, command);
this.noteRepository.saveAndFlush(newNote);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(newNote.getId()) //
.withClientId(client.getId()) //
.withOfficeId(client.officeId()) //
.build();
}
@Override
public void createAndPersistClientNote(final Client client, final JsonCommand command) {
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note newNote = new Note(client, noteText);
this.noteRepository.save(newNote);
}
}
public NoteCreateResponse createNote(final NoteCreateRequest request) {
Note note;
Long officeId;
private CommandProcessingResult createGroupNote(final JsonCommand command) {
final Long resourceId = command.getGroupId();
final Group group = this.groupRepository.findById(resourceId).orElseThrow(() -> new GroupNotFoundException(resourceId));
final Note newNote = Note.groupNoteFromJson(group, command);
this.noteRepository.saveAndFlush(newNote);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(newNote.getId()) //
.withGroupId(group.getId()) //
.withOfficeId(group.officeId()) //
.build();
}
private CommandProcessingResult createLoanNote(final JsonCommand command) {
final Long resourceId = command.getLoanId();
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(resourceId);
final String note = command.stringValueOfParameterNamed("note");
final Note newNote = Note.loanNote(loan, note);
this.noteRepository.saveAndFlush(newNote);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(newNote.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withLoanId(loan.getId()) //
.build();
}
private CommandProcessingResult createLoanTransactionNote(final JsonCommand command) {
final Long resourceId = command.subentityId();
final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(resourceId)
.orElseThrow(() -> new LoanTransactionNotFoundException(resourceId));
final Loan loan = loanTransaction.getLoan();
final String note = command.stringValueOfParameterNamed("note");
final Note newNote = Note.loanTransactionNote(loan, loanTransaction, note);
this.noteRepository.saveAndFlush(newNote);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(newNote.getId()) //
.withOfficeId(loan.getOfficeId())//
.withLoanId(loan.getId())// Loan can be associated
.build();
}
private CommandProcessingResult createSavingAccountNote(final JsonCommand command) {
final Long resourceId = command.getSavingsId();
final SavingsAccount savingAccount = this.savingsAccountRepository.findById(resourceId)
.orElseThrow(() -> new SavingsAccountNotFoundException(resourceId));
final String note = command.stringValueOfParameterNamed("note");
final Note newNote = Note.savingNote(savingAccount, note);
this.noteRepository.saveAndFlush(newNote);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(newNote.getId()) //
.withOfficeId(savingAccount.getClient().getOffice().getId()) //
.withSavingsId(savingAccount.getId()) //
.build();
}
@Override
public CommandProcessingResult createNote(final JsonCommand command) {
this.fromApiJsonDeserializer.validateNote(command.json());
final String resourceUrl = getResourceUrlFromCommand(command); // command.getSupportedEntityType();
final NoteType type = NoteType.fromApiUrl(resourceUrl);
switch (type) {
switch (request.getType()) {
case CLIENT: {
return createClientNote(command);
final var client = this.clientRepository.findOneWithNotFoundDetection(request.getResourceId());
note = noteRepository.saveAndFlush(Note.clientNote(client, request.getNote()));
officeId = client.officeId();
}
break;
case GROUP: {
return createGroupNote(command);
final var group = groupRepository.findById(request.getResourceId())
.orElseThrow(() -> new GroupNotFoundException(request.getResourceId()));
note = noteRepository.saveAndFlush(Note.groupNote(group, request.getNote()));
officeId = group.officeId();
}
break;
case LOAN: {
return createLoanNote(command);
final var loan = loanRepository.findOneWithNotFoundDetection(request.getResourceId());
note = noteRepository.saveAndFlush(Note.loanNote(loan, request.getNote()));
officeId = loan.getOfficeId();
}
break;
case LOAN_TRANSACTION: {
return createLoanTransactionNote(command);
final var loanTransaction = this.loanTransactionRepository.findById(request.getResourceId())
.orElseThrow(() -> new LoanTransactionNotFoundException(request.getResourceId()));
note = noteRepository.saveAndFlush(Note.loanTransactionNote(loanTransaction.getLoan(), loanTransaction, request.getNote()));
officeId = loanTransaction.getLoan().getOfficeId();
}
break;
case SAVING_ACCOUNT: {
return createSavingAccountNote(command);
final var savingAccount = savingsAccountRepository.findById(request.getResourceId())
.orElseThrow(() -> new SavingsAccountNotFoundException(request.getResourceId()));
note = noteRepository.saveAndFlush(Note.savingNote(savingAccount, request.getNote()));
officeId = savingAccount.getClient().getOffice().getId();
}
break;
default:
throw new NoteResourceNotSupportedException(resourceUrl);
throw new NoteResourceNotSupportedException(request.getType().getApiUrl());
}
return NoteCreateResponse.builder().entityId(note.getId()).resourceId(note.getId()).officeId(officeId).build();
}
@Override
public void createLoanTransactionNote(final Long loanTransactionId, final String note) {
final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(loanTransactionId)
.orElseThrow(() -> new LoanTransactionNotFoundException(loanTransactionId));
public NoteUpdateResponse updateNote(final NoteUpdateRequest request) {
final var result = getNote(request.getType(), request.getResourceId(), request.getId());
final var note = result.getLeft();
final var response = NoteUpdateResponse.builder().officeId(result.getRight()).resourceId(request.getResourceId());
final Loan loan = loanTransaction.getLoan();
final Note newNote = Note.loanTransactionNote(loan, loanTransaction, note);
this.noteRepository.save(newNote);
if (!Strings.CI.equals(note.getNote(), request.getNote())) {
response.changes(note.update(request.getNote()));
noteRepository.saveAndFlush(note);
}
private String getResourceUrlFromCommand(JsonCommand command) {
final String resourceUrl;
if (command.getClientId() != null) {
resourceUrl = NoteType.CLIENT.getApiUrl();
} else if (command.getGroupId() != null) {
resourceUrl = NoteType.GROUP.getApiUrl();
} else if (command.getLoanId() != null) {
if (command.subentityId() != null) {
resourceUrl = NoteType.LOAN_TRANSACTION.getApiUrl();
} else {
resourceUrl = NoteType.LOAN.getApiUrl();
}
} else if (command.getSavingsId() != null) {
// TODO: SAVING_TRANSACTION type need to be add.
resourceUrl = NoteType.SAVING_ACCOUNT.getApiUrl();
} else {
resourceUrl = "";
return response.build();
}
return resourceUrl;
@Override
public NoteDeleteResponse deleteNote(final NoteDeleteRequest request) {
var note = getNote(request.getType(), request.getResourceId(), request.getId());
noteRepository.delete(note.getLeft());
return NoteDeleteResponse.builder().resourceId(request.getId()).build();
}
private CommandProcessingResult updateClientNote(final JsonCommand command) {
private Pair<Note, Long> getNote(NoteType type, Long resourceId, Long noteId) {
Note note = null;
Long officeId = null;
final Long resourceId = command.getClientId();
final Long noteId = command.entityId();
final NoteType type = NoteType.CLIENT;
final Client client = this.clientRepository.findOneWithNotFoundDetection(resourceId);
final Note noteForUpdate = this.noteRepository.findByClientAndId(client, noteId);
if (noteForUpdate == null) {
throw new NoteNotFoundException(noteId, resourceId, type.name().toLowerCase());
switch (type) {
case CLIENT: {
final var client = clientRepository.findOneWithNotFoundDetection(resourceId);
note = noteRepository.findByClientAndId(client, noteId);
officeId = client.officeId();
}
final Map<String, Object> changes = noteForUpdate.update(command);
if (!changes.isEmpty()) {
this.noteRepository.saveAndFlush(noteForUpdate);
break;
case GROUP: {
final var group = groupRepository.findById(resourceId).orElseThrow(() -> new GroupNotFoundException(resourceId));
note = noteRepository.findByGroupAndId(group, noteId);
officeId = group.officeId();
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(noteForUpdate.getId()) //
.withClientId(client.getId()) //
.withOfficeId(client.officeId()) //
.with(changes) //
.build();
break;
case LOAN: {
final var loan = loanRepository.findOneWithNotFoundDetection(resourceId);
note = noteRepository.findByLoanAndId(loan, noteId);
officeId = loan.getOfficeId();
}
private CommandProcessingResult updateGroupNote(final JsonCommand command) {
final Long resourceId = command.getGroupId();
final Long noteId = command.entityId();
final NoteType type = NoteType.GROUP;
final Group group = this.groupRepository.findById(resourceId).orElseThrow(() -> new GroupNotFoundException(resourceId));
final Note noteForUpdate = this.noteRepository.findByGroupAndId(group, noteId);
if (noteForUpdate == null) {
throw new NoteNotFoundException(noteId, resourceId, type.name().toLowerCase());
}
final Map<String, Object> changes = noteForUpdate.update(command);
if (!changes.isEmpty()) {
this.noteRepository.saveAndFlush(noteForUpdate);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(noteForUpdate.getId()) //
.withGroupId(group.getId()) //
.withOfficeId(group.officeId()) //
.with(changes).build();
}
private CommandProcessingResult updateLoanNote(final JsonCommand command) {
final Long resourceId = command.getLoanId();
final Long noteId = command.entityId();
final NoteType type = NoteType.LOAN;
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(resourceId);
final Note noteForUpdate = this.noteRepository.findByLoanAndId(loan, noteId);
if (noteForUpdate == null) {
throw new NoteNotFoundException(noteId, resourceId, type.name().toLowerCase());
}
final Map<String, Object> changes = noteForUpdate.update(command);
if (!changes.isEmpty()) {
this.noteRepository.saveAndFlush(noteForUpdate);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(noteForUpdate.getId())
.withLoanId(loan.getId()).withOfficeId(loan.getOfficeId()).with(changes).build();
}
private CommandProcessingResult updateLoanTransactionNote(final JsonCommand command) {
final Long resourceId = command.subentityId();
final Long noteId = command.entityId();
final NoteType type = NoteType.LOAN_TRANSACTION;
final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(resourceId)
break;
case LOAN_TRANSACTION: {
final var loanTransaction = loanTransactionRepository.findById(resourceId)
.orElseThrow(() -> new LoanTransactionNotFoundException(resourceId));
final Loan loan = loanTransaction.getLoan();
final Note noteForUpdate = this.noteRepository.findByLoanTransactionAndId(loanTransaction, noteId);
if (noteForUpdate == null) {
throw new NoteNotFoundException(noteId, resourceId, type.name().toLowerCase());
note = noteRepository.findByLoanTransactionAndId(loanTransaction, noteId);
officeId = loanTransaction.getLoan().getOfficeId();
}
final Map<String, Object> changes = noteForUpdate.update(command);
if (!changes.isEmpty()) {
this.noteRepository.saveAndFlush(noteForUpdate);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(noteForUpdate.getId())
.withLoanId(loan.getId()).withOfficeId(loan.getOfficeId()).with(changes).build();
}
private CommandProcessingResult updateSavingAccountNote(final JsonCommand command) {
final Long resourceId = command.getSavingsId();
final Long noteId = command.entityId();
final NoteType type = NoteType.SAVING_ACCOUNT;
final SavingsAccount savingAccount = this.savingsAccountRepository.findById(resourceId)
break;
case SAVING_ACCOUNT: {
final var savingAccount = savingsAccountRepository.findById(resourceId)
.orElseThrow(() -> new SavingsAccountNotFoundException(resourceId));
final Note noteForUpdate = this.noteRepository.findBySavingsAccountAndId(savingAccount, noteId);
if (noteForUpdate == null) {
throw new NoteNotFoundException(noteId, resourceId, type.name().toLowerCase());
}
final Map<String, Object> changes = noteForUpdate.update(command);
if (!changes.isEmpty()) {
this.noteRepository.saveAndFlush(noteForUpdate);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(noteForUpdate.getId()) //
.withOfficeId(savingAccount.getClient().getOffice().getId()) //
.withSavingsId(savingAccount.getId()) //
.with(changes) //
.build();
}
@Override
public CommandProcessingResult updateNote(final JsonCommand command) {
this.fromApiJsonDeserializer.validateNote(command.json());
final String resourceUrl = getResourceUrlFromCommand(command); // command.getSupportedEntityType();
final NoteType type = NoteType.fromApiUrl(resourceUrl);
switch (type) {
case CLIENT: {
return updateClientNote(command);
}
case GROUP: {
return updateGroupNote(command);
}
case LOAN: {
return updateLoanNote(command);
}
case LOAN_TRANSACTION: {
return updateLoanTransactionNote(command);
}
case SAVING_ACCOUNT: {
return updateSavingAccountNote(command);
}
default:
throw new NoteResourceNotSupportedException(resourceUrl);
}
}
@Override
public CommandProcessingResult deleteNote(final JsonCommand command) {
final Note noteForDelete = getNoteForDelete(command);
this.noteRepository.delete(noteForDelete);
return new CommandProcessingResultBuilder() //
.withCommandId(null) //
.withEntityId(command.entityId()) //
.build();
}
private Note getNoteForDelete(final JsonCommand command) {
final String resourceUrl = getResourceUrlFromCommand(command);// command.getSupportedEntityType();
final Long noteId = command.entityId();
final NoteType type = NoteType.fromApiUrl(resourceUrl);
Long resourceId = null;
Note noteForUpdate = null;
switch (type) {
case CLIENT: {
resourceId = command.getClientId();
final Client client = this.clientRepository.findOneWithNotFoundDetection(resourceId);
noteForUpdate = this.noteRepository.findByClientAndId(client, noteId);
}
break;
case GROUP: {
final Long groupId = command.getGroupId();
resourceId = groupId;
Group group = this.groupRepository.findById(groupId).orElseThrow(() -> new GroupNotFoundException(groupId));
noteForUpdate = this.noteRepository.findByGroupAndId(group, noteId);
}
break;
case LOAN: {
resourceId = command.getLoanId();
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(resourceId);
noteForUpdate = this.noteRepository.findByLoanAndId(loan, noteId);
}
break;
case LOAN_TRANSACTION: {
resourceId = command.subentityId();
final Long loanTransactionId = resourceId;
final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(loanTransactionId)
.orElseThrow(() -> new LoanTransactionNotFoundException(loanTransactionId));
noteForUpdate = this.noteRepository.findByLoanTransactionAndId(loanTransaction, noteId);
}
break;
case SAVING_ACCOUNT: {
final Long savinsAccountId = command.getSavingsId();
final SavingsAccount savingAccount = this.savingsAccountRepository.findById(savinsAccountId)
.orElseThrow(() -> new SavingsAccountNotFoundException(savinsAccountId));
noteForUpdate = this.noteRepository.findBySavingsAccountAndId(savingAccount, noteId);
note = noteRepository.findBySavingsAccountAndId(savingAccount, noteId);
officeId = savingAccount.getClient().getOffice().getId();
}
break;
case SHARE_ACCOUNT:
log.error("TODO Implement getNoteForDelete for SHARE_ACCOUNT");
break;
case SAVINGS_TRANSACTION:
log.error("TODO Implement getNoteForDelete for SAVINGS_TRANSACTION");
log.error("Not yet implemented: {}", type);
break;
}
if (noteForUpdate == null) {
throw new NoteNotFoundException(noteId, resourceId, type.name().toLowerCase());
}
return noteForUpdate;
}
if (note == null) {
throw new NoteNotFoundException(noteId, resourceId, type.name().toLowerCase());
}
return Pair.of(note, officeId);
}
}
@@ -23,7 +23,6 @@ import org.apache.fineract.portfolio.group.domain.GroupRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.note.serialization.NoteCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.note.service.NoteReadPlatformService;
import org.apache.fineract.portfolio.note.service.NoteReadPlatformServiceImpl;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
@@ -47,8 +46,8 @@ public class NoteAutoConfiguration {
@ConditionalOnMissingBean
public NoteWritePlatformService noteWritePlatformService(NoteRepository noteRepository, ClientRepositoryWrapper clientRepository,
GroupRepository groupRepository, LoanRepositoryWrapper loanRepository, LoanTransactionRepository loanTransactionRepository,
NoteCommandFromApiJsonDeserializer fromApiJsonDeserializer, SavingsAccountRepository savingsAccountRepository) {
SavingsAccountRepository savingsAccountRepository) {
return new NoteWritePlatformServiceJpaRepositoryImpl(noteRepository, clientRepository, groupRepository, loanRepository,
loanTransactionRepository, fromApiJsonDeserializer, savingsAccountRepository);
loanTransactionRepository, savingsAccountRepository);
}
}
@@ -57,7 +57,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.portfolio.note.domain.NoteType;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
@@ -67,6 +68,7 @@ import org.apache.fineract.portfolio.transfer.exception.ClientNotAwaitingTransfe
import org.apache.fineract.portfolio.transfer.exception.ClientNotAwaitingTransferApprovalOrOnHoldException;
import org.apache.fineract.portfolio.transfer.exception.TransferNotSupportedException;
import org.apache.fineract.portfolio.transfer.exception.TransferNotSupportedException.TransferNotSupportedReason;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@@ -81,11 +83,11 @@ public class TransferWritePlatformServiceJpaRepositoryImpl implements TransferWr
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper;
private final TransfersDataValidator transfersDataValidator;
private final NoteWritePlatformService noteWritePlatformService;
private final StaffRepositoryWrapper staffRepositoryWrapper;
private final ClientTransferDetailsRepositoryWrapper clientTransferDetailsRepositoryWrapper;
private final PlatformSecurityContext context;
private final LoanOfficerService loanOfficerService;
private final ApplicationEventPublisher eventPublisher;
@Override
@Transactional
@@ -466,7 +468,8 @@ public class TransferWritePlatformServiceJpaRepositoryImpl implements TransferWr
client.updateProposedTransferDate(null);
}
this.noteWritePlatformService.createAndPersistClientNote(client, jsonCommand);
this.eventPublisher.publishEvent(NoteCreateRequest.builder().type(NoteType.CLIENT).resourceId(client.getId())
.note(jsonCommand.stringValueOfParameterNamed("note")).build());
this.clientTransferDetailsRepositoryWrapper
.save(ClientTransferDetails.instance(client.getId(), client.getOffice().getId(), destinationOffice.getId(), transferDate,
transferEventType.getValue(), DateUtils.getBusinessLocalDate(), this.context.authenticatedUser().getId()));
@@ -28,13 +28,13 @@ import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.apache.fineract.portfolio.transfer.data.TransfersDataValidator;
import org.apache.fineract.portfolio.transfer.service.TransferWritePlatformService;
import org.apache.fineract.portfolio.transfer.service.TransferWritePlatformServiceJpaRepositoryImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -47,14 +47,13 @@ public class TransferConfiguration {
OfficeRepositoryWrapper officeRepository, CalendarInstanceRepository calendarInstanceRepository,
LoanWritePlatformService loanWritePlatformService, GroupRepositoryWrapper groupRepository,
LoanRepositoryWrapper loanRepositoryWrapper, TransfersDataValidator transfersDataValidator,
NoteWritePlatformService noteWritePlatformService, StaffRepositoryWrapper staffRepositoryWrapper,
SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper,
StaffRepositoryWrapper staffRepositoryWrapper, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper,
SavingsAccountWritePlatformService savingsAccountWritePlatformService,
ClientTransferDetailsRepositoryWrapper clientTransferDetailsRepositoryWrapper, PlatformSecurityContext context,
LoanOfficerService loanOfficerService) {
LoanOfficerService loanOfficerService, ApplicationEventPublisher eventPublisher) {
return new TransferWritePlatformServiceJpaRepositoryImpl(clientRepositoryWrapper, officeRepository, calendarInstanceRepository,
groupRepository, loanWritePlatformService, savingsAccountWritePlatformService, loanRepositoryWrapper,
savingsAccountRepositoryWrapper, transfersDataValidator, noteWritePlatformService, staffRepositoryWrapper,
clientTransferDetailsRepositoryWrapper, context, loanOfficerService);
savingsAccountRepositoryWrapper, transfersDataValidator, staffRepositoryWrapper, clientTransferDetailsRepositoryWrapper,
context, loanOfficerService, eventPublisher);
}
}
@@ -487,30 +487,58 @@ resilience4j.retry.instances.postInterest.enable-exponential-backoff=${FINERACT_
resilience4j.retry.instances.postInterest.exponential-backoff-multiplier=${FINERACT_PROCESS_POST_INTEREST_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.postInterest.retryExceptions=${FINERACT_PROCESS_POST_INTEREST_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
# business date
resilience4j.retry.instances.commandBusinessDateUpdate.max-attempts=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandBusinessDateUpdate.wait-duration=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.commandBusinessDateUpdate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.commandBusinessDateUpdate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.commandBusinessDateUpdate.retryExceptions=${FINERACT_PROCESS_COMMAND_BUSINESS_DATE_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
# cache
resilience4j.retry.instances.commandCacheSwitch.max-attempts=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandCacheSwitch.wait-duration=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.commandCacheSwitch.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.commandCacheSwitch.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.commandCacheSwitch.retryExceptions=${FINERACT_PROCESS_COMMAND_CACHE_SWITCH_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
# currency
resilience4j.retry.instances.commandCurrencyUpdate.max-attempts=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandCurrencyUpdate.wait-duration=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.commandCurrencyUpdate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.commandCurrencyUpdate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.commandCurrencyUpdate.retryExceptions=${FINERACT_PROCESS_COMMAND_CURRENCY_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
# external event configuration
resilience4j.retry.instances.commandExternalEventConfigurationUpdate.max-attempts=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandExternalEventConfigurationUpdate.wait-duration=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.commandExternalEventConfigurationUpdate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.commandExternalEventConfigurationUpdate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.commandExternalEventConfigurationUpdate.retryExceptions=${FINERACT_PROCESS_COMMAND_EXTERNAL_EVENT_CONFIGURATION_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
# note
resilience4j.retry.instances.commandNoteCreate.max-attempts=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandNoteCreate.wait-duration=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.commandNoteCreate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.commandNoteCreate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.commandNoteCreate.retryExceptions=${FINERACT_PROCESS_COMMAND_NOTE_CREATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
resilience4j.retry.instances.commandNoteUpdate.max-attempts=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandNoteUpdate.wait-duration=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.commandNoteUpdate.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.commandNoteUpdate.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.commandNoteUpdate.retryExceptions=${FINERACT_PROCESS_COMMAND_NOTE_UPDATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
resilience4j.retry.instances.commandNoteDelete.max-attempts=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandNoteDelete.wait-duration=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.commandNoteDelete.enable-exponential-backoff=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.commandNoteDelete.exponential-backoff-multiplier=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.commandNoteDelete.retryExceptions=${FINERACT_PROCESS_COMMAND_NOTE_DELETE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException}
fineract.command.enabled=true
fineract.command.executor=${FINERACT_COMMAND_EXECUTOR:sync}
fineract.command.ring-buffer-size=${FINERACT_COMMAND_RING_BUFFER_SIZE:1024}
@@ -518,6 +546,10 @@ fineract.command.producer-type=${FINERACT_COMMAND_PRODUCER_TYPE:single}
fineract.command.auditable=${FINERACT_COMMAND_AUDITABLE:true}
# this is duplicated on purpose to keep fineract-command independent; once all modules are migrated we clean up
fineract.command.idempotency-key-header-name=${FINERACT_IDEMPOTENCY_KEY_HEADER_NAME:Idempotency-Key}
fineract.command.file-dead-letter-queue-enabled=${FINERACT_FILE_DEAD_LETTER_QUEUE_ENABLED:false}
fineract.command.file-dead-letter-queue-path=${FINERACT_FILE_DEAD_LETTER_QUEUE_PATH:/tmp/fineract-command-audit}
# command
resilience4j.retry.instances.commandAuditProcessing.max-attempts=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSING_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.commandAuditProcessing.wait-duration=${FINERACT_PROCESS_COMMAND_AUDIT_PROCESSING_RETRY_WAIT_DURATION:1s}
@@ -53,3 +53,8 @@ org.apache.fineract.reage.start-date.not-blank=The parameter 'startDate' is mand
org.apache.fineract.reage.number-of-installments.not-blank=The parameter 'numberOfInstallments' is mandatory.
org.apache.fineract.reage.number-of-installments.min=The parameter 'numberOfInstallments' must be at least 1.
org.apache.fineract.frequency-type.invalid=The parameter 'frequencyType' must be valid PeriodFrequencyType value. Provided value: '${validatedValue}'.
# Note
org.apache.fineract.portfolio.note.note.size=The parameter 'note' cannot exceed 1000 characters
org.apache.fineract.portfolio.note.note.not-null=The parameter 'note' cannot be empty
@@ -28,8 +28,6 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.apache.fineract.client.models.NoteData;
import org.apache.fineract.client.models.PostResourceTypeResourceIdNotesResponse;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
import org.apache.fineract.integrationtests.common.GroupHelper;
@@ -41,6 +39,8 @@ import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtens
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
import org.apache.fineract.portfolio.note.data.NoteData;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -217,8 +217,7 @@ public class NotesTest {
// Notes
final String payload = "{\"note\": \"" + noteText + "\"}";
final PostResourceTypeResourceIdNotesResponse postNoteResponse = NotesHelper.createSavingsNote(requestSpec, responseSpec, savingsId,
payload);
final NoteCreateResponse postNoteResponse = NotesHelper.createSavingsNote(requestSpec, responseSpec, savingsId, payload);
Assertions.assertNotNull(postNoteResponse);
Assertions.assertNotNull(postNoteResponse.getResourceId());
@@ -21,9 +21,9 @@ package org.apache.fineract.integrationtests.common;
import com.google.gson.Gson;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import org.apache.fineract.client.models.NoteData;
import org.apache.fineract.client.models.PostResourceTypeResourceIdNotesResponse;
import org.apache.fineract.client.util.JSON;
import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
import org.apache.fineract.portfolio.note.data.NoteData;
@SuppressWarnings({ "rawtypes", "unchecked" })
public final class NotesHelper {
@@ -210,11 +210,11 @@ public final class NotesHelper {
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest)
@Deprecated(forRemoval = true)
public static PostResourceTypeResourceIdNotesResponse createSavingsNote(RequestSpecification requestSpec,
ResponseSpecification responseSpec, Integer savingsId, String request) {
public static NoteCreateResponse createSavingsNote(RequestSpecification requestSpec, ResponseSpecification responseSpec,
Integer savingsId, String request) {
final String noteURL = SAVINGS_URL + "/" + savingsId + "/notes?" + Utils.TENANT_IDENTIFIER;
final String response = Utils.performServerPost(requestSpec, responseSpec, noteURL, request);
return GSON.fromJson(response, PostResourceTypeResourceIdNotesResponse.class);
return GSON.fromJson(response, NoteCreateResponse.class);
}
public static NoteData retrieveSavingsNote(RequestSpecification requestSpec, ResponseSpecification responseSpec, Integer savingsId,