Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(FSADT1-1397): Backend Fuzzy matching on Business Information for BC Registered Business #1065

Merged
merged 19 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ RUN microdnf update --nodocs -y && \
microdnf clean all

# Add Maven to the PATH environment variable
ENV MAVEN_HOME /opt/maven
ENV PATH $MAVEN_HOME/bin:$PATH
ENV MAVEN_HOME=/opt/maven
ENV PATH=$MAVEN_HOME/bin:$PATH

# Receiving app version
ARG APP_VERSION=0.0.1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,15 +209,54 @@ public Flux<ForestClientDto> searchDocument(

}

/**
* Searches for a list of {@link ForestClientDto} in the legacy API based on the given search type
* and value. This method constructs a query parameter map using the provided search type and
* value, sends a GET request to the legacy API, and converts the response into a Flux of
* ForestClientDto objects. It also logs the search parameters and the results for debugging
* purposes.
*
* @param searchType The type of search to perform (e.g., "registrationNumber", "companyName").
* @param value The value to search for.
* @return A Flux of ForestClientDto objects matching the search criteria.
*/
public Flux<ForestClientDto> searchGeneric(
String searchType,
String value
) {
return searchGeneric(searchType, searchType, value);
}

public Flux<ForestClientDto> searchGeneric(
String searchType,
String paramName,
String value
) {

if (StringUtils.isBlank(value))
if (StringUtils.isAnyBlank(searchType, paramName, value)) {
return Flux.empty();
}

Map<String, List<String>> parameters = Map.of(searchType, List.of(value));
Map<String, List<String>> parameters = Map.of(paramName, List.of(value));

return searchGeneric(searchType, parameters);
}

