diff --git a/.github/workflows/pr-open.yml b/.github/workflows/pr-open.yml index 0302926e58..6ed25fcb74 100644 --- a/.github/workflows/pr-open.yml +++ b/.github/workflows/pr-open.yml @@ -30,7 +30,7 @@ jobs: matrix: package: [backend, database, frontend, legacy, legacydb, processor] steps: - - uses: bcgov-nr/action-builder-ghcr@v2.3.0 + - uses: bcgov-nr/action-builder-ghcr@v2.2.0 name: Build (${{ matrix.package }}) with: package: ${{ matrix.package }} diff --git a/backend/Dockerfile b/backend/Dockerfile index c2a086b80d..3519ab4b1e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -30,7 +30,6 @@ RUN mvn versions:set -DnewVersion=${APP_VERSION} -f pom.xml -DskipTests -Dtests. # Build RUN mvn -Pnative native:compile - ### Deployer FROM gcr.io/distroless/java-base:nonroot AS deploy ARG PORT=8080 @@ -44,5 +43,7 @@ USER 1001 EXPOSE ${PORT} HEALTHCHECK CMD curl -f http://localhost:${PORT}/actuator/health | grep '"status":"UP"' +ENV SPRING_PROFILES_ACTIVE=container + # Startup -ENTRYPOINT ["/app/nr-forest-client-backend","--spring.profiles.active=container"] \ No newline at end of file +ENTRYPOINT ["/app/nr-forest-client-backend"] diff --git a/backend/src/main/java/ca/bc/gov/app/controller/client/ClientController.java b/backend/src/main/java/ca/bc/gov/app/controller/client/ClientController.java index 0735350f0b..c1d48d06fd 100644 --- a/backend/src/main/java/ca/bc/gov/app/controller/client/ClientController.java +++ b/backend/src/main/java/ca/bc/gov/app/controller/client/ClientController.java @@ -4,14 +4,15 @@ import ca.bc.gov.app.dto.bcregistry.ClientDetailsDto; import ca.bc.gov.app.dto.client.ClientListDto; import ca.bc.gov.app.dto.client.ClientLookUpDto; +import ca.bc.gov.app.dto.legacy.ForestClientDetailsDto; import ca.bc.gov.app.exception.NoClientDataFound; import ca.bc.gov.app.service.client.ClientLegacyService; import ca.bc.gov.app.service.client.ClientService; import ca.bc.gov.app.util.JwtPrincipalUtil; import io.micrometer.observation.annotation.Observed; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.util.List; import org.apache.commons.text.WordUtils; import org.springframework.data.util.Pair; import org.springframework.http.MediaType; @@ -35,8 +36,19 @@ public class ClientController { private final ClientService clientService; private final ClientLegacyService clientLegacyService; + /** + * Retrieves the details of a client based on the provided incorporation number. + * + *

This endpoint is used to fetch client details by their incorporation number. The request is + * authenticated using a JWT, and additional information (such as user ID, business ID, and + * provider) is extracted from the token to authorize the request. + * + * @param clientNumber the incorporation number of the client whose details are being requested + * @param principal the JWT authentication token containing user and business information + * @return a {@link Mono} emitting the {@link ClientDetailsDto} containing the client's details + */ @GetMapping("/{clientNumber}") - public Mono getClientDetails( + public Mono getClientDetailsByIncorporationNumber( @PathVariable String clientNumber, JwtAuthenticationToken principal ) { @@ -45,7 +57,7 @@ public Mono getClientDetails( JwtPrincipalUtil.getUserId(principal) ); return clientService - .getClientDetails( + .getClientDetailsByIncorporationNumber( clientNumber, JwtPrincipalUtil.getUserId(principal), JwtPrincipalUtil.getBusinessId(principal), @@ -53,40 +65,75 @@ public Mono getClientDetails( ); } + /** + * Handles HTTP GET requests to retrieve client details based on the provided client number. + * + *

This method fetches the details of a client from the {@code ClientService} using the + * specified {@code clientNumber}. The caller's JWT authentication token is used to extract + * user-related information such as groups and user ID.

+ * + * @param clientNumber the unique identifier of the client whose details are to be retrieved. + * @param principal the {@link JwtAuthenticationToken} containing the authenticated user's + * information, including their roles and groups. + * @return a {@link Mono} emitting the {@link ForestClientDetailsDto} containing the requested + * client details, or an error if the client cannot be found or accessed. + */ + @GetMapping("/details/{clientNumber}") + public Mono getClientDetailsByClientNumber( + @PathVariable String clientNumber, + JwtAuthenticationToken principal + ) { + log.info("Requesting client details for client number {} from the client service. {}", + clientNumber, + JwtPrincipalUtil.getUserId(principal) + ); + return clientService.getClientDetailsByClientNumber( + clientNumber, + JwtPrincipalUtil.getGroups(principal)); + } + + /** + * Performs a full-text search for clients based on the provided keyword, with pagination support. + * + *

