Skip to content

Commit

Permalink
feat(FSADT1-1363): backend fuzzy match for individuals (#996)
Browse files Browse the repository at this point in the history
* feat(FSADT1-1363): adding new endpoint and cleaning old code

* chore(FSADT1-1363): changing testcontainer for oracle

changing it due to the issues with Mac M2 chip. Increased the timeout to allow the oracle instance to spin up properly

* chore(FSADT1-1363): updating test data

updating test data for oracle

* feat(FSADT1-1363): fuzzy match for individuals

initial commit with some of the heavy lifting. not final

* chore(FSADT1-1363): adding temporary api auth

this entry is temporary and it will be changed before final push

* feat(FSADT1-1363): updated api auth to matches

* chore(FSADT1-1363): refactoring matcher code

Refactored the code to allow easy to use on next iterations

* test(FSADT1-1363): adding tests

* chore: code cleanup

* feat(FSADT1-1363): setting flag to allow match endpoint to work

* feat(FSADT1-1363): add fe stub

* chore(FSADT1-1363): code refactor

* chore(FSADT1-1363): updating oracle sql files

* fix(FSADT1-1363): mergind and rebasing

* fix(FSADT1-1363): fixed branch due to dto changes

* test(FSADT1-1363): updating test
  • Loading branch information
paulushcgcj authored Jun 24, 2024
1 parent 4b733d4 commit bf1dcd3
Show file tree
Hide file tree
Showing 33 changed files with 2,711 additions and 342 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ca.bc.gov.app.configuration.flags;

import ca.bc.gov.app.dto.bcregistry.BcRegistryAddressDto;
import ca.bc.gov.app.dto.client.ClientSubmissionDto;
import java.util.function.Predicate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
Expand All @@ -14,7 +15,6 @@ public class FeatureFlagsConfiguration {
@Bean
@ConditionalOnProperty(name = "features.bcregistry.multiaddress", havingValue = "false")
public Predicate<BcRegistryAddressDto> isMultiAddressDisabled() {
log.info("Feature :: Multi address is disabled");
return addressDto -> addressDto.addressType().equalsIgnoreCase("mailing");
}

Expand All @@ -24,4 +24,17 @@ public Predicate<BcRegistryAddressDto> isMultiAddressEnabled() {
return addressDto -> true;
}

@Bean
@ConditionalOnProperty(name = "features.staff.match", havingValue = "true")
public Predicate<ClientSubmissionDto> isMatcherEnabled() {
return dto -> true;
}

@Bean
@ConditionalOnProperty(name = "features.staff.match", matchIfMissing = true)
public Predicate<ClientSubmissionDto> isMatcherDisabled() {
return dto -> false;
}


}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ca.bc.gov.app.controller;

import ca.bc.gov.app.exception.DataMatchException;
import ca.bc.gov.app.exception.ValidationException;
import java.util.Arrays;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -118,6 +119,13 @@ private Mono<ServerResponse> renderErrorResponse(
.body(BodyInserters.fromValue(validationException.getErrors()));
}

if(exception instanceof DataMatchException matchException){
return ServerResponse
.status(matchException.getStatusCode())
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(matchException.getMatches()));
}

// Get the error message
String errorMessage = exception.getMessage();
// Set the default error status to INTERNAL_SERVER_ERROR
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package ca.bc.gov.app.controller.client;

import ca.bc.gov.app.dto.client.ClientSubmissionDto;
import ca.bc.gov.app.service.client.ClientMatchService;
import io.micrometer.observation.annotation.Observed;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

