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-1517): CLIENT3 increment #1203

Merged
merged 56 commits into from
Oct 16, 2024
Merged
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
0bd89eb
no message
mamartinezmejia Oct 2, 2024
b858d1f
Merge branch 'main' into feat/client3
mamartinezmejia Oct 2, 2024
676d35c
no message
mamartinezmejia Oct 2, 2024
6b4b375
no message
mamartinezmejia Oct 2, 2024
f1be697
no message
mamartinezmejia Oct 2, 2024
6926aa2
Renamed variable
mamartinezmejia Oct 2, 2024
2556c09
Renamed endpoint
mamartinezmejia Oct 2, 2024
0c62401
Merge branch 'main' into feat/client3
mamartinezmejia Oct 2, 2024
acff049
Merge branch 'main' into feat/client3
mamartinezmejia Oct 3, 2024
6fa9ce1
no message
mamartinezmejia Oct 3, 2024
3fbc7d3
Renamed variable
mamartinezmejia Oct 3, 2024
1187889
feat(FSADT1-1519): Draft for Create API endpoint for predictive search
mamartinezmejia Oct 3, 2024
4921a2c
Merge branch 'main' into feat/client3
mamartinezmejia Oct 3, 2024
e235bb4
Added temporary feature flag for the search functionality
mamartinezmejia Oct 3, 2024
9b989dc
Merge branch 'main' into feat/client3
mamartinezmejia Oct 4, 2024
cc6f1b2
feat(FSADT1-1519): Predictive search endpoint #1
mamartinezmejia Oct 4, 2024
b4b34c3
feat(FSADT1-1519): Predictive search #2
mamartinezmejia Oct 4, 2024
b1c28ec
Merge branch 'main' into feat/client3
mamartinezmejia Oct 4, 2024
b467da6
feat(be:FSADT1-1519): Create API endpoint for predictive search
mamartinezmejia Oct 7, 2024
a2ccc23
Merge branch 'main' into feat/client3
mamartinezmejia Oct 7, 2024
c7caf8e
Merge branch 'feat/client3' into feat/be/FSADT1-1519
mamartinezmejia Oct 7, 2024
eacba2f
Merge branch 'main' into feat/client3
mamartinezmejia Oct 7, 2024
0bbacd1
Merge branch 'feat/client3' into feat/be/FSADT1-1519
mamartinezmejia Oct 7, 2024
3b2941c
feat(be:FSADT1-1519): Create API endpoint for predictive search
mamartinezmejia Oct 7, 2024
54be803
Merge branch 'feat/be/FSADT1-1519' into feat/client3
mamartinezmejia Oct 7, 2024
b92383e
chore: Added javadocs
mamartinezmejia Oct 7, 2024
bd5a9b1
Merge branch 'main' into feat/client3
mamartinezmejia Oct 7, 2024
b2c5453
Removed unused imports
mamartinezmejia Oct 7, 2024
8ca00b1
feat(be:FSADT1-1519): Create API endpoint for predictive search
mamartinezmejia Oct 7, 2024
91395fe
Merge branch 'main' into feat/FSADT1-1519-1520
mamartinezmejia Oct 8, 2024
ef4da6d
Refactored code and included full seach
mamartinezmejia Oct 8, 2024
1dcd78b
Merge branch 'feat/FSADT1-1519-1520' into feat/client3
mamartinezmejia Oct 8, 2024
8a426d2
Merge branch 'main' into feat/client3
mamartinezmejia Oct 8, 2024
9cf3e22
Refactored code to have 1 query only
mamartinezmejia Oct 9, 2024
5b7034e
Merge branch 'main' into feat/client3
mamartinezmejia Oct 9, 2024
209e5a3
Added count in the API
mamartinezmejia Oct 9, 2024
7127d58
feat(fe:FSADT1-1541): Display an error message when the BE is down (#…
mamartinezmejia Oct 9, 2024
258ed26
feat(fe:FSADT1-1521): create the predictive search (#1223)
fterra-encora Oct 10, 2024
cf0c932
Merge branch 'main' into feat/client3
mamartinezmejia Oct 10, 2024
1d68a0c
Renamed files
mamartinezmejia Oct 10, 2024
d4be90a
Added missing key
mamartinezmejia Oct 10, 2024
4aa61e6
Merge branch 'main' into feat/client3
mamartinezmejia Oct 10, 2024
c413195
Merge branch 'main' into feat/client3
mamartinezmejia Oct 10, 2024
13db50d
fix(fe:FSADT1-1521): clear the AutoComplete field when preventSelecti…
fterra-encora Oct 10, 2024
eccaf5c
Merge branch 'main' into feat/client3
mamartinezmejia Oct 10, 2024
e13e35f
Added stubs for full search
mamartinezmejia Oct 10, 2024
fd7623c
Merge branch 'main' into feat/client3
mamartinezmejia Oct 10, 2024
a62b07c
Merge branch 'main' into feat/client3
mamartinezmejia Oct 10, 2024
9055198
fix: apply the feature flag to the Client search button
fterra-encora Oct 11, 2024
43124b4
fix: open client details
fterra-encora Oct 14, 2024
ddcc2ff
Merge branch 'main' into feat/client3
mamartinezmejia Oct 15, 2024
6c4f1a9
Cleaned up code
mamartinezmejia Oct 15, 2024
4257e9d
Cleaned up code
mamartinezmejia Oct 15, 2024
899707d
Made changes after code reviews
mamartinezmejia Oct 15, 2024
79b8652
Merge branch 'main' into feat/client3
mamartinezmejia Oct 15, 2024
bd40718
Made changes after code reviews
mamartinezmejia Oct 15, 2024
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
Original file line number Diff line number Diff line change
@@ -6,10 +6,8 @@
import ca.bc.gov.app.service.client.ClientCodeService;
import ca.bc.gov.app.service.client.ClientCountryProvinceService;
import ca.bc.gov.app.service.client.ClientDistrictService;
import ca.bc.gov.app.service.client.ClientService;
import io.micrometer.observation.annotation.Observed;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
@@ -28,7 +26,6 @@
@Observed
public class ClientCodesController {

private final ClientService clientService;
private final ClientDistrictService clientDistrictService;
private final ClientCountryProvinceService clientCountryProvinceService;
private final ClientCodeService clientCodeService;
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package ca.bc.gov.app.controller.client;

import ca.bc.gov.app.ApplicationConstant;
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.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 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;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -27,6 +33,7 @@
public class ClientController {

private final ClientService clientService;
private final ClientLegacyService clientLegacyService;

@GetMapping("/{clientNumber}")
public Mono<ClientDetailsDto> getClientDetails(
@@ -45,7 +52,44 @@ public Mono<ClientDetailsDto> getClientDetails(
JwtPrincipalUtil.getProvider(principal)
);
}

@GetMapping("/search")
public Flux<ClientListDto> fullSearch(
@RequestParam(required = false, defaultValue = "0") int page,
@RequestParam(required = false, defaultValue = "10") int size,
@RequestParam(required = false, defaultValue = "") String keyword,
ServerHttpResponse serverResponse) {

log.info("Listing clients: page={}, size={}, keyword={}", page, size, keyword);

return clientLegacyService
.search(
page,
size,
keyword
)
.doOnNext(pair -> {
Long count = pair.getSecond();

serverResponse
.getHeaders()
.putIfAbsent(
ApplicationConstant.X_TOTAL_COUNT,
List.of(count.toString())
);
}
)
.map(Pair::getFirst)
.doFinally(signalType ->
serverResponse
.getHeaders()
.putIfAbsent(
ApplicationConstant.X_TOTAL_COUNT,
List.of("0")
)
);
}

/**
* Retrieve a Flux of ClientLookUpDto objects by searching for clients with a specific name.
*
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ca.bc.gov.app.dto.client;

public record ClientListDto(
String clientNumber,
String clientAcronym,
String clientFullName,
String clientType,
String city,
String clientStatus) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ca.bc.gov.app.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.server.ResponseStatusException;

@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
public class MissingRequiredParameterException extends ResponseStatusException {
public MissingRequiredParameterException(String parameterName) {
super(HttpStatus.EXPECTATION_FAILED,
String.format("Missing value for parameter %s", parameterName));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ca.bc.gov.app.service.client;

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.ForestClientDto;
@@ -11,6 +12,7 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.BodyInserters;
@@ -307,6 +309,38 @@ public Flux<ForestClientDto> searchContact(ContactSearchDto dto) {
client.clientNumber())
);
}

public Flux<Pair<ClientListDto, Long>> search(int page, int size, String keyword) {
log.info(
"Searching clients by keyword {} with page {} and size {}",
keyword,
page,
size
);

return legacyApi
.get()
.uri(builder ->
builder
.path("/api/search")
.queryParam("page", page)
.queryParam("size", size)
.queryParam("value", keyword)
.build(Map.of())
)
.exchangeToFlux(response -> {
List<String> totalCountHeader = response.headers().header("X-Total-Count");
Long count = totalCountHeader.isEmpty() ? 0L : Long.valueOf(totalCountHeader.get(0));

return response
.bodyToFlux(ClientListDto.class)
.map(dto -> Pair.of(dto, count));
})
.doOnNext(pair -> {
ClientListDto dto = pair.getFirst();
Long totalCount = pair.getSecond();
log.info("Found clients by keyword {}, total count: {}", dto.clientNumber(), totalCount);
});
}

}
Original file line number Diff line number Diff line change
@@ -11,13 +11,11 @@
import ca.bc.gov.app.dto.client.ClientContactDto;
import ca.bc.gov.app.dto.client.ClientListSubmissionDto;
import ca.bc.gov.app.dto.client.ClientSubmissionDto;
import ca.bc.gov.app.dto.client.DistrictDto;
import ca.bc.gov.app.dto.submissions.SubmissionAddressDto;
import ca.bc.gov.app.dto.submissions.SubmissionApproveRejectDto;
import ca.bc.gov.app.dto.submissions.SubmissionBusinessDto;
import ca.bc.gov.app.dto.submissions.SubmissionContactDto;
import ca.bc.gov.app.dto.submissions.SubmissionDetailsDto;
import ca.bc.gov.app.entity.client.ClientTypeCodeEntity;
import ca.bc.gov.app.entity.client.SubmissionDetailEntity;
import ca.bc.gov.app.entity.client.SubmissionEntity;
import ca.bc.gov.app.entity.client.SubmissionLocationContactEntity;
@@ -30,7 +28,6 @@
import ca.bc.gov.app.predicates.QueryPredicates;
import ca.bc.gov.app.predicates.SubmissionDetailPredicates;
import ca.bc.gov.app.predicates.SubmissionPredicates;
import ca.bc.gov.app.repository.client.DistrictCodeRepository;
import ca.bc.gov.app.repository.client.SubmissionContactRepository;
import ca.bc.gov.app.repository.client.SubmissionDetailRepository;
import ca.bc.gov.app.repository.client.SubmissionLocationContactRepository;
134 changes: 134 additions & 0 deletions frontend/cypress/e2e/pages/SearchPage.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import type { ClientSearchResult } from "@/dto/CommonTypesDto";

describe("Search Page", () => {
const predictiveSearchCounter = {
count: 0,
};

const checkDisplayedResults = (clientList: ClientSearchResult[]) => {
clientList.forEach((client) => {
cy.get("#search-box")
.find(`cds-combo-box-item[data-value^="${client.clientNumber}"]`)
.should("exist");
});
};
beforeEach(() => {
// reset counter
predictiveSearchCounter.count = 0;

cy.intercept("/api/clients/search?keyword=*", (req) => {
predictiveSearchCounter.count++;
req.continue();
}).as("predictiveSearch");

cy.viewport(1920, 1080);
cy.visit("/");

cy.login("uattest@gov.bc.ca", "Uat Test", "idir", {
given_name: "James",
family_name: "Baxter",
"cognito:groups": ["CLIENT_VIEWER"],
});

// Check if the Client search button is visible
cy.get("#menu-list-search").should("be.visible").click();

cy.get("h1").should("be.visible").should("contain", "Client search");

cy.window().then((win) => {
cy.stub(win, "open").as("windowOpen");
});
});

describe("when user fills in the search box with a valid value", () => {
beforeEach(() => {
cy.fillFormEntry("#search-box", "car", { skipBlur: true });
});

it("makes the API call with the entered keywords", () => {
cy.wait("@predictiveSearch").then((interception) => {
expect(interception.request.query.keyword).to.eq("car");
});
cy.wrap(predictiveSearchCounter).its("count").should("eq", 1);
});

it("displays autocomplete results", () => {
cy.wait("@predictiveSearch").then((interception) => {
const data = interception.response.body;

cy.wrap(data).should("be.an", "array").and("have.length.greaterThan", 0);

cy.get("#search-box")
.find("cds-combo-box-item")
.should("have.length", data.length)
.should("be.visible");

checkDisplayedResults(data);
});
});

describe("and types more characters", () => {
beforeEach(() => {
cy.wait("@predictiveSearch");
cy.fillFormEntry("#search-box", "d", { skipBlur: true });
});

it("makes another the API call with the updated keywords", () => {
cy.wait("@predictiveSearch").then((interception) => {
expect(interception.request.query.keyword).to.eq("card");
});
cy.wrap(predictiveSearchCounter).its("count").should("eq", 2);
});

it("updates the autocomplete results", () => {
cy.wait("@predictiveSearch").then((interception) => {
const data = interception.response.body;

cy.wrap(data).should("be.an", "array").and("have.length.greaterThan", 0);

cy.get("#search-box")
.find("cds-combo-box-item")
.should("have.length", data.length)
.should("be.visible");

checkDisplayedResults(data);
});
});
});

describe("and user clicks a result", () => {
const clientNumber = "00054076";
beforeEach(() => {
cy.get("#search-box")
.find("cds-combo-box-item")
.should("have.length.greaterThan", 0)
.should("be.visible");

cy.get("#search-box").find(`cds-combo-box-item[data-value^="${clientNumber}"]`).click();
});
it("navigates to the client details", () => {
const greenDomain = "green-domain.com";
cy.get("@windowOpen").should(
"be.calledWith",
`https://${greenDomain}/int/client/client02MaintenanceAction.do?bean.clientNumber=${clientNumber}`,
"_blank",
"noopener",
);
});
});
});

describe("when user fills in the search box with an invalid value", () => {
beforeEach(() => {
cy.fillFormEntry("#search-box", "até", { skipBlur: true });
});

it("shows an error message", () => {
cy.contains("The search terms can only contain: A-Z, a-z, 0-9, space or common symbols");
});
it("makes no API call", () => {
cy.wait(500); // This time has to be greater than the debouncing time
cy.wrap(predictiveSearchCounter).its("count").should("eq", 0);
});
});
});
46 changes: 38 additions & 8 deletions frontend/cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -146,15 +146,45 @@ Cypress.Commands.add("getMany", (names: string[]): Cypress.Chainable<any[]> => {
return cy.wrap(values);
});

Cypress.Commands.add("fillFormEntry",(field: string, value: string, delayMS: number = 10, area: boolean = false) =>{
interface FillFormEntryOptions {
delayMS?: number;
area?: boolean;
skipBlur?: boolean;
}

interface FillFormEntry {
(field: string, value: string, delayMS?: number, area?: boolean): void;
(field: string, value: string, options?: FillFormEntryOptions): void;
}

const fillFormEntry: FillFormEntry = (
field: string,
value: string,
arg3: number | FillFormEntryOptions = 10,
arg4: boolean | never = false,
) => {
const options =
typeof arg3 === "object"
? arg3
: {
delayMS: arg3,
area: arg4,
};
const { delayMS, area, skipBlur } = options;
cy.get(field)
.should("exist")
.shadow()
.find(area ? "textarea" : "input")
.focus()
.type(value,{ delay: delayMS })
.blur();
});
.should("exist")
.shadow()
.find(area ? "textarea" : "input")
.focus()
.type(value, { delay: delayMS })
.then((subject) => {
if (!skipBlur) {
cy.wrap(subject).blur();
}
});
};

Cypress.Commands.add("fillFormEntry", fillFormEntry);

Cypress.Commands.add("clearFormEntry",(field: string, area: boolean = false) =>{
cy.get(field)
Loading
Loading