This endpoint allows searching for clients by a keyword. The results are paginated, and the + * total count of matching records is included in the response headers. + * + * @param page the page number to retrieve (default is 0) + * @param size the number of records per page (default is 10) + * @param keyword the keyword to search for (default is an empty string, which returns all + * records) + * @param serverResponse the HTTP response to include the total count of records in the headers + * @return a {@link Flux} emitting {@link ClientListDto} objects containing the search results + */ @GetMapping("/search") public Flux fullSearch( @RequestParam(required = false, defaultValue = "0") int page, @RequestParam(required = false, defaultValue = "10") int size, @RequestParam(required = false, defaultValue = "") String keyword, - ServerHttpResponse serverResponse) { - + ServerHttpResponse serverResponse + ) { log.info("Listing clients: page={}, size={}, keyword={}", page, size, keyword); - + return clientLegacyService - .search( - page, - size, - keyword - ) + .search(page, size, keyword) .doOnNext(pair -> { Long count = pair.getSecond(); serverResponse - .getHeaders() - .putIfAbsent( - ApplicationConstant.X_TOTAL_COUNT, - List.of(count.toString()) - ); - } - ) + .getHeaders() + .putIfAbsent( + ApplicationConstant.X_TOTAL_COUNT, + List.of(count.toString()) + ); + }) .map(Pair::getFirst) - .doFinally(signalType -> + .doFinally(signalType -> serverResponse - .getHeaders() - .putIfAbsent( - ApplicationConstant.X_TOTAL_COUNT, - List.of("0") - ) + .getHeaders() + .putIfAbsent( + ApplicationConstant.X_TOTAL_COUNT, + List.of("0") + ) ); } @@ -104,24 +151,45 @@ public Flux findByClientName(@PathVariable String name) { .map(client -> client.withName(WordUtils.capitalize(client.name()))); } + /** + * Finds a client based on their registration number. + * + *

This endpoint retrieves client information by searching for a registration number. + * If no client is found, an error is returned. + * + * @param registrationNumber the registration number of the client to look up + * @return a {@link Mono} emitting the {@link ClientLookUpDto} if found, or an error + * if no data exists + */ @GetMapping(value = "/incorporation/{registrationNumber}") public Mono findByRegistrationNumber( @PathVariable String registrationNumber) { log.info("Requesting a client with registration number {} from the client service.", - registrationNumber); + registrationNumber); return clientService .findByClientNameOrIncorporation(registrationNumber) .next() .switchIfEmpty(Mono.error(new NoClientDataFound(registrationNumber))); } + /** + * Searches for an individual client by user ID and last name. + * + *

This endpoint fetches an individual client using their user ID and last name. + * The request is validated against existing records in the system. + * + * @param userId the unique identifier of the individual to search for + * @param lastName the last name of the individual to search for + * @return a {@link Mono} indicating completion, or an error if the individual is not found + */ @GetMapping(value = "/individual/{userId}") public Mono findByIndividual( @PathVariable String userId, @RequestParam String lastName ) { - log.info("Receiving request to search individual with id {} and last name {}", userId, - lastName); + log.info("Receiving request to search individual with id {} and last name {}", + userId, + lastName); return clientService.findByIndividual(userId, lastName); } diff --git a/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientContactDto.java b/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientContactDto.java new file mode 100644 index 0000000000..b7eda00585 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientContactDto.java @@ -0,0 +1,17 @@ +package ca.bc.gov.app.dto.legacy; + +public record ForestClientContactDto( + String clientNumber, + String clientLocnCode, + String contactCode, + String contactName, + String businessPhone, + String secondaryPhone, + String faxNumber, + String emailAddress, + String createdBy, + String updatedBy, + Long orgUnit +) { + +} diff --git a/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientDetailsDto.java b/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientDetailsDto.java new file mode 100644 index 0000000000..2a83f8f709 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientDetailsDto.java @@ -0,0 +1,35 @@ +package ca.bc.gov.app.dto.legacy; + +import java.time.LocalDate; +import java.util.List; +import lombok.With; + +@With +public record ForestClientDetailsDto( + String clientNumber, + String clientName, + String legalFirstName, + String legalMiddleName, + String clientStatusCode, + String clientStatusDesc, + String clientTypeCode, + String clientTypeDesc, + String clientIdTypeCode, + String clientIdTypeDesc, + String clientIdentification, + String registryCompanyTypeCode, + String corpRegnNmbr, + String clientAcronym, + String wcbFirmNumber, + String clientComment, + LocalDate clientCommentUpdateDate, + String clientCommentUpdateUser, + String goodStandingInd, + LocalDate birthdate, + + List addresses, + List contacts, + List doingBusinessAs +) { + +} diff --git a/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientDoingBusinessAsDto.java b/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientDoingBusinessAsDto.java new file mode 100644 index 0000000000..fe97663ad7 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientDoingBusinessAsDto.java @@ -0,0 +1,14 @@ +package ca.bc.gov.app.dto.legacy; + +import lombok.With; + +@With +public record ForestClientDoingBusinessAsDto( + String clientNumber, + String doingBusinessAsName, + String createdBy, + String updatedBy, + Long orgUnit +) { + +} diff --git a/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientLocationDto.java b/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientLocationDto.java new file mode 100644 index 0000000000..a396f4afc6 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientLocationDto.java @@ -0,0 +1,33 @@ +package ca.bc.gov.app.dto.legacy; + +import java.time.LocalDate; +import lombok.With; + +@With +public record ForestClientLocationDto( + String clientNumber, + String clientLocnCode, + String clientLocnName, + String addressOne, + String addressTwo, + String addressThree, + String city, + String province, + String postalCode, + String country, + String businessPhone, + String homePhone, + String cellPhone, + String faxNumber, + String emailAddress, + String locnExpiredInd, + LocalDate returnedMailDate, + String trustLocationInd, + String cliLocnComment, + String createdBy, + String updatedBy, + Long orgUnit +) { + +} + diff --git a/backend/src/main/java/ca/bc/gov/app/service/client/ClientLegacyService.java b/backend/src/main/java/ca/bc/gov/app/service/client/ClientLegacyService.java index c56903e793..98f28e4acb 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/client/ClientLegacyService.java +++ b/backend/src/main/java/ca/bc/gov/app/service/client/ClientLegacyService.java @@ -3,6 +3,7 @@ import ca.bc.gov.app.dto.client.ClientListDto; import ca.bc.gov.app.dto.legacy.AddressSearchDto; import ca.bc.gov.app.dto.legacy.ContactSearchDto; +import ca.bc.gov.app.dto.legacy.ForestClientDetailsDto; import ca.bc.gov.app.dto.legacy.ForestClientDto; import io.micrometer.observation.annotation.Observed; import java.time.LocalDate; @@ -18,17 +19,18 @@ import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** - * This class is responsible for interacting with the legacy API to fetch client data. It uses the - * WebClient to send HTTP requests to the legacy API and converts the responses into Flux of - * ForestClientDto objects. It provides several methods to search for clients in the legacy system - * using different search criteria. - *

- * It is annotated with @Slf4j for logging, @Service to indicate that it's a Spring service bean, - * and @Observed for metrics. - *

- * Each method logs the search parameters and the results for debugging purposes. + * This class is responsible for interacting with the legacy API to fetch client data. + * It uses the WebClient to send HTTP requests to the legacy API and converts the responses + * into Flux of ForestClientDto objects. It provides several methods to search for clients + * in the legacy system using different search criteria. + * + *

It is annotated with @Slf4j for logging, @Service to indicate that it's a + * Spring service bean, and @Observed for metrics. + * + *