/**
* This class is a REST controller for client matching operations.
* It uses the ClientMatchService to perform the matching operations.
*/
@RestController
@RequestMapping(value = "/api/clients/matches", produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
@Slf4j
@Observed
public class ClientMatchController {

/**
* The ClientMatchService used to perform the matching operations.
*/
private final ClientMatchService matchService;

/**
* This method is a POST endpoint for fuzzy matching clients.
* It takes a ClientSubmissionDto object and a step number as input.
* It uses the ClientMatchService to perform the matching operation.
*
* @param dto The ClientSubmissionDto object containing the client data to be matched.
* @param step The step number for the matching operation.
* @return A Mono<Void> indicating when the matching process is complete.
*/
@PostMapping
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<Void> fuzzyMatchClients(
@RequestBody ClientSubmissionDto dto,
@RequestHeader(name = "X-STEP") int step
) {
return matchService.matchClients(dto, step);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import lombok.With;
import org.apache.commons.lang3.StringUtils;

@With
public record ClientBusinessInformationDto(
String registrationNumber,
String registrationNumber,
String businessName,
String businessType,
String clientType,
String goodStandingInd,
String businessType,
String clientType,
String goodStandingInd,
String legalType,
LocalDate birthdate,
LocalDate birthdate,
String district,
String workSafeBcNumber,
String doingBusinessAs,
Expand All @@ -26,6 +28,7 @@ public record ClientBusinessInformationDto(
String clientIdentification,
String identificationCountry,
String identificationProvince) {

/**
* Returns a map containing the description of the client's business information.
*
Expand All @@ -48,10 +51,45 @@ public Map<String, Object> description() {
descMap.put("middleName", StringUtils.defaultString(middleName));
descMap.put("lastName", StringUtils.defaultString(lastName));
descMap.put("notes", StringUtils.defaultString(notes));
descMap.put("identificationType", StringUtils.defaultString(identificationType));
descMap.put("identificationType", StringUtils.defaultString(idType()));
descMap.put("clientIdentification", StringUtils.defaultString(clientIdentification));
descMap.put("identificationCountry", StringUtils.defaultString(identificationCountry));
descMap.put("identificationProvince", StringUtils.defaultString(identificationProvince));
return descMap;
}

/**
* This method is used to determine the identification type of the client.
* <p>
* It first checks if both the identificationType and clientIdentification fields are not blank.
* If they are not, it then checks if the identificationType is either CDDL or USDL and if the
* identificationProvince is not blank. If these conditions are met, it returns the
* identificationProvince concatenated with "DL". If these conditions are not met, it simply
* returns the identificationType.
* <p>
* If either the identificationType or clientIdentification fields are blank, it returns null.
*
* @return The identification type of the client, or null if the necessary fields are blank.
*/
public String idType() {

if (StringUtils.isNotBlank(identificationType) && StringUtils.isNotBlank(
clientIdentification)) {

if (
(
IdentificationTypeEnum.CDDL.equals(IdentificationTypeEnum.valueOf(identificationType))
&&
IdentificationTypeEnum.USDL.equals(
IdentificationTypeEnum.valueOf(identificationType))
)
&& StringUtils.isNotBlank(identificationProvince)) {
return identificationProvince + "DL";
}

return identificationType;
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
package ca.bc.gov.app.dto.client;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public record ClientLocationDto(
List<ClientAddressDto> addresses,

List<ClientContactDto> contacts
) {

public Map<String, Object> description() {
return
Stream.concat(
contacts
.stream()
.map(ClientContactDto::description),
addresses
.stream()
.map(ClientAddressDto::description)
)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> v2
)
);
}
}
package ca.bc.gov.app.dto.client;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.With;

@With
public record ClientLocationDto(
List<ClientAddressDto> addresses,

List<ClientContactDto> contacts
) {

public Map<String, Object> description() {
return
Stream.concat(
contacts
.stream()
.map(ClientContactDto::description),
addresses
.stream()
.map(ClientAddressDto::description)
)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> v2
)
);
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
package ca.bc.gov.app.dto.client;

import java.util.Map;

public record ClientSubmissionDto(
ClientBusinessInformationDto businessInformation,
ClientLocationDto location,
String userId,
String userLastName) {
/**
* Returns a map containing the description of the client's business information.
*
* @return a map with keys representing the description fields and corresponding values
*/
public Map<String, Object> description(String userName) {
Map<String, Object> descriptions = location.description();
descriptions.put("business", businessInformation.description());
descriptions.put("userName", userName);
return descriptions;
}

}
package ca.bc.gov.app.dto.client;

import java.util.Map;
import lombok.With;

@With
public record ClientSubmissionDto(
ClientBusinessInformationDto businessInformation,
ClientLocationDto location,
String userId,
String userLastName) {
/**
* Returns a map containing the description of the client's business information.
*
* @return a map with keys representing the description fields and corresponding values
*/
public Map<String, Object> description(String userName) {
Map<String, Object> descriptions = location.description();
descriptions.put("business", businessInformation.description());
descriptions.put("userName", userName);
return descriptions;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ca.bc.gov.app.dto.client;

public record MatchResult(
String field,
String match,
boolean fuzzy
) {
}
12 changes: 12 additions & 0 deletions backend/src/main/java/ca/bc/gov/app/dto/client/StepMatchEnum.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ca.bc.gov.app.dto.client;

public enum StepMatchEnum {
STEP1INDIVIDUAL,
STEP1REGISTERED,
STEP1UNREGISTERED,
STEP1FIRSTNATION,
STEP1FORESTS,
STEP1GOVERNMENT,
STEP2,
STEP3
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ca.bc.gov.app.exception;

import ca.bc.gov.app.dto.client.MatchResult;
import java.util.List;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.server.ResponseStatusException;

@Getter
@ResponseStatus(HttpStatus.CONFLICT)
public class DataMatchException extends ResponseStatusException {

private final transient List<MatchResult> matches;

public DataMatchException(List<MatchResult> matches) {
super(HttpStatus.CONFLICT, "Match found on existing data.");
this.matches = matches;
}

}
Loading

0 comments on commit bf1dcd3

Please sign in to comment.