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. # under the License.
# #
version: "3.8"
services: services:
fineract: fineract:
# user: "${FINERACT_USER}:${FINERACT_GROUP}" # 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 FINERACT_INSECURE_HTTP_CLIENT=true
SPRING_PROFILES_ACTIVE=test,diagnostics SPRING_PROFILES_ACTIVE=test,diagnostics
OTEL_SERVICE_NAME=fineract 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; package com.acme.fineract.portfolio.note.service;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
import org.apache.fineract.portfolio.client.domain.Client; 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.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -38,27 +41,17 @@ public class AcmeNoteWritePlatformService implements NoteWritePlatformService, I
} }
@Override @Override
public CommandProcessingResult createNote(JsonCommand command) { public NoteCreateResponse createNote(NoteCreateRequest request) {
throw new UnsupportedOperationException("createNote() is not yet implemented."); throw new UnsupportedOperationException("createNote() is not yet implemented.");
} }
@Override @Override
public void createLoanTransactionNote(Long loanTransactionId, String note) { public NoteUpdateResponse updateNote(NoteUpdateRequest request) {
throw new UnsupportedOperationException("createLoanTransactionNote() is not yet implemented.");
}
@Override
public CommandProcessingResult updateNote(JsonCommand command) {
throw new UnsupportedOperationException("updateNote() is not yet implemented."); throw new UnsupportedOperationException("updateNote() is not yet implemented.");
} }
@Override @Override
public CommandProcessingResult deleteNote(JsonCommand command) { public NoteDeleteResponse deleteNote(NoteDeleteRequest request) {
throw new UnsupportedOperationException("deleteNote() is not yet implemented."); 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 static org.mockito.Mockito.mock;
import org.apache.fineract.infrastructure.core.config.FineractProperties; 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.RoutingDataSource;
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSourceServiceFactory; import org.apache.fineract.infrastructure.core.service.database.RoutingDataSourceServiceFactory;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; 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.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.note.domain.NoteRepository; 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.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
@@ -38,11 +36,6 @@ import org.springframework.jdbc.core.JdbcTemplate;
public class TestDefaultConfiguration { public class TestDefaultConfiguration {
// NOTE: unfortunately an abastract base class that contains all these mock functions won't work // NOTE: unfortunately an abastract base class that contains all these mock functions won't work
@Bean
public FromJsonHelper fromJsonHelper() {
return mock(FromJsonHelper.class);
}
@Bean @Bean
public RoutingDataSourceServiceFactory routingDataSourceServiceFactory() { public RoutingDataSourceServiceFactory routingDataSourceServiceFactory() {
return mock(RoutingDataSourceServiceFactory.class); return mock(RoutingDataSourceServiceFactory.class);
@@ -82,9 +75,4 @@ public class TestDefaultConfiguration {
public LoanTransactionRepository loanTransactionRepository() { public LoanTransactionRepository loanTransactionRepository() {
return mock(LoanTransactionRepository.class); 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 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.RoutingDataSource;
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSourceServiceFactory; import org.apache.fineract.infrastructure.core.service.database.RoutingDataSourceServiceFactory;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; 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.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.note.domain.NoteRepository; 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.Bean;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
@@ -37,11 +35,6 @@ import org.springframework.jdbc.core.JdbcTemplate;
public class TestOverrideConfiguration { public class TestOverrideConfiguration {
// NOTE: unfortunately an abastract base class that contains all these mock functions won't work // NOTE: unfortunately an abastract base class that contains all these mock functions won't work
@Bean
public FromJsonHelper fromJsonHelper() {
return mock(FromJsonHelper.class);
}
@Bean @Bean
public RoutingDataSourceServiceFactory routingDataSourceServiceFactory() { public RoutingDataSourceServiceFactory routingDataSourceServiceFactory() {
return mock(RoutingDataSourceServiceFactory.class); return mock(RoutingDataSourceServiceFactory.class);
@@ -81,9 +74,4 @@ public class TestOverrideConfiguration {
public LoanTransactionRepository loanTransactionRepository() { public LoanTransactionRepository loanTransactionRepository() {
return mock(LoanTransactionRepository.class); return mock(LoanTransactionRepository.class);
} }
@Bean
public NoteCommandFromApiJsonDeserializer fromApiJsonDeserializer(FromJsonHelper fromJsonHelper) {
return new NoteCommandFromApiJsonDeserializer(fromJsonHelper);
}
} }
+1
View File
@@ -29,6 +29,7 @@ services:
service: fineract service: fineract
ports: ports:
- "8443:8443" - "8443:8443"
- "5000:5000"
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
@@ -18,6 +18,8 @@
*/ */
package org.apache.fineract.infrastructure.cache.data; package org.apache.fineract.infrastructure.cache.data;
import java.io.Serial;
import java.io.Serializable;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@@ -28,7 +30,10 @@ import org.apache.fineract.infrastructure.core.data.EnumOptionData;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public final class CacheData { public final class CacheData implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@SuppressWarnings("unused") @SuppressWarnings("unused")
private EnumOptionData cacheType; 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; package org.apache.fineract.portfolio.note.data;
import java.io.Serial;
import java.io.Serializable;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.data.EnumOptionData; 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 @Builder
public class NoteData { @Data
@NoArgsConstructor
@AllArgsConstructor
public class NoteData implements Serializable {
private final Long id; @Serial
private final Long clientId; private static final long serialVersionUID = 1L;
private final Long groupId;
private final Long loanId; private Long id;
private final Long loanTransactionId; private Long clientId;
private final Long depositAccountId; private Long groupId;
private final Long savingAccountId; private Long loanId;
private final EnumOptionData noteType; private Long loanTransactionId;
private final String note; private Long depositAccountId;
private final Long createdById; private Long savingAccountId;
private final String createdByUsername; private EnumOptionData noteType;
private final OffsetDateTime createdOn; private String note;
private final Long updatedById; private Long createdById;
private final String updatedByUsername; private String createdByUsername;
private final OffsetDateTime updatedOn; 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.Serial;
import java.io.Serializable; import java.io.Serializable;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@Builder
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class NoteRequest implements Serializable { public class NoteDeleteResponse implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; 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; package org.apache.fineract.portfolio.note.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
import org.apache.fineract.portfolio.client.domain.Client; 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 { 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); NoteDeleteResponse deleteNote(NoteDeleteRequest request);
CommandProcessingResult deleteNote(JsonCommand command);
void createAndPersistClientNote(Client client, JsonCommand command);
} }
@@ -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.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.repository.LoanBuyDownFeeBalanceRepository; 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.domain.PaymentDetail;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Slf4j @Slf4j
@@ -63,11 +65,11 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
private final LoanTransactionRepository loanTransactionRepository; private final LoanTransactionRepository loanTransactionRepository;
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
private final LoanJournalEntryPoster loanJournalEntryPoster; private final LoanJournalEntryPoster loanJournalEntryPoster;
private final NoteWritePlatformService noteWritePlatformService;
private final ExternalIdFactory externalIdFactory; private final ExternalIdFactory externalIdFactory;
private final LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository; private final LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository;
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
private final CodeValueRepository codeValueRepository; private final CodeValueRepository codeValueRepository;
private final ApplicationEventPublisher eventPublisher;
@Transactional @Transactional
@Override @Override
@@ -108,7 +110,8 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
// Add note if provided // Add note if provided
final String noteText = command.stringValueOfParameterNamed("note"); final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) { 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); loanJournalEntryPoster.postJournalEntriesForLoanTransaction(buyDownFeeTransaction, false, false);
@@ -177,7 +180,8 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
// Create a note if provided // Create a note if provided
final String noteText = command.stringValueOfParameterNamed("note"); final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) { 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); 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.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository; 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.domain.PaymentDetail;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Slf4j @Slf4j
@@ -62,7 +64,6 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
private final LoanTransactionRepository loanTransactionRepository; private final LoanTransactionRepository loanTransactionRepository;
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
private final LoanJournalEntryPoster journalEntryPoster; private final LoanJournalEntryPoster journalEntryPoster;
private final NoteWritePlatformService noteWritePlatformService;
private final ExternalIdFactory externalIdFactory; private final ExternalIdFactory externalIdFactory;
private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository; private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService; private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
@@ -71,6 +72,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
private final CodeValueRepository codeValueRepository; private final CodeValueRepository codeValueRepository;
private final LoanScheduleService loanScheduleService; private final LoanScheduleService loanScheduleService;
private final ApplicationEventPublisher eventPublisher;
@Transactional @Transactional
@Override @Override
@@ -107,7 +109,8 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
// Create a note if provided // Create a note if provided
final String noteText = command.stringValueOfParameterNamed("note"); final String noteText = command.stringValueOfParameterNamed("note");
if (noteText != null && !noteText.isEmpty()) { 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 // Create journal entries immediately for this transaction
@@ -156,7 +159,8 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
// Create a note if provided // Create a note if provided
final String noteText = command.stringValueOfParameterNamed("note"); final String noteText = command.stringValueOfParameterNamed("note");
if (noteText != null && !noteText.isEmpty()) { 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 // Create journal entries immediately for this transaction
journalEntryPoster.postJournalEntriesForLoanTransaction(savedCapitalizedIncomeAdjustment, false, false); 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.ProgressiveLoanTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidatorImpl; import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidatorImpl;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService; 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.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -56,15 +56,14 @@ public class ProgressiveLoanAccountConfiguration {
public CapitalizedIncomePlatformService capitalizedIncomePlatformService(ProgressiveLoanTransactionValidator loanTransactionValidator, public CapitalizedIncomePlatformService capitalizedIncomePlatformService(ProgressiveLoanTransactionValidator loanTransactionValidator,
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository, LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster journalEntryPoster, PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster journalEntryPoster,
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory, ExternalIdFactory externalIdFactory, LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository,
LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository,
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanBalanceService loanBalanceService, ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanBalanceService loanBalanceService,
LoanLifecycleStateMachine loanLifecycleStateMachine, BusinessEventNotifierService businessEventNotifierService, LoanLifecycleStateMachine loanLifecycleStateMachine, BusinessEventNotifierService businessEventNotifierService,
CodeValueRepository codeValueRepository, LoanScheduleService loanScheduleService) { CodeValueRepository codeValueRepository, LoanScheduleService loanScheduleService, ApplicationEventPublisher eventPublisher) {
return new CapitalizedIncomeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository, return new CapitalizedIncomeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
paymentDetailWritePlatformService, journalEntryPoster, noteWritePlatformService, externalIdFactory, paymentDetailWritePlatformService, journalEntryPoster, externalIdFactory, capitalizedIncomeBalanceRepository,
capitalizedIncomeBalanceRepository, reprocessLoanTransactionsService, loanBalanceService, loanLifecycleStateMachine, reprocessLoanTransactionsService, loanBalanceService, loanLifecycleStateMachine, businessEventNotifierService,
businessEventNotifierService, codeValueRepository, loanScheduleService); codeValueRepository, loanScheduleService, eventPublisher);
} }
@Bean @Bean
@@ -157,6 +157,50 @@ public class SecurityConfig {
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_CURRENCY") .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_CURRENCY")
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/currencies")) .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/currencies"))
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_CURRENCY") .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(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated()
.requestMatchers(API_MATCHER.matcher("/api/*/twofactor")).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.LoanDropdownReadPlatformService;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
import org.apache.fineract.portfolio.note.domain.NoteRepository; 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.paymentdetail.service.PaymentDetailWritePlatformService;
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService;
import org.apache.fineract.portfolio.rate.service.RateAssembler; 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.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.service.GSIMReadPlatformService; import org.apache.fineract.portfolio.savings.service.GSIMReadPlatformService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@@ -397,12 +397,12 @@ public class LoanAccountConfiguration {
public BuyDownFeePlatformService buyDownFeePlatformService(ProgressiveLoanTransactionValidator loanTransactionValidator, public BuyDownFeePlatformService buyDownFeePlatformService(ProgressiveLoanTransactionValidator loanTransactionValidator,
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository, LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster loanJournalEntryPoster, PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster loanJournalEntryPoster,
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory, ExternalIdFactory externalIdFactory, LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository,
LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository, BusinessEventNotifierService businessEventNotifierService, BusinessEventNotifierService businessEventNotifierService, CodeValueRepository codeValueRepository,
CodeValueRepository codeValueRepository) { ApplicationEventPublisher eventPublisher) {
return new BuyDownFeeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository, return new BuyDownFeeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
paymentDetailWritePlatformService, loanJournalEntryPoster, noteWritePlatformService, externalIdFactory, paymentDetailWritePlatformService, loanJournalEntryPoster, externalIdFactory, loanBuyDownFeeBalanceRepository,
loanBuyDownFeeBalanceRepository, businessEventNotifierService, codeValueRepository); businessEventNotifierService, codeValueRepository, eventPublisher);
} }
@Bean @Bean
@@ -19,13 +19,8 @@
package org.apache.fineract.portfolio.note.api; package org.apache.fineract.portfolio.note.api;
import io.swagger.v3.oas.annotations.Operation; 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 io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE; import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
@@ -35,201 +30,160 @@ import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam; import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces; import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.function.Supplier;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.command.core.CommandPipeline;
import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.portfolio.note.command.NoteCreateCommand;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.portfolio.note.command.NoteDeleteCommand;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; import org.apache.fineract.portfolio.note.command.NoteUpdateCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.note.data.NoteData; 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.domain.NoteType;
import org.apache.fineract.portfolio.note.exception.NoteResourceNotSupportedException; import org.apache.fineract.portfolio.note.exception.NoteResourceNotSupportedException;
import org.apache.fineract.portfolio.note.service.NoteReadPlatformService; import org.apache.fineract.portfolio.note.service.NoteReadPlatformService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Path("/v1/{resourceType}/{resourceId}/notes") @Path("/v1/{resourceType}/{resourceId}/notes")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Component @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 @RequiredArgsConstructor
public class NotesApiResource { 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 NoteReadPlatformService readPlatformService;
private final DefaultToApiJsonSerializer<NoteData> toApiJsonSerializer; private final CommandPipeline commandPipeline;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET @GET
@Consumes({ MediaType.APPLICATION_JSON }) @Operation(summary = "Retrieve a Resource's description", description = """
@Produces({ MediaType.APPLICATION_JSON }) Retrieves a resource's notes
@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) {
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) { if (noteType == null) {
throw new NoteResourceNotSupportedException(resourceType); throw new NoteResourceNotSupportedException(resourceType);
} }
this.context.authenticatedUser().validateHasReadPermission(getResourceDetails(noteType, resourceId).entityName()); return readPlatformService.retrieveNotesByResource(resourceId, noteType.getValue());
final Integer noteTypeId = noteType.getValue();
return readPlatformService.retrieveNotesByResource(resourceId, noteTypeId);
} }
@GET @GET
@Path("{noteId}") @Path("{noteId}")
@Consumes({ MediaType.APPLICATION_JSON }) @Operation(summary = "Retrieve a Resource Note", description = """
@Produces({ MediaType.APPLICATION_JSON }) Retrieves a resource Note
@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) {
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); final NoteType noteType = NoteType.fromApiUrl(resourceType);
if (noteType == null) { if (noteType == null) {
throw new NoteResourceNotSupportedException(resourceType); throw new NoteResourceNotSupportedException(resourceType);
} }
this.context.authenticatedUser().validateHasReadPermission(getResourceDetails(noteType, resourceId).entityName()); return readPlatformService.retrieveNote(noteId, resourceId, noteType.getValue());
final Integer noteTypeId = noteType.getValue();
final NoteData note = this.readPlatformService.retrieveNote(noteId, resourceId, noteTypeId);
return note;
} }
@POST @POST
@Consumes({ MediaType.APPLICATION_JSON }) @Operation(summary = "Add a Resource Note", description = """
@Produces({ MediaType.APPLICATION_JSON }) Adds a new note to a supported resource.
@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) {
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); throw new NoteResourceNotSupportedException(resourceType);
} }
final CommandWrapper resourceDetails = getResourceDetails(noteType, resourceId); request.setResourceId(resourceId);
final CommandWrapper commandRequest = new CommandWrapperBuilder().createNote(resourceDetails, resourceType, resourceId) request.setType(type);
.withJson(toApiJsonSerializer.serialize(noteRequest)).build();
return commandsSourceWritePlatformService.logCommandSource(commandRequest); final var command = new NoteCreateCommand();
command.setPayload(request);
final Supplier<NoteCreateResponse> response = commandPipeline.send(command);
return response.get();
} }
@PUT @PUT
@Path("{noteId}") @Path("{noteId}")
@Consumes({ MediaType.APPLICATION_JSON }) @Operation(summary = "Update a Resource Note", description = """
@Produces({ MediaType.APPLICATION_JSON }) Updates a Resource Note
@Operation(summary = "Update a Resource Note", description = "Updates a Resource Note") """)
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = NoteRequest.class))) public NoteUpdateResponse updateNote(@PathParam("resourceType") final String resourceType,
@ApiResponses({ @PathParam("resourceId") final Long resourceId, @PathParam("noteId") final Long noteId,
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = NotesApiResourceSwagger.PutResourceTypeResourceIdNotesNoteIdResponse.class))) }) @Valid final NoteUpdateRequest request) {
public CommandProcessingResult updateNote(@PathParam("resourceType") @Parameter(description = "resourceType") final String resourceType, final var type = NoteType.fromApiUrl(resourceType);
@PathParam("resourceId") @Parameter(description = "resourceId") final Long resourceId,
@PathParam("noteId") @Parameter(description = "noteId") final Long noteId,
@Parameter(hidden = true) final NoteRequest noteRequest) {
final NoteType noteType = NoteType.fromApiUrl(resourceType); if (type == null) {
if (noteType == null) {
throw new NoteResourceNotSupportedException(resourceType); 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) final var command = new NoteUpdateCommand();
.withJson(toApiJsonSerializer.serialize(noteRequest)).build();
return commandsSourceWritePlatformService.logCommandSource(commandRequest); command.setPayload(request);
final Supplier<NoteUpdateResponse> response = commandPipeline.send(command);
return response.get();
} }
@DELETE @DELETE
@Path("{noteId}") @Path("{noteId}")
@Consumes({ MediaType.APPLICATION_JSON }) @Operation(summary = "Delete a Resource Note", description = """
@Produces({ MediaType.APPLICATION_JSON }) Deletes a Resource Note
@Operation(summary = "Delete a Resource Note", description = "Deletes a Resource Note") """)
@ApiResponses({ public NoteDeleteResponse deleteNote(@PathParam("resourceType") final String resourceType,
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = NotesApiResourceSwagger.DeleteResourceTypeResourceIdNotesNoteIdResponse.class))) }) @PathParam("resourceId") final Long resourceId, @PathParam("noteId") final Long noteId) {
public CommandProcessingResult deleteNote(@PathParam("resourceType") @Parameter(description = "resourceType") final String resourceType, final var type = NoteType.fromApiUrl(resourceType);
@PathParam("resourceId") @Parameter(description = "resourceId") final Long resourceId,
@PathParam("noteId") @Parameter(description = "noteId") final Long noteId) {
final NoteType noteType = NoteType.fromApiUrl(resourceType); if (type == null) {
if (noteType == null) {
throw new NoteResourceNotSupportedException(resourceType); 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) final var command = new NoteDeleteCommand();
.build();
return commandsSourceWritePlatformService.logCommandSource(commandRequest); command.setPayload(request);
}
private CommandWrapper getResourceDetails(final NoteType type, final Long resourceId) { final Supplier<NoteDeleteResponse> response = commandPipeline.send(command);
CommandWrapperBuilder resourceDetails = new CommandWrapperBuilder();
String resourceNameForPermissions; return response.get();
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();
} }
} }
@@ -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; package org.apache.fineract.portfolio.note.command;
/** import lombok.Data;
* Immutable command used for create or update of notes. import lombok.EqualsAndHashCode;
*/ import org.apache.fineract.command.core.Command;
public class NoteCommand { import org.apache.fineract.portfolio.note.data.NoteCreateRequest;
@SuppressWarnings("unused") @Data
private final String note; @EqualsAndHashCode(callSuper = true)
public class NoteCreateCommand extends Command<NoteCreateRequest> {}
public NoteCommand(final String note) {
this.note = note;
}
}
@@ -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.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import org.apache.commons.lang3.StringUtils; 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.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.group.domain.Group; 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) @JoinColumn(name = "share_account_id", nullable = true)
private ShareAccount shareAccount; private ShareAccount shareAccount;
public static Note clientNoteFromJson(final Client client, final JsonCommand command) { public static Note clientNote(final Client client, final String note) {
final String note = command.stringValueOfParameterNamed("note");
return new Note(client, note); return new Note(client, note);
} }
public static Note groupNoteFromJson(final Group group, final JsonCommand command) { public static Note groupNote(final Group group, final String note) {
final String note = command.stringValueOfParameterNamed("note");
return new Note(group, note); return new Note(group, note);
} }
@@ -164,20 +161,12 @@ public class Note extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.noteTypeId = NoteType.SHARE_ACCOUNT.getValue(); this.noteTypeId = NoteType.SHARE_ACCOUNT.getValue();
} }
public Map<String, Object> update(final JsonCommand command) { public Map<String, Object> update(final String note) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(7); if (!Strings.CI.equals(note, this.note)) {
this.note = StringUtils.defaultIfEmpty(note, null);
final String noteParamName = "note"; return Map.of("note", note);
if (command.isChangeInStringParameterNamed(noteParamName, this.note)) {
final String newValue = command.stringValueOfParameterNamed(noteParamName);
actualChanges.put(noteParamName, newValue);
this.note = StringUtils.defaultIfEmpty(newValue, null);
} }
return actualChanges; return Map.of();
}
public boolean isNotAgainstClientWithIdOf(final Long clientId) {
return !this.client.identifiedBy(clientId);
} }
public String getNote() { public String getNote() {
@@ -18,27 +18,34 @@
*/ */
package org.apache.fineract.portfolio.note.handler; package org.apache.fineract.portfolio.note.handler;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import io.github.resilience4j.retry.annotation.Retry;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; 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.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Service @Slf4j
public class DeleteNoteCommandHandler implements NewCommandSourceHandler { @Component
@RequiredArgsConstructor
public class NoteCreateCommandHandler implements CommandHandler<NoteCreateRequest, NoteCreateResponse> {
private final NoteWritePlatformService writePlatformService; private final NoteWritePlatformService writePlatformService;
@Autowired @Retry(name = "commandNoteCreate", fallbackMethod = "fallback")
public DeleteNoteCommandHandler(final NoteWritePlatformService writePlatformService) { @Override
this.writePlatformService = writePlatformService; @Transactional
public NoteCreateResponse handle(Command<NoteCreateRequest> command) {
return writePlatformService.createNote(command.getPayload());
} }
@Transactional
@Override @Override
public CommandProcessingResult processCommand(final JsonCommand command) { public NoteCreateResponse fallback(Command<NoteCreateRequest> command, Throwable t) {
return this.writePlatformService.deleteNote(command); // 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; package org.apache.fineract.portfolio.note.handler;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import io.github.resilience4j.retry.annotation.Retry;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; 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.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Service @Slf4j
public class UpdateNoteCommandHandler implements NewCommandSourceHandler { @Component
@RequiredArgsConstructor
public class NoteDeleteCommandHandler implements CommandHandler<NoteDeleteRequest, NoteDeleteResponse> {
private final NoteWritePlatformService writePlatformService; private final NoteWritePlatformService writePlatformService;
@Autowired @Retry(name = "commandNoteDelete", fallbackMethod = "fallback")
public UpdateNoteCommandHandler(final NoteWritePlatformService noteWritePlatformService) {
this.writePlatformService = noteWritePlatformService;
}
@Transactional
@Override @Override
public CommandProcessingResult processCommand(final JsonCommand command) { @Transactional
public NoteDeleteResponse handle(Command<NoteDeleteRequest> command) {
return this.writePlatformService.updateNote(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; package org.apache.fineract.portfolio.note.handler;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import io.github.resilience4j.retry.annotation.Retry;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; 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.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Service @Slf4j
public class CreateNoteCommandHandler implements NewCommandSourceHandler { @Component
@RequiredArgsConstructor
public class NoteUpdateCommandHandler implements CommandHandler<NoteUpdateRequest, NoteUpdateResponse> {
private final NoteWritePlatformService writePlatformService; private final NoteWritePlatformService writePlatformService;
@Autowired @Retry(name = "commandNoteUpdate", fallbackMethod = "fallback")
public CreateNoteCommandHandler(final NoteWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override @Override
public CommandProcessingResult processCommand(final JsonCommand command) { @Transactional
return this.writePlatformService.createNote(command); 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; package org.apache.fineract.portfolio.note.service;
import java.util.Map;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.commons.lang3.tuple.Pair;
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.fineract.portfolio.client.domain.ClientRepositoryWrapper; 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.domain.GroupRepository;
import org.apache.fineract.portfolio.group.exception.GroupNotFoundException; 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.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException; 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.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository; import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.note.domain.NoteType; import org.apache.fineract.portfolio.note.domain.NoteType;
import org.apache.fineract.portfolio.note.exception.NoteNotFoundException; import org.apache.fineract.portfolio.note.exception.NoteNotFoundException;
import org.apache.fineract.portfolio.note.exception.NoteResourceNotSupportedException; 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.domain.SavingsAccountRepository;
import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException; import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException;
@@ -53,419 +49,135 @@ public class NoteWritePlatformServiceJpaRepositoryImpl implements NoteWritePlatf
private final GroupRepository groupRepository; private final GroupRepository groupRepository;
private final LoanRepositoryWrapper loanRepository; private final LoanRepositoryWrapper loanRepository;
private final LoanTransactionRepository loanTransactionRepository; private final LoanTransactionRepository loanTransactionRepository;
private final NoteCommandFromApiJsonDeserializer fromApiJsonDeserializer;
private final SavingsAccountRepository savingsAccountRepository; private final SavingsAccountRepository savingsAccountRepository;
public NoteWritePlatformServiceJpaRepositoryImpl(final NoteRepository noteRepository, final ClientRepositoryWrapper clientRepository, public NoteWritePlatformServiceJpaRepositoryImpl(final NoteRepository noteRepository, final ClientRepositoryWrapper clientRepository,
final GroupRepository groupRepository, final LoanRepositoryWrapper loanRepository, final GroupRepository groupRepository, final LoanRepositoryWrapper loanRepository,
final LoanTransactionRepository loanTransactionRepository, final NoteCommandFromApiJsonDeserializer fromApiJsonDeserializer, final LoanTransactionRepository loanTransactionRepository, final SavingsAccountRepository savingsAccountRepository) {
final SavingsAccountRepository savingsAccountRepository) {
this.noteRepository = noteRepository; this.noteRepository = noteRepository;
this.clientRepository = clientRepository; this.clientRepository = clientRepository;
this.groupRepository = groupRepository; this.groupRepository = groupRepository;
this.loanRepository = loanRepository; this.loanRepository = loanRepository;
this.loanTransactionRepository = loanTransactionRepository; this.loanTransactionRepository = loanTransactionRepository;
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
this.savingsAccountRepository = savingsAccountRepository; 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 @Override
public void createAndPersistClientNote(final Client client, final JsonCommand command) { public NoteCreateResponse createNote(final NoteCreateRequest request) {
final String noteText = command.stringValueOfParameterNamed("note"); Note note;
if (StringUtils.isNotBlank(noteText)) { Long officeId;
final Note newNote = new Note(client, noteText);
this.noteRepository.save(newNote);
}
}
private CommandProcessingResult createGroupNote(final JsonCommand command) { switch (request.getType()) {
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) {
case CLIENT: { 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: { 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: { 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: { 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: { 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: default:
throw new NoteResourceNotSupportedException(resourceUrl); throw new NoteResourceNotSupportedException(request.getType().getApiUrl());
} }
return NoteCreateResponse.builder().entityId(note.getId()).resourceId(note.getId()).officeId(officeId).build();
} }
@Override @Override
public void createLoanTransactionNote(final Long loanTransactionId, final String note) { public NoteUpdateResponse updateNote(final NoteUpdateRequest request) {
final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(loanTransactionId) final var result = getNote(request.getType(), request.getResourceId(), request.getId());
.orElseThrow(() -> new LoanTransactionNotFoundException(loanTransactionId)); final var note = result.getLeft();
final var response = NoteUpdateResponse.builder().officeId(result.getRight()).resourceId(request.getResourceId());
final Loan loan = loanTransaction.getLoan(); if (!Strings.CI.equals(note.getNote(), request.getNote())) {
response.changes(note.update(request.getNote()));
final Note newNote = Note.loanTransactionNote(loan, loanTransaction, note); noteRepository.saveAndFlush(note);
this.noteRepository.save(newNote);
} }
private String getResourceUrlFromCommand(JsonCommand command) { return response.build();
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 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(); switch (type) {
final Long noteId = command.entityId(); case CLIENT: {
final var client = clientRepository.findOneWithNotFoundDetection(resourceId);
final NoteType type = NoteType.CLIENT; note = noteRepository.findByClientAndId(client, noteId);
officeId = client.officeId();
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());
} }
break;
final Map<String, Object> changes = noteForUpdate.update(command); case GROUP: {
final var group = groupRepository.findById(resourceId).orElseThrow(() -> new GroupNotFoundException(resourceId));
if (!changes.isEmpty()) { note = noteRepository.findByGroupAndId(group, noteId);
this.noteRepository.saveAndFlush(noteForUpdate); officeId = group.officeId();
} }
break;
return new CommandProcessingResultBuilder() // case LOAN: {
.withCommandId(command.commandId()) // final var loan = loanRepository.findOneWithNotFoundDetection(resourceId);
.withEntityId(noteForUpdate.getId()) // note = noteRepository.findByLoanAndId(loan, noteId);
.withClientId(client.getId()) // officeId = loan.getOfficeId();
.withOfficeId(client.officeId()) //
.with(changes) //
.build();
} }
break;
private CommandProcessingResult updateGroupNote(final JsonCommand command) { case LOAN_TRANSACTION: {
final var loanTransaction = loanTransactionRepository.findById(resourceId)
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)
.orElseThrow(() -> new LoanTransactionNotFoundException(resourceId)); .orElseThrow(() -> new LoanTransactionNotFoundException(resourceId));
final Loan loan = loanTransaction.getLoan(); note = noteRepository.findByLoanTransactionAndId(loanTransaction, noteId);
officeId = loanTransaction.getLoan().getOfficeId();
final Note noteForUpdate = this.noteRepository.findByLoanTransactionAndId(loanTransaction, noteId);
if (noteForUpdate == null) {
throw new NoteNotFoundException(noteId, resourceId, type.name().toLowerCase());
} }
break;
final Map<String, Object> changes = noteForUpdate.update(command); case SAVING_ACCOUNT: {
final var savingAccount = savingsAccountRepository.findById(resourceId)
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)
.orElseThrow(() -> new SavingsAccountNotFoundException(resourceId)); .orElseThrow(() -> new SavingsAccountNotFoundException(resourceId));
note = noteRepository.findBySavingsAccountAndId(savingAccount, noteId);
final Note noteForUpdate = this.noteRepository.findBySavingsAccountAndId(savingAccount, noteId); officeId = savingAccount.getClient().getOffice().getId();
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);
} }
break; break;
case SHARE_ACCOUNT: case SHARE_ACCOUNT:
log.error("TODO Implement getNoteForDelete for SHARE_ACCOUNT");
break;
case SAVINGS_TRANSACTION: case SAVINGS_TRANSACTION:
log.error("TODO Implement getNoteForDelete for SAVINGS_TRANSACTION"); log.error("Not yet implemented: {}", type);
break; 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.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.note.domain.NoteRepository; 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.NoteReadPlatformService;
import org.apache.fineract.portfolio.note.service.NoteReadPlatformServiceImpl; import org.apache.fineract.portfolio.note.service.NoteReadPlatformServiceImpl;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService; import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
@@ -47,8 +46,8 @@ public class NoteAutoConfiguration {
@ConditionalOnMissingBean @ConditionalOnMissingBean
public NoteWritePlatformService noteWritePlatformService(NoteRepository noteRepository, ClientRepositoryWrapper clientRepository, public NoteWritePlatformService noteWritePlatformService(NoteRepository noteRepository, ClientRepositoryWrapper clientRepository,
GroupRepository groupRepository, LoanRepositoryWrapper loanRepository, LoanTransactionRepository loanTransactionRepository, GroupRepository groupRepository, LoanRepositoryWrapper loanRepository, LoanTransactionRepository loanTransactionRepository,
NoteCommandFromApiJsonDeserializer fromApiJsonDeserializer, SavingsAccountRepository savingsAccountRepository) { SavingsAccountRepository savingsAccountRepository) {
return new NoteWritePlatformServiceJpaRepositoryImpl(noteRepository, clientRepository, groupRepository, loanRepository, 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.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService; import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; 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.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; 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.ClientNotAwaitingTransferApprovalOrOnHoldException;
import org.apache.fineract.portfolio.transfer.exception.TransferNotSupportedException; import org.apache.fineract.portfolio.transfer.exception.TransferNotSupportedException;
import org.apache.fineract.portfolio.transfer.exception.TransferNotSupportedException.TransferNotSupportedReason; import org.apache.fineract.portfolio.transfer.exception.TransferNotSupportedException.TransferNotSupportedReason;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -81,11 +83,11 @@ public class TransferWritePlatformServiceJpaRepositoryImpl implements TransferWr
private final LoanRepositoryWrapper loanRepositoryWrapper; private final LoanRepositoryWrapper loanRepositoryWrapper;
private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper; private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper;
private final TransfersDataValidator transfersDataValidator; private final TransfersDataValidator transfersDataValidator;
private final NoteWritePlatformService noteWritePlatformService;
private final StaffRepositoryWrapper staffRepositoryWrapper; private final StaffRepositoryWrapper staffRepositoryWrapper;
private final ClientTransferDetailsRepositoryWrapper clientTransferDetailsRepositoryWrapper; private final ClientTransferDetailsRepositoryWrapper clientTransferDetailsRepositoryWrapper;
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final LoanOfficerService loanOfficerService; private final LoanOfficerService loanOfficerService;
private final ApplicationEventPublisher eventPublisher;
@Override @Override
@Transactional @Transactional
@@ -466,7 +468,8 @@ public class TransferWritePlatformServiceJpaRepositoryImpl implements TransferWr
client.updateProposedTransferDate(null); 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 this.clientTransferDetailsRepositoryWrapper
.save(ClientTransferDetails.instance(client.getId(), client.getOffice().getId(), destinationOffice.getId(), transferDate, .save(ClientTransferDetails.instance(client.getId(), client.getOffice().getId(), destinationOffice.getId(), transferDate,
transferEventType.getValue(), DateUtils.getBusinessLocalDate(), this.context.authenticatedUser().getId())); 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.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService; import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; 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.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.apache.fineract.portfolio.transfer.data.TransfersDataValidator; import org.apache.fineract.portfolio.transfer.data.TransfersDataValidator;
import org.apache.fineract.portfolio.transfer.service.TransferWritePlatformService; import org.apache.fineract.portfolio.transfer.service.TransferWritePlatformService;
import org.apache.fineract.portfolio.transfer.service.TransferWritePlatformServiceJpaRepositoryImpl; import org.apache.fineract.portfolio.transfer.service.TransferWritePlatformServiceJpaRepositoryImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -47,14 +47,13 @@ public class TransferConfiguration {
OfficeRepositoryWrapper officeRepository, CalendarInstanceRepository calendarInstanceRepository, OfficeRepositoryWrapper officeRepository, CalendarInstanceRepository calendarInstanceRepository,
LoanWritePlatformService loanWritePlatformService, GroupRepositoryWrapper groupRepository, LoanWritePlatformService loanWritePlatformService, GroupRepositoryWrapper groupRepository,
LoanRepositoryWrapper loanRepositoryWrapper, TransfersDataValidator transfersDataValidator, LoanRepositoryWrapper loanRepositoryWrapper, TransfersDataValidator transfersDataValidator,
NoteWritePlatformService noteWritePlatformService, StaffRepositoryWrapper staffRepositoryWrapper, StaffRepositoryWrapper staffRepositoryWrapper, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper,
SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper,
SavingsAccountWritePlatformService savingsAccountWritePlatformService, SavingsAccountWritePlatformService savingsAccountWritePlatformService,
ClientTransferDetailsRepositoryWrapper clientTransferDetailsRepositoryWrapper, PlatformSecurityContext context, ClientTransferDetailsRepositoryWrapper clientTransferDetailsRepositoryWrapper, PlatformSecurityContext context,
LoanOfficerService loanOfficerService) { LoanOfficerService loanOfficerService, ApplicationEventPublisher eventPublisher) {
return new TransferWritePlatformServiceJpaRepositoryImpl(clientRepositoryWrapper, officeRepository, calendarInstanceRepository, return new TransferWritePlatformServiceJpaRepositoryImpl(clientRepositoryWrapper, officeRepository, calendarInstanceRepository,
groupRepository, loanWritePlatformService, savingsAccountWritePlatformService, loanRepositoryWrapper, groupRepository, loanWritePlatformService, savingsAccountWritePlatformService, loanRepositoryWrapper,
savingsAccountRepositoryWrapper, transfersDataValidator, noteWritePlatformService, staffRepositoryWrapper, savingsAccountRepositoryWrapper, transfersDataValidator, staffRepositoryWrapper, clientTransferDetailsRepositoryWrapper,
clientTransferDetailsRepositoryWrapper, context, loanOfficerService); 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.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} 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.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.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.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.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} 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.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.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.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.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} 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.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.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.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.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} 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.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.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.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.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} 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.enabled=true
fineract.command.executor=${FINERACT_COMMAND_EXECUTOR:sync} fineract.command.executor=${FINERACT_COMMAND_EXECUTOR:sync}
fineract.command.ring-buffer-size=${FINERACT_COMMAND_RING_BUFFER_SIZE:1024} 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} 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 # 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.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.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} 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.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.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}'. 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.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; 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.ClientHelper;
import org.apache.fineract.integrationtests.common.CollateralManagementHelper; import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
import org.apache.fineract.integrationtests.common.GroupHelper; 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.loans.LoanTransactionHelper;
import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper; import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper; 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.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -217,8 +217,7 @@ public class NotesTest {
// Notes // Notes
final String payload = "{\"note\": \"" + noteText + "\"}"; final String payload = "{\"note\": \"" + noteText + "\"}";
final PostResourceTypeResourceIdNotesResponse postNoteResponse = NotesHelper.createSavingsNote(requestSpec, responseSpec, savingsId, final NoteCreateResponse postNoteResponse = NotesHelper.createSavingsNote(requestSpec, responseSpec, savingsId, payload);
payload);
Assertions.assertNotNull(postNoteResponse); Assertions.assertNotNull(postNoteResponse);
Assertions.assertNotNull(postNoteResponse.getResourceId()); Assertions.assertNotNull(postNoteResponse.getResourceId());
@@ -21,9 +21,9 @@ package org.apache.fineract.integrationtests.common;
import com.google.gson.Gson; import com.google.gson.Gson;
import io.restassured.specification.RequestSpecification; import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification; 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.client.util.JSON;
import org.apache.fineract.portfolio.note.data.NoteCreateResponse;
import org.apache.fineract.portfolio.note.data.NoteData;
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings({ "rawtypes", "unchecked" })
public final class NotesHelper { public final class NotesHelper {
@@ -210,11 +210,11 @@ public final class NotesHelper {
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long, // Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest) // org.apache.fineract.client.models.PostLoansLoanIdRequest)
@Deprecated(forRemoval = true) @Deprecated(forRemoval = true)
public static PostResourceTypeResourceIdNotesResponse createSavingsNote(RequestSpecification requestSpec, public static NoteCreateResponse createSavingsNote(RequestSpecification requestSpec, ResponseSpecification responseSpec,
ResponseSpecification responseSpec, Integer savingsId, String request) { Integer savingsId, String request) {
final String noteURL = SAVINGS_URL + "/" + savingsId + "/notes?" + Utils.TENANT_IDENTIFIER; final String noteURL = SAVINGS_URL + "/" + savingsId + "/notes?" + Utils.TENANT_IDENTIFIER;
final String response = Utils.performServerPost(requestSpec, responseSpec, noteURL, request); 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, public static NoteData retrieveSavingsNote(RequestSpecification requestSpec, ResponseSpecification responseSpec, Integer savingsId,