From 9b9c112de0ea821bea8e60b9b28ff63a3dede565 Mon Sep 17 00:00:00 2001 From: edk12564 Date: Mon, 8 Jun 2026 09:53:46 -0500 Subject: [PATCH 1/4] FINERACT-2634: Implement Registration Feature - created registration feature - created user feature service - created fineract client - added custom exceptions and handling - removed gitkeep placeholders - removed ping feature - expanded liquibase user columns - implemented cqrs separation - added command/query aspects for cross cutting concerns --- consumer/build.gradle | 1 + consumer/compose.yaml | 16 +++ .../consumer/ConsumerApplication.java | 2 + .../fineract/consumer/audit/api/.gitkeep | 0 .../fineract/consumer/audit/data/.gitkeep | 0 .../fineract/consumer/audit/domain/.gitkeep | 0 .../consumer/audit/exception/.gitkeep | 0 .../consumer/audit/repository/.gitkeep | 0 .../fineract/consumer/audit/service/.gitkeep | 0 .../consumer/authentication/api/.gitkeep | 0 .../consumer/authentication/data/.gitkeep | 0 .../consumer/authentication/domain/.gitkeep | 0 .../authentication/exception/.gitkeep | 0 .../authentication/repository/.gitkeep | 0 .../consumer/authentication/service/.gitkeep | 0 .../fineract/consumer/identity/api/.gitkeep | 0 .../fineract/consumer/identity/data/.gitkeep | 0 .../consumer/identity/domain/.gitkeep | 0 .../consumer/identity/exception/.gitkeep | 0 .../consumer/identity/repository/.gitkeep | 0 .../consumer/identity/service/.gitkeep | 0 .../infrastructure/command/Command.java | 31 +++++ .../{config => configs}/OpenApiConfig.java | 2 +- .../{config => configs}/SecurityConfig.java | 5 +- .../exception/AbstractConsumerException.java | 42 +++++++ .../exception/ConsumerExceptionHandler.java | 45 +++++++ .../exception/DefaultExceptionHandler.java | 42 +++++++ ...ttpMessageNotReadableExceptionHandler.java | 43 +++++++ ...ethodArgumentNotValidExceptionHandler.java | 43 +++++++ .../MissingRequestHeaderExceptionHandler.java | 43 +++++++ .../FineractClientIdentifiersClient.java} | 26 ++-- .../configs/FineractClientConfig.java | 41 ++++++ .../configs/FineractClientProperties.java | 35 ++++++ .../data/FineractClientIdentifierData.java | 46 +++++++ .../FineractBasicAuthInterceptor.java | 43 +++++++ .../FineractTenantHeaderInterceptor.java | 37 ++++++ .../consumer/infrastructure/query/Query.java | 31 +++++ .../fineract/consumer/loans/api/.gitkeep | 0 .../fineract/consumer/loans/data/.gitkeep | 0 .../fineract/consumer/loans/domain/.gitkeep | 0 .../consumer/loans/exception/.gitkeep | 0 .../consumer/loans/repository/.gitkeep | 0 .../fineract/consumer/loans/service/.gitkeep | 0 .../consumer/openbanking/api/.gitkeep | 0 .../consumer/openbanking/data/.gitkeep | 0 .../consumer/openbanking/domain/.gitkeep | 0 .../consumer/openbanking/exception/.gitkeep | 0 .../consumer/openbanking/repository/.gitkeep | 0 .../consumer/openbanking/service/.gitkeep | 0 .../consumer/registration/api/.gitkeep | 0 .../api/RegistrationCommandController.java | 83 ++++++++++++ .../command/data/SendOtpCommand.java | 37 ++++++ .../command/data/SendOtpCommandData.java | 38 ++++++ .../command/data/SendOtpCommandRequest.java} | 37 +++--- .../data/SubmitRegistrationCommand.java | 38 ++++++ .../data/SubmitRegistrationCommandData.java | 40 ++++++ .../SubmitRegistrationCommandRequest.java | 50 ++++++++ .../command/data/VerifyOtpCommand.java | 36 ++++++ .../command/data/VerifyOtpCommandData.java | 36 ++++++ .../command/data/VerifyOtpCommandRequest.java | 41 ++++++ .../IdentityNotVerifiedException.java | 30 +++++ .../service/RegistrationCommandService.java | 36 ++++++ .../RegistrationCommandServiceImpl.java | 119 ++++++++++++++++++ .../consumer/registration/data/.gitkeep | 0 .../consumer/registration/domain/.gitkeep | 0 .../consumer/registration/exception/.gitkeep | 0 .../query/data/IdentityVerificationQuery.java | 36 ++++++ .../data/IdentityVerificationQueryData.java | 43 +++++++ .../IdentityVerificationException.java | 30 +++++ .../query/service/IdentityQueryService.java | 28 +++++ .../service/IdentityQueryServiceImpl.java | 79 ++++++++++++ .../consumer/registration/repository/.gitkeep | 0 .../consumer/registration/service/.gitkeep | 0 .../fineract/consumer/savings/api/.gitkeep | 0 .../fineract/consumer/savings/data/.gitkeep | 0 .../fineract/consumer/savings/domain/.gitkeep | 0 .../consumer/savings/exception/.gitkeep | 0 .../consumer/savings/repository/.gitkeep | 0 .../consumer/savings/service/.gitkeep | 0 .../user/command/data/CreateUserCommand.java | 37 ++++++ .../command/data/UserCreatedCommandData.java | 37 ++++++ .../consumer/user/command/domain/User.java | 105 ++++++++++++++++ .../user/command/domain/UserStatus.java} | 18 +-- .../InvalidBindingStateException.java | 30 +++++ .../exception/UserAlreadyExistsException.java | 30 +++++ .../exception/UserNotFoundException.java | 30 +++++ .../repository/UserCommandRepository.java | 31 +++++ .../command/service/UserCommandService.java | 32 +++++ .../service/UserCommandServiceImpl.java | 81 ++++++++++++ .../user/query/data/UserQueryData.java | 40 ++++++ .../query/repository/UserQueryRepository.java | 34 +++++ .../user/query/service/UserQueryService.java | 28 +++++ .../query/service/UserQueryServiceImpl.java | 42 +++++++ .../consumer/useradministration/api/.gitkeep | 0 .../consumer/useradministration/data/.gitkeep | 0 .../useradministration/exception/.gitkeep | 0 .../useradministration/repository/.gitkeep | 0 .../useradministration/service/.gitkeep | 0 .../src/main/resources/application.properties | 17 ++- .../changes/001-create-users-table.yaml | 45 ++++++- 100 files changed, 1990 insertions(+), 58 deletions(-) delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/audit/api/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/audit/data/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/audit/domain/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/audit/exception/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/audit/repository/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/audit/service/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/authentication/api/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/authentication/data/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/authentication/domain/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/authentication/exception/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/authentication/repository/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/authentication/service/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/identity/api/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/identity/data/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/identity/domain/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/identity/exception/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/identity/repository/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/identity/service/.gitkeep create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/command/Command.java rename consumer/src/main/java/org/apache/fineract/consumer/infrastructure/{config => configs}/OpenApiConfig.java (96%) rename consumer/src/main/java/org/apache/fineract/consumer/infrastructure/{config => configs}/SecurityConfig.java (91%) create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/AbstractConsumerException.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/ConsumerExceptionHandler.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/DefaultExceptionHandler.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/HttpMessageNotReadableExceptionHandler.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MethodArgumentNotValidExceptionHandler.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MissingRequestHeaderExceptionHandler.java rename consumer/src/main/java/org/apache/fineract/consumer/{ping/PingController.java => infrastructure/fineractclient/clients/FineractClientIdentifiersClient.java} (55%) create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/configs/FineractClientConfig.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/configs/FineractClientProperties.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/data/FineractClientIdentifierData.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/interceptors/FineractBasicAuthInterceptor.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/interceptors/FineractTenantHeaderInterceptor.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/query/Query.java delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/loans/api/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/loans/data/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/loans/domain/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/loans/exception/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/loans/repository/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/loans/service/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/openbanking/api/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/openbanking/data/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/openbanking/domain/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/openbanking/exception/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/openbanking/repository/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/openbanking/service/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/api/.gitkeep create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/command/api/RegistrationCommandController.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SendOtpCommand.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SendOtpCommandData.java rename consumer/src/main/java/org/apache/fineract/consumer/{useradministration/domain/User.java => registration/command/data/SendOtpCommandRequest.java} (53%) create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SubmitRegistrationCommand.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SubmitRegistrationCommandData.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SubmitRegistrationCommandRequest.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/VerifyOtpCommand.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/VerifyOtpCommandData.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/VerifyOtpCommandRequest.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/command/exception/IdentityNotVerifiedException.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/command/service/RegistrationCommandService.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/command/service/RegistrationCommandServiceImpl.java delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/data/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/domain/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/exception/.gitkeep create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/query/data/IdentityVerificationQuery.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/query/data/IdentityVerificationQueryData.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/query/exception/IdentityVerificationException.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/query/service/IdentityQueryService.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/query/service/IdentityQueryServiceImpl.java delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/repository/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/registration/service/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/savings/api/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/savings/data/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/savings/domain/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/savings/exception/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/savings/repository/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/savings/service/.gitkeep create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/user/command/data/CreateUserCommand.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/user/command/data/UserCreatedCommandData.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/user/command/domain/User.java rename consumer/src/{test/java/org/apache/fineract/consumer/ConsumerApplicationTests.java => main/java/org/apache/fineract/consumer/user/command/domain/UserStatus.java} (72%) create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/InvalidBindingStateException.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserAlreadyExistsException.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserNotFoundException.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/user/command/repository/UserCommandRepository.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/user/command/service/UserCommandService.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/user/command/service/UserCommandServiceImpl.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/user/query/data/UserQueryData.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/user/query/repository/UserQueryRepository.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/user/query/service/UserQueryService.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/user/query/service/UserQueryServiceImpl.java delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/useradministration/api/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/useradministration/data/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/useradministration/exception/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/useradministration/repository/.gitkeep delete mode 100644 consumer/src/main/java/org/apache/fineract/consumer/useradministration/service/.gitkeep diff --git a/consumer/build.gradle b/consumer/build.gradle index 76cb8d7..84b0c75 100644 --- a/consumer/build.gradle +++ b/consumer/build.gradle @@ -53,6 +53,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-liquibase' + implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3' implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' compileOnly 'org.projectlombok:lombok' diff --git a/consumer/compose.yaml b/consumer/compose.yaml index 40f7d1d..547fb8d 100644 --- a/consumer/compose.yaml +++ b/consumer/compose.yaml @@ -91,11 +91,14 @@ services: condition: service_healthy fineract: condition: service_healthy + mailpit: + condition: service_healthy environment: SPRING_DATASOURCE_URL: jdbc:postgresql://bff-db:5432/${POSTGRES_DB:-consumerapp} SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-consumerapp} SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-password} FINERACT_BASE_URL: http://fineract:8080/fineract-provider/api/v1 + SPRING_MAIL_HOST: mailpit SERVER_FORWARD_HEADERS_STRATEGY: framework JAVA_TOOL_OPTIONS: "-Xmx768m -Xms256m" ports: @@ -108,6 +111,19 @@ services: start_period: 60s mem_limit: 1g + mailpit: + image: axllent/mailpit:latest + container_name: mailpit + ports: + - "8025:8025" + - "1025:1025" + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:8025/api/v1/info"] + interval: 5s + timeout: 5s + retries: 10 + mem_limit: 128m + configs: fineract_init_sql: content: | diff --git a/consumer/src/main/java/org/apache/fineract/consumer/ConsumerApplication.java b/consumer/src/main/java/org/apache/fineract/consumer/ConsumerApplication.java index cbcf1e7..3b3142f 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/ConsumerApplication.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/ConsumerApplication.java @@ -21,8 +21,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication +@EnableFeignClients(basePackages = "org.apache.fineract.consumer") public class ConsumerApplication { public static void main(String[] args) { diff --git a/consumer/src/main/java/org/apache/fineract/consumer/audit/api/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/audit/api/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/audit/data/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/audit/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/audit/domain/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/audit/domain/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/audit/exception/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/audit/exception/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/audit/repository/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/audit/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/audit/service/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/audit/service/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/authentication/api/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/authentication/api/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/authentication/data/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/authentication/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/authentication/domain/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/authentication/domain/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/authentication/exception/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/authentication/exception/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/authentication/repository/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/authentication/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/authentication/service/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/authentication/service/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/identity/api/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/identity/api/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/identity/data/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/identity/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/identity/domain/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/identity/domain/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/identity/exception/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/identity/exception/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/identity/repository/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/identity/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/identity/service/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/identity/service/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/command/Command.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/command/Command.java new file mode 100644 index 0000000..ea9e3be --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/command/Command.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.infrastructure.command; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.transaction.annotation.Transactional; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Transactional +public @interface Command {} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/config/OpenApiConfig.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/configs/OpenApiConfig.java similarity index 96% rename from consumer/src/main/java/org/apache/fineract/consumer/infrastructure/config/OpenApiConfig.java rename to consumer/src/main/java/org/apache/fineract/consumer/infrastructure/configs/OpenApiConfig.java index 4dc45aa..709572b 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/config/OpenApiConfig.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/configs/OpenApiConfig.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.fineract.consumer.infrastructure.config; +package org.apache.fineract.consumer.infrastructure.configs; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/config/SecurityConfig.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/configs/SecurityConfig.java similarity index 91% rename from consumer/src/main/java/org/apache/fineract/consumer/infrastructure/config/SecurityConfig.java rename to consumer/src/main/java/org/apache/fineract/consumer/infrastructure/configs/SecurityConfig.java index 367aa09..27c7c90 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/config/SecurityConfig.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/configs/SecurityConfig.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.fineract.consumer.infrastructure.config; +package org.apache.fineract.consumer.infrastructure.configs; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -38,7 +38,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/v3/api-docs.yaml", "/swagger-ui/**", "/swagger-ui.html", - "/actuator/health" + "/actuator/health", + "/api/v1/registration/**" ).permitAll() .anyRequest().authenticated()) // TODO: Setup CSRF diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/AbstractConsumerException.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/AbstractConsumerException.java new file mode 100644 index 0000000..1bfa476 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/AbstractConsumerException.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.infrastructure.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public abstract class AbstractConsumerException extends RuntimeException { + + private final HttpStatus httpStatus; + private final String errorMessage; + + protected AbstractConsumerException(HttpStatus httpStatus, String errorMessage) { + super(errorMessage); + this.httpStatus = httpStatus; + this.errorMessage = errorMessage; + } + + protected AbstractConsumerException(HttpStatus httpStatus, String errorMessage, Throwable cause) { + super(errorMessage, cause); + this.httpStatus = httpStatus; + this.errorMessage = errorMessage; + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/ConsumerExceptionHandler.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/ConsumerExceptionHandler.java new file mode 100644 index 0000000..04ff18b --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/ConsumerExceptionHandler.java @@ -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.consumer.infrastructure.exception; + +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@Order(Ordered.HIGHEST_PRECEDENCE) +@RestControllerAdvice +public class ConsumerExceptionHandler { + + @ExceptionHandler(AbstractConsumerException.class) + public ResponseEntity> handle(AbstractConsumerException ex) { + if (ex.getHttpStatus().is5xxServerError()) { + log.error("{}: {}", ex.getClass().getSimpleName(), ex.getErrorMessage(), ex); + } else { + log.warn("{}: {}", ex.getClass().getSimpleName(), ex.getErrorMessage()); + } + return ResponseEntity.status(ex.getHttpStatus()) + .body(Map.of("error", ex.getErrorMessage())); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/DefaultExceptionHandler.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/DefaultExceptionHandler.java new file mode 100644 index 0000000..604a0e9 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/DefaultExceptionHandler.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.infrastructure.exception; + +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@Order(Ordered.LOWEST_PRECEDENCE) +@RestControllerAdvice +public class DefaultExceptionHandler { + + @ExceptionHandler(Throwable.class) + public ResponseEntity> handle(Throwable ex) { + log.error("unexpected error: {}", ex.getClass().getSimpleName(), ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "internal error")); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/HttpMessageNotReadableExceptionHandler.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/HttpMessageNotReadableExceptionHandler.java new file mode 100644 index 0000000..6588ca6 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/HttpMessageNotReadableExceptionHandler.java @@ -0,0 +1,43 @@ +/* + * 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.consumer.infrastructure.exception; + +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@Order(Ordered.HIGHEST_PRECEDENCE) +@RestControllerAdvice +public class HttpMessageNotReadableExceptionHandler { + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handle(HttpMessageNotReadableException ex) { + log.info("malformed request body: {}", ex.getClass().getSimpleName()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("error", "invalid request")); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MethodArgumentNotValidExceptionHandler.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MethodArgumentNotValidExceptionHandler.java new file mode 100644 index 0000000..9df7139 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MethodArgumentNotValidExceptionHandler.java @@ -0,0 +1,43 @@ +/* + * 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.consumer.infrastructure.exception; + +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@Order(Ordered.HIGHEST_PRECEDENCE) +@RestControllerAdvice +public class MethodArgumentNotValidExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handle(MethodArgumentNotValidException ex) { + log.info("invalid request body: {}", ex.getClass().getSimpleName()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("error", "invalid request")); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MissingRequestHeaderExceptionHandler.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MissingRequestHeaderExceptionHandler.java new file mode 100644 index 0000000..f52fd78 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MissingRequestHeaderExceptionHandler.java @@ -0,0 +1,43 @@ +/* + * 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.consumer.infrastructure.exception; + +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MissingRequestHeaderException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@Order(Ordered.HIGHEST_PRECEDENCE) +@RestControllerAdvice +public class MissingRequestHeaderExceptionHandler { + + @ExceptionHandler(MissingRequestHeaderException.class) + public ResponseEntity> handle(MissingRequestHeaderException ex) { + log.info("missing request header: {}", ex.getHeaderName()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("error", "invalid request")); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/ping/PingController.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/clients/FineractClientIdentifiersClient.java similarity index 55% rename from consumer/src/main/java/org/apache/fineract/consumer/ping/PingController.java rename to consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/clients/FineractClientIdentifiersClient.java index ddec242..e55e5cb 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/ping/PingController.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/clients/FineractClientIdentifiersClient.java @@ -12,28 +12,22 @@ * 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 + * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.consumer.ping; +package org.apache.fineract.consumer.infrastructure.fineractclient.clients; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import java.util.Map; +import java.util.List; +import org.apache.fineract.consumer.infrastructure.fineractclient.data.FineractClientIdentifierData; +import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.PathVariable; -@RestController -@RequestMapping("/api/v1/ping") -@Tag(name = "Ping", description = "Liveness probe") -public class PingController { +@FeignClient(name = "fineract-client-identifiers", url = "${fineract.client.base-url}") +public interface FineractClientIdentifiersClient { - @GetMapping - @Operation(summary = "Ping the BFF") - public Map ping() { - return Map.of("status", "ok"); - } + @GetMapping("/clients/{clientId}/identifiers") + List getIdentifiers(@PathVariable("clientId") Long clientId); } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/configs/FineractClientConfig.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/configs/FineractClientConfig.java new file mode 100644 index 0000000..2c3b049 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/configs/FineractClientConfig.java @@ -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.consumer.infrastructure.fineractclient.configs; + +import org.apache.fineract.consumer.infrastructure.fineractclient.interceptors.FineractBasicAuthInterceptor; +import org.apache.fineract.consumer.infrastructure.fineractclient.interceptors.FineractTenantHeaderInterceptor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(FineractClientProperties.class) +public class FineractClientConfig { + + @Bean + public FineractBasicAuthInterceptor fineractBasicAuthInterceptor(FineractClientProperties properties) { + return new FineractBasicAuthInterceptor(properties); + } + + @Bean + public FineractTenantHeaderInterceptor fineractTenantHeaderInterceptor(FineractClientProperties properties) { + return new FineractTenantHeaderInterceptor(properties); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/configs/FineractClientProperties.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/configs/FineractClientProperties.java new file mode 100644 index 0000000..fd7f2e6 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/configs/FineractClientProperties.java @@ -0,0 +1,35 @@ +/* + * 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.consumer.infrastructure.fineractclient.configs; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@RequiredArgsConstructor +@ConfigurationProperties("fineract.client") +public class FineractClientProperties { + + private final String baseUrl; + private final String username; + private final String password; + private final String tenantId; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/data/FineractClientIdentifierData.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/data/FineractClientIdentifierData.java new file mode 100644 index 0000000..830ea6d --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/data/FineractClientIdentifierData.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.infrastructure.fineractclient.data; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +public final class FineractClientIdentifierData { + + private final Long id; + private final Long clientId; + private final DocumentType documentType; + private final String documentKey; + private final String description; + private final String status; + + @Getter + @RequiredArgsConstructor + @EqualsAndHashCode + @ToString + public static final class DocumentType { + private final Long id; + private final String name; + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/interceptors/FineractBasicAuthInterceptor.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/interceptors/FineractBasicAuthInterceptor.java new file mode 100644 index 0000000..298162a --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/interceptors/FineractBasicAuthInterceptor.java @@ -0,0 +1,43 @@ +/* + * 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.consumer.infrastructure.fineractclient.interceptors; + +import feign.RequestInterceptor; +import feign.RequestTemplate; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import org.apache.fineract.consumer.infrastructure.fineractclient.configs.FineractClientProperties; + +public final class FineractBasicAuthInterceptor implements RequestInterceptor { + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private final String authorizationHeaderValue; + + public FineractBasicAuthInterceptor(FineractClientProperties properties) { + String credentials = properties.getUsername() + ":" + properties.getPassword(); + String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + this.authorizationHeaderValue = "Basic " + encoded; + } + + @Override + public void apply(RequestTemplate template) { + template.header(AUTHORIZATION_HEADER, authorizationHeaderValue); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/interceptors/FineractTenantHeaderInterceptor.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/interceptors/FineractTenantHeaderInterceptor.java new file mode 100644 index 0000000..65e4730 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/interceptors/FineractTenantHeaderInterceptor.java @@ -0,0 +1,37 @@ +/* + * 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.consumer.infrastructure.fineractclient.interceptors; + +import feign.RequestInterceptor; +import feign.RequestTemplate; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.consumer.infrastructure.fineractclient.configs.FineractClientProperties; + +@RequiredArgsConstructor +public class FineractTenantHeaderInterceptor implements RequestInterceptor { + + private static final String TENANT_HEADER = "Fineract-Platform-TenantId"; + private final FineractClientProperties properties; + + @Override + public void apply(RequestTemplate template) { + template.header(TENANT_HEADER, properties.getTenantId()); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/query/Query.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/query/Query.java new file mode 100644 index 0000000..6b43b56 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/query/Query.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.infrastructure.query; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.transaction.annotation.Transactional; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Transactional(readOnly = true) +public @interface Query {} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/loans/api/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/loans/api/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/loans/data/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/loans/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/loans/domain/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/loans/domain/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/loans/exception/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/loans/exception/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/loans/repository/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/loans/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/loans/service/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/loans/service/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/openbanking/api/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/openbanking/api/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/openbanking/data/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/openbanking/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/openbanking/domain/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/openbanking/domain/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/openbanking/exception/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/openbanking/exception/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/openbanking/repository/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/openbanking/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/openbanking/service/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/openbanking/service/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/api/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/registration/api/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/command/api/RegistrationCommandController.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/api/RegistrationCommandController.java new file mode 100644 index 0000000..dd7d4df --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/api/RegistrationCommandController.java @@ -0,0 +1,83 @@ +/* + * 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.consumer.registration.command.api; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.consumer.registration.command.data.SendOtpCommand; +import org.apache.fineract.consumer.registration.command.data.SendOtpCommandData; +import org.apache.fineract.consumer.registration.command.data.SendOtpCommandRequest; +import org.apache.fineract.consumer.registration.command.data.SubmitRegistrationCommand; +import org.apache.fineract.consumer.registration.command.data.SubmitRegistrationCommandData; +import org.apache.fineract.consumer.registration.command.data.SubmitRegistrationCommandRequest; +import org.apache.fineract.consumer.registration.command.data.VerifyOtpCommand; +import org.apache.fineract.consumer.registration.command.data.VerifyOtpCommandData; +import org.apache.fineract.consumer.registration.command.data.VerifyOtpCommandRequest; +import org.apache.fineract.consumer.registration.command.service.RegistrationCommandService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/registration") +@RequiredArgsConstructor +public class RegistrationCommandController { + + private final RegistrationCommandService registrationCommandService; + + @PostMapping("/submit") + public ResponseEntity submit( + @Valid @RequestBody SubmitRegistrationCommandRequest request, + @RequestHeader("X-Device-Fingerprint") String deviceFingerprint) { + SubmitRegistrationCommand command = SubmitRegistrationCommand.builder() + .fineractClientId(request.getFineractClientId()) + .email(request.getEmail()) + .documentTypeName(request.getDocumentTypeName()) + .documentKey(request.getDocumentKey()) + .deviceFingerprint(deviceFingerprint) + .build(); + SubmitRegistrationCommandData data = registrationCommandService.submit(command); + return ResponseEntity.status(HttpStatus.CREATED).body(data); + } + + @PostMapping("/otp/send") + public ResponseEntity sendOtp(@Valid @RequestBody SendOtpCommandRequest request) { + SendOtpCommand command = SendOtpCommand.builder() + .registrationId(request.getRegistrationId()) + .deliveryMethod(request.getDeliveryMethod()) + .build(); + SendOtpCommandData data = registrationCommandService.sendOtp(command); + return ResponseEntity.status(HttpStatus.CREATED).body(data); + } + + @PostMapping("/otp/verify") + public ResponseEntity verifyOtp(@Valid @RequestBody VerifyOtpCommandRequest request) { + VerifyOtpCommand command = VerifyOtpCommand.builder() + .registrationId(request.getRegistrationId()) + .token(request.getToken()) + .build(); + VerifyOtpCommandData data = registrationCommandService.verifyOtp(command); + return ResponseEntity.ok(data); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SendOtpCommand.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SendOtpCommand.java new file mode 100644 index 0000000..bc8a5b4 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SendOtpCommand.java @@ -0,0 +1,37 @@ +/* + * 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.consumer.registration.command.data; + +import java.util.UUID; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +@Builder +@EqualsAndHashCode +@ToString +public final class SendOtpCommand { + private final UUID registrationId; + private final String deliveryMethod; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SendOtpCommandData.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SendOtpCommandData.java new file mode 100644 index 0000000..411e328 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SendOtpCommandData.java @@ -0,0 +1,38 @@ +/* + * 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.consumer.registration.command.data; + +import java.time.ZonedDateTime; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +@Builder +@EqualsAndHashCode +@ToString +public final class SendOtpCommandData { + private final String sentTo; + private final ZonedDateTime expiresAt; + private final int tokenLiveTimeInSec; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/useradministration/domain/User.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SendOtpCommandRequest.java similarity index 53% rename from consumer/src/main/java/org/apache/fineract/consumer/useradministration/domain/User.java rename to consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SendOtpCommandRequest.java index 5d82bd2..bd41c79 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/useradministration/domain/User.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SendOtpCommandRequest.java @@ -12,35 +12,30 @@ * 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 + * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.consumer.useradministration.domain; +package org.apache.fineract.consumer.registration.command.data; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; +import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; -@Entity -@Table(name = "users") @Getter -@Setter -@NoArgsConstructor -public class User { +@RequiredArgsConstructor +@Builder +@ToString(onlyExplicitlyIncluded = true) +public final class SendOtpCommandRequest { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", nullable = false) - private Long id; + @NotNull + private final UUID registrationId; - @Column(name = "client_id", nullable = false) - private Long clientId; + @NotBlank + private final String deliveryMethod; } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SubmitRegistrationCommand.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SubmitRegistrationCommand.java new file mode 100644 index 0000000..d2391a8 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SubmitRegistrationCommand.java @@ -0,0 +1,38 @@ +/* + * 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.consumer.registration.command.data; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +@Builder +@ToString(onlyExplicitlyIncluded = true) +public final class SubmitRegistrationCommand { + + private final Long fineractClientId; + private final String email; + private final String documentTypeName; + private final String documentKey; + private final String deviceFingerprint; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SubmitRegistrationCommandData.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SubmitRegistrationCommandData.java new file mode 100644 index 0000000..6a71da1 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SubmitRegistrationCommandData.java @@ -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.consumer.registration.command.data; + +import java.util.UUID; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.apache.fineract.consumer.user.command.domain.UserStatus; + +@Getter +@RequiredArgsConstructor +@Builder +@EqualsAndHashCode +@ToString +public final class SubmitRegistrationCommandData { + + private final UUID registrationId; + private final UserStatus status; + private final String maskedLastFour; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SubmitRegistrationCommandRequest.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SubmitRegistrationCommandRequest.java new file mode 100644 index 0000000..7f4cce5 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/SubmitRegistrationCommandRequest.java @@ -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.consumer.registration.command.data; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +@Builder +@ToString(onlyExplicitlyIncluded = true) +public final class SubmitRegistrationCommandRequest { + + @NotNull + @Positive + private final Long fineractClientId; + + @NotBlank + @Email + private final String email; + + @NotBlank + private final String documentTypeName; + + @NotBlank + private final String documentKey; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/VerifyOtpCommand.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/VerifyOtpCommand.java new file mode 100644 index 0000000..23be90d --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/VerifyOtpCommand.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.registration.command.data; + +import java.util.UUID; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +@Builder +@ToString(onlyExplicitlyIncluded = true) +public final class VerifyOtpCommand { + + private final UUID registrationId; + private final String token; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/VerifyOtpCommandData.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/VerifyOtpCommandData.java new file mode 100644 index 0000000..4a4dbec --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/VerifyOtpCommandData.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.registration.command.data; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.apache.fineract.consumer.user.command.domain.UserStatus; + +@Getter +@RequiredArgsConstructor +@Builder +@EqualsAndHashCode +@ToString +public final class VerifyOtpCommandData { + private final UserStatus status; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/VerifyOtpCommandRequest.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/VerifyOtpCommandRequest.java new file mode 100644 index 0000000..0a55442 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/data/VerifyOtpCommandRequest.java @@ -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.consumer.registration.command.data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +@Builder +@ToString(onlyExplicitlyIncluded = true) +public final class VerifyOtpCommandRequest { + + @NotNull + private final UUID registrationId; + + @NotBlank + private final String token; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/command/exception/IdentityNotVerifiedException.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/exception/IdentityNotVerifiedException.java new file mode 100644 index 0000000..ac6363a --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/exception/IdentityNotVerifiedException.java @@ -0,0 +1,30 @@ +/* + * 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.consumer.registration.command.exception; + +import org.apache.fineract.consumer.infrastructure.exception.AbstractConsumerException; +import org.springframework.http.HttpStatus; + +public class IdentityNotVerifiedException extends AbstractConsumerException { + + public IdentityNotVerifiedException() { + super(HttpStatus.FORBIDDEN, "registration could not be completed"); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/command/service/RegistrationCommandService.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/service/RegistrationCommandService.java new file mode 100644 index 0000000..0872c8d --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/service/RegistrationCommandService.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.registration.command.service; + +import org.apache.fineract.consumer.registration.command.data.SendOtpCommand; +import org.apache.fineract.consumer.registration.command.data.SendOtpCommandData; +import org.apache.fineract.consumer.registration.command.data.SubmitRegistrationCommand; +import org.apache.fineract.consumer.registration.command.data.SubmitRegistrationCommandData; +import org.apache.fineract.consumer.registration.command.data.VerifyOtpCommand; +import org.apache.fineract.consumer.registration.command.data.VerifyOtpCommandData; + +public interface RegistrationCommandService { + + SubmitRegistrationCommandData submit(SubmitRegistrationCommand command); + + SendOtpCommandData sendOtp(SendOtpCommand command); + + VerifyOtpCommandData verifyOtp(VerifyOtpCommand command); +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/command/service/RegistrationCommandServiceImpl.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/service/RegistrationCommandServiceImpl.java new file mode 100644 index 0000000..324faa8 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/service/RegistrationCommandServiceImpl.java @@ -0,0 +1,119 @@ +/* + * 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.consumer.registration.command.service; + +import java.time.ZonedDateTime; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.consumer.infrastructure.command.Command; +import org.apache.fineract.consumer.otp.command.data.OtpDeliveryMethod; +import org.apache.fineract.consumer.otp.command.data.PendingOtp; +import org.apache.fineract.consumer.otp.command.service.OtpCommandService; +import org.apache.fineract.consumer.registration.command.data.SendOtpCommand; +import org.apache.fineract.consumer.registration.command.data.SendOtpCommandData; +import org.apache.fineract.consumer.registration.command.data.SubmitRegistrationCommand; +import org.apache.fineract.consumer.registration.command.data.SubmitRegistrationCommandData; +import org.apache.fineract.consumer.registration.command.data.VerifyOtpCommand; +import org.apache.fineract.consumer.registration.command.data.VerifyOtpCommandData; +import org.apache.fineract.consumer.registration.command.exception.IdentityNotVerifiedException; +import org.apache.fineract.consumer.registration.query.data.IdentityVerificationQueryData; +import org.apache.fineract.consumer.registration.query.data.IdentityVerificationQuery; +import org.apache.fineract.consumer.registration.query.service.IdentityQueryService; +import org.apache.fineract.consumer.user.command.data.CreateUserCommand; +import org.apache.fineract.consumer.user.command.data.UserCreatedCommandData; +import org.apache.fineract.consumer.user.command.domain.UserStatus; +import org.apache.fineract.consumer.user.command.service.UserCommandService; +import org.apache.fineract.consumer.user.query.data.UserQueryData; +import org.apache.fineract.consumer.user.query.service.UserQueryService; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class RegistrationCommandServiceImpl implements RegistrationCommandService { + + private final IdentityQueryService identityQueryService; + private final UserCommandService userCommandService; + private final UserQueryService userQueryService; + private final OtpCommandService otpCommandService; + + @Override + @Command + public SubmitRegistrationCommandData submit(SubmitRegistrationCommand command) { + IdentityVerificationQuery verification = IdentityVerificationQuery.builder() + .fineractClientId(command.getFineractClientId()) + .documentTypeName(command.getDocumentTypeName()) + .documentKey(command.getDocumentKey()) + .build(); + + IdentityVerificationQueryData verificationResult = identityQueryService.verifyIdentity(verification); + if (!verificationResult.isVerified()) { + throw new IdentityNotVerifiedException(); + } + + CreateUserCommand createUser = CreateUserCommand.builder() + .email(command.getEmail()) + .fineractClientId(command.getFineractClientId()) + .deviceFingerprint(command.getDeviceFingerprint()) + .build(); + UserCreatedCommandData createdUser = userCommandService.create(createUser); + + return SubmitRegistrationCommandData.builder() + .registrationId(createdUser.getExternalId()) + .status(UserStatus.PENDING_OTP) + .maskedLastFour(verificationResult.getMaskedLastFour()) + .build(); + } + + @Override + @Command + public SendOtpCommandData sendOtp(SendOtpCommand command) { + UserQueryData user = userQueryService.findByExternalId(command.getRegistrationId()); + OtpDeliveryMethod method = OtpDeliveryMethod.builder() + .name(command.getDeliveryMethod()) + .target(user.getEmail()) + .build(); + PendingOtp request = otpCommandService.createOtp(user.getExternalId(), method); + ZonedDateTime expiresAt = request.getMetadata().getRequestTime() + .plusSeconds(request.getMetadata().getTokenLiveTimeInSec()); + return SendOtpCommandData.builder() + .sentTo(maskEmail(user.getEmail())) + .expiresAt(expiresAt) + .tokenLiveTimeInSec(request.getMetadata().getTokenLiveTimeInSec()) + .build(); + } + + @Override + @Command + public VerifyOtpCommandData verifyOtp(VerifyOtpCommand command) { + UserQueryData user = userQueryService.findByExternalId(command.getRegistrationId()); + otpCommandService.validateOtp(user.getExternalId(), command.getToken()); + userCommandService.markOtpVerified(user.getId()); + return VerifyOtpCommandData.builder() + .status(UserStatus.PENDING_2FA) + .build(); + } + + private static String maskEmail(String email) { + int at = email.indexOf('@'); + if (at < 1) { + return "***"; + } + return email.charAt(0) + "***" + email.substring(at); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/data/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/registration/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/domain/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/registration/domain/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/exception/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/registration/exception/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/query/data/IdentityVerificationQuery.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/query/data/IdentityVerificationQuery.java new file mode 100644 index 0000000..b4d9b4a --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/query/data/IdentityVerificationQuery.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.registration.query.data; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +@Builder +@ToString(onlyExplicitlyIncluded = true) +public final class IdentityVerificationQuery { + + private final Long fineractClientId; + private final String documentTypeName; + private final String documentKey; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/query/data/IdentityVerificationQueryData.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/query/data/IdentityVerificationQueryData.java new file mode 100644 index 0000000..2cb42f8 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/query/data/IdentityVerificationQueryData.java @@ -0,0 +1,43 @@ +/* + * 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.consumer.registration.query.data; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +@EqualsAndHashCode +@ToString +public final class IdentityVerificationQueryData { + + private final boolean verified; + private final String maskedLastFour; + + public static IdentityVerificationQueryData denied() { + return new IdentityVerificationQueryData(false, null); + } + + public static IdentityVerificationQueryData verified(String maskedLastFour) { + return new IdentityVerificationQueryData(true, maskedLastFour); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/query/exception/IdentityVerificationException.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/query/exception/IdentityVerificationException.java new file mode 100644 index 0000000..ef7050a --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/query/exception/IdentityVerificationException.java @@ -0,0 +1,30 @@ +/* + * 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.consumer.registration.query.exception; + +import org.apache.fineract.consumer.infrastructure.exception.AbstractConsumerException; +import org.springframework.http.HttpStatus; + +public class IdentityVerificationException extends AbstractConsumerException { + + public IdentityVerificationException(Throwable cause) { + super(HttpStatus.BAD_GATEWAY, "identity verification temporarily unavailable", cause); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/query/service/IdentityQueryService.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/query/service/IdentityQueryService.java new file mode 100644 index 0000000..99ecc59 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/query/service/IdentityQueryService.java @@ -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.consumer.registration.query.service; + +import org.apache.fineract.consumer.registration.query.data.IdentityVerificationQuery; +import org.apache.fineract.consumer.registration.query.data.IdentityVerificationQueryData; + +public interface IdentityQueryService { + + IdentityVerificationQueryData verifyIdentity(IdentityVerificationQuery query); +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/query/service/IdentityQueryServiceImpl.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/query/service/IdentityQueryServiceImpl.java new file mode 100644 index 0000000..a98f042 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/query/service/IdentityQueryServiceImpl.java @@ -0,0 +1,79 @@ +/* + * 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.consumer.registration.query.service; + +import feign.FeignException; +import java.util.List; +import java.util.function.Predicate; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.consumer.infrastructure.fineractclient.clients.FineractClientIdentifiersClient; +import org.apache.fineract.consumer.infrastructure.fineractclient.data.FineractClientIdentifierData; +import org.apache.fineract.consumer.infrastructure.query.Query; +import org.apache.fineract.consumer.registration.query.data.IdentityVerificationQuery; +import org.apache.fineract.consumer.registration.query.data.IdentityVerificationQueryData; +import org.apache.fineract.consumer.registration.query.exception.IdentityVerificationException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class IdentityQueryServiceImpl implements IdentityQueryService { + + private final FineractClientIdentifiersClient identifiersClient; + + @Override + @Query + public IdentityVerificationQueryData verifyIdentity(IdentityVerificationQuery query) { + return fetchIdentifiers(query.getFineractClientId()).stream() + .filter(matchesDocumentType(query.getDocumentTypeName())) + .filter(matchesNormalizedKey(query.getDocumentKey())) + .findFirst() + .map(i -> IdentityVerificationQueryData.verified(lastFour(i.getDocumentKey()))) + .orElseGet(IdentityVerificationQueryData::denied); + } + + private List fetchIdentifiers(Long clientId) { + try { + List result = identifiersClient.getIdentifiers(clientId); + return result == null ? List.of() : result; + } catch (FeignException.NotFound e) { + return List.of(); + } catch (FeignException e) { + throw new IdentityVerificationException(e); + } + } + + private static Predicate matchesDocumentType(String typeName) { + return i -> i.getDocumentType() != null && typeName.equals(i.getDocumentType().getName()); + } + + private static Predicate matchesNormalizedKey(String documentKey) { + String target = normalize(documentKey); + return i -> i.getDocumentKey() != null && normalize(i.getDocumentKey()).equals(target); + } + + private static String normalize(String input) { + return input.trim().replace("-", "").replace(" ", "").toUpperCase(); + } + + private static String lastFour(String value) { + String normalized = normalize(value); + return normalized.length() < 4 ? null : normalized.substring(normalized.length() - 4); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/repository/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/registration/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/service/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/registration/service/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/savings/api/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/savings/api/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/savings/data/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/savings/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/savings/domain/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/savings/domain/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/savings/exception/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/savings/exception/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/savings/repository/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/savings/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/savings/service/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/savings/service/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/command/data/CreateUserCommand.java b/consumer/src/main/java/org/apache/fineract/consumer/user/command/data/CreateUserCommand.java new file mode 100644 index 0000000..4f19c0e --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/command/data/CreateUserCommand.java @@ -0,0 +1,37 @@ +/* + * 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.consumer.user.command.data; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +@Builder +@EqualsAndHashCode +@ToString +public final class CreateUserCommand { + private final String email; + private final Long fineractClientId; + private final String deviceFingerprint; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/command/data/UserCreatedCommandData.java b/consumer/src/main/java/org/apache/fineract/consumer/user/command/data/UserCreatedCommandData.java new file mode 100644 index 0000000..ac725be --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/command/data/UserCreatedCommandData.java @@ -0,0 +1,37 @@ +/* + * 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.consumer.user.command.data; + +import java.util.UUID; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +@Builder +@EqualsAndHashCode +@ToString +public final class UserCreatedCommandData { + private final Long userId; + private final UUID externalId; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/command/domain/User.java b/consumer/src/main/java/org/apache/fineract/consumer/user/command/domain/User.java new file mode 100644 index 0000000..cc9766e --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/command/domain/User.java @@ -0,0 +1,105 @@ +/* + * 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.consumer.user.command.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.Instant; +import java.util.UUID; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.apache.fineract.consumer.user.command.exception.InvalidBindingStateException; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +@Entity +@Table(name = "users") +@Getter +@NoArgsConstructor +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "external_id", nullable = false, unique = true, updatable = false) + private UUID externalId; + + @Column(name = "email", nullable = false, unique = true) + private String email; + + @Column(name = "client_id", nullable = false, unique = true) + private Long fineractClientId; + + @Column(name = "status", nullable = false, columnDefinition = "user_status") + @Enumerated(EnumType.STRING) + @JdbcTypeCode(SqlTypes.NAMED_ENUM) + private UserStatus status; + + @Column(name = "bound_at") + private Instant boundAt; + + @Column(name = "device_fingerprint", nullable = false) + private String deviceFingerprint; + + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt; + + @Column(name = "updated_at", nullable = false) + private Instant updatedAt; + + public static User createPendingOtp(UUID externalId, String email, Long fineractClientId, String deviceFingerprint) { + Instant now = Instant.now(); + User user = new User(); + user.externalId = externalId; + user.email = email; + user.fineractClientId = fineractClientId; + user.status = UserStatus.PENDING_OTP; + user.deviceFingerprint = deviceFingerprint; + user.createdAt = now; + user.updatedAt = now; + return user; + } + + public void markOtpVerified() { + if (status != UserStatus.PENDING_OTP) { + throw new InvalidBindingStateException(); + } + status = UserStatus.PENDING_2FA; + updatedAt = Instant.now(); + } + + public void completeBinding() { + if (status != UserStatus.PENDING_2FA) { + throw new InvalidBindingStateException(); + } + Instant now = Instant.now(); + status = UserStatus.BOUND; + boundAt = now; + updatedAt = now; + } +} diff --git a/consumer/src/test/java/org/apache/fineract/consumer/ConsumerApplicationTests.java b/consumer/src/main/java/org/apache/fineract/consumer/user/command/domain/UserStatus.java similarity index 72% rename from consumer/src/test/java/org/apache/fineract/consumer/ConsumerApplicationTests.java rename to consumer/src/main/java/org/apache/fineract/consumer/user/command/domain/UserStatus.java index 34bf77a..062f740 100644 --- a/consumer/src/test/java/org/apache/fineract/consumer/ConsumerApplicationTests.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/command/domain/UserStatus.java @@ -12,21 +12,15 @@ * 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 + * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.consumer; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ConsumerApplicationTests { - - @Test - void contextLoads() { - } +package org.apache.fineract.consumer.user.command.domain; +public enum UserStatus { + PENDING_OTP, + PENDING_2FA, + BOUND } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/InvalidBindingStateException.java b/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/InvalidBindingStateException.java new file mode 100644 index 0000000..d25b2fd --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/InvalidBindingStateException.java @@ -0,0 +1,30 @@ +/* + * 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.consumer.user.command.exception; + +import org.apache.fineract.consumer.infrastructure.exception.AbstractConsumerException; +import org.springframework.http.HttpStatus; + +public class InvalidBindingStateException extends AbstractConsumerException { + + public InvalidBindingStateException() { + super(HttpStatus.CONFLICT, "user is not in the expected binding state"); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserAlreadyExistsException.java b/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserAlreadyExistsException.java new file mode 100644 index 0000000..8fb511e --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserAlreadyExistsException.java @@ -0,0 +1,30 @@ +/* + * 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.consumer.user.command.exception; + +import org.apache.fineract.consumer.infrastructure.exception.AbstractConsumerException; +import org.springframework.http.HttpStatus; + +public class UserAlreadyExistsException extends AbstractConsumerException { + + public UserAlreadyExistsException() { + super(HttpStatus.CONFLICT, "user already exists for this email or fineract client"); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserNotFoundException.java b/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserNotFoundException.java new file mode 100644 index 0000000..f017386 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserNotFoundException.java @@ -0,0 +1,30 @@ +/* + * 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.consumer.user.command.exception; + +import org.apache.fineract.consumer.infrastructure.exception.AbstractConsumerException; +import org.springframework.http.HttpStatus; + +public class UserNotFoundException extends AbstractConsumerException { + + public UserNotFoundException() { + super(HttpStatus.NOT_FOUND, "user not found"); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/command/repository/UserCommandRepository.java b/consumer/src/main/java/org/apache/fineract/consumer/user/command/repository/UserCommandRepository.java new file mode 100644 index 0000000..3625d82 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/command/repository/UserCommandRepository.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.user.command.repository; + +import java.util.Optional; +import org.apache.fineract.consumer.user.command.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserCommandRepository extends JpaRepository { + + Optional findByEmail(String email); + + Optional findByFineractClientId(Long fineractClientId); +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/command/service/UserCommandService.java b/consumer/src/main/java/org/apache/fineract/consumer/user/command/service/UserCommandService.java new file mode 100644 index 0000000..8ec34d8 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/command/service/UserCommandService.java @@ -0,0 +1,32 @@ +/* + * 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.consumer.user.command.service; + +import org.apache.fineract.consumer.user.command.data.CreateUserCommand; +import org.apache.fineract.consumer.user.command.data.UserCreatedCommandData; + +public interface UserCommandService { + + UserCreatedCommandData create(CreateUserCommand command); + + void markOtpVerified(Long userId); + + void completeBinding(Long userId); +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/command/service/UserCommandServiceImpl.java b/consumer/src/main/java/org/apache/fineract/consumer/user/command/service/UserCommandServiceImpl.java new file mode 100644 index 0000000..8c3bbaf --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/command/service/UserCommandServiceImpl.java @@ -0,0 +1,81 @@ +/* + * 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.consumer.user.command.service; + +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.consumer.infrastructure.command.Command; +import org.apache.fineract.consumer.user.command.data.CreateUserCommand; +import org.apache.fineract.consumer.user.command.data.UserCreatedCommandData; +import org.apache.fineract.consumer.user.command.domain.User; +import org.apache.fineract.consumer.user.command.exception.UserAlreadyExistsException; +import org.apache.fineract.consumer.user.command.exception.UserNotFoundException; +import org.apache.fineract.consumer.user.command.repository.UserCommandRepository; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserCommandServiceImpl implements UserCommandService { + + private final UserCommandRepository repository; + + @Override + @Command + public UserCreatedCommandData create(CreateUserCommand command) { + repository.findByEmail(command.getEmail()).ifPresent(u -> { + throw new UserAlreadyExistsException(); + }); + repository.findByFineractClientId(command.getFineractClientId()).ifPresent(u -> { + throw new UserAlreadyExistsException(); + }); + User user = User.createPendingOtp( + UUID.randomUUID(), + command.getEmail(), + command.getFineractClientId(), + command.getDeviceFingerprint()); + User saved; + try { + saved = repository.save(user); + } catch (DataIntegrityViolationException e) { + throw new UserAlreadyExistsException(); + } + return UserCreatedCommandData.builder() + .userId(saved.getId()) + .externalId(saved.getExternalId()) + .build(); + } + + @Override + @Command + public void markOtpVerified(Long userId) { + User user = repository.findById(userId).orElseThrow(UserNotFoundException::new); + user.markOtpVerified(); + repository.save(user); + } + + @Override + @Command + public void completeBinding(Long userId) { + User user = repository.findById(userId).orElseThrow(UserNotFoundException::new); + user.completeBinding(); + repository.save(user); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/query/data/UserQueryData.java b/consumer/src/main/java/org/apache/fineract/consumer/user/query/data/UserQueryData.java new file mode 100644 index 0000000..b285030 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/query/data/UserQueryData.java @@ -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.consumer.user.query.data; + +import java.util.UUID; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.apache.fineract.consumer.user.command.domain.UserStatus; + +@Getter +@RequiredArgsConstructor +@Builder +@EqualsAndHashCode +@ToString +public final class UserQueryData { + private final Long id; + private final UUID externalId; + private final String email; + private final UserStatus status; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/query/repository/UserQueryRepository.java b/consumer/src/main/java/org/apache/fineract/consumer/user/query/repository/UserQueryRepository.java new file mode 100644 index 0000000..b459ef1 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/query/repository/UserQueryRepository.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.user.query.repository; + +import java.util.Optional; +import java.util.UUID; +import org.apache.fineract.consumer.user.command.domain.User; +import org.apache.fineract.consumer.user.query.data.UserQueryData; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; + +public interface UserQueryRepository extends Repository { + + @Query("SELECT new org.apache.fineract.consumer.user.query.data.UserQueryData(u.id, u.externalId, u.email, u.status) " + + "FROM User u WHERE u.externalId = :externalId") + Optional findByExternalId(UUID externalId); +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/query/service/UserQueryService.java b/consumer/src/main/java/org/apache/fineract/consumer/user/query/service/UserQueryService.java new file mode 100644 index 0000000..09e7e3b --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/query/service/UserQueryService.java @@ -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.consumer.user.query.service; + +import java.util.UUID; +import org.apache.fineract.consumer.user.query.data.UserQueryData; + +public interface UserQueryService { + + UserQueryData findByExternalId(UUID externalId); +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/query/service/UserQueryServiceImpl.java b/consumer/src/main/java/org/apache/fineract/consumer/user/query/service/UserQueryServiceImpl.java new file mode 100644 index 0000000..e5f5e22 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/query/service/UserQueryServiceImpl.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.user.query.service; + +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.consumer.infrastructure.query.Query; +import org.apache.fineract.consumer.user.command.exception.UserNotFoundException; +import org.apache.fineract.consumer.user.query.data.UserQueryData; +import org.apache.fineract.consumer.user.query.repository.UserQueryRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserQueryServiceImpl implements UserQueryService { + + private final UserQueryRepository repository; + + @Override + @Query + public UserQueryData findByExternalId(UUID externalId) { + return repository.findByExternalId(externalId) + .orElseThrow(UserNotFoundException::new); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/useradministration/api/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/useradministration/api/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/useradministration/data/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/useradministration/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/useradministration/exception/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/useradministration/exception/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/useradministration/repository/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/useradministration/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/java/org/apache/fineract/consumer/useradministration/service/.gitkeep b/consumer/src/main/java/org/apache/fineract/consumer/useradministration/service/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/consumer/src/main/resources/application.properties b/consumer/src/main/resources/application.properties index e3b218d..541a18e 100644 --- a/consumer/src/main/resources/application.properties +++ b/consumer/src/main/resources/application.properties @@ -15,8 +15,6 @@ # specific language governing permissions and limitations # under the License. -# Apache Software Foundation -# spring.application.name=consumer spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/consumerapp} @@ -25,6 +23,17 @@ spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:password} spring.jpa.hibernate.ddl-auto=validate -# Honor X-Forwarded-* headers from upstream reverse proxies (nginx in docker-compose.e2e.yml). -# Without this, Spring treats requests as plain HTTP even when the original was HTTPS through nginx. +# Honor X-Forwarded-* headers from upstream reverse proxies (nginx in docker-compose.e2e.yml) for HTTPS. server.forward-headers-strategy=framework + +# Fineract service account variables +fineract.client.base-url=${FINERACT_BASE_URL:http://localhost:8888/fineract-provider/api/v1} +fineract.client.username=${FINERACT_SERVICE_USERNAME:mifos} +fineract.client.password=${FINERACT_SERVICE_PASSWORD:password} +fineract.client.tenant-id=${FINERACT_SERVICE_TENANT:default} + +# OTP email delivery — Mailpit in docker-compose; institution SMTP in prod +spring.mail.host=${SPRING_MAIL_HOST:localhost} +spring.mail.port=${SPRING_MAIL_PORT:1025} +otp.email.from=${OTP_EMAIL_FROM:no-reply@fineract-consumer.local} +otp.email.subject=${OTP_EMAIL_SUBJECT:Your verification code} diff --git a/consumer/src/main/resources/db/changelog/changes/001-create-users-table.yaml b/consumer/src/main/resources/db/changelog/changes/001-create-users-table.yaml index a776b2e..3f01e23 100644 --- a/consumer/src/main/resources/db/changelog/changes/001-create-users-table.yaml +++ b/consumer/src/main/resources/db/changelog/changes/001-create-users-table.yaml @@ -15,13 +15,13 @@ # specific language governing permissions and limitations # under the License. -# Apache Software Foundation -# databaseChangeLog: - changeSet: id: 001-create-users-table author: fineract-consumer-facing changes: + - sql: + sql: "CREATE TYPE user_status AS ENUM ('PENDING_OTP', 'PENDING_2FA', 'BOUND')" - createTable: tableName: users columns: @@ -32,8 +32,49 @@ databaseChangeLog: constraints: primaryKey: true nullable: false + - column: + name: external_id + type: UUID + constraints: + nullable: false + unique: true + - column: + name: email + type: VARCHAR(255) + constraints: + nullable: false + unique: true - column: name: client_id type: BIGINT constraints: nullable: false + unique: true + - column: + name: status + type: user_status + constraints: + nullable: false + - column: + name: bound_at + type: TIMESTAMP WITH TIME ZONE + - column: + name: device_fingerprint + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: created_at + type: TIMESTAMP WITH TIME ZONE + constraints: + nullable: false + - column: + name: updated_at + type: TIMESTAMP WITH TIME ZONE + constraints: + nullable: false + rollback: + - dropTable: + tableName: users + - sql: + sql: "DROP TYPE user_status" From b5eeb7f46f700df2ba772995e1d76ba22df972f1 Mon Sep 17 00:00:00 2001 From: edk12564 Date: Mon, 8 Jun 2026 09:55:22 -0500 Subject: [PATCH 2/4] FINERACT-2634: Implement Registration Feature - added cucumber tests for some happy paths - added otp feature service - added cucumber infrastructure for mailpit client --- .../otp/command/data/OtpConstants.java | 29 +++ .../otp/command/data/OtpDeliveryMethod.java | 36 ++++ .../otp/command/data/OtpMetadata.java | 38 ++++ .../consumer/otp/command/data/PendingOtp.java | 50 +++++ .../exception/OtpDeliveryFailedException.java | 30 +++ .../OtpDeliveryMethodInvalidException.java | 30 +++ .../exception/OtpTokenInvalidException.java | 30 +++ .../repository/OtpCommandRepository.java | 48 +++++ .../command/service/OtpCommandService.java | 31 +++ .../service/OtpCommandServiceImpl.java | 80 ++++++++ .../service/OtpEmailDeliveryService.java | 57 +++++ .../clients/FineractClientSeedClient.java | 31 +++ .../clients/FineractCodeLookupClient.java | 34 +++ .../clients/FineractIdentifierSeedClient.java | 32 +++ .../cucumber/helpers/FineractSeeder.java | 121 +++++++++++ .../cucumber/helpers/MailpitProbe.java | 109 ++++++++++ .../consumer/cucumber/steps/HealthSteps.java | 62 ------ .../cucumber/steps/RegistrationSteps.java | 194 ++++++++++++++++++ .../test/resources/features/health.feature | 23 --- .../resources/features/registration.feature | 46 +++++ 20 files changed, 1026 insertions(+), 85 deletions(-) create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/OtpConstants.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/OtpDeliveryMethod.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/OtpMetadata.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/PendingOtp.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryFailedException.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryMethodInvalidException.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpTokenInvalidException.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/otp/command/repository/OtpCommandRepository.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/otp/command/service/OtpCommandService.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/otp/command/service/OtpCommandServiceImpl.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/otp/command/service/OtpEmailDeliveryService.java create mode 100644 consumer/src/test/java/org/apache/fineract/consumer/cucumber/clients/FineractClientSeedClient.java create mode 100644 consumer/src/test/java/org/apache/fineract/consumer/cucumber/clients/FineractCodeLookupClient.java create mode 100644 consumer/src/test/java/org/apache/fineract/consumer/cucumber/clients/FineractIdentifierSeedClient.java create mode 100644 consumer/src/test/java/org/apache/fineract/consumer/cucumber/helpers/FineractSeeder.java create mode 100644 consumer/src/test/java/org/apache/fineract/consumer/cucumber/helpers/MailpitProbe.java delete mode 100644 consumer/src/test/java/org/apache/fineract/consumer/cucumber/steps/HealthSteps.java create mode 100644 consumer/src/test/java/org/apache/fineract/consumer/cucumber/steps/RegistrationSteps.java delete mode 100644 consumer/src/test/resources/features/health.feature create mode 100644 consumer/src/test/resources/features/registration.feature diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/OtpConstants.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/OtpConstants.java new file mode 100644 index 0000000..3cac095 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/OtpConstants.java @@ -0,0 +1,29 @@ +/* + * 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.consumer.otp.command.data; + +public final class OtpConstants { + + private OtpConstants() { + } + + public static final String EMAIL_DELIVERY_METHOD_NAME = "email"; + public static final String SMS_DELIVERY_METHOD_NAME = "sms"; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/OtpDeliveryMethod.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/OtpDeliveryMethod.java new file mode 100644 index 0000000..ff19fb2 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/OtpDeliveryMethod.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.otp.command.data; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +@Builder +@EqualsAndHashCode +@ToString +public final class OtpDeliveryMethod { + private final String name; + private final String target; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/OtpMetadata.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/OtpMetadata.java new file mode 100644 index 0000000..4026139 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/OtpMetadata.java @@ -0,0 +1,38 @@ +/* + * 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.consumer.otp.command.data; + +import java.time.ZonedDateTime; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +@Builder +@EqualsAndHashCode +@ToString +public final class OtpMetadata { + private final ZonedDateTime requestTime; + private final int tokenLiveTimeInSec; + private final OtpDeliveryMethod deliveryMethod; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/PendingOtp.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/PendingOtp.java new file mode 100644 index 0000000..e3fe09d --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/data/PendingOtp.java @@ -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.consumer.otp.command.data; + +import java.time.ZonedDateTime; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +@EqualsAndHashCode +@ToString +public final class PendingOtp { + + private final String token; + private final OtpMetadata metadata; + + public static PendingOtp create(String token, int tokenLiveTimeInSec, OtpDeliveryMethod deliveryMethod) { + OtpMetadata metadata = OtpMetadata.builder() + .requestTime(ZonedDateTime.now()) + .tokenLiveTimeInSec(tokenLiveTimeInSec) + .deliveryMethod(deliveryMethod) + .build(); + return new PendingOtp(token, metadata); + } + + public boolean isValid() { + ZonedDateTime expireTime = metadata.getRequestTime().plusSeconds(metadata.getTokenLiveTimeInSec()); + return ZonedDateTime.now().isBefore(expireTime); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryFailedException.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryFailedException.java new file mode 100644 index 0000000..024e65b --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryFailedException.java @@ -0,0 +1,30 @@ +/* + * 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.consumer.otp.command.exception; + +import org.apache.fineract.consumer.infrastructure.exception.AbstractConsumerException; +import org.springframework.http.HttpStatus; + +public class OtpDeliveryFailedException extends AbstractConsumerException { + + public OtpDeliveryFailedException(Throwable cause) { + super(HttpStatus.BAD_GATEWAY, "verification code could not be sent", cause); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryMethodInvalidException.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryMethodInvalidException.java new file mode 100644 index 0000000..e037686 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryMethodInvalidException.java @@ -0,0 +1,30 @@ +/* + * 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.consumer.otp.command.exception; + +import org.apache.fineract.consumer.infrastructure.exception.AbstractConsumerException; +import org.springframework.http.HttpStatus; + +public class OtpDeliveryMethodInvalidException extends AbstractConsumerException { + + public OtpDeliveryMethodInvalidException() { + super(HttpStatus.BAD_REQUEST, "unsupported delivery method"); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpTokenInvalidException.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpTokenInvalidException.java new file mode 100644 index 0000000..114d962 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpTokenInvalidException.java @@ -0,0 +1,30 @@ +/* + * 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.consumer.otp.command.exception; + +import org.apache.fineract.consumer.infrastructure.exception.AbstractConsumerException; +import org.springframework.http.HttpStatus; + +public class OtpTokenInvalidException extends AbstractConsumerException { + + public OtpTokenInvalidException() { + super(HttpStatus.BAD_REQUEST, "invalid or expired otp"); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/repository/OtpCommandRepository.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/repository/OtpCommandRepository.java new file mode 100644 index 0000000..65793e7 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/repository/OtpCommandRepository.java @@ -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.consumer.otp.command.repository; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.fineract.consumer.otp.command.data.PendingOtp; +import org.springframework.stereotype.Repository; +import org.springframework.util.Assert; + +@Repository +public class OtpCommandRepository { + + private final ConcurrentHashMap store = new ConcurrentHashMap<>(); + + public PendingOtp getPendingOtpForUser(UUID externalId) { + Assert.notNull(externalId, "externalId must not be null"); + return store.get(externalId); + } + + public void addPendingOtp(UUID externalId, PendingOtp request) { + Assert.notNull(externalId, "externalId must not be null"); + Assert.notNull(request, "request must not be null"); + store.put(externalId, request); + } + + public void deletePendingOtpForUser(UUID externalId) { + Assert.notNull(externalId, "externalId must not be null"); + store.remove(externalId); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/service/OtpCommandService.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/service/OtpCommandService.java new file mode 100644 index 0000000..4ba95c8 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/service/OtpCommandService.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.otp.command.service; + +import java.util.UUID; +import org.apache.fineract.consumer.otp.command.data.OtpDeliveryMethod; +import org.apache.fineract.consumer.otp.command.data.PendingOtp; + +public interface OtpCommandService { + + PendingOtp createOtp(UUID externalId, OtpDeliveryMethod method); + + void validateOtp(UUID externalId, String token); +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/service/OtpCommandServiceImpl.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/service/OtpCommandServiceImpl.java new file mode 100644 index 0000000..13a0bf9 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/service/OtpCommandServiceImpl.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.otp.command.service; + +import java.security.SecureRandom; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.consumer.infrastructure.command.Command; +import org.apache.fineract.consumer.otp.command.data.OtpConstants; +import org.apache.fineract.consumer.otp.command.data.OtpDeliveryMethod; +import org.apache.fineract.consumer.otp.command.data.PendingOtp; +import org.apache.fineract.consumer.otp.command.exception.OtpDeliveryMethodInvalidException; +import org.apache.fineract.consumer.otp.command.exception.OtpTokenInvalidException; +import org.apache.fineract.consumer.otp.command.repository.OtpCommandRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class OtpCommandServiceImpl implements OtpCommandService { + + private static final int OTP_LENGTH = 6; + private static final int OTP_TTL_SECONDS = 300; + private static final String ALLOWED_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final SecureRandom RANDOM = new SecureRandom(); + + private final OtpCommandRepository otpCommandRepository; + private final OtpEmailDeliveryService otpEmailDeliveryService; + + @Override + @Command + public PendingOtp createOtp(UUID externalId, OtpDeliveryMethod method) { + if (OtpConstants.EMAIL_DELIVERY_METHOD_NAME.equalsIgnoreCase(method.getName())) { + PendingOtp request = generateNewToken(method); + otpEmailDeliveryService.deliver(method.getTarget(), request.getToken()); + otpCommandRepository.addPendingOtp(externalId, request); + return request; + } + throw new OtpDeliveryMethodInvalidException(); + } + + @Override + @Command + public void validateOtp(UUID externalId, String token) { + PendingOtp otpRequest = otpCommandRepository.getPendingOtpForUser(externalId); + if (otpRequest == null || !otpRequest.isValid() || !otpRequest.getToken().equalsIgnoreCase(token)) { + throw new OtpTokenInvalidException(); + } + otpCommandRepository.deletePendingOtpForUser(externalId); + } + + private PendingOtp generateNewToken(OtpDeliveryMethod deliveryMethod) { + String token = generateToken(OTP_LENGTH); + return PendingOtp.create(token, OTP_TTL_SECONDS, deliveryMethod); + } + + private static String generateToken(int length) { + StringBuilder builder = new StringBuilder(length); + for (int i = 0; i < length; i++) { + builder.append(ALLOWED_CHARS.charAt(RANDOM.nextInt(ALLOWED_CHARS.length()))); + } + return builder.toString(); + } +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/service/OtpEmailDeliveryService.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/service/OtpEmailDeliveryService.java new file mode 100644 index 0000000..533a27b --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/service/OtpEmailDeliveryService.java @@ -0,0 +1,57 @@ +/* + * 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.consumer.otp.command.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.consumer.otp.command.exception.OtpDeliveryFailedException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.MailException; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class OtpEmailDeliveryService { + + private final JavaMailSender javaMailSender; + + @Value("${otp.email.from}") + private String fromAddress; + + @Value("${otp.email.subject}") + private String subject; + + public void deliver(String target, String token) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(fromAddress); + message.setTo(target); + message.setSubject(subject); + message.setText("Your verification code is: " + token + + "\n\nThis code expires in 5 minutes."); + try { + javaMailSender.send(message); + } catch (MailException e) { + throw new OtpDeliveryFailedException(e); + } + } +} diff --git a/consumer/src/test/java/org/apache/fineract/consumer/cucumber/clients/FineractClientSeedClient.java b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/clients/FineractClientSeedClient.java new file mode 100644 index 0000000..9cad29e --- /dev/null +++ b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/clients/FineractClientSeedClient.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.cucumber.clients; + +import feign.Headers; +import feign.RequestLine; +import java.util.Map; + +public interface FineractClientSeedClient { + + @RequestLine("POST /clients") + @Headers("Content-Type: application/json") + Map createClient(Map body); +} diff --git a/consumer/src/test/java/org/apache/fineract/consumer/cucumber/clients/FineractCodeLookupClient.java b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/clients/FineractCodeLookupClient.java new file mode 100644 index 0000000..9ff575c --- /dev/null +++ b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/clients/FineractCodeLookupClient.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.consumer.cucumber.clients; + +import feign.Param; +import feign.RequestLine; +import java.util.List; +import java.util.Map; + +public interface FineractCodeLookupClient { + + @RequestLine("GET /codes") + List> listCodes(); + + @RequestLine("GET /codes/{codeId}/codevalues") + List> listCodeValues(@Param("codeId") long codeId); +} diff --git a/consumer/src/test/java/org/apache/fineract/consumer/cucumber/clients/FineractIdentifierSeedClient.java b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/clients/FineractIdentifierSeedClient.java new file mode 100644 index 0000000..f26f2cd --- /dev/null +++ b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/clients/FineractIdentifierSeedClient.java @@ -0,0 +1,32 @@ +/* + * 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.consumer.cucumber.clients; + +import feign.Headers; +import feign.Param; +import feign.RequestLine; +import java.util.Map; + +public interface FineractIdentifierSeedClient { + + @RequestLine("POST /clients/{clientId}/identifiers") + @Headers("Content-Type: application/json") + Map createIdentifier(@Param("clientId") long clientId, Map body); +} diff --git a/consumer/src/test/java/org/apache/fineract/consumer/cucumber/helpers/FineractSeeder.java b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/helpers/FineractSeeder.java new file mode 100644 index 0000000..b32c71b --- /dev/null +++ b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/helpers/FineractSeeder.java @@ -0,0 +1,121 @@ +/* + * 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.consumer.cucumber.helpers; + +import feign.Feign; +import feign.RequestInterceptor; +import feign.auth.BasicAuthRequestInterceptor; +import feign.jackson.JacksonDecoder; +import feign.jackson.JacksonEncoder; +import feign.okhttp.OkHttpClient; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.apache.fineract.consumer.cucumber.clients.FineractClientSeedClient; +import org.apache.fineract.consumer.cucumber.clients.FineractCodeLookupClient; +import org.apache.fineract.consumer.cucumber.clients.FineractIdentifierSeedClient; + +public class FineractSeeder { + + private static final String BASE_URL = System.getenv().getOrDefault( + "FINERACT_BASE_URL", "http://localhost:8888/fineract-provider/api/v1"); + private static final String USERNAME = System.getenv().getOrDefault("FINERACT_USERNAME", "mifos"); + private static final String PASSWORD = System.getenv().getOrDefault("FINERACT_PASSWORD", "password"); + private static final String TENANT = System.getenv().getOrDefault("FINERACT_TENANT", "default"); + private static final long HEAD_OFFICE_ID = 1L; + private static final String CUSTOMER_IDENTIFIER_CODE = "Customer Identifier"; + private static final String PASSPORT_DOCUMENT_TYPE = "Passport"; + + private static final FineractClientSeedClient CLIENTS = buildFeignClient(FineractClientSeedClient.class); + private static final FineractIdentifierSeedClient IDENTIFIERS = buildFeignClient(FineractIdentifierSeedClient.class); + private static final FineractCodeLookupClient CODES = buildFeignClient(FineractCodeLookupClient.class); + + private static volatile Long cachedPassportCodeValueId; + + public record SeededClient(long fineractClientId, String documentTypeName, String documentKey) {} + + public SeededClient seedClientWithPassport() { + long clientId = createClient(); + String documentKey = "PASS-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + attachPassportIdentifier(clientId, documentKey); + return new SeededClient(clientId, PASSPORT_DOCUMENT_TYPE, documentKey); + } + + private long createClient() { + String suffix = UUID.randomUUID().toString().substring(0, 8); + Map body = Map.of( + "officeId", HEAD_OFFICE_ID, + "firstname", "Test", + "lastname", "User-" + suffix, + "active", false, + "legalFormId", 1, + "locale", "en", + "dateFormat", "dd MMMM yyyy"); + Map response = CLIENTS.createClient(body); + return ((Number) response.get("clientId")).longValue(); + } + + private void attachPassportIdentifier(long clientId, String documentKey) { + long passportId = resolvePassportCodeValueId(); + Map body = Map.of( + "documentTypeId", passportId, + "documentKey", documentKey, + "status", "ACTIVE"); + IDENTIFIERS.createIdentifier(clientId, body); + } + + private long resolvePassportCodeValueId() { + Long cached = cachedPassportCodeValueId; + if (cached != null) { + return cached; + } + synchronized (FineractSeeder.class) { + if (cachedPassportCodeValueId != null) { + return cachedPassportCodeValueId; + } + long codeId = findIdByName(CODES.listCodes(), CUSTOMER_IDENTIFIER_CODE); + long passportId = findIdByName(CODES.listCodeValues(codeId), PASSPORT_DOCUMENT_TYPE); + cachedPassportCodeValueId = passportId; + return passportId; + } + } + + private long findIdByName(List> entries, String name) { + return entries.stream() + .filter(entry -> name.equals(entry.get("name"))) + .map(entry -> ((Number) entry.get("id")).longValue()) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Fineract entry not found: " + name)); + } + + private static T buildFeignClient(Class apiType) { + return Feign.builder() + .client(new OkHttpClient()) + .encoder(new JacksonEncoder()) + .decoder(new JacksonDecoder()) + .requestInterceptor(new BasicAuthRequestInterceptor(USERNAME, PASSWORD)) + .requestInterceptor(tenantInterceptor()) + .target(apiType, BASE_URL); + } + + private static RequestInterceptor tenantInterceptor() { + return template -> template.header("Fineract-Platform-TenantId", TENANT); + } +} diff --git a/consumer/src/test/java/org/apache/fineract/consumer/cucumber/helpers/MailpitProbe.java b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/helpers/MailpitProbe.java new file mode 100644 index 0000000..21366dc --- /dev/null +++ b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/helpers/MailpitProbe.java @@ -0,0 +1,109 @@ +/* + * 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.consumer.cucumber.helpers; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MailpitProbe { + + private static final String BASE_URL = System.getenv().getOrDefault("MAILPIT_URL", "http://localhost:8025"); + private static final Duration POLL_TIMEOUT = Duration.ofSeconds(5); + private static final Duration POLL_INTERVAL = Duration.ofMillis(200); + private static final Pattern OTP_PATTERN = Pattern.compile("Your verification code is: ([A-Z0-9]{6})"); + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final HttpClient HTTP = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(5)) + .build(); + + public String waitForOtp(String recipient) { + long deadline = System.nanoTime() + POLL_TIMEOUT.toNanos(); + while (System.nanoTime() < deadline) { + Optional token = tryFetchOtp(recipient); + if (token.isPresent()) { + return token.get(); + } + sleep(POLL_INTERVAL); + } + throw new AssertionError("No OTP email arrived in Mailpit for " + recipient + + " within " + POLL_TIMEOUT.toMillis() + "ms"); + } + + private Optional tryFetchOtp(String recipient) { + JsonNode search = get("/api/v1/search?query=" + urlEncode("to:" + recipient)); + JsonNode messages = search.path("messages"); + if (!messages.isArray() || messages.isEmpty()) { + return Optional.empty(); + } + String messageId = messages.get(0).path("ID").asText(); + JsonNode message = get("/api/v1/message/" + messageId); + String body = message.path("Text").asText(); + Matcher matcher = OTP_PATTERN.matcher(body); + if (matcher.find()) { + return Optional.of(matcher.group(1)); + } + return Optional.empty(); + } + + private JsonNode get(String path) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + path)) + .timeout(Duration.ofSeconds(5)) + .header("Accept", "application/json") + .GET() + .build(); + HttpResponse response = HTTP.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() >= 400) { + throw new RuntimeException("Mailpit GET " + path + " failed with " + response.statusCode() + + ": " + response.body()); + } + return MAPPER.readTree(response.body()); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String urlEncode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + + private static void sleep(Duration duration) { + try { + Thread.sleep(duration.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } +} diff --git a/consumer/src/test/java/org/apache/fineract/consumer/cucumber/steps/HealthSteps.java b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/steps/HealthSteps.java deleted file mode 100644 index 47381f3..0000000 --- a/consumer/src/test/java/org/apache/fineract/consumer/cucumber/steps/HealthSteps.java +++ /dev/null @@ -1,62 +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.consumer.cucumber.steps; - -import io.cucumber.java.en.Then; -import io.cucumber.java.en.When; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; - -import static org.assertj.core.api.Assertions.assertThat; - -public class HealthSteps { - - private static final String BASE_URL = System.getenv().getOrDefault("BASE_URL", "http://localhost:8080"); - - private final HttpClient client = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(5)) - .build(); - - private HttpResponse response; - - @When("I GET {string}") - public void iGet(String path) throws Exception { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(BASE_URL + path)) - .timeout(Duration.ofSeconds(10)) - .GET() - .build(); - response = client.send(request, HttpResponse.BodyHandlers.ofString()); - } - - @Then("the response status is {int}") - public void theResponseStatusIs(int expected) { - assertThat(response.statusCode()).isEqualTo(expected); - } - - @Then("the response body contains {string}") - public void theResponseBodyContains(String text) { - assertThat(response.body()).contains(text); - } -} diff --git a/consumer/src/test/java/org/apache/fineract/consumer/cucumber/steps/RegistrationSteps.java b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/steps/RegistrationSteps.java new file mode 100644 index 0000000..bb1a215 --- /dev/null +++ b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/steps/RegistrationSteps.java @@ -0,0 +1,194 @@ +/* + * 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.consumer.cucumber.steps; + +import static org.assertj.core.api.Assertions.assertThat; + +import feign.FeignException; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import java.util.UUID; +import org.apache.fineract.consumer.client.ApiClient; +import org.apache.fineract.consumer.client.api.RegistrationCommandControllerApi; +import org.apache.fineract.consumer.client.model.SendOtpCommandRequest; +import org.apache.fineract.consumer.client.model.SubmitRegistrationCommandData; +import org.apache.fineract.consumer.client.model.SubmitRegistrationCommandRequest; +import org.apache.fineract.consumer.client.model.VerifyOtpCommandData; +import org.apache.fineract.consumer.client.model.VerifyOtpCommandRequest; +import org.apache.fineract.consumer.cucumber.helpers.FineractSeeder; +import org.apache.fineract.consumer.cucumber.helpers.MailpitProbe; + +public class RegistrationSteps { + + private static final String BFF_BASE_URL = System.getenv().getOrDefault("BASE_URL", "http://localhost:8080"); + private static final String DEVICE_FINGERPRINT = "cucumber-test-device"; + private static final String WRONG_OTP = "WRONG1"; + + private final FineractSeeder fineractSeed = new FineractSeeder(); + private final MailpitProbe mailpit = new MailpitProbe(); + private final RegistrationCommandControllerApi bff = buildBffClient(); + + private FineractSeeder.SeededClient seededClient; + private String email; + private UUID registrationId; + private SubmitRegistrationCommandData lastSubmit; + private VerifyOtpCommandData lastVerify; + private FeignException lastError; + private String otpToken; + + @Given("a fresh Fineract client exists with a Passport identifier") + public void freshFineractClient() { + seededClient = fineractSeed.seedClientWithPassport(); + email = "user-" + UUID.randomUUID().toString().substring(0, 8) + "@test.com"; + } + + @When("I submit registration with the matching Passport") + public void submitMatching() { + submit(seededClient.documentKey()); + } + + @When("I submit registration with a non-matching Passport value") + public void submitMismatch() { + submit("WRONG-VALUE-" + UUID.randomUUID().toString().substring(0, 4).toUpperCase()); + } + + @Then("registration is accepted in PENDING_OTP state") + public void acceptedPendingOtp() { + assertThat(lastError).as("expected success, got error").isNull(); + assertThat(lastSubmit).isNotNull(); + assertThat(lastSubmit.getRegistrationId()).isNotNull(); + assertThat(lastSubmit.getStatus()).isEqualTo(SubmitRegistrationCommandData.StatusEnum.PENDING_OTP); + registrationId = lastSubmit.getRegistrationId(); + } + + @Then("registration is rejected") + public void registrationRejected() { + assertThat(lastError).as("expected rejection, got success").isNotNull(); + assertThat(lastError.status()).isEqualTo(403); + } + + @Then("the rejection does not reveal which field failed") + public void rejectionDoesNotRevealField() { + String body = lastError.contentUTF8(); + assertThat(body) + .doesNotContain("documentKey") + .doesNotContain("documentTypeName") + .doesNotContain("fineractClientId"); + } + + @When("I request an email OTP") + public void requestEmailOtp() { + SendOtpCommandRequest request = new SendOtpCommandRequest() + .registrationId(registrationId) + .deliveryMethod("email"); + try { + bff.sendOtp(request); + lastError = null; + } catch (FeignException e) { + lastError = e; + } + } + + @Then("an OTP is delivered to my email") + public void otpDelivered() { + assertThat(lastError).as("expected OTP send success, got error").isNull(); + } + + @When("I retrieve the OTP from Mailpit") + public void retrieveOtpFromMailpit() { + otpToken = mailpit.waitForOtp(email); + assertThat(otpToken).isNotBlank(); + } + + @When("I verify the OTP") + public void verifyCorrectOtp() { + verifyWithToken(otpToken); + } + + @When("I verify a wrong OTP") + public void verifyWrongOtp() { + verifyWithToken(WRONG_OTP); + } + + @When("I verify the same OTP a second time") + public void verifyOtpAgain() { + verifyWithToken(otpToken); + } + + @When("I complete an OTP verification successfully") + public void completeOtpVerification() { + freshFineractClient(); + submitMatching(); + acceptedPendingOtp(); + requestEmailOtp(); + otpDelivered(); + retrieveOtpFromMailpit(); + verifyCorrectOtp(); + advancedToPending2fa(); + } + + @Then("my registration advances to PENDING_2FA") + public void advancedToPending2fa() { + assertThat(lastError).as("expected verify success, got error").isNull(); + assertThat(lastVerify).isNotNull(); + assertThat(lastVerify.getStatus()).isEqualTo(VerifyOtpCommandData.StatusEnum.PENDING_2_FA); + } + + @Then("the OTP is rejected as invalid") + public void otpRejected() { + assertThat(lastError).as("expected OTP rejection, got success").isNotNull(); + assertThat(lastError.status()).isEqualTo(400); + } + + private void submit(String documentKey) { + SubmitRegistrationCommandRequest request = new SubmitRegistrationCommandRequest() + .fineractClientId(seededClient.fineractClientId()) + .email(email) + .documentTypeName(seededClient.documentTypeName()) + .documentKey(documentKey); + try { + lastSubmit = bff.submit(DEVICE_FINGERPRINT, request); + lastError = null; + } catch (FeignException e) { + lastError = e; + lastSubmit = null; + } + } + + private void verifyWithToken(String token) { + VerifyOtpCommandRequest request = new VerifyOtpCommandRequest() + .registrationId(registrationId) + .token(token); + try { + lastVerify = bff.verifyOtp(request); + lastError = null; + } catch (FeignException e) { + lastError = e; + lastVerify = null; + } + } + + private static RegistrationCommandControllerApi buildBffClient() { + ApiClient apiClient = new ApiClient(); + apiClient.setBasePath(BFF_BASE_URL); + return apiClient.buildClient(RegistrationCommandControllerApi.class); + } +} diff --git a/consumer/src/test/resources/features/health.feature b/consumer/src/test/resources/features/health.feature deleted file mode 100644 index bd243fa..0000000 --- a/consumer/src/test/resources/features/health.feature +++ /dev/null @@ -1,23 +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. - -Feature: App liveness - - Scenario: Actuator health endpoint reports UP - When I GET "/actuator/health" - Then the response status is 200 - And the response body contains "UP" diff --git a/consumer/src/test/resources/features/registration.feature b/consumer/src/test/resources/features/registration.feature new file mode 100644 index 0000000..3fa3872 --- /dev/null +++ b/consumer/src/test/resources/features/registration.feature @@ -0,0 +1,46 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +Feature: Consumer registration + + Background: + Given a fresh Fineract client exists with a Passport identifier + + Scenario: Successful registration through OTP verification + When I submit registration with the matching Passport + Then registration is accepted in PENDING_OTP state + When I request an email OTP + Then an OTP is delivered to my email + When I retrieve the OTP from Mailpit + And I verify the OTP + Then my registration advances to PENDING_2FA + + Scenario: Submit with mismatched identifier is rejected + When I submit registration with a non-matching Passport value + Then registration is rejected + And the rejection does not reveal which field failed + + Scenario: Replaying a verified OTP is rejected + When I complete an OTP verification successfully + And I verify the same OTP a second time + Then the OTP is rejected as invalid + + Scenario: Verifying a wrong OTP is rejected + When I submit registration with the matching Passport + And I request an email OTP + And I verify a wrong OTP + Then the OTP is rejected as invalid From 2bbbb060b6aa79d18b85029ed4024326a25e610a Mon Sep 17 00:00:00 2001 From: edk12564 Date: Mon, 8 Jun 2026 14:45:54 -0500 Subject: [PATCH 3/4] - used error message constants instead of hardcoded messages - used assertion constants instead of hardcoded strings when applicable - added FineractHeaders.java for header constants - small code quality fixes --- .../exception/AbstractConsumerException.java | 7 +++- .../exception/ConsumerApiError.java | 37 +++++++++++++++++++ .../exception/ConsumerExceptionHandler.java | 8 ++-- .../exception/DefaultExceptionHandler.java | 11 ++++-- ...ttpMessageNotReadableExceptionHandler.java | 11 ++++-- ...ethodArgumentNotValidExceptionHandler.java | 11 ++++-- .../MissingRequestHeaderExceptionHandler.java | 11 ++++-- .../fineractclient/FineractHeaders.java | 29 +++++++++++++++ .../FineractTenantHeaderInterceptor.java | 4 +- .../exception/OtpDeliveryFailedException.java | 4 +- .../OtpDeliveryMethodInvalidException.java | 4 +- .../exception/OtpTokenInvalidException.java | 4 +- .../repository/OtpCommandRepository.java | 11 ++++-- .../IdentityNotVerifiedException.java | 4 +- .../IdentityVerificationException.java | 4 +- .../InvalidBindingStateException.java | 4 +- .../exception/UserAlreadyExistsException.java | 4 +- .../exception/UserNotFoundException.java | 4 +- .../query/repository/UserQueryRepository.java | 7 +++- .../cucumber/helpers/FineractSeeder.java | 3 +- .../cucumber/steps/RegistrationSteps.java | 18 ++++++++- 21 files changed, 165 insertions(+), 35 deletions(-) create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/ConsumerApiError.java create mode 100644 consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/FineractHeaders.java diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/AbstractConsumerException.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/AbstractConsumerException.java index 1bfa476..d7d7b75 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/AbstractConsumerException.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/AbstractConsumerException.java @@ -26,17 +26,20 @@ public abstract class AbstractConsumerException extends RuntimeException { private final HttpStatus httpStatus; + private final String code; private final String errorMessage; - protected AbstractConsumerException(HttpStatus httpStatus, String errorMessage) { + protected AbstractConsumerException(HttpStatus httpStatus, String code, String errorMessage) { super(errorMessage); this.httpStatus = httpStatus; + this.code = code; this.errorMessage = errorMessage; } - protected AbstractConsumerException(HttpStatus httpStatus, String errorMessage, Throwable cause) { + protected AbstractConsumerException(HttpStatus httpStatus, String code, String errorMessage, Throwable cause) { super(errorMessage, cause); this.httpStatus = httpStatus; + this.code = code; this.errorMessage = errorMessage; } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/ConsumerApiError.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/ConsumerApiError.java new file mode 100644 index 0000000..e9a64ac --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/ConsumerApiError.java @@ -0,0 +1,37 @@ +/* + * 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.consumer.infrastructure.exception; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@RequiredArgsConstructor +@Builder +@EqualsAndHashCode +@ToString +public final class ConsumerApiError { + + private final String code; + private final String defaultMessage; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/ConsumerExceptionHandler.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/ConsumerExceptionHandler.java index 04ff18b..e240b68 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/ConsumerExceptionHandler.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/ConsumerExceptionHandler.java @@ -19,7 +19,6 @@ package org.apache.fineract.consumer.infrastructure.exception; -import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -33,13 +32,16 @@ public class ConsumerExceptionHandler { @ExceptionHandler(AbstractConsumerException.class) - public ResponseEntity> handle(AbstractConsumerException ex) { + public ResponseEntity handle(AbstractConsumerException ex) { if (ex.getHttpStatus().is5xxServerError()) { log.error("{}: {}", ex.getClass().getSimpleName(), ex.getErrorMessage(), ex); } else { log.warn("{}: {}", ex.getClass().getSimpleName(), ex.getErrorMessage()); } return ResponseEntity.status(ex.getHttpStatus()) - .body(Map.of("error", ex.getErrorMessage())); + .body(ConsumerApiError.builder() + .code(ex.getCode()) + .defaultMessage(ex.getErrorMessage()) + .build()); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/DefaultExceptionHandler.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/DefaultExceptionHandler.java index 604a0e9..673e5c1 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/DefaultExceptionHandler.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/DefaultExceptionHandler.java @@ -19,7 +19,6 @@ package org.apache.fineract.consumer.infrastructure.exception; -import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -33,10 +32,16 @@ @RestControllerAdvice public class DefaultExceptionHandler { + public static final String CODE = "error.msg.consumer.internal.error"; + private static final String DEFAULT_MESSAGE = "internal error"; + @ExceptionHandler(Throwable.class) - public ResponseEntity> handle(Throwable ex) { + public ResponseEntity handle(Throwable ex) { log.error("unexpected error: {}", ex.getClass().getSimpleName(), ex); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(Map.of("error", "internal error")); + .body(ConsumerApiError.builder() + .code(CODE) + .defaultMessage(DEFAULT_MESSAGE) + .build()); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/HttpMessageNotReadableExceptionHandler.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/HttpMessageNotReadableExceptionHandler.java index 6588ca6..f1904f9 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/HttpMessageNotReadableExceptionHandler.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/HttpMessageNotReadableExceptionHandler.java @@ -19,7 +19,6 @@ package org.apache.fineract.consumer.infrastructure.exception; -import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -34,10 +33,16 @@ @RestControllerAdvice public class HttpMessageNotReadableExceptionHandler { + public static final String CODE = "error.msg.consumer.request.body.malformed"; + private static final String DEFAULT_MESSAGE = "invalid request"; + @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity> handle(HttpMessageNotReadableException ex) { + public ResponseEntity handle(HttpMessageNotReadableException ex) { log.info("malformed request body: {}", ex.getClass().getSimpleName()); return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(Map.of("error", "invalid request")); + .body(ConsumerApiError.builder() + .code(CODE) + .defaultMessage(DEFAULT_MESSAGE) + .build()); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MethodArgumentNotValidExceptionHandler.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MethodArgumentNotValidExceptionHandler.java index 9df7139..dc8c02e 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MethodArgumentNotValidExceptionHandler.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MethodArgumentNotValidExceptionHandler.java @@ -19,7 +19,6 @@ package org.apache.fineract.consumer.infrastructure.exception; -import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -34,10 +33,16 @@ @RestControllerAdvice public class MethodArgumentNotValidExceptionHandler { + public static final String CODE = "error.msg.consumer.request.validation.failed"; + private static final String DEFAULT_MESSAGE = "invalid request"; + @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handle(MethodArgumentNotValidException ex) { + public ResponseEntity handle(MethodArgumentNotValidException ex) { log.info("invalid request body: {}", ex.getClass().getSimpleName()); return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(Map.of("error", "invalid request")); + .body(ConsumerApiError.builder() + .code(CODE) + .defaultMessage(DEFAULT_MESSAGE) + .build()); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MissingRequestHeaderExceptionHandler.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MissingRequestHeaderExceptionHandler.java index f52fd78..cc1dcdb 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MissingRequestHeaderExceptionHandler.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/exception/MissingRequestHeaderExceptionHandler.java @@ -19,7 +19,6 @@ package org.apache.fineract.consumer.infrastructure.exception; -import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -34,10 +33,16 @@ @RestControllerAdvice public class MissingRequestHeaderExceptionHandler { + public static final String CODE = "error.msg.consumer.request.header.missing"; + private static final String DEFAULT_MESSAGE = "invalid request"; + @ExceptionHandler(MissingRequestHeaderException.class) - public ResponseEntity> handle(MissingRequestHeaderException ex) { + public ResponseEntity handle(MissingRequestHeaderException ex) { log.info("missing request header: {}", ex.getHeaderName()); return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(Map.of("error", "invalid request")); + .body(ConsumerApiError.builder() + .code(CODE) + .defaultMessage(DEFAULT_MESSAGE) + .build()); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/FineractHeaders.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/FineractHeaders.java new file mode 100644 index 0000000..87d46c1 --- /dev/null +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/FineractHeaders.java @@ -0,0 +1,29 @@ +/* + * 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.consumer.infrastructure.fineractclient; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class FineractHeaders { + + public static final String TENANT_ID = "Fineract-Platform-TenantId"; +} diff --git a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/interceptors/FineractTenantHeaderInterceptor.java b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/interceptors/FineractTenantHeaderInterceptor.java index 65e4730..928cd6e 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/interceptors/FineractTenantHeaderInterceptor.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/infrastructure/fineractclient/interceptors/FineractTenantHeaderInterceptor.java @@ -22,16 +22,16 @@ import feign.RequestInterceptor; import feign.RequestTemplate; import lombok.RequiredArgsConstructor; +import org.apache.fineract.consumer.infrastructure.fineractclient.FineractHeaders; import org.apache.fineract.consumer.infrastructure.fineractclient.configs.FineractClientProperties; @RequiredArgsConstructor public class FineractTenantHeaderInterceptor implements RequestInterceptor { - private static final String TENANT_HEADER = "Fineract-Platform-TenantId"; private final FineractClientProperties properties; @Override public void apply(RequestTemplate template) { - template.header(TENANT_HEADER, properties.getTenantId()); + template.header(FineractHeaders.TENANT_ID, properties.getTenantId()); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryFailedException.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryFailedException.java index 024e65b..e7a51ac 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryFailedException.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryFailedException.java @@ -24,7 +24,9 @@ public class OtpDeliveryFailedException extends AbstractConsumerException { + public static final String CODE = "error.msg.consumer.otp.delivery.failed"; + public OtpDeliveryFailedException(Throwable cause) { - super(HttpStatus.BAD_GATEWAY, "verification code could not be sent", cause); + super(HttpStatus.BAD_GATEWAY, CODE, "verification code could not be sent", cause); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryMethodInvalidException.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryMethodInvalidException.java index e037686..cb590dc 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryMethodInvalidException.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpDeliveryMethodInvalidException.java @@ -24,7 +24,9 @@ public class OtpDeliveryMethodInvalidException extends AbstractConsumerException { + public static final String CODE = "error.msg.consumer.otp.delivery.method.invalid"; + public OtpDeliveryMethodInvalidException() { - super(HttpStatus.BAD_REQUEST, "unsupported delivery method"); + super(HttpStatus.BAD_REQUEST, CODE, "unsupported delivery method"); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpTokenInvalidException.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpTokenInvalidException.java index 114d962..60b86e9 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpTokenInvalidException.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/exception/OtpTokenInvalidException.java @@ -24,7 +24,9 @@ public class OtpTokenInvalidException extends AbstractConsumerException { + public static final String CODE = "error.msg.consumer.otp.invalid"; + public OtpTokenInvalidException() { - super(HttpStatus.BAD_REQUEST, "invalid or expired otp"); + super(HttpStatus.BAD_REQUEST, CODE, "invalid or expired otp"); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/repository/OtpCommandRepository.java b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/repository/OtpCommandRepository.java index 65793e7..1bde183 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/otp/command/repository/OtpCommandRepository.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/otp/command/repository/OtpCommandRepository.java @@ -28,21 +28,24 @@ @Repository public class OtpCommandRepository { + private static final String EXTERNAL_ID_NULL = "externalId must not be null"; + private static final String REQUEST_NULL = "request must not be null"; + private final ConcurrentHashMap store = new ConcurrentHashMap<>(); public PendingOtp getPendingOtpForUser(UUID externalId) { - Assert.notNull(externalId, "externalId must not be null"); + Assert.notNull(externalId, EXTERNAL_ID_NULL); return store.get(externalId); } public void addPendingOtp(UUID externalId, PendingOtp request) { - Assert.notNull(externalId, "externalId must not be null"); - Assert.notNull(request, "request must not be null"); + Assert.notNull(externalId, EXTERNAL_ID_NULL); + Assert.notNull(request, REQUEST_NULL); store.put(externalId, request); } public void deletePendingOtpForUser(UUID externalId) { - Assert.notNull(externalId, "externalId must not be null"); + Assert.notNull(externalId, EXTERNAL_ID_NULL); store.remove(externalId); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/command/exception/IdentityNotVerifiedException.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/exception/IdentityNotVerifiedException.java index ac6363a..3209a71 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/registration/command/exception/IdentityNotVerifiedException.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/command/exception/IdentityNotVerifiedException.java @@ -24,7 +24,9 @@ public class IdentityNotVerifiedException extends AbstractConsumerException { + public static final String CODE = "error.msg.consumer.registration.identity.not.verified"; + public IdentityNotVerifiedException() { - super(HttpStatus.FORBIDDEN, "registration could not be completed"); + super(HttpStatus.FORBIDDEN, CODE, "registration could not be completed"); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/registration/query/exception/IdentityVerificationException.java b/consumer/src/main/java/org/apache/fineract/consumer/registration/query/exception/IdentityVerificationException.java index ef7050a..d9be4af 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/registration/query/exception/IdentityVerificationException.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/registration/query/exception/IdentityVerificationException.java @@ -24,7 +24,9 @@ public class IdentityVerificationException extends AbstractConsumerException { + public static final String CODE = "error.msg.consumer.identity.verification.unavailable"; + public IdentityVerificationException(Throwable cause) { - super(HttpStatus.BAD_GATEWAY, "identity verification temporarily unavailable", cause); + super(HttpStatus.BAD_GATEWAY, CODE, "identity verification temporarily unavailable", cause); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/InvalidBindingStateException.java b/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/InvalidBindingStateException.java index d25b2fd..3d5b03c 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/InvalidBindingStateException.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/InvalidBindingStateException.java @@ -24,7 +24,9 @@ public class InvalidBindingStateException extends AbstractConsumerException { + public static final String CODE = "error.msg.consumer.user.binding.state.invalid"; + public InvalidBindingStateException() { - super(HttpStatus.CONFLICT, "user is not in the expected binding state"); + super(HttpStatus.CONFLICT, CODE, "user is not in the expected binding state"); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserAlreadyExistsException.java b/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserAlreadyExistsException.java index 8fb511e..f3a4632 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserAlreadyExistsException.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserAlreadyExistsException.java @@ -24,7 +24,9 @@ public class UserAlreadyExistsException extends AbstractConsumerException { + public static final String CODE = "error.msg.consumer.user.already.exists"; + public UserAlreadyExistsException() { - super(HttpStatus.CONFLICT, "user already exists for this email or fineract client"); + super(HttpStatus.CONFLICT, CODE, "user already exists for this email or fineract client"); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserNotFoundException.java b/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserNotFoundException.java index f017386..2b6ee93 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserNotFoundException.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/command/exception/UserNotFoundException.java @@ -24,7 +24,9 @@ public class UserNotFoundException extends AbstractConsumerException { + public static final String CODE = "error.msg.consumer.user.not.found"; + public UserNotFoundException() { - super(HttpStatus.NOT_FOUND, "user not found"); + super(HttpStatus.NOT_FOUND, CODE, "user not found"); } } diff --git a/consumer/src/main/java/org/apache/fineract/consumer/user/query/repository/UserQueryRepository.java b/consumer/src/main/java/org/apache/fineract/consumer/user/query/repository/UserQueryRepository.java index b459ef1..3e8291d 100644 --- a/consumer/src/main/java/org/apache/fineract/consumer/user/query/repository/UserQueryRepository.java +++ b/consumer/src/main/java/org/apache/fineract/consumer/user/query/repository/UserQueryRepository.java @@ -28,7 +28,10 @@ public interface UserQueryRepository extends Repository { - @Query("SELECT new org.apache.fineract.consumer.user.query.data.UserQueryData(u.id, u.externalId, u.email, u.status) " - + "FROM User u WHERE u.externalId = :externalId") + @Query(""" + SELECT new org.apache.fineract.consumer.user.query.data.UserQueryData(u.id, u.externalId, u.email, u.status) + FROM User u + WHERE u.externalId = :externalId + """) Optional findByExternalId(UUID externalId); } diff --git a/consumer/src/test/java/org/apache/fineract/consumer/cucumber/helpers/FineractSeeder.java b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/helpers/FineractSeeder.java index b32c71b..4a83df7 100644 --- a/consumer/src/test/java/org/apache/fineract/consumer/cucumber/helpers/FineractSeeder.java +++ b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/helpers/FineractSeeder.java @@ -31,6 +31,7 @@ import org.apache.fineract.consumer.cucumber.clients.FineractClientSeedClient; import org.apache.fineract.consumer.cucumber.clients.FineractCodeLookupClient; import org.apache.fineract.consumer.cucumber.clients.FineractIdentifierSeedClient; +import org.apache.fineract.consumer.infrastructure.fineractclient.FineractHeaders; public class FineractSeeder { @@ -116,6 +117,6 @@ private static T buildFeignClient(Class apiType) { } private static RequestInterceptor tenantInterceptor() { - return template -> template.header("Fineract-Platform-TenantId", TENANT); + return template -> template.header(FineractHeaders.TENANT_ID, TENANT); } } diff --git a/consumer/src/test/java/org/apache/fineract/consumer/cucumber/steps/RegistrationSteps.java b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/steps/RegistrationSteps.java index bb1a215..5fefaf6 100644 --- a/consumer/src/test/java/org/apache/fineract/consumer/cucumber/steps/RegistrationSteps.java +++ b/consumer/src/test/java/org/apache/fineract/consumer/cucumber/steps/RegistrationSteps.java @@ -35,12 +35,18 @@ import org.apache.fineract.consumer.client.model.VerifyOtpCommandRequest; import org.apache.fineract.consumer.cucumber.helpers.FineractSeeder; import org.apache.fineract.consumer.cucumber.helpers.MailpitProbe; +import org.apache.fineract.consumer.otp.command.data.OtpConstants; +import org.apache.fineract.consumer.otp.command.exception.OtpTokenInvalidException; +import org.apache.fineract.consumer.registration.command.exception.IdentityNotVerifiedException; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; public class RegistrationSteps { private static final String BFF_BASE_URL = System.getenv().getOrDefault("BASE_URL", "http://localhost:8080"); private static final String DEVICE_FINGERPRINT = "cucumber-test-device"; private static final String WRONG_OTP = "WRONG1"; + private static final ObjectMapper JSON = JsonMapper.builder().build(); private final FineractSeeder fineractSeed = new FineractSeeder(); private final MailpitProbe mailpit = new MailpitProbe(); @@ -83,6 +89,7 @@ public void acceptedPendingOtp() { public void registrationRejected() { assertThat(lastError).as("expected rejection, got success").isNotNull(); assertThat(lastError.status()).isEqualTo(403); + assertThat(readCode(lastError.contentUTF8())).isEqualTo(IdentityNotVerifiedException.CODE); } @Then("the rejection does not reveal which field failed") @@ -98,7 +105,7 @@ public void rejectionDoesNotRevealField() { public void requestEmailOtp() { SendOtpCommandRequest request = new SendOtpCommandRequest() .registrationId(registrationId) - .deliveryMethod("email"); + .deliveryMethod(OtpConstants.EMAIL_DELIVERY_METHOD_NAME); try { bff.sendOtp(request); lastError = null; @@ -156,6 +163,7 @@ public void advancedToPending2fa() { public void otpRejected() { assertThat(lastError).as("expected OTP rejection, got success").isNotNull(); assertThat(lastError.status()).isEqualTo(400); + assertThat(readCode(lastError.contentUTF8())).isEqualTo(OtpTokenInvalidException.CODE); } private void submit(String documentKey) { @@ -186,6 +194,14 @@ private void verifyWithToken(String token) { } } + private static String readCode(String body) { + try { + return JSON.readTree(body).path("code").asString(); + } catch (Exception e) { + throw new IllegalStateException("could not parse error response body: " + body, e); + } + } + private static RegistrationCommandControllerApi buildBffClient() { ApiClient apiClient = new ApiClient(); apiClient.setBasePath(BFF_BASE_URL); From 92e7363140b485caa9dc496ebf429e532846ced3 Mon Sep 17 00:00:00 2001 From: edk12564 Date: Tue, 9 Jun 2026 10:13:35 -0500 Subject: [PATCH 4/4] - fixed registration cucumber test --- consumer/src/test/resources/features/registration.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/consumer/src/test/resources/features/registration.feature b/consumer/src/test/resources/features/registration.feature index 3fa3872..d17910d 100644 --- a/consumer/src/test/resources/features/registration.feature +++ b/consumer/src/test/resources/features/registration.feature @@ -41,6 +41,7 @@ Feature: Consumer registration Scenario: Verifying a wrong OTP is rejected When I submit registration with the matching Passport + Then registration is accepted in PENDING_OTP state And I request an email OTP And I verify a wrong OTP Then the OTP is rejected as invalid