Each method logs the search parameters and the results for debugging purposes. */ @Slf4j @Service @@ -87,6 +89,41 @@ public Flux searchLegacy( registrationNumber, companyName, dto.clientNumber())); } + /** + * Searches for client details by client number using the legacy API. + * + *

This method communicates with the legacy API to retrieve client information based on the + * provided client number. Optionally, a list of groups can be specified to refine the search + * criteria. If a matching record is found, it is returned as a {@link ForestClientDetailsDto}. + * + * @param clientNumber the client number to search for + * @param groups a list of groups to filter the search (optional) + * @return a {@link Mono} emitting the {@link ForestClientDetailsDto} if the client is found + */ + public Mono searchByClientNumber( + String clientNumber, + List groups + ) { + log.info("Searching for client number {} in legacy", clientNumber); + + return + legacyApi + .get() + .uri(builder -> + builder + .path("/api/search/clientNumber") + .queryParam("clientNumber", clientNumber) + .queryParam("groups", groups) + .build(Map.of()) + ) + .exchangeToMono(response -> response.bodyToMono(ForestClientDetailsDto.class)) + .doOnNext( + dto -> log.info( + "Found Legacy data for in legacy with client number {}", + dto.clientNumber()) + ); + } + /** * This method is used to search for a client in the legacy system using the client's ID and last * name. @@ -168,10 +205,11 @@ public Flux searchIndividual( // Convert the response to a Flux of ForestClientDto objects .exchangeToFlux(response -> response.bodyToFlux(ForestClientDto.class)) // Log the results for debugging purposes - .doOnNext( - dto -> log.info( - "Found Legacy data for first name {} and last name {} in legacy with client number {}", - firstName, lastName, dto.clientNumber()) + .doOnNext(dto -> + log.info( + "Found data for first {} and last name {} in legacy with client number {}", + firstName, lastName, dto.clientNumber() + ) ); } @@ -205,7 +243,7 @@ public Flux searchDocument( // Log the results for debugging purposes .doOnNext( dto -> log.info( - "Found Legacy data for id type {} and identification {} in legacy with client number {}", + "Found data for id type {} and identification {} in legacy with client number {}", idType, identification, dto.clientNumber()) ); @@ -275,7 +313,7 @@ public Flux searchGeneric( // Log the results for debugging purposes .doOnNext( dto -> log.info( - "Found Legacy data for {} with {} in legacy with client number {}", + "Found data for {} with {} in legacy with client number {}", searchType, parameters, dto.clientNumber() diff --git a/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java b/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java index 0766b36644..4ee8e1203f 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java +++ b/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java @@ -13,6 +13,7 @@ import ca.bc.gov.app.dto.client.ClientValueTextDto; import ca.bc.gov.app.dto.client.EmailRequestDto; import ca.bc.gov.app.dto.client.LegalTypeEnum; +import ca.bc.gov.app.dto.legacy.ForestClientDetailsDto; import ca.bc.gov.app.dto.legacy.ForestClientDto; import ca.bc.gov.app.exception.ClientAlreadyExistException; import ca.bc.gov.app.exception.InvalidAccessTokenException; @@ -33,6 +34,7 @@ import java.util.function.Predicate; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; @@ -58,7 +60,7 @@ public class ClientService { * @param clientNumber the client number for which to retrieve details * @return a Mono that emits a ClientDetailsDto object representing the details of the client */ - public Mono getClientDetails( + public Mono getClientDetailsByIncorporationNumber( String clientNumber, String userId, String businessId, @@ -131,16 +133,65 @@ public Mono getClientDetails( }) // If document type is SP and party contains only one entry that is not a person, fail - .filter(document -> provider.equalsIgnoreCase("idir") || - !("SP".equalsIgnoreCase(document.business().legalType()) && - document.parties().size() == 1 && - !document.parties().get(0).isPerson()) + .filter(document -> provider.equalsIgnoreCase("idir") + || !("SP".equalsIgnoreCase(document.business().legalType()) + && document.parties().size() == 1 + && !document.parties().get(0).isPerson()) ) .flatMap(buildDetails()) .switchIfEmpty(Mono.error(new UnableToProcessRequestException( "Unable to process request. This sole proprietor is not owned by a person" ))); } + + public Mono getClientDetailsByClientNumber( + String clientNumber, + List groups + ) { + log.info("Loading details for {} for user role {}", clientNumber, groups.toString()); + + return legacyService + .searchByClientNumber(clientNumber, groups) + .flatMap(forestClientDetailsDto -> { + String corpRegnNmbr = forestClientDetailsDto.corpRegnNmbr(); + + if (corpRegnNmbr == null || corpRegnNmbr.isEmpty()) { + log.info("Corporation registration number not provided. Returning legacy details."); + return Mono.just(forestClientDetailsDto); + } + + log.info("Retrieved corporation registration number: {}", corpRegnNmbr); + + return bcRegistryService + .requestDocumentData(corpRegnNmbr) + .next() + .flatMap(documentMono -> + populateGoodStandingInd(forestClientDetailsDto, + documentMono) + ); + }); + } + + private Mono populateGoodStandingInd( + ForestClientDetailsDto forestClientDetailsDto, + BcRegistryDocumentDto document + ) { + Boolean goodStandingInd = document.business().goodStanding(); + String goodStanding = BooleanUtils.toString( + goodStandingInd, + "Y", + "N", + StringUtils.EMPTY + ); + + log.info("Setting goodStandingInd for client: {} to {}", + forestClientDetailsDto.clientNumber(), goodStanding); + + ForestClientDetailsDto updatedDetails = + forestClientDetailsDto.withGoodStandingInd(goodStanding); + + return Mono.just(updatedDetails); + } /** * Searches the BC Registry API for {@link BcRegistryFacetSearchResultEntryDto} instances matching @@ -170,7 +221,8 @@ public Mono findByIndividual(String userId, String lastName) { return legacyService .searchIdAndLastName(userId, lastName) .doOnNext(legacy -> log.info("Found legacy entry for {} {}", userId, lastName)) - //If we have result, we return a Mono.error with the exception, otherwise return a Mono.empty + //If we have result, we return a Mono.error with the exception, + //otherwise return a Mono.empty .next() .flatMap(legacy -> Mono .error(new ClientAlreadyExistException(legacy.clientNumber())) diff --git a/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java b/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java index b9d98c9a14..df2147df20 100644 --- a/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java +++ b/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java @@ -1,7 +1,10 @@ package ca.bc.gov.app.util; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Stream; import lombok.NoArgsConstructor; import org.apache.commons.lang3.StringUtils; @@ -24,7 +27,7 @@ public class JwtPrincipalUtil { * * @param principal JwtAuthenticationToken object from which the provider is to be extracted. * @return The provider of the JWT token in uppercase, or an empty string if the provider is - * blank. + * blank. */ public static String getProvider(JwtAuthenticationToken principal) { return getProviderValue(principal.getTokenAttributes()); @@ -38,7 +41,7 @@ public static String getProvider(JwtAuthenticationToken principal) { * * @param principal Jwt object from which the provider is to be extracted. * @return The provider of the JWT token in uppercase, or an empty string if the provider is - * blank. + * blank. */ public static String getProvider(Jwt principal) { return getProviderValue(principal.getClaims()); @@ -53,7 +56,7 @@ public static String getProvider(Jwt principal) { * * @param principal JwtAuthenticationToken object from which the user ID is to be extracted. * @return The user ID prefixed with the provider in uppercase and a backslash, or an empty string - * if the user ID is blank. + * if the user ID is blank. */ public static String getUserId(JwtAuthenticationToken principal) { return getUserIdValue(principal.getTokenAttributes()); @@ -67,7 +70,7 @@ public static String getUserId(JwtAuthenticationToken principal) { * * @param principal Jwt object from which the user ID is to be extracted. * @return The user ID prefixed with the provider in uppercase and a backslash, or an empty string - * if the user ID is blank. + * if the user ID is blank. */ public static String getUserId(Jwt principal) { return getUserIdValue(principal.getClaims()); @@ -169,7 +172,7 @@ public static String getName(JwtAuthenticationToken principal) { * * @param principal Jwt object from which the display name is to be extracted. * @return The display name, or the concatenated first and last names, or an empty string if both - * the display name and the first and last names are blank. + * the display name and the first and last names are blank. */ public static String getName(Jwt principal) { return getNameValue(principal.getClaims()); @@ -206,7 +209,7 @@ public static String getLastName(Jwt principal) { * @param claims The map containing the JWT claims. * @param claimName The name of the claim to retrieve. * @return The value of the specified claim as a String, or an empty string if the claim is not - * present. + * present. */ private static String getClaimValue(Map claims, String claimName) { return claims @@ -222,7 +225,7 @@ private static String getClaimValue(Map claims, String claimName * * @param claims The map containing the JWT claims. * @return The provider's name in uppercase or "BCSC" if it starts with "ca.bc.gov.flnr.fam.", or - * an empty string if the provider is not specified. + * an empty string if the provider is not specified. */ private static String getProviderValue(Map claims) { String provider = getClaimValue(claims, "custom:idp_name"); @@ -256,7 +259,7 @@ private static String getBusinessNameValue(Map claims) { * * @param claims The map containing the JWT claims. * @return The constructed user ID in the format "Provider\Username" or "Provider\UserID", or an - * empty string if neither the username nor the user ID is present in the claims. + * empty string if neither the username nor the user ID is present in the claims. */ private static String getUserIdValue(Map claims) { return @@ -306,7 +309,7 @@ private static String getEmailValue(Map claims) { * * @param claims The map containing the JWT claims. * @return The display name value as a String, or an empty string if the "custom:idp_display_name" - * claim is not present. + * claim is not present. */ private static String getDisplayNameValue(Map claims) { return getClaimValue(claims, "custom:idp_display_name"); @@ -321,9 +324,10 @@ private static String getDisplayNameValue(Map claims) { * * @param claims The map containing the JWT claims from which the name information is to be * extracted. - * @return A map with keys "businessName", "firstName", "lastName", and "fullName", containing the - * extracted and/or computed name information. If specific name components are not found, their - * values in the map will be empty strings. + * @return A map with keys "businessName", "firstName", "lastName", and "fullName", + * containing the extracted and/or computed name information. + * If specific name components are not found, their values in the map + * will be empty strings. */ private static Map processName(Map claims) { Map additionalInfo = new HashMap<>(); @@ -374,10 +378,40 @@ private static String getLastNameValue(Map claims) { * * @param claims The map containing the JWT claims. * @return The full name (concatenation of first and last names) extracted from the JWT claims, or - * an empty string if not specified. + * an empty string if not specified. */ private static String getNameValue(Map claims) { return processName(claims).get("fullName"); } + + /** + * Retrieves a list of groups from the given JwtPrincipal. + * + * This method extracts the token attributes from the provided {@link JwtPrincipal}, then looks for the key "cognito:groups" + * in the token attributes. If the value associated with this key is a {@link List}, the method filters the elements to only + * include non-null values of type {@link String}. The resulting list of strings is returned. + * + * @param jwtPrincipal The {@link JwtPrincipal} containing the token attributes. It must have the "cognito:groups" key. + * If the key does not exist or the value is not a list of strings, an empty list is returned. + * @return A list of group names, or an empty list if the key is missing or the value is not a list of strings. + */ + public static List getGroups(JwtAuthenticationToken jwtPrincipal) { + if (jwtPrincipal == null || jwtPrincipal.getTokenAttributes() == null) { + return Collections.emptyList(); + } + + Map tokenAttributes = jwtPrincipal.getTokenAttributes(); + Object groups = tokenAttributes.get("cognito:groups"); + + if (groups instanceof List) { + return ((List) groups).stream() + .filter(Objects::nonNull) + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + } + + return Collections.emptyList(); + } } diff --git a/backend/src/test/java/ca/bc/gov/app/service/client/ClientDistrictServiceIntegrationTest.java b/backend/src/test/java/ca/bc/gov/app/service/client/ClientDistrictServiceIntegrationTest.java index a39f17f6e1..99c14a7c7c 100644 --- a/backend/src/test/java/ca/bc/gov/app/service/client/ClientDistrictServiceIntegrationTest.java +++ b/backend/src/test/java/ca/bc/gov/app/service/client/ClientDistrictServiceIntegrationTest.java @@ -10,7 +10,7 @@ @Slf4j @DisplayName("Integrated Test | FSA Client District Service") -public class ClientDistrictServiceIntegrationTest extends AbstractTestContainerIntegrationTest { +class ClientDistrictServiceIntegrationTest extends AbstractTestContainerIntegrationTest { @Autowired private ClientDistrictService service; diff --git a/backend/src/test/java/ca/bc/gov/app/service/client/ClientLegacyServiceIntegrationTest.java b/backend/src/test/java/ca/bc/gov/app/service/client/ClientLegacyServiceIntegrationTest.java index 181d984d18..09d123c2a8 100644 --- a/backend/src/test/java/ca/bc/gov/app/service/client/ClientLegacyServiceIntegrationTest.java +++ b/backend/src/test/java/ca/bc/gov/app/service/client/ClientLegacyServiceIntegrationTest.java @@ -6,10 +6,12 @@ import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import ca.bc.gov.app.dto.legacy.AddressSearchDto; import ca.bc.gov.app.dto.legacy.ContactSearchDto; +import ca.bc.gov.app.dto.legacy.ForestClientDetailsDto; import ca.bc.gov.app.extensions.AbstractTestContainerIntegrationTest; import ca.bc.gov.app.extensions.WiremockLogNotifier; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; @@ -98,7 +100,7 @@ void shouldNotSearchWhenInvalidCasesHitGeneric(Map> paramet @Test @DisplayName("searching legacy for location") - void shouldSearchALocation(){ + void shouldSearchALocation() { legacyStub .stubFor( @@ -114,7 +116,7 @@ void shouldSearchALocation(){ @Test @DisplayName("searching legacy for contact") - void shouldSearchAContact(){ + void shouldSearchAContact() { legacyStub .stubFor( post(urlPathEqualTo("/api/search/contact")) @@ -136,7 +138,7 @@ private static Stream invalidValues() { ); } - private static Stream>> invalidValuesForMap(){ + private static Stream>> invalidValuesForMap() { return Stream.of( Map.of("email",List.of("")), Map.of("email",List.of(" ")), @@ -145,5 +147,105 @@ private static Stream>> invalidValuesForMap(){ Map.of(" ",List.of()) ); } + + @Test + @DisplayName("searching legacy by client number") + void shouldSearchLegacyByClientNumber() { + String clientNumber = "00000001"; + List groups = List.of("CLIENT_ADMIN"); + + ForestClientDetailsDto expectedDto = new ForestClientDetailsDto( + clientNumber, + "MY COMPANY LTD.", + null, + null, + "ACT", + "Active", + "C", + "Corporation", + null, + null, + null, + "BC", + "9607514", + null, + "678", + "THIS TEST", + null, + null, + "Y", + null, + null, + null, + null + ); + + legacyStub + .stubFor( + get(urlPathEqualTo("/api/search/clientNumber")) + .withQueryParam("clientNumber", equalTo(clientNumber)) + .withQueryParam("groups", equalTo("CLIENT_ADMIN")) + .willReturn(okJson("{" + + "\"clientNumber\":\"00000001\"," + + "\"clientName\":\"MY COMPANY LTD.\"," + + "\"legalFirstName\":null," + + "\"legalMiddleName\":null," + + "\"clientStatusCode\":\"ACT\"," + + "\"clientStatusDesc\":\"Active\"," + + "\"clientTypeCode\":\"C\"," + + "\"clientTypeDesc\":\"Corporation\"," + + "\"clientIdTypeCode\":null," + + "\"clientIdTypeDesc\":null," + + "\"clientIdentification\":null," + + "\"registryCompanyTypeCode\":\"BC\"," + + "\"corpRegnNmbr\":\"9607514\"," + + "\"clientAcronym\":null," + + "\"wcbFirmNumber\":\"678\"," + + "\"ocgSupplierNmbr\":null," + + "\"clientComment\":\"THIS TEST\"," + + "\"clientCommentUpdateDate\":null," + + "\"clientCommentUpdateUser\":null," + + "\"goodStandingInd\":\"Y\"," + + "\"birthdate\":null," + + "\"addresses\":null," + + "\"contacts\":null," + + "\"doingBusinessAs\":null" + + "}")) + .withHeader("Content-Type", equalTo("application/json")) + ); + + service.searchByClientNumber(clientNumber, groups) + .as(StepVerifier::create) + .assertNext(clientDetailsDto -> { + assertThat(clientDetailsDto) + .extracting( + ForestClientDetailsDto::clientNumber, + ForestClientDetailsDto::clientName, + ForestClientDetailsDto::clientStatusCode, + ForestClientDetailsDto::clientStatusDesc, + ForestClientDetailsDto::clientTypeCode, + ForestClientDetailsDto::clientTypeDesc, + ForestClientDetailsDto::registryCompanyTypeCode, + ForestClientDetailsDto::corpRegnNmbr, + ForestClientDetailsDto::wcbFirmNumber, + ForestClientDetailsDto::clientComment, + ForestClientDetailsDto::goodStandingInd + ) + .containsExactly( + expectedDto.clientNumber(), + expectedDto.clientName(), + expectedDto.clientStatusCode(), + expectedDto.clientStatusDesc(), + expectedDto.clientTypeCode(), + expectedDto.clientTypeDesc(), + expectedDto.registryCompanyTypeCode(), + expectedDto.corpRegnNmbr(), + expectedDto.wcbFirmNumber(), + expectedDto.clientComment(), + expectedDto.goodStandingInd() + ); + }) + .verifyComplete(); + } } \ No newline at end of file diff --git a/backend/src/test/java/ca/bc/gov/app/service/client/ClientServiceIntegrationTest.java b/backend/src/test/java/ca/bc/gov/app/service/client/ClientServiceIntegrationTest.java new file mode 100644 index 0000000000..779d4f1b75 --- /dev/null +++ b/backend/src/test/java/ca/bc/gov/app/service/client/ClientServiceIntegrationTest.java @@ -0,0 +1,171 @@ +package ca.bc.gov.app.service.client; + +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.junit.jupiter.api.Test; +import ca.bc.gov.app.dto.bcregistry.BcRegistryAddressDto; +import ca.bc.gov.app.dto.bcregistry.BcRegistryAlternateNameDto; +import ca.bc.gov.app.dto.bcregistry.BcRegistryBusinessAdressesDto; +import ca.bc.gov.app.dto.bcregistry.BcRegistryBusinessDto; +import ca.bc.gov.app.dto.bcregistry.BcRegistryDocumentDto; +import ca.bc.gov.app.dto.bcregistry.BcRegistryOfficerDto; +import ca.bc.gov.app.dto.bcregistry.BcRegistryOfficesDto; +import ca.bc.gov.app.dto.bcregistry.BcRegistryPartyDto; +import ca.bc.gov.app.dto.bcregistry.BcRegistryRoleDto; +import ca.bc.gov.app.dto.legacy.ForestClientDetailsDto; +import ca.bc.gov.app.extensions.AbstractTestContainerIntegrationTest; +import ca.bc.gov.app.service.bcregistry.BcRegistryService; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +@DisplayName("Integrated Test | Client Service") +class ClientServiceIntegrationTest extends AbstractTestContainerIntegrationTest { + + @Autowired + private ClientService service; + + @MockBean + private ClientLegacyService legacyService; + + @MockBean + private BcRegistryService bcRegistryService; + + @Test + @DisplayName("Return client details with good standing indicator derived from BcRegistryDocumentDto") + void testGetClientDetailsWithGoodStandingIndicator() { + String clientNumber = "123456"; + String corpRegnNmbr = "9607514"; + List groups = List.of("CLIENT_ADMIN"); + + ForestClientDetailsDto initialDto = new ForestClientDetailsDto( + clientNumber, + "MY COMPANY LTD.", + null, + null, + "ACT", + "Active", + "C", + "Corporation", + "ID", + "Client Identification", + "123456789", + "BC", + corpRegnNmbr, + "MYCO", + "678", + "Test Client", + LocalDate.now(), + "Admin", + null, + null, + null, + null, + null); + + BcRegistryOfficerDto mockOfficer = new BcRegistryOfficerDto( + "officer@email.com", + "John", + "Doe", + "D", + "123456", + "My Company Ltd.", + "Person"); + + BcRegistryAddressDto mockAddress = new BcRegistryAddressDto( + "City", + "Canada", + "BC", + "A1B2C3", + "Street", + "", + "", + ""); + + BcRegistryRoleDto mockRole = new BcRegistryRoleDto( + LocalDate.now().minusYears(1), + null, + "Owner"); + + BcRegistryPartyDto mockParty = new BcRegistryPartyDto( + mockAddress, + mockAddress, + mockOfficer, + List.of(mockRole)); + + BcRegistryAddressDto mockMailingAddress = mockAddress; + BcRegistryAddressDto mockDeliveryAddress = mockAddress; + BcRegistryBusinessAdressesDto mockBusinessOffice = new BcRegistryBusinessAdressesDto( + mockMailingAddress, + mockDeliveryAddress); + + BcRegistryAlternateNameDto mockAlternateName = new BcRegistryAlternateNameDto( + "EntityType", + corpRegnNmbr, + "Alternate Name", + ZonedDateTime.now(), + LocalDate.now()); + + BcRegistryBusinessDto mockBusinessDto = new BcRegistryBusinessDto( + List.of(mockAlternateName), + true, + false, + false, + false, + corpRegnNmbr, + "MY COMPANY LTD.", + "Corporation", + "Active"); + + BcRegistryOfficesDto mockOffices = new BcRegistryOfficesDto(mockBusinessOffice); + + BcRegistryDocumentDto mockDocumentDto = + new BcRegistryDocumentDto(mockBusinessDto, mockOffices, List.of(mockParty)); + + ForestClientDetailsDto expectedDto = new ForestClientDetailsDto( + clientNumber, + "MY COMPANY LTD.", + null, + null, + "ACT", + "Active", + "C", + "Corporation", + "ID", + "Client Identification", + "123456789", + "BC", + corpRegnNmbr, + "MYCO", + "678", + "Test Client", + LocalDate.now(), + "Admin", + "Y", + null, + null, + null, + null); + + Mockito + .when(legacyService.searchByClientNumber(clientNumber, groups)) + .thenReturn(Mono.just(initialDto)); + + Mockito + .when(bcRegistryService + .requestDocumentData(corpRegnNmbr)) + .thenReturn(Flux.just(mockDocumentDto)); + + service + .getClientDetailsByClientNumber(clientNumber, groups) + .as(StepVerifier::create) + .expectNext(expectedDto) + .verifyComplete(); + } + +} diff --git a/backend/src/test/java/ca/bc/gov/app/utils/JwtPrincipalUtilTest.java b/backend/src/test/java/ca/bc/gov/app/utils/JwtPrincipalUtilTest.java index d6ed63d317..6da8e96b39 100644 --- a/backend/src/test/java/ca/bc/gov/app/utils/JwtPrincipalUtilTest.java +++ b/backend/src/test/java/ca/bc/gov/app/utils/JwtPrincipalUtilTest.java @@ -5,12 +5,16 @@ import ca.bc.gov.app.util.JwtPrincipalUtil; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; @@ -178,4 +182,50 @@ private JwtAuthenticationToken createJwtAuthenticationTokenWithAttributes( attributes ); } + + @ParameterizedTest + @DisplayName("getGroups should return expected group list") + @MethodSource("provideGroupsTestData") + void shouldGetGroups(Map tokenAttributes, List expectedGroups) { + JwtAuthenticationToken jwtAuthenticationToken = tokenAttributes == null + ? null + : createJwtAuthenticationTokenWithAttributes(tokenAttributes); + + List actualGroups = JwtPrincipalUtil.getGroups(jwtAuthenticationToken); + + assertEquals(expectedGroups, actualGroups); + } + + private static Stream provideGroupsTestData() { + return Stream.of( + // Case 1: Token attributes contain "CLIENT_ADMIN" + Arguments.of( + Map.of("cognito:groups", List.of("CLIENT_ADMIN")), + List.of("CLIENT_ADMIN") + ), + // Case 2: Token attributes contain an empty group list + Arguments.of( + Map.of("cognito:groups", List.of()), + List.of() + ), + // Case 3: Token attributes contain null groups + Arguments.of( + new HashMap<>() {{ + put("cognito:groups", null); + }}, + List.of() + ), + // Case 4: Token attributes missing "cognito:groups" + Arguments.of( + Map.of("otherKey", "someValue"), + List.of() + ), + // Case 5: Null JwtAuthenticationToken + Arguments.of( + null, + List.of() + ) + ); + } + } \ No newline at end of file diff --git a/legacy/Dockerfile b/legacy/Dockerfile index 62009b82f0..4bc585f391 100644 --- a/legacy/Dockerfile +++ b/legacy/Dockerfile @@ -46,4 +46,4 @@ HEALTHCHECK CMD curl -f http://localhost:${PORT}/actuator/health | grep '"status ENV SPRING_PROFILES_ACTIVE=container # Startup -ENTRYPOINT ["/app/nr-forest-client-legacy"] \ No newline at end of file +ENTRYPOINT ["/app/nr-forest-client-legacy"] diff --git a/legacy/src/main/java/ca/bc/gov/app/ApplicationConstants.java b/legacy/src/main/java/ca/bc/gov/app/ApplicationConstants.java index 8feee9894d..c312619c52 100644 --- a/legacy/src/main/java/ca/bc/gov/app/ApplicationConstants.java +++ b/legacy/src/main/java/ca/bc/gov/app/ApplicationConstants.java @@ -5,51 +5,9 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ApplicationConstants { - public static final String ORACLE_ATTRIBUTE_SCHEMA = "THE"; - public static final String SUBMISSION_POSTPROCESSOR_CHANNEL = "submissionCompletedChannel"; - public static final String NOTIFICATION_PROCESSING_CHANNEL = "notificationProcessingChannel"; - public static final String SUBMISSION_COMPLETION_CHANNEL = "submissionCompletionChannel"; - public static final String SUBMISSION_LEGACY_CHANNEL = "saveToLegacyChannel"; - public static final String SUBMISSION_LIST_CHANNEL = "submissionListChannel"; - public static final String MATCH_CHECKING_CHANNEL = "matchCheckingChannel"; - public static final String FORWARD_CHANNEL = "forwardChannel"; - public static final String AUTO_APPROVE_CHANNEL = "autoApproveChannel"; - public static final String REVIEW_CHANNEL = "reviewChannel"; - public static final String SUBMISSION_MAIL_CHANNEL = "submissionMailChannel"; - public static final String SUBMISSION_LEGACY_CLIENT_CHANNEL = "submissionLegacyClientChannel"; - public static final String SUBMISSION_LEGACY_CLIENT_PERSIST_CHANNEL = "submissionLegacyClientPersistChannel"; - public static final String SUBMISSION_LEGACY_LOCATION_CHANNEL = "submissionLegacyLocationChannel"; - public static final String SUBMISSION_LEGACY_CONTACT_CHANNEL = "submissionLegacyContactChannel"; - public static final String SUBMISSION_LEGACY_AGGREGATE_CHANNEL = "submissionLegacyAggregateChannel"; - public static final String SUBMISSION_LEGACY_NOTIFY_CHANNEL = "submissionLegacyNotifyChannel"; - public static final String SUBMISSION_ID = "submission-id"; - public static final String SUBMISSION_STATUS = "submission-status"; - public static final String SUBMISSION_CLIENTID = "submission-clientid"; - public static final String SUBMISSION_TYPE = "submission-type-code"; - public static final String SUBMISSION_NAME = "submission-name"; - public static final String SUBMISSION_MESSAGE_SOURCE = "submissionMessages"; - public static final String PROCESSED_MESSAGE_SOURCE = "processedMessage"; - public static final String CREATED_BY = "createdBy"; - public static final String UPDATED_BY = "updatedBy"; - public static final String FOREST_CLIENT_NUMBER = "forestClientNumber"; - public static final String FOREST_CLIENT_NAME = "forestClientName"; - public static final String REGISTRATION_NUMBER = "registrationNumber"; - public static final String LOCATION_ID = "locationId"; - public static final String TOTAL = "total"; - public static final String INDEX = "index"; - public static final String PROCESSOR_USER_NAME = "IDIR\\OTTOMATED"; - public static final long ORG_UNIT = 70L; - public static final String LOCATION_CODE = "locationCode"; - public static final String SUBMISSION_MAIL_BUILD_CHANNEL = "submissionMailBuildChannel"; + public static final String ORACLE_ATTRIBUTE_SCHEMA = "THE"; public static final String CLIENT_NUMBER = "CLIENT_NUMBER"; - public static final String CLIENT_TYPE_CODE = "CLIENT_TYPE_CODE"; - public static final String SUBMISSION_LEGACY_INDIVIDUAL_CHANNEL = "submissionLegacyIndividualChannel"; - public static final String SUBMISSION_LEGACY_USP_CHANNEL = "submissionLegacyUSPChannel"; - public static final String SUBMISSION_LEGACY_RSP_CHANNEL = "submissionLegacyRSPChannel"; - public static final String SUBMISSION_LEGACY_OTHER_CHANNEL = "submissionLegacyOtherChannel"; - public static final String CLIENT_EXISTS = "client-exists"; - public static final String CLIENT_SUBMITTER_NAME = "client-submitter-name"; - public static final String CLIENT_NUMBER_LITERAL = "clientNumber"; + } diff --git a/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java b/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java index 1617ab7c04..4c64a0cf58 100644 --- a/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java +++ b/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java @@ -2,6 +2,7 @@ import ca.bc.gov.app.dto.AddressSearchDto; import ca.bc.gov.app.dto.ContactSearchDto; +import ca.bc.gov.app.dto.ForestClientDetailsDto; import ca.bc.gov.app.dto.ForestClientDto; import ca.bc.gov.app.dto.PredictiveSearchResultDto; import ca.bc.gov.app.service.ClientSearchService; @@ -24,6 +25,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; @RestController @Slf4j @@ -54,7 +56,7 @@ public Flux findIndividuals( ) { log.info("Receiving request to search by individual {} {} {} {}", firstName, lastName, dob, identification); - return service.findByIndividual(firstName, lastName, dob, identification,true); + return service.findByIndividual(firstName, lastName, dob, identification, true); } @GetMapping("/match") @@ -73,6 +75,24 @@ public Flux findByIdAndLastName( log.info("Receiving request to search by ID {} and Last Name {}", clientId, lastName); return service.findByIdAndLastName(clientId, lastName); } + + /** + * Handles the HTTP GET request to retrieve client details by client number. + * + * @param clientNumber the client number to search for + * @param groups the list of user groups for authorization or filtering purposes + * @return a Mono containing the client details if found, or an empty Mono if not found + */ + @GetMapping("/clientNumber") + public Mono findByClientNumber( + @RequestParam String clientNumber, + @RequestParam List groups + ) { + log.info("Receiving request to search by ID {} and groups {}", + clientNumber, + groups); + return service.findByClientNumber(clientNumber, groups); + } @GetMapping("/id/{idType}/{identification}") public Flux findByIdentification( @@ -129,9 +149,9 @@ public Flux findByDoingBusinessAs( @RequestParam(required = false,defaultValue = "true") Boolean isFuzzy ) { log.info("Receiving request to search by doing business as name {} being a {} match", - dbaName, BooleanUtils.toString(isFuzzy,"fuzzy","full") + dbaName, BooleanUtils.toString(isFuzzy, "fuzzy", "full") ); - return service.findByDoingBusinessAs(dbaName,isFuzzy); + return service.findByDoingBusinessAs(dbaName, isFuzzy); } @GetMapping("/clientName") diff --git a/legacy/src/main/java/ca/bc/gov/app/dto/ForestClientDetailsDto.java b/legacy/src/main/java/ca/bc/gov/app/dto/ForestClientDetailsDto.java new file mode 100644 index 0000000000..738fe6fb0b --- /dev/null +++ b/legacy/src/main/java/ca/bc/gov/app/dto/ForestClientDetailsDto.java @@ -0,0 +1,35 @@ +package ca.bc.gov.app.dto; + +import java.time.LocalDate; +import java.util.List; +import lombok.With; + +@With +public record ForestClientDetailsDto( + String clientNumber, + String clientName, + String legalFirstName, + String legalMiddleName, + String clientStatusCode, + String clientStatusDesc, + String clientTypeCode, + String clientTypeDesc, + String clientIdTypeCode, + String clientIdTypeDesc, + String clientIdentification, + String registryCompanyTypeCode, + String corpRegnNmbr, + String clientAcronym, + String wcbFirmNumber, + String clientComment, + LocalDate clientCommentUpdateDate, + String clientCommentUpdateUser, + String goodStandingInd, + LocalDate birthdate, + + List addresses, + List contacts, + List doingBusinessAs + ) { + + } diff --git a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientRepository.java b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientRepository.java index d23343c824..1bcbce10c6 100644 --- a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientRepository.java +++ b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientRepository.java @@ -1,5 +1,6 @@ package ca.bc.gov.app.repository; +import ca.bc.gov.app.dto.ForestClientDetailsDto; import ca.bc.gov.app.dto.PredictiveSearchResultDto; import ca.bc.gov.app.entity.ForestClientEntity; import java.time.LocalDateTime; @@ -59,6 +60,53 @@ Flux findClientByIncorporationOrName( Flux matchBy(String companyName); Mono findByClientNumber(String clientNumber); + + @Query(""" + select + c.client_number, + c.client_name, + c.legal_first_name, + c.legal_middle_name, + c.client_status_code, + s.description as client_status_desc, + c.client_type_code, + t.description as client_type_desc, + c.client_id_type_code, + it.description as client_id_type_desc, + c.client_identification, + c.registry_company_type_code, + c.corp_regn_nmbr, + c.client_acronym, + c.wcb_firm_number, + c.client_comment, + fca.update_userid as latest_update_userid, + fca.update_timestamp as latest_update_timestamp, + '' as good_standing_ind, + c.birthdate + from the.forest_client c + inner join the.client_status_code s on c.client_status_code = s.client_status_code + inner join the.client_type_code t on c.client_type_code = t.client_type_code + left join the.client_id_type_code it on c.client_id_type_code = it.client_id_type_code + left join ( + select + client_number, + update_userid, + update_timestamp + from ( + select + client_number, + update_userid, + update_timestamp, + row_number() over (partition by client_number order by update_timestamp desc) as rn + from + the.for_cli_audit + where + client_comment is not null + ) + where rn = 1 + ) fca on c.client_number = fca.client_number + where c.client_number = :clientNumber""") + Mono findDetailsByClientNumber(String clientNumber); @Query(""" SELECT diff --git a/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java b/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java index 3107023a12..f7546bde53 100644 --- a/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java +++ b/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java @@ -6,6 +6,7 @@ import ca.bc.gov.app.configuration.ForestClientConfiguration; import ca.bc.gov.app.dto.AddressSearchDto; import ca.bc.gov.app.dto.ContactSearchDto; +import ca.bc.gov.app.dto.ForestClientDetailsDto; import ca.bc.gov.app.dto.ForestClientDto; import ca.bc.gov.app.dto.PredictiveSearchResultDto; import ca.bc.gov.app.entity.ClientDoingBusinessAsEntity; @@ -13,6 +14,7 @@ import ca.bc.gov.app.entity.ForestClientEntity; import ca.bc.gov.app.entity.ForestClientLocationEntity; import ca.bc.gov.app.exception.MissingRequiredParameterException; +import ca.bc.gov.app.exception.NoValueFoundException; import ca.bc.gov.app.mappers.AbstractForestClientMapper; import ca.bc.gov.app.repository.ClientDoingBusinessAsRepository; import ca.bc.gov.app.repository.ForestClientContactRepository; @@ -22,6 +24,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Comparator; +import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -36,6 +39,7 @@ import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.query.Query; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -536,6 +540,30 @@ public Flux findByClientName(String clientName) { dto.clientNumber(), dto.clientName()) ); } + + public Mono findByClientNumber(String clientNumber, List groups) { + log.info("Searching for client with number {}", clientNumber); + + if (StringUtils.isBlank(clientNumber)) { + return Mono.error(new MissingRequiredParameterException("clientNumber")); + } + + if (CollectionUtils.isEmpty(groups)) { + return Mono.error(new MissingRequiredParameterException("groups")); + } + + return forestClientRepository.findDetailsByClientNumber(clientNumber) + .switchIfEmpty( + Mono.error( + new NoValueFoundException("Client not found with number: " + clientNumber) + ) + ) + .doOnNext( + dto -> log.info("Found client with client number {}", + clientNumber, + dto.clientNumber()) + ); + } public Flux> complexSearch(String value, Pageable page) { // This condition is for predictive search, and we will stop here if no query param is provided diff --git a/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java b/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java index 8489dfd236..9bc6fdf0ac 100644 --- a/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java +++ b/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java @@ -3,8 +3,10 @@ import ca.bc.gov.app.dto.AddressSearchDto; import ca.bc.gov.app.dto.ContactSearchDto; import ca.bc.gov.app.exception.MissingRequiredParameterException; +import ca.bc.gov.app.exception.NoValueFoundException; import ca.bc.gov.app.extensions.AbstractTestContainerIntegrationTest; import java.util.HashMap; +import java.util.List; import java.util.Optional; import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; @@ -578,5 +580,57 @@ private static Stream byPredictive() { Arguments.of("matelda", null, null, "00000137", "MATELDA LINDHE (JABBERTYPE)") ); } + + @ParameterizedTest + @MethodSource("byClientNumber") + @DisplayName("Search client by client number and groups") + void shouldFindByClientNumber( + String clientNumber, + List groups, + String expectedClientNumber, + Class exception + ) { + ResponseSpec response = + client + .get() + .uri(uriBuilder -> + uriBuilder + .path("/api/search/clientNumber") + .queryParam("clientNumber", clientNumber) + .queryParam("groups", String.join(",", groups)) + .build(new HashMap<>()) + ) + .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .exchange(); + + if (StringUtils.isNotBlank(expectedClientNumber)) { + response + .expectStatus().isOk() + .expectBody() + .jsonPath("$.clientNumber").isNotEmpty() + .jsonPath("$.clientNumber").isEqualTo(expectedClientNumber) + .consumeWith(System.out::println); + } + + if (exception != null) { + response.expectStatus().is4xxClientError(); + } + } + + private static Stream byClientNumber() { + return Stream.of( + // Valid case + Arguments.of("00000123", List.of("CLIENT_ADMIN"), "00000123", null), + + // Invalid case: missing client number + Arguments.of(null, List.of("CLIENT_ADMIN"), null, + MissingRequiredParameterException.class), + + // Invalid case: missing groups + Arguments.of("00000123", List.of(), null, MissingRequiredParameterException.class), + + // Invalid case: client not found + Arguments.of("99999999", List.of("CLIENT_ADMIN"), null, NoValueFoundException.class)); + } } diff --git a/legacy/src/test/java/ca/bc/gov/app/service/ClientSearchServiceIntegrationTest.java b/legacy/src/test/java/ca/bc/gov/app/service/ClientSearchServiceIntegrationTest.java index 107f0383f4..5ab6f1207a 100644 --- a/legacy/src/test/java/ca/bc/gov/app/service/ClientSearchServiceIntegrationTest.java +++ b/legacy/src/test/java/ca/bc/gov/app/service/ClientSearchServiceIntegrationTest.java @@ -114,7 +114,7 @@ void shouldSearchWithComplexSearch( .complexSearch(searchValue, PageRequest.of(0, 5)) .as(StepVerifier::create); - if(StringUtils.isNotBlank(expectedClientNumber)) { + if (StringUtils.isNotBlank(expectedClientNumber)) { test .assertNext(dto -> { assertNotNull(dto); diff --git a/processor/Dockerfile b/processor/Dockerfile index 3b368597a7..c0c67fb645 100644 --- a/processor/Dockerfile +++ b/processor/Dockerfile @@ -1,4 +1,3 @@ -### Builder FROM eclipse-temurin:17.0.8.1_1-jdk-jammy AS build # Install Maven @@ -43,5 +42,7 @@ USER 1001 EXPOSE ${PORT} HEALTHCHECK CMD curl -f http://localhost:${PORT}/actuator/health | grep '"status":"UP"' +ENV SPRING_PROFILES_ACTIVE=container + # Startup -ENTRYPOINT ["java", "-jar", "/app/nr-forest-client-processor.jar", "--spring.profiles.active=container"] +ENTRYPOINT ["java", "-jar", "/app/nr-forest-client-processor.jar"]