public Flux<ForestClientDto> searchGeneric(
String searchType,
Map<String, List<String>> parameters
) {

if (
StringUtils.isBlank(searchType)
|| parameters == null
|| parameters.isEmpty()
|| parameters.values().stream().anyMatch(CollectionUtils::isEmpty)
|| parameters.values().stream().flatMap(List::stream).anyMatch(StringUtils::isBlank)
|| parameters.keySet().stream().anyMatch(StringUtils::isBlank)
) {
return Flux.empty();
}

return
legacyApi
Expand Down Expand Up @@ -251,7 +290,8 @@ public Flux<ForestClientDto> searchLocation(AddressSearchDto dto) {
.body(BodyInserters.fromValue(dto))
.exchangeToFlux(response -> response.bodyToFlux(ForestClientDto.class))
.doOnNext(
client -> log.info("Found Legacy data for location search with client number {}", client.clientNumber())
client -> log.info("Found Legacy data for location search with client number {}",
client.clientNumber())
);
}

Expand All @@ -263,8 +303,10 @@ public Flux<ForestClientDto> searchContact(ContactSearchDto dto) {
.body(BodyInserters.fromValue(dto))
.exchangeToFlux(response -> response.bodyToFlux(ForestClientDto.class))
.doOnNext(
client -> log.info("Found Legacy data for contact search with client number {}", client.clientNumber())
client -> log.info("Found Legacy data for contact search with client number {}",
client.clientNumber())
);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public Mono<Void> matchClients(
private Mono<Void> matchStep1(ClientSubmissionDto dto) {

switch (dto.businessInformation().clientType()) {
case "BCR" -> {
case "C", "RSP", "S", "A", "P", "L" -> {
return findAndRunMatcher(dto, StepMatchEnum.STEP1REGISTERED);
}
case "R" -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package ca.bc.gov.app.service.client.matches;

import ca.bc.gov.app.dto.client.ClientSubmissionDto;
import ca.bc.gov.app.dto.client.StepMatchEnum;
import ca.bc.gov.app.dto.legacy.ForestClientDto;
import ca.bc.gov.app.service.client.ClientLegacyService;
import io.micrometer.observation.annotation.Observed;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Component
@Slf4j
@Observed
@RequiredArgsConstructor
public class RegisteredStepMatcher implements StepMatcher {

private static final String BUSINESS_FIELD_NAME = "businessInformation.businessName";

/**
* The ClientLegacyService used to search for registered companies and other data.
*/
private final ClientLegacyService legacyService;

/**
* This method is used to get the logger for this class. This is just to allow the default methods
* to access the logger.
*
* @return The Logger object used for logging in this class.
*/
public Logger getLogger() {
return log;
}

/**
* This method returns the step matcher enumeration value for registered steps.
*
* @return The StepMatchEnum value representing the registered step.
*/
public StepMatchEnum getStepMatcher() {
return StepMatchEnum.STEP1REGISTERED;
}

/**
* <p>This method matches the client submission data to the registered step. It performs three
* searches:</p>
* <ol>
* <li>A fuzzy match should happen for the Client name</li>
* <li>A full match should happen for the Incorporation number</li>
* <li>A fuzzy match should happen for the Doing Business as</li>
* <li>A full match should happen for the Client name</li>
* <li>A full match should happen for the Doing Business as</li>
* <li>A full match should happen for the Acronym</li>
* <li>A full match should happen for the combination of First name, Last name and date of birth if the user is a Sole Proprietor</li>
* </ol>
* <p>The results of these searches are then processed and reduced to a single result.</p>
*
* @param dto The ClientSubmissionDto object containing the client data to be matched.
* @return A Mono<Void> indicating when the matching process is complete.
*/
@Override
public Mono<Void> matchStep(ClientSubmissionDto dto) {

// Search for individual without document id
Flux<ForestClientDto> individualFuzzyMatch =
Mono
.just(dto.businessInformation())
.filter(businessInformation -> businessInformation.clientType().equals("RSP"))
.flatMapMany(businessInformation ->
legacyService.searchIndividual(
businessInformation.firstName(),
businessInformation.lastName(),
businessInformation.birthdate(),
null
)
)
.doOnNext(client -> log.info("Match found for sole proprietor fuzzy match: {}",
client.clientNumber())
);

Flux<ForestClientDto> clientRegistrationFullMatch =
legacyService
.searchLegacy(
dto.businessInformation().registrationNumber(),
null,
null,
null
).doOnNext(client -> log.info("Match found for registration number full match: {}",
client.clientNumber())
);

Flux<ForestClientDto> clientNameFullMatch =
legacyService
.searchGeneric(
"clientName",
dto.businessInformation().businessName()
).doOnNext(client -> log.info("Match found for client name full match: {}",
client.clientNumber())
);

Flux<ForestClientDto> clientNameFuzzyMatch =
legacyService
.searchGeneric(
"match",
"companyName",
dto.businessInformation().businessName()
).doOnNext(client -> log.info("Match found for client name fuzzy match: {}",
client.clientNumber())
);

Flux<ForestClientDto> clientAcronymFullMatch =
legacyService
.searchGeneric(
"acronym",
dto.businessInformation().clientAcronym()
).doOnNext(client -> log.info("Match found for client acronym full match: {}",
client.clientNumber())
);

Flux<ForestClientDto> dbaFuzzyMatch =
legacyService
.searchGeneric(
"doingBusinessAs",
"dbaName",
dto.businessInformation().doingBusinessAs()
).doOnNext(client -> log.info("Match found for doing business as fuzzy match: {}",
client.clientNumber())
);

Flux<ForestClientDto> dbaFullMatch =
legacyService
.searchGeneric(
"doingBusinessAs",
Map.of(
"dbaName", List.of(dto.businessInformation().doingBusinessAs()),
"isFuzzy", List.of("false")
)
).doOnNext(client -> log.info("Match found for doing business as full match: {}",
client.clientNumber())
);

return reduceMatchResults(
Flux.concat(

//A fuzzy match should happen for the Client name
processResult(
clientNameFuzzyMatch,
BUSINESS_FIELD_NAME,
true
),

//A full match should happen for the Client name
processResult(
clientNameFullMatch,
BUSINESS_FIELD_NAME,
false
),

//A full match should happen for the Incorporation number
//We point to the businessName as this is the only field the user has access to
processResult(
clientRegistrationFullMatch,
BUSINESS_FIELD_NAME,
false
),

//A fuzzy match should happen for the Doing Business as
processResult(
dbaFuzzyMatch,
"businessInformation.doingBusinessAs",
true
),

//A full match should happen for the Doing Business as
processResult(
dbaFullMatch,
"businessInformation.doingBusinessAs",
false
),

//A full match should happen for the Acronym
processResult(
clientAcronymFullMatch,
"businessInformation.clientAcronym",
false
),

//A full match should happen for the combination of First name, Last name and date of birth if the user is a Sole Proprietor
//We point to the businessName as this is the only field the user has access to
processResult(
individualFuzzyMatch,
BUSINESS_FIELD_NAME,
true
)
)
);
}
}
2 changes: 1 addition & 1 deletion backend/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ ca:
- X-Total-Count
- x-client-id
- X-Client-Id
- X-Step
- x-step
- X-Step
methods:
- OPTIONS
- GET
Expand Down
Loading
Loading