diff --git a/backend/pom.xml b/backend/pom.xml index e8c50967..805d78c8 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -129,6 +129,11 @@ 1.18.30 true + + org.ocpsoft.prettytime + prettytime + 5.0.7.Final + diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/NotFoundGenericException.java b/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/NotFoundGenericException.java new file mode 100644 index 00000000..417c8efd --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/NotFoundGenericException.java @@ -0,0 +1,14 @@ +package ca.bc.gov.restapi.results.common.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.server.ResponseStatusException; + +/** This class represents a generic not found request. */ +@ResponseStatus(value = HttpStatus.NOT_FOUND) +public class NotFoundGenericException extends ResponseStatusException { + + public NotFoundGenericException(String entityName) { + super(HttpStatus.NOT_FOUND, String.format("%s record(s) not found!", entityName)); + } +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/OpeningNotFoundException.java b/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/OpeningNotFoundException.java new file mode 100644 index 00000000..75bee872 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/OpeningNotFoundException.java @@ -0,0 +1,9 @@ +package ca.bc.gov.restapi.results.common.exception; + +/** This class represents an error, when a Opening was not found. */ +public class OpeningNotFoundException extends NotFoundGenericException { + + public OpeningNotFoundException() { + super("UserOpening"); + } +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/UserOpeningNotFoundException.java b/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/UserOpeningNotFoundException.java new file mode 100644 index 00000000..a6d69255 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/UserOpeningNotFoundException.java @@ -0,0 +1,9 @@ +package ca.bc.gov.restapi.results.common.exception; + +/** This class represents a UserOpeningEntity not found in the database. */ +public class UserOpeningNotFoundException extends NotFoundGenericException { + + public UserOpeningNotFoundException() { + super("UserOpeningEntity"); + } +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/common/util/TimestampUtil.java b/backend/src/main/java/ca/bc/gov/restapi/results/common/util/TimestampUtil.java new file mode 100644 index 00000000..a79a0528 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/common/util/TimestampUtil.java @@ -0,0 +1,56 @@ +package ca.bc.gov.restapi.results.common.util; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Period; +import java.time.format.DateTimeFormatter; +import java.util.Objects; + +/** This class contains useful methods for parsing and handling timestamps. */ +public class TimestampUtil { + + private TimestampUtil() {} + + /** + * Parses a date string to a {@link LocalDateTime} instance. Format: yyyy-MM-dd. + * + * @param dateStr The date to be parsed + * @return LocalDateTime parsed or null if a null value is found. + */ + public static LocalDateTime parseDateString(String dateStr) { + if (Objects.isNull(dateStr)) { + return null; + } + + LocalDate entryDateStartLd = + LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd")); + return entryDateStartLd.atStartOfDay(); + } + + /** + * Extract the number based on the difference between today and the day the Opening got created. + * + * @param entryLocalDateTime The LocalDateTime representing the opening creation timestamp. + * @return An integer representing the index + */ + public static int getLocalDateTimeIndex(LocalDateTime entryLocalDateTime) { + // index 0 -> 0 to 5 months + // index 1 -> 6 to 11 months + // index 2 -> 12 to 17 months + // index 3 -> 18+ months + LocalDate entryLocalDate = entryLocalDateTime.toLocalDate(); + LocalDate now = LocalDate.now(); + + Period diff = Period.between(entryLocalDate, now); + int totalMonths = diff.getMonths() + (diff.getYears() * 12); + if (totalMonths <= 5) { + return 0; + } else if (totalMonths <= 11) { + return 1; + } else if (totalMonths <= 17) { + return 2; + } else { + return 3; + } + } +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningEntity.java index eac1ea3a..784078a7 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningEntity.java @@ -20,11 +20,9 @@ public class OpeningEntity { private Long id; @Column(name = "OPENING_STATUS_CODE", length = 3) - // private OpeningStatusEnum status; private String status; @Column(name = "OPEN_CATEGORY_CODE", length = 7) - // private OpeningCategoryEnum category; private String category; @Column(name = "ENTRY_USERID", length = 30) diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/config/PostgresJpaConfig.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/config/PostgresJpaConfig.java index 1065f5fd..2ece5ac0 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/config/PostgresJpaConfig.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/config/PostgresJpaConfig.java @@ -48,7 +48,7 @@ public LocalContainerEntityManagerFactoryBean postgresEntityManagerFactory( Properties jpaProps = new Properties(); jpaProps.setProperty("hibernate.default_schema", "silva"); jpaProps.setProperty("hibernate.ddl-auto", "update"); - jpaProps.setProperty("defer-datasource-initialization", "true"); + jpaProps.setProperty("jpa.defer-datasource-initialization", "true"); jpaProps.setProperty("sql.init.mode", "always"); LocalContainerEntityManagerFactoryBean build = diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/OpeningsPerYearFiltersDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/DashboardFiltesDto.java similarity index 50% rename from backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/OpeningsPerYearFiltersDto.java rename to backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/DashboardFiltesDto.java index 5d2a5dec..d6da148e 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/OpeningsPerYearFiltersDto.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/DashboardFiltesDto.java @@ -3,5 +3,9 @@ import java.time.LocalDateTime; /** This record contains all possible filters for the dashboard openings per years api. */ -public record OpeningsPerYearFiltersDto( - String orgUnit, String status, LocalDateTime entryDateStart, LocalDateTime entryDateEnd) {} +public record DashboardFiltesDto( + String orgUnit, + String status, + LocalDateTime entryDateStart, + LocalDateTime entryDateEnd, + String clientNumber) {} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/FreeGrowingMilestonesDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/FreeGrowingMilestonesDto.java new file mode 100644 index 00000000..bb823946 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/FreeGrowingMilestonesDto.java @@ -0,0 +1,19 @@ +package ca.bc.gov.restapi.results.postgres.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; + +/** This record represent a slice of the free growing milestone chart. */ +public record FreeGrowingMilestonesDto( + @Schema(description = "Number representing the index, between 0 and 3.", example = "1") + Integer index, + @Schema( + description = "Label of the current slice. E.g.: 0 - 5 month, 6 - 11 months", + example = "12 - 17 months") + String label, + @Schema(description = "Amount of openings in the slice", example = "33") Integer amount, + @Schema( + description = + "Percentage of this slice considering the total of openings on the period.", + example = "28") + BigDecimal percentage) {} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/MyRecentActionsRequestsDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/MyRecentActionsRequestsDto.java new file mode 100644 index 00000000..fa317ae8 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/MyRecentActionsRequestsDto.java @@ -0,0 +1,34 @@ +package ca.bc.gov.restapi.results.postgres.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +/** This record represents an opening activity in the requests tab. */ +public record MyRecentActionsRequestsDto( + @Schema(description = "Full description of the action made by the user.", example = "Update") + String activityType, + @Schema( + description = "System generated value uniquely identifying the opening.", + example = "1541297") + Long openingId, + @Schema( + description = + """ + A code indicating the status of the prescription. Examples include but are not + limited to DFT (draft) and APP (approved). A subset of the STATUS_CODE table. + """, + example = "APP") + String statusCode, + @Schema( + description = + """ + The code description indicating the status of the prescription. Examples include but + are not limited to Draft (DFT) and Approved (APP). A subset of the STATUS_CODE table. + """, + example = "Approved") + String statusDescription, + @Schema( + description = "The date and time of the activity action in for 'time ago' format", + example = "1 minute ago") + String lastUpdatedLabel, + @Schema(description = "The date and time of the activity action") LocalDateTime lastUpdated) {} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpoint.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpoint.java index 0c48ad1f..d33e5895 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpoint.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpoint.java @@ -1,7 +1,10 @@ package ca.bc.gov.restapi.results.postgres.endpoint; +import ca.bc.gov.restapi.results.common.util.TimestampUtil; +import ca.bc.gov.restapi.results.postgres.dto.DashboardFiltesDto; +import ca.bc.gov.restapi.results.postgres.dto.FreeGrowingMilestonesDto; +import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto; import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearDto; -import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearFiltersDto; import ca.bc.gov.restapi.results.postgres.service.DashboardMetricsService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -10,11 +13,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.List; -import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -25,32 +24,36 @@ /** This class holds resources for the dashboard metrics page. */ @RestController @RequestMapping("/api/dashboard-metrics") -@Tag(name = "Dashboard Metrics", description = "Resources fot the Dashboard metrics charts") +@Tag( + name = "Dashboard Metrics (SILVA)", + description = "Endpoints fot the Dashboard metrics charts in the `SILVA` schema") @RequiredArgsConstructor public class DashboardMetricsEndpoint { private final DashboardMetricsService dashboardMetricsService; /** - * Get data for the Submission Trends Chart, Openings per Year. + * Gets data for the Opening submission trends Chart (Openings per year) on the Dashboard SILVA + * page. * - * @param orgUnitCode The district code to filter. - * @param statusCode The opening status code to filter. - * @param entryDateStart The opening entry timestamp start date filter. - * @param entryDateEnd The opening entry timestamp end date filter. + * @param orgUnitCode Optional district code filter. + * @param statusCode Optional opening status code filter. + * @param entryDateStart Optional opening entry timestamp start date filter. + * @param entryDateEnd Optional opening entry timestamp end date filter. * @return A list of values to populate the chart or 204 no content if no data. */ @GetMapping("/submission-trends") @Operation( - summary = "Get data for the Submission Trends Chart, Openings per Year", - description = "Fetches data from the last years for the openings per year chart.", + summary = "Gets data for the Opening submission trends Chart (Openings per year).", + description = "Fetches data from the last twelve months for the openings per year chart.", responses = { @ApiResponse( responseCode = "200", - description = "An array with twelve objects for the last 12 months."), + description = "An array with twelve objects for the last 12 months.", + content = @Content(mediaType = "application/json")), @ApiResponse( responseCode = "204", - description = "No data found in the dable. No response body."), + description = "No data found on the table. No response body."), @ApiResponse( responseCode = "401", description = "Access token is missing or invalid", @@ -89,31 +92,132 @@ public ResponseEntity> getOpeningsSubmissionTrends( required = false, example = "2024-03-11") String entryDateEnd) { - LocalDateTime entryDateStartDate = null; - LocalDateTime entryDateEndDate = null; - DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + DashboardFiltesDto filtersDto = + new DashboardFiltesDto( + orgUnitCode, + statusCode, + TimestampUtil.parseDateString(entryDateStart), + TimestampUtil.parseDateString(entryDateEnd), + null); - if (!Objects.isNull(entryDateStart)) { - LocalDate entryDateStartLd = LocalDate.parse(entryDateStart, fmt); - entryDateStartDate = entryDateStartLd.atStartOfDay(); + List resultList = + dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto); + + if (resultList.isEmpty()) { + return ResponseEntity.noContent().build(); } - if (!Objects.isNull(entryDateEnd)) { - LocalDate entryDateEndLd = LocalDate.parse(entryDateEnd, fmt); - entryDateEndDate = entryDateEndLd.atStartOfDay(); + return ResponseEntity.ok(resultList); + } + + /** + * Gets data for the Free growing Chart on the Dashboard SILVA page. + * + * @param orgUnitCode Optional district code filter. + * @param clientNumber Optional client number filter. + * @param entryDateStart Optional opening entry timestamp start date filter. + * @param entryDateEnd Optional opening entry timestamp end date filter. + * @return A list of values to populate the chart or 204 no content if no data. + */ + @GetMapping("/free-growing-milestones") + @Operation( + summary = "Gets data for the Free growing Chart on the Dashboard SILVA page.", + description = + "Fetches data from the last 24 months and group them into periods for the Free growing" + + " chart.", + responses = { + @ApiResponse( + responseCode = "200", + description = "An array with four objects, one for each piece of the chart.", + content = @Content(mediaType = "application/json")), + @ApiResponse( + responseCode = "204", + description = "No data found on the table. No response body."), + @ApiResponse( + responseCode = "401", + description = "Access token is missing or invalid", + content = @Content(schema = @Schema(implementation = Void.class))) + }) + public ResponseEntity> getFreeGrowingMilestonesData( + @RequestParam(value = "orgUnitCode", required = false) + @Parameter( + name = "orgUnitCode", + in = ParameterIn.QUERY, + description = "The Org Unit code to filter, same as District", + required = false, + example = "DCR") + String orgUnitCode, + @RequestParam(value = "clientNumber", required = false) + @Parameter( + name = "clientNumber", + in = ParameterIn.QUERY, + description = "The Client Number to filter", + required = false, + example = "00012797") + String clientNumber, + @RequestParam(value = "entryDateStart", required = false) + @Parameter( + name = "entryDateStart", + in = ParameterIn.QUERY, + description = "The Openins entry timestamp start date to filter, format yyyy-MM-dd", + required = false, + example = "2024-03-11") + String entryDateStart, + @RequestParam(value = "entryDateEnd", required = false) + @Parameter( + name = "entryDateEnd", + in = ParameterIn.QUERY, + description = "The Openins entry timestamp end date to filter, format yyyy-MM-dd", + required = false, + example = "2024-03-11") + String entryDateEnd) { + DashboardFiltesDto filtersDto = + new DashboardFiltesDto( + orgUnitCode, + null, + TimestampUtil.parseDateString(entryDateStart), + TimestampUtil.parseDateString(entryDateEnd), + clientNumber); + List milestonesDto = + dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto); + + if (milestonesDto.isEmpty()) { + return ResponseEntity.noContent().build(); } - OpeningsPerYearFiltersDto filtersDto = - new OpeningsPerYearFiltersDto( - orgUnitCode, statusCode, entryDateStartDate, entryDateEndDate); + return ResponseEntity.ok(milestonesDto); + } - List resultList = - dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto); + /** + * Gets the last 5 most recent updated openings for the request user. + * + * @return A list of values to populate the chart or 204 no content if no data. + */ + @GetMapping("/my-recent-actions/requests") + @Operation( + summary = "Gets the last 5 most recent updated openings for the request user.", + description = "Fetches data for the My recent actions table, Requests tab", + responses = { + @ApiResponse( + responseCode = "200", + description = "An array with five objects, one for opening row.", + content = @Content(mediaType = "application/json")), + @ApiResponse( + responseCode = "204", + description = "No data found for the user. No response body."), + @ApiResponse( + responseCode = "401", + description = "Access token is missing or invalid", + content = @Content(schema = @Schema(implementation = Void.class))) + }) + public ResponseEntity> getUserRecentOpeningsActions() { + List actionsDto = + dashboardMetricsService.getUserRecentOpeningsActions(); - if (resultList.isEmpty()) { + if (actionsDto.isEmpty()) { return ResponseEntity.noContent().build(); } - return ResponseEntity.ok(resultList); + return ResponseEntity.ok(actionsDto); } } diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpoint.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpoint.java index 0b156b38..3f783bab 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpoint.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpoint.java @@ -1,30 +1,129 @@ package ca.bc.gov.restapi.results.postgres.endpoint; -import ca.bc.gov.restapi.results.postgres.entity.UserOpeningEntity; -import ca.bc.gov.restapi.results.postgres.repository.UserOpeningRepository; +import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto; +import ca.bc.gov.restapi.results.postgres.service.UserOpeningService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; -import org.springframework.http.MediaType; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** This class holds resources for exposing user openings saved as favourites. */ @RestController -@RequestMapping(path = "/api/user-openings", produces = MediaType.APPLICATION_JSON_VALUE) +@RequestMapping("/api/user-openings") @Tag( - name = "User Opennings (SILVA)", + name = "User Openings (SILVA)", description = "Endpoints to handle user favourite Openings in the `SILVA` schema.") +@RequiredArgsConstructor public class UserOpeningEndpoint { - private final UserOpeningRepository userOpeningRepository; + private final UserOpeningService userOpeningService; - UserOpeningEndpoint(UserOpeningRepository userOpeningRepository) { - this.userOpeningRepository = userOpeningRepository; + /** + * Gets up to three tracked Openings to a user. + * + * @return A list of openings or the http code 204-No Content. + */ + @GetMapping("/dashboard-track-openings") + @Operation( + summary = "Gets up to three updated Openings to a user", + description = "Gets up to three updated openings that got saved by the user.", + responses = { + @ApiResponse( + responseCode = "200", + description = "An array containing up to three Openings for the user.", + content = @Content(mediaType = "application/json")), + @ApiResponse( + responseCode = "204", + description = "No data found for this user. No response body."), + @ApiResponse( + responseCode = "401", + description = "Access token is missing or invalid", + content = @Content(schema = @Schema(implementation = Void.class))) + }) + public ResponseEntity> getUserTrackedOpenings() { + List userOpenings = userOpeningService.getUserTrackedOpenings(); + if (userOpenings.isEmpty()) { + return ResponseEntity.noContent().build(); + } + + return ResponseEntity.ok(userOpenings); + } + + /** + * Saves one Opening ID as favourite to an user. + * + * @param id The opening ID. + * @return HTTP status code 201 if success, no response body. + */ + @PostMapping("/{id}") + @Operation( + summary = "Saves one Opening ID as favourite to an user", + description = "Allow users to save Opening IDs to track them easily in the dashboard.", + responses = { + @ApiResponse(responseCode = "201", description = "Opening successfully saved to the user."), + @ApiResponse( + responseCode = "401", + description = "Access token is missing or invalid", + content = @Content(schema = @Schema(implementation = Void.class))) + }) + public ResponseEntity saveUserOpening( + @Parameter( + name = "id", + in = ParameterIn.PATH, + description = "Opening ID", + required = true, + schema = @Schema(type = "integer", format = "int64")) + @PathVariable + Long id) { + userOpeningService.saveOpeningToUser(id); + return ResponseEntity.status(HttpStatus.CREATED).build(); } - @GetMapping - public List getAll() { - return userOpeningRepository.findAll(); + /** + * Deletes one or more user openings from their favourite list. + * + * @param id The opening ID. + * @return HTTP status code 204 if success, no response body. + */ + @DeleteMapping("/{id}") + @Operation( + summary = "Deleted an Opening ID from the user's favourite", + description = "Allow users to remove saved Opening IDs from their favourite list.", + responses = { + @ApiResponse( + responseCode = "204", + description = "Opening successfully removed. No content body."), + @ApiResponse( + responseCode = "401", + description = "Access token is missing or invalid", + content = @Content(schema = @Schema(implementation = Void.class))), + @ApiResponse( + responseCode = "404", + description = "Opening not found in the user's favourite.") + }) + public ResponseEntity deleteUserOpening( + @Parameter( + name = "id", + in = ParameterIn.PATH, + description = "Opening ID", + required = true, + schema = @Schema(type = "integer", format = "int64")) + @PathVariable + Long id) { + userOpeningService.deleteOpeningFromUserFavourite(id); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } } diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsActivityEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsActivityEntity.java new file mode 100644 index 00000000..d33a8878 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsActivityEntity.java @@ -0,0 +1,39 @@ +package ca.bc.gov.restapi.results.postgres.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.Setter; + +/** This class represents a record in the database for the openings_activity table. */ +@Getter +@Setter +@Entity +@Table(name = "openings_activity") +public class OpeningsActivityEntity { + + @Id + @Column(name = "opening_id") + private Long openingId; + + @Column(name = "activity_type_code", length = 3) + private String activityTypeCode; + + @Column(name = "activity_type_desc", length = 120) + private String activityTypeDesc; + + @Column(name = "status_code", length = 3, nullable = false) + private String statusCode; + + @Column(name = "status_desc", length = 120, nullable = false) + private String statusDesc; + + @Column(name = "last_updated", nullable = false) + private LocalDateTime lastUpdated; + + @Column(name = "entry_userid", length = 30, nullable = false) + private String entryUserid; +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsLastYearEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsLastYearEntity.java index 5950a4e5..60360441 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsLastYearEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsLastYearEntity.java @@ -17,9 +17,9 @@ public class OpeningsLastYearEntity { @Id @Column(name = "opening_id") - private String openingId; + private Long openingId; - @Column(name = "opening_entry_userid", nullable = false) + @Column(name = "opening_entry_userid", nullable = false, length = 30) private String userId; @Column(name = "entry_timestamp", nullable = false) @@ -28,9 +28,12 @@ public class OpeningsLastYearEntity { @Column(name = "update_timestamp", nullable = false) private LocalDateTime updateTimestamp; - @Column(name = "status_code", nullable = false) + @Column(name = "status_code", nullable = false, length = 3) private String status; - @Column(name = "org_unit_code", nullable = false) + @Column(name = "org_unit_code", nullable = false, length = 6) private String orgUnitCode; + + @Column(name = "client_number", nullable = false, length = 8) + private String clientNumber; } diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntity.java index ad10a717..2ab815fa 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntity.java @@ -26,5 +26,5 @@ public class UserOpeningEntity { @Id @Column(name = "opening_id") - private String openingId; + private Long openingId; } diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntityId.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntityId.java index 343493c7..e7af80be 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntityId.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntityId.java @@ -19,5 +19,5 @@ public class UserOpeningEntityId { @NonNull private String userId; - @NonNull private String openingId; + @NonNull private Long openingId; } diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsActivityRepository.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsActivityRepository.java new file mode 100644 index 00000000..c173c972 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsActivityRepository.java @@ -0,0 +1,12 @@ +package ca.bc.gov.restapi.results.postgres.repository; + +import ca.bc.gov.restapi.results.postgres.entity.OpeningsActivityEntity; +import java.util.List; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.JpaRepository; + +/** This interface provides access to the database for the OpeningsActivityEntity entity. */ +public interface OpeningsActivityRepository extends JpaRepository { + + List findAllByEntryUserid(String userId, Sort sort); +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsLastYearRepository.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsLastYearRepository.java index 418cd656..ce7d34cd 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsLastYearRepository.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsLastYearRepository.java @@ -1,7 +1,13 @@ package ca.bc.gov.restapi.results.postgres.repository; import ca.bc.gov.restapi.results.postgres.entity.OpeningsLastYearEntity; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; /** This interface provides access to the database for the OpeningsLastYearEntity entity. */ -public interface OpeningsLastYearRepository extends JpaRepository {} +public interface OpeningsLastYearRepository extends JpaRepository { + + @Query("from OpeningsLastYearEntity o where o.openingId in ?1") + List findAllByOpeningIdInList(List openingIdList); +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/UserOpeningRepository.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/UserOpeningRepository.java index fb208a22..377be33c 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/UserOpeningRepository.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/UserOpeningRepository.java @@ -2,8 +2,16 @@ import ca.bc.gov.restapi.results.postgres.entity.UserOpeningEntity; import ca.bc.gov.restapi.results.postgres.entity.UserOpeningEntityId; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; /** This interface holds methods for handling {@link UserOpeningEntity} data in the database. */ public interface UserOpeningRepository - extends JpaRepository {} + extends JpaRepository { + + List findAllByUserId(String userId); + + @Query("from UserOpeningEntity o where o.openingId in ?1 and o.userId = ?2") + List findAllByOpeningIdInAndUserId(List openingIdList, String userId); +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsService.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsService.java index 09eee9dc..f71a7e75 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsService.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsService.java @@ -1,9 +1,17 @@ package ca.bc.gov.restapi.results.postgres.service; +import ca.bc.gov.restapi.results.common.security.LoggedUserService; +import ca.bc.gov.restapi.results.common.util.TimestampUtil; +import ca.bc.gov.restapi.results.postgres.dto.DashboardFiltesDto; +import ca.bc.gov.restapi.results.postgres.dto.FreeGrowingMilestonesDto; +import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto; import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearDto; -import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearFiltersDto; +import ca.bc.gov.restapi.results.postgres.entity.OpeningsActivityEntity; import ca.bc.gov.restapi.results.postgres.entity.OpeningsLastYearEntity; +import ca.bc.gov.restapi.results.postgres.repository.OpeningsActivityRepository; import ca.bc.gov.restapi.results.postgres.repository.OpeningsLastYearRepository; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.Month; import java.util.ArrayList; import java.util.HashMap; @@ -13,6 +21,7 @@ import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.ocpsoft.prettytime.PrettyTime; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; @@ -24,13 +33,17 @@ public class DashboardMetricsService { private final OpeningsLastYearRepository openingsLastYearRepository; + private final OpeningsActivityRepository openingsActivityRepository; + + private final LoggedUserService loggedUserService; + /** * Get openings submission trends data for the opening per year chart. * - * @param filters Possible filter, see {@link OpeningsPerYearFiltersDto} for more. + * @param filters Possible filter, see {@link DashboardFiltesDto} for more. * @return A list of {@link OpeningsPerYearDto} for the opening chart. */ - public List getOpeningsSubmissionTrends(OpeningsPerYearFiltersDto filters) { + public List getOpeningsSubmissionTrends(DashboardFiltesDto filters) { log.info("Getting Opening Submission Trends with filters {}", filters.toString()); List entities = @@ -60,6 +73,7 @@ public List getOpeningsSubmissionTrends(OpeningsPerYearFilte } } + // Iterate over the found records filtering and putting them into the right month for (OpeningsLastYearEntity entity : entities) { // Org Unit filter - District if (!Objects.isNull(filters.orgUnit()) @@ -91,10 +105,153 @@ public List getOpeningsSubmissionTrends(OpeningsPerYearFilte for (Integer monthKey : resultMap.keySet()) { List monthDataList = resultMap.get(monthKey); String monthName = monthNamesMap.get(monthKey); - log.info("Month: {}", monthName); + log.info("Value {} for the month: {}", monthDataList.size(), monthName); chartData.add(new OpeningsPerYearDto(monthKey, monthName, monthDataList.size())); } return chartData; } + + /** + * Get free growing milestone declarations data for the chart. + * + * @param filters Possible filter, see {@link DashboardFiltesDto} for more. + * @return A list of {@link FreeGrowingMilestonesDto} for the chart. + */ + public List getFreeGrowingMilestoneChartData( + DashboardFiltesDto filters) { + log.info("Getting Free growing milestones with filters {}", filters.toString()); + + List entities = + openingsLastYearRepository.findAll(Sort.by("openingId").ascending()); + + if (entities.isEmpty()) { + log.info("No Free growing milestones data found!"); + return List.of(); + } + + Map> resultMap = new LinkedHashMap<>(); + + // Fill with all four pieces + for (int i = 0; i < 4; i++) { + resultMap.put(i, new ArrayList<>()); + } + + Map labelsMap = new HashMap<>(); + labelsMap.put(0, "0 - 5 months"); + labelsMap.put(1, "6 - 11 months"); + labelsMap.put(2, "12 - 17 months"); + labelsMap.put(3, "18 months"); + + int totalRecordsFiltered = 0; + + // Iterate over the found records filtering and putting them into the right piece + for (OpeningsLastYearEntity entity : entities) { + // Org Unit filter - District + if (!Objects.isNull(filters.orgUnit()) + && !entity.getOrgUnitCode().equals(filters.orgUnit())) { + continue; + } + + // Client number + if (!Objects.isNull(filters.clientNumber()) + && !entity.getClientNumber().equals(filters.clientNumber())) { + continue; + } + + // Entry start date filter + if (!Objects.isNull(filters.entryDateStart()) + && entity.getEntryTimestamp().isBefore(filters.entryDateStart())) { + continue; + } + + // Entry end date filter + if (!Objects.isNull(filters.entryDateEnd()) + && entity.getEntryTimestamp().isAfter(filters.entryDateEnd())) { + continue; + } + + int index = TimestampUtil.getLocalDateTimeIndex(entity.getEntryTimestamp()); + resultMap.get(index).add(entity); + totalRecordsFiltered++; + } + + List chartData = new ArrayList<>(); + BigDecimal hundred = new BigDecimal("100"); + BigDecimal hundredSum = new BigDecimal("100"); + for (Integer index : resultMap.keySet()) { + List groupList = resultMap.get(index); + String label = labelsMap.get(index); + int value = groupList.size(); + log.info("{} openings of {} for label '{}'", value, totalRecordsFiltered, label); + + BigDecimal percentage = BigDecimal.ZERO; + if (totalRecordsFiltered > 0) { + percentage = + new BigDecimal(String.valueOf(value)) + .divide( + new BigDecimal(String.valueOf(totalRecordsFiltered)), 10, RoundingMode.HALF_UP) + .setScale(2, RoundingMode.HALF_UP) + .multiply(hundred); + } + + if (index < 3) { + hundredSum = hundredSum.subtract(percentage); + } else if (index == 3 && totalRecordsFiltered > 0) { + percentage = hundredSum; + } + + log.info("Percentage {}% for the label: {}", percentage, label); + chartData.add(new FreeGrowingMilestonesDto(index, label, value, percentage)); + } + + return chartData; + } + + /** + * Get My recent actions table data for the Request tabs. + * + * @return A list of {@link MyRecentActionsRequestsDto} with 5 rows. + */ + public List getUserRecentOpeningsActions() { + log.info("Getting up to the last 5 openings activities for the requests tab"); + + String userId = loggedUserService.getLoggedUserId(); + + Sort sort = Sort.by("lastUpdated").descending(); + List openingList = + openingsActivityRepository.findAllByEntryUserid(userId, sort); + + if (openingList.isEmpty()) { + log.info("No recent activities data found for the user!"); + return List.of(); + } + + PrettyTime prettyTime = new PrettyTime(); + + List chartData = new ArrayList<>(); + for (OpeningsActivityEntity entity : openingList) { + String statusDesc = entity.getActivityTypeDesc(); + if (Objects.isNull(statusDesc)) { + statusDesc = "Created"; + } + + MyRecentActionsRequestsDto dto = + new MyRecentActionsRequestsDto( + statusDesc, + entity.getOpeningId(), + entity.getStatusCode(), + entity.getStatusDesc(), + prettyTime.format(entity.getLastUpdated()), + entity.getLastUpdated()); + + chartData.add(dto); + + if (chartData.size() == 5) { + break; + } + } + + return chartData; + } } diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningService.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningService.java new file mode 100644 index 00000000..5175b3e2 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningService.java @@ -0,0 +1,123 @@ +package ca.bc.gov.restapi.results.postgres.service; + +import ca.bc.gov.restapi.results.common.exception.UserOpeningNotFoundException; +import ca.bc.gov.restapi.results.common.security.LoggedUserService; +import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto; +import ca.bc.gov.restapi.results.postgres.entity.OpeningsActivityEntity; +import ca.bc.gov.restapi.results.postgres.entity.UserOpeningEntity; +import ca.bc.gov.restapi.results.postgres.entity.UserOpeningEntityId; +import ca.bc.gov.restapi.results.postgres.repository.OpeningsActivityRepository; +import ca.bc.gov.restapi.results.postgres.repository.UserOpeningRepository; +import jakarta.transaction.Transactional; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ocpsoft.prettytime.PrettyTime; +import org.springframework.stereotype.Service; + +/** This class contains methods for handling User favourite Openings. */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UserOpeningService { + + private final LoggedUserService loggedUserService; + + private final UserOpeningRepository userOpeningRepository; + + private final OpeningsActivityRepository openingsActivityRepository; + + /** + * Gets user's tracked Openings. + * + * @return A list of {@link MyRecentActionsRequestsDto} containing the found records. + */ + public List getUserTrackedOpenings() { + log.info("Getting all user openings for the Track openings table"); + + String userId = loggedUserService.getLoggedUserId(); + List userList = userOpeningRepository.findAllByUserId(userId); + + if (userList.isEmpty()) { + log.info("No saved openings for the current user!"); + return List.of(); + } + + List openingIds = userList.stream().map(UserOpeningEntity::getOpeningId).toList(); + List openingActivities = + openingsActivityRepository.findAllById(openingIds); + + if (openingActivities.isEmpty()) { + log.info("No records found on the opening activity table for the opening ID list!"); + return List.of(); + } + + List resultList = new ArrayList<>(); + + PrettyTime prettyTime = new PrettyTime(); + + for (OpeningsActivityEntity activityEntity : openingActivities) { + MyRecentActionsRequestsDto requestsDto = + new MyRecentActionsRequestsDto( + activityEntity.getActivityTypeDesc(), + activityEntity.getOpeningId(), + activityEntity.getStatusCode(), + activityEntity.getStatusDesc(), + prettyTime.format(activityEntity.getLastUpdated()), + activityEntity.getLastUpdated()); + + resultList.add(requestsDto); + + if (resultList.size() == 3) { + break; + } + } + + return resultList; + } + + /** + * Saves one or more Openings IDs to an user. + * + * @param openingId The opening ID. + */ + @Transactional + public void saveOpeningToUser(Long openingId) { + log.info("Opening ID to save in the user favourites: {}", openingId); + + final String userId = loggedUserService.getLoggedUserId(); + + UserOpeningEntity entity = new UserOpeningEntity(); + entity.setUserId(userId); + entity.setOpeningId(openingId); + + userOpeningRepository.saveAndFlush(entity); + log.info("Opening ID saved in the user's favourites!"); + } + + /** + * Deletes one or more user opening from favourite. + * + * @param openingId The opening ID. + */ + @Transactional + public void deleteOpeningFromUserFavourite(Long openingId) { + log.info("Opening ID to delete from the user's favourites: {}", openingId); + String userId = loggedUserService.getLoggedUserId(); + + UserOpeningEntityId openingPk = new UserOpeningEntityId(userId, openingId); + + Optional userOpeningsOp = userOpeningRepository.findById(openingPk); + + if (userOpeningsOp.isEmpty()) { + log.info("Opening id {} not found in the user's favourite list!", openingId); + throw new UserOpeningNotFoundException(); + } + + userOpeningRepository.delete(userOpeningsOp.get()); + userOpeningRepository.flush(); + log.info("Opening ID deleted from the favourites!"); + } +} diff --git a/backend/src/main/resources/db/migration/V5__add_client_number_metrics_table.sql b/backend/src/main/resources/db/migration/V5__add_client_number_metrics_table.sql new file mode 100644 index 00000000..0e87629a --- /dev/null +++ b/backend/src/main/resources/db/migration/V5__add_client_number_metrics_table.sql @@ -0,0 +1,17 @@ +alter table silva.openings_last_year + add column client_number VARCHAR(8) NOT NULL; + +-- drop table and recreate fixing the table name and columns +drop table silva.openings_activitiy; + +create table IF NOT EXISTS silva.openings_activity ( + opening_id DECIMAL(10,0) NOT NULL, + activity_type_code VARCHAR(3), + activity_type_desc VARCHAR(120), + status_code VARCHAR(3) NOT NULL, + status_desc VARCHAR(120) NOT NULL, + last_updated TIMESTAMP NOT NULL, + entry_userid VARCHAR(30) NOT NULL, + constraint openings_activitiy_pk + primary key(opening_id) +); diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/common/util/TimestampUtilTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/common/util/TimestampUtilTest.java new file mode 100644 index 00000000..c885e0b6 --- /dev/null +++ b/backend/src/test/java/ca/bc/gov/restapi/results/common/util/TimestampUtilTest.java @@ -0,0 +1,45 @@ +package ca.bc.gov.restapi.results.common.util; + +import java.time.LocalDateTime; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class TimestampUtilTest { + + @Test + void parseDateStringTest() { + LocalDateTime dateTimeParsed = TimestampUtil.parseDateString("2024-04-09"); + Assertions.assertNotNull(dateTimeParsed); + Assertions.assertEquals(2024, dateTimeParsed.getYear()); + Assertions.assertEquals(4, dateTimeParsed.getMonthValue()); + Assertions.assertEquals(9, dateTimeParsed.getDayOfMonth()); + + LocalDateTime dateTimeNull = TimestampUtil.parseDateString(null); + Assertions.assertNull(dateTimeNull); + } + + @Test + void getLocalDateTimeIndexTest() { + LocalDateTime now = LocalDateTime.now(); + + Assertions.assertEquals(0, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(1L))); + Assertions.assertEquals(0, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(2L))); + Assertions.assertEquals(0, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(3L))); + Assertions.assertEquals(0, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(4L))); + Assertions.assertEquals(0, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(5L))); + Assertions.assertEquals(1, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(6L))); + Assertions.assertEquals(1, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(7L))); + Assertions.assertEquals(1, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(8L))); + Assertions.assertEquals(1, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(9L))); + Assertions.assertEquals(1, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(10L))); + Assertions.assertEquals(1, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(11L))); + Assertions.assertEquals(2, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(12L))); + Assertions.assertEquals(2, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(13L))); + Assertions.assertEquals(2, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(14L))); + Assertions.assertEquals(2, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(15L))); + Assertions.assertEquals(2, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(16L))); + Assertions.assertEquals(2, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(17L))); + Assertions.assertEquals(3, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(18L))); + Assertions.assertEquals(3, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(36L))); + } +} diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/enums/OpeningCategoryEnumTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/enums/OpeningCategoryEnumTest.java similarity index 83% rename from backend/src/test/java/ca/bc/gov/restapi/results/enums/OpeningCategoryEnumTest.java rename to backend/src/test/java/ca/bc/gov/restapi/results/oracle/enums/OpeningCategoryEnumTest.java index 029c0f9d..b725e5e5 100644 --- a/backend/src/test/java/ca/bc/gov/restapi/results/enums/OpeningCategoryEnumTest.java +++ b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/enums/OpeningCategoryEnumTest.java @@ -1,6 +1,5 @@ -package ca.bc.gov.restapi.results.enums; +package ca.bc.gov.restapi.results.oracle.enums; -import ca.bc.gov.restapi.results.oracle.enums.OpeningCategoryEnum; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/enums/OpeningStatusEnumTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/enums/OpeningStatusEnumTest.java similarity index 83% rename from backend/src/test/java/ca/bc/gov/restapi/results/enums/OpeningStatusEnumTest.java rename to backend/src/test/java/ca/bc/gov/restapi/results/oracle/enums/OpeningStatusEnumTest.java index 4a48a358..ba0a8ca5 100644 --- a/backend/src/test/java/ca/bc/gov/restapi/results/enums/OpeningStatusEnumTest.java +++ b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/enums/OpeningStatusEnumTest.java @@ -1,6 +1,5 @@ -package ca.bc.gov.restapi.results.enums; +package ca.bc.gov.restapi.results.oracle.enums; -import ca.bc.gov.restapi.results.oracle.enums.OpeningStatusEnum; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpointTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpointTest.java index d64315e0..4ee94da3 100644 --- a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpointTest.java +++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpointTest.java @@ -7,9 +7,14 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import ca.bc.gov.restapi.results.postgres.dto.DashboardFiltesDto; +import ca.bc.gov.restapi.results.postgres.dto.FreeGrowingMilestonesDto; +import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto; import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearDto; -import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearFiltersDto; import ca.bc.gov.restapi.results.postgres.service.DashboardMetricsService; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -31,7 +36,7 @@ class DashboardMetricsEndpointTest { @Test @DisplayName("Opening submission trends with no filters should succeed") void getOpeningsSubmissionTrends_noFilters_shouldSucceed() throws Exception { - OpeningsPerYearFiltersDto filtersDto = new OpeningsPerYearFiltersDto(null, null, null, null); + DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, null, null, null, null); OpeningsPerYearDto dto = new OpeningsPerYearDto(1, "Jan", 70); when(dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto)).thenReturn(List.of(dto)); @@ -53,7 +58,7 @@ void getOpeningsSubmissionTrends_noFilters_shouldSucceed() throws Exception { @Test @DisplayName("Opening submission trends with no data should succeed") void getOpeningsSubmissionTrends_orgUnitFilter_shouldSucceed() throws Exception { - OpeningsPerYearFiltersDto filtersDto = new OpeningsPerYearFiltersDto("DCR", null, null, null); + DashboardFiltesDto filtersDto = new DashboardFiltesDto("DCR", null, null, null, null); when(dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto)).thenReturn(List.of()); @@ -66,4 +71,126 @@ void getOpeningsSubmissionTrends_orgUnitFilter_shouldSucceed() throws Exception .andExpect(status().isNoContent()) .andReturn(); } + + @Test + @DisplayName("Free growing milestones test with no filters should succeed") + void getFreeGrowingMilestonesData_noFilters_shouldSucceed() throws Exception { + DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, null, null, null, null); + + FreeGrowingMilestonesDto milestonesDto = + new FreeGrowingMilestonesDto(0, "0 - 5 months", 25, new BigDecimal("100")); + when(dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto)) + .thenReturn(List.of(milestonesDto)); + + mockMvc + .perform( + get("/api/dashboard-metrics/free-growing-milestones") + .with(csrf().asHeader()) + .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$[0].index").value("0")) + .andExpect(jsonPath("$[0].label").value("0 - 5 months")) + .andExpect(jsonPath("$[0].amount").value("25")) + .andExpect(jsonPath("$[0].percentage").value(new BigDecimal("100"))) + .andReturn(); + } + + @Test + @DisplayName("Free growing milestones test with client number filter should succeed") + void getFreeGrowingMilestonesData_clientNumberFilter_shouldSucceed() throws Exception { + List dtoList = new ArrayList<>(); + dtoList.add(new FreeGrowingMilestonesDto(0, "0 - 5 months", 25, new BigDecimal("25"))); + dtoList.add(new FreeGrowingMilestonesDto(1, "6 - 11 months", 25, new BigDecimal("25"))); + dtoList.add(new FreeGrowingMilestonesDto(2, "12 - 17 months", 25, new BigDecimal("25"))); + dtoList.add(new FreeGrowingMilestonesDto(3, "18 months", 25, new BigDecimal("25"))); + + DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, null, null, null, "00012797"); + + when(dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto)).thenReturn(dtoList); + + mockMvc + .perform( + get("/api/dashboard-metrics/free-growing-milestones?clientNumber=00012797") + .with(csrf().asHeader()) + .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$[0].index").value("0")) + .andExpect(jsonPath("$[0].label").value("0 - 5 months")) + .andExpect(jsonPath("$[0].amount").value("25")) + .andExpect(jsonPath("$[0].percentage").value(new BigDecimal("25"))) + .andExpect(jsonPath("$[1].index").value("1")) + .andExpect(jsonPath("$[1].label").value("6 - 11 months")) + .andExpect(jsonPath("$[1].amount").value("25")) + .andExpect(jsonPath("$[1].percentage").value(new BigDecimal("25"))) + .andExpect(jsonPath("$[2].index").value("2")) + .andExpect(jsonPath("$[2].label").value("12 - 17 months")) + .andExpect(jsonPath("$[2].amount").value("25")) + .andExpect(jsonPath("$[2].percentage").value(new BigDecimal("25"))) + .andExpect(jsonPath("$[3].index").value("3")) + .andExpect(jsonPath("$[3].label").value("18 months")) + .andExpect(jsonPath("$[3].amount").value("25")) + .andExpect(jsonPath("$[3].percentage").value(new BigDecimal("25"))) + .andReturn(); + } + + @Test + @DisplayName("Free growing milestones test with no content should succeed") + void getFreeGrowingMilestonesData_noData_shouldSucceed() throws Exception { + DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, null, null, null, "00012579"); + + when(dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto)) + .thenReturn(List.of()); + + mockMvc + .perform( + get("/api/dashboard-metrics/free-growing-milestones") + .with(csrf().asHeader()) + .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andReturn(); + } + + @Test + @DisplayName("User recent actions requests test happy path should succeed") + void getUserRecentOpeningsActions_happyPath_shouldSucceed() throws Exception { + MyRecentActionsRequestsDto actionDto = + new MyRecentActionsRequestsDto( + "Created", 48L, "PEN", "Pending", "2 minutes ago", LocalDateTime.now()); + when(dashboardMetricsService.getUserRecentOpeningsActions()).thenReturn(List.of(actionDto)); + + mockMvc + .perform( + get("/api/dashboard-metrics/my-recent-actions/requests") + .with(csrf().asHeader()) + .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$[0].activityType").value("Created")) + .andExpect(jsonPath("$[0].openingId").value("48")) + .andExpect(jsonPath("$[0].statusCode").value("PEN")) + .andExpect(jsonPath("$[0].statusDescription").value("Pending")) + .andExpect(jsonPath("$[0].lastUpdatedLabel").value("2 minutes ago")) + .andReturn(); + } + + @Test + @DisplayName("User recent actions requests test no data should succeed") + void getUserRecentOpeningsActions_noData_shouldSucceed() throws Exception { + when(dashboardMetricsService.getUserRecentOpeningsActions()).thenReturn(List.of()); + + mockMvc + .perform( + get("/api/dashboard-metrics/my-recent-actions/requests") + .with(csrf().asHeader()) + .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andReturn(); + } } diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpointTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpointTest.java new file mode 100644 index 00000000..e5d450ce --- /dev/null +++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpointTest.java @@ -0,0 +1,81 @@ +package ca.bc.gov.restapi.results.postgres.endpoint; + +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto; +import ca.bc.gov.restapi.results.postgres.service.UserOpeningService; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(UserOpeningEndpoint.class) +@WithMockUser +class UserOpeningEndpointTest { + + @Autowired private MockMvc mockMvc; + + @MockBean private UserOpeningService userOpeningService; + + @Test + @DisplayName("Get user tracked openings happy path should succeed") + void getUserTrackedOpenings_happyPath_shoudSucceed() throws Exception { + MyRecentActionsRequestsDto action = + new MyRecentActionsRequestsDto( + "Update", 123456L, "APP", "Approved", "2 minutes ago", LocalDateTime.now()); + when(userOpeningService.getUserTrackedOpenings()).thenReturn(List.of(action)); + + mockMvc + .perform( + get("/api/user-openings/dashboard-track-openings") + .with(csrf().asHeader()) + .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$[0].activityType").value("Update")) + .andExpect(jsonPath("$[0].openingId").value("123456")) + .andExpect(jsonPath("$[0].statusCode").value("APP")) + .andExpect(jsonPath("$[0].statusDescription").value("Approved")) + .andExpect(jsonPath("$[0].lastUpdatedLabel").value("2 minutes ago")) + .andReturn(); + } + + @Test + @DisplayName("Get user tracked openings no data should succeed") + void getUserTrackedOpenings_noData_shoudSucceed() throws Exception { + when(userOpeningService.getUserTrackedOpenings()).thenReturn(List.of()); + + mockMvc + .perform( + get("/api/user-openings/dashboard-track-openings") + .with(csrf().asHeader()) + .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andReturn(); + } + + void saveUserOpening_happyPath_shoudSucceed() throws Exception { + // + } + + void deleteUserOpening_happyPath_shoudSucceed() throws Exception { + // + } + + void deleteUserOpening_notFound_shoudFail() throws Exception { + // + } +} diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsLastYearRepositoryIntegrationTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsLastYearRepositoryIntegrationTest.java new file mode 100644 index 00000000..7e5fcfdc --- /dev/null +++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsLastYearRepositoryIntegrationTest.java @@ -0,0 +1,38 @@ +package ca.bc.gov.restapi.results.postgres.repository; + +import ca.bc.gov.restapi.results.postgres.entity.OpeningsLastYearEntity; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.jdbc.Sql; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = Replace.NONE) +@Sql(scripts = {"classpath:sql_scripts/OpeningsLastYearRepositoryIntegrationTest.sql"}) +class OpeningsLastYearRepositoryIntegrationTest { + + @Autowired private OpeningsLastYearRepository openingsLastYearRepository; + + @Test + @DisplayName("find all by Opening ID in List") + void findAllByOpeningIdInListTest() { + List idList = List.of(7012511L, 7012512L, 7012513L); + List openingList = + openingsLastYearRepository.findAllByOpeningIdInList(idList); + + Assertions.assertFalse(openingList.isEmpty()); + Assertions.assertEquals(3, openingList.size()); + + OpeningsLastYearEntity first = openingList.get(0); + + Assertions.assertEquals(7012511L, first.getOpeningId()); + Assertions.assertEquals("TEST", first.getUserId()); + Assertions.assertEquals("APP", first.getStatus()); + Assertions.assertEquals("DCR", first.getOrgUnitCode()); + } +} diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/repository/UserOpeningRepositoryIntegrationTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/repository/UserOpeningRepositoryIntegrationTest.java new file mode 100644 index 00000000..e644b25d --- /dev/null +++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/repository/UserOpeningRepositoryIntegrationTest.java @@ -0,0 +1,5 @@ +package ca.bc.gov.restapi.results.postgres.repository; + +class UserOpeningRepositoryIntegrationTest { + +} diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsServiceTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsServiceTest.java index 85203c09..839f6fdc 100644 --- a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsServiceTest.java +++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsServiceTest.java @@ -2,10 +2,16 @@ import static org.mockito.Mockito.when; +import ca.bc.gov.restapi.results.common.security.LoggedUserService; +import ca.bc.gov.restapi.results.postgres.dto.DashboardFiltesDto; +import ca.bc.gov.restapi.results.postgres.dto.FreeGrowingMilestonesDto; +import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto; import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearDto; -import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearFiltersDto; +import ca.bc.gov.restapi.results.postgres.entity.OpeningsActivityEntity; import ca.bc.gov.restapi.results.postgres.entity.OpeningsLastYearEntity; +import ca.bc.gov.restapi.results.postgres.repository.OpeningsActivityRepository; import ca.bc.gov.restapi.results.postgres.repository.OpeningsLastYearRepository; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.Assertions; @@ -22,26 +28,35 @@ class DashboardMetricsServiceTest { @Mock OpeningsLastYearRepository openingsLastYearRepository; + @Mock OpeningsActivityRepository openingsActivityRepository; + + @Mock LoggedUserService loggedUserService; + private DashboardMetricsService dashboardMetricsService; private static final Sort SORT = Sort.by("entryTimestamp").ascending(); + private static final Sort SORT_BY_ID = Sort.by("openingId").ascending(); + private List mockOpeningsEntityList() { LocalDateTime entryTimestamp = LocalDateTime.now(); OpeningsLastYearEntity entity = new OpeningsLastYearEntity(); - entity.setOpeningId("123456"); + entity.setOpeningId(123456L); entity.setUserId("userId"); entity.setEntryTimestamp(entryTimestamp); entity.setUpdateTimestamp(entryTimestamp); entity.setStatus("APP"); entity.setOrgUnitCode("DCR"); + entity.setClientNumber("00012797"); return List.of(entity); } @BeforeEach void setup() { - dashboardMetricsService = new DashboardMetricsService(openingsLastYearRepository); + dashboardMetricsService = + new DashboardMetricsService( + openingsLastYearRepository, openingsActivityRepository, loggedUserService); } @Test @@ -51,7 +66,7 @@ void getOpeningsSubmissionTrends_noFilters_shouldSucceed() throws Exception { List entities = mockOpeningsEntityList(); when(openingsLastYearRepository.findAll(SORT)).thenReturn(entities); - OpeningsPerYearFiltersDto filtersDto = new OpeningsPerYearFiltersDto(null, null, null, null); + DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, null, null, null, null); List list = dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto); String monthName = now.getMonth().name().toLowerCase(); @@ -71,7 +86,7 @@ void getOpeningsSubmissionTrends_orgUnitFilter_shouldSucceed() throws Exception List entities = mockOpeningsEntityList(); when(openingsLastYearRepository.findAll(SORT)).thenReturn(entities); - OpeningsPerYearFiltersDto filtersDto = new OpeningsPerYearFiltersDto("AAA", null, null, null); + DashboardFiltesDto filtersDto = new DashboardFiltesDto("AAA", null, null, null, null); List list = dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto); String monthName = now.getMonth().name().toLowerCase(); @@ -91,7 +106,7 @@ void getOpeningsSubmissionTrends_statusFilter_shouldSucceed() { List entities = mockOpeningsEntityList(); when(openingsLastYearRepository.findAll(SORT)).thenReturn(entities); - OpeningsPerYearFiltersDto filtersDto = new OpeningsPerYearFiltersDto(null, "APP", null, null); + DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, "APP", null, null, null); List list = dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto); String monthName = now.getMonth().name().toLowerCase(); @@ -105,17 +120,17 @@ void getOpeningsSubmissionTrends_statusFilter_shouldSucceed() { } @Test - @DisplayName("Opening submission trends with Status filter should succeed") + @DisplayName("Opening submission trends with Dates filter should succeed") void getOpeningsSubmissionTrends_datesFilter_shouldSucceed() { - LocalDateTime now = LocalDateTime.now(); List entities = mockOpeningsEntityList(); when(openingsLastYearRepository.findAll(SORT)).thenReturn(entities); + LocalDateTime now = LocalDateTime.now(); LocalDateTime oneMonthBefore = now.minusMonths(1L); LocalDateTime oneMonthLater = now.plusMonths(1L); - OpeningsPerYearFiltersDto filtersDto = - new OpeningsPerYearFiltersDto(null, null, oneMonthBefore, oneMonthLater); + DashboardFiltesDto filtersDto = + new DashboardFiltesDto(null, null, oneMonthBefore, oneMonthLater, null); List list = dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto); String monthName = now.getMonth().name().toLowerCase(); @@ -127,4 +142,193 @@ void getOpeningsSubmissionTrends_datesFilter_shouldSucceed() { Assertions.assertEquals(monthName, list.get(0).monthName()); Assertions.assertEquals(1, list.get(0).amount()); } + + @Test + @DisplayName("Free growing milestones without filters should succeed") + void getFreeGrowingMilestoneChartData_noFilters_shouldSucceed() { + List entities = mockOpeningsEntityList(); + when(openingsLastYearRepository.findAll(SORT_BY_ID)).thenReturn(entities); + + DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, null, null, null, null); + List list = + dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto); + + Assertions.assertFalse(list.isEmpty()); + Assertions.assertEquals(4, list.size()); + Assertions.assertEquals(0, list.get(0).index()); + Assertions.assertEquals("0 - 5 months", list.get(0).label()); + Assertions.assertEquals(1, list.get(0).amount()); + Assertions.assertEquals(new BigDecimal("100.00"), list.get(0).percentage()); + + Assertions.assertEquals(1, list.get(1).index()); + Assertions.assertEquals("6 - 11 months", list.get(1).label()); + Assertions.assertEquals(0, list.get(1).amount()); + Assertions.assertEquals(new BigDecimal("0.00"), list.get(1).percentage()); + + Assertions.assertEquals(2, list.get(2).index()); + Assertions.assertEquals("12 - 17 months", list.get(2).label()); + Assertions.assertEquals(0, list.get(2).amount()); + Assertions.assertEquals(new BigDecimal("0.00"), list.get(2).percentage()); + + Assertions.assertEquals(3, list.get(3).index()); + Assertions.assertEquals("18 months", list.get(3).label()); + Assertions.assertEquals(0, list.get(3).amount()); + Assertions.assertEquals(new BigDecimal("0.00"), list.get(3).percentage()); + } + + @Test + @DisplayName("Free growing milestones with org unit filter should succeed") + void getFreeGrowingMilestoneChartData_orgUnitFilter_shouldSucceed() { + List entities = mockOpeningsEntityList(); + when(openingsLastYearRepository.findAll(SORT_BY_ID)).thenReturn(entities); + + DashboardFiltesDto filtersDto = new DashboardFiltesDto("AAA", null, null, null, null); + List list = + dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto); + + Assertions.assertFalse(list.isEmpty()); + Assertions.assertEquals(4, list.size()); + Assertions.assertEquals(0, list.get(0).index()); + Assertions.assertEquals("0 - 5 months", list.get(0).label()); + Assertions.assertEquals(0, list.get(0).amount()); + Assertions.assertEquals(BigDecimal.ZERO, list.get(0).percentage()); + + Assertions.assertEquals(1, list.get(1).index()); + Assertions.assertEquals("6 - 11 months", list.get(1).label()); + Assertions.assertEquals(0, list.get(1).amount()); + Assertions.assertEquals(BigDecimal.ZERO, list.get(1).percentage()); + + Assertions.assertEquals(2, list.get(2).index()); + Assertions.assertEquals("12 - 17 months", list.get(2).label()); + Assertions.assertEquals(0, list.get(2).amount()); + Assertions.assertEquals(BigDecimal.ZERO, list.get(2).percentage()); + + Assertions.assertEquals(3, list.get(3).index()); + Assertions.assertEquals("18 months", list.get(3).label()); + Assertions.assertEquals(0, list.get(3).amount()); + Assertions.assertEquals(BigDecimal.ZERO, list.get(3).percentage()); + } + + @Test + @DisplayName("Free growing milestones with client number filter should succeed") + void getFreeGrowingMilestoneChartData_clientNumberFilter_shouldSucceed() { + List entities = mockOpeningsEntityList(); + when(openingsLastYearRepository.findAll(SORT_BY_ID)).thenReturn(entities); + + DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, null, null, null, "00011254"); + List list = + dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto); + + Assertions.assertFalse(list.isEmpty()); + Assertions.assertEquals(4, list.size()); + Assertions.assertEquals(0, list.get(0).index()); + Assertions.assertEquals("0 - 5 months", list.get(0).label()); + Assertions.assertEquals(0, list.get(0).amount()); + Assertions.assertEquals(BigDecimal.ZERO, list.get(0).percentage()); + + Assertions.assertEquals(1, list.get(1).index()); + Assertions.assertEquals("6 - 11 months", list.get(1).label()); + Assertions.assertEquals(0, list.get(1).amount()); + Assertions.assertEquals(BigDecimal.ZERO, list.get(1).percentage()); + + Assertions.assertEquals(2, list.get(2).index()); + Assertions.assertEquals("12 - 17 months", list.get(2).label()); + Assertions.assertEquals(0, list.get(2).amount()); + Assertions.assertEquals(BigDecimal.ZERO, list.get(2).percentage()); + + Assertions.assertEquals(3, list.get(3).index()); + Assertions.assertEquals("18 months", list.get(3).label()); + Assertions.assertEquals(0, list.get(3).amount()); + Assertions.assertEquals(BigDecimal.ZERO, list.get(3).percentage()); + } + + @Test + @DisplayName("Free growing milestones with dates filter should succeed") + void getFreeGrowingMilestoneChartData_datesFilter_shouldSucceed() { + List entities = mockOpeningsEntityList(); + when(openingsLastYearRepository.findAll(SORT_BY_ID)).thenReturn(entities); + + LocalDateTime now = LocalDateTime.now(); + LocalDateTime oneMonthBefore = now.minusMonths(1L); + LocalDateTime oneMonthLater = now.plusMonths(1L); + + DashboardFiltesDto filtersDto = + new DashboardFiltesDto(null, null, oneMonthBefore, oneMonthLater, null); + List list = + dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto); + + Assertions.assertFalse(list.isEmpty()); + Assertions.assertEquals(4, list.size()); + Assertions.assertEquals(0, list.get(0).index()); + Assertions.assertEquals("0 - 5 months", list.get(0).label()); + Assertions.assertEquals(1, list.get(0).amount()); + Assertions.assertEquals(new BigDecimal("100.00"), list.get(0).percentage()); + + Assertions.assertEquals(1, list.get(1).index()); + Assertions.assertEquals("6 - 11 months", list.get(1).label()); + Assertions.assertEquals(0, list.get(1).amount()); + Assertions.assertEquals(new BigDecimal("0.00"), list.get(1).percentage()); + + Assertions.assertEquals(2, list.get(2).index()); + Assertions.assertEquals("12 - 17 months", list.get(2).label()); + Assertions.assertEquals(0, list.get(2).amount()); + Assertions.assertEquals(new BigDecimal("0.00"), list.get(2).percentage()); + + Assertions.assertEquals(3, list.get(3).index()); + Assertions.assertEquals("18 months", list.get(3).label()); + Assertions.assertEquals(0, list.get(3).amount()); + Assertions.assertEquals(new BigDecimal("0.00"), list.get(3).percentage()); + } + + @Test + @DisplayName("My recent activities request table data test happy path should succeed") + void getUserRecentOpeningsActions_happyPath_shouldSucceed() { + String userId = "TEST"; + + when(loggedUserService.getLoggedUserId()).thenReturn(userId); + + OpeningsActivityEntity activity = new OpeningsActivityEntity(); + activity.setOpeningId(112233L); + // If you're here looking for more codes and descriptions, you may want to take a look on + // the jira issue: https://apps.nrs.gov.bc.ca/int/jira/browse/SILVA-362 look for the comment + // with the 'fire' emoji, made on 20/Mar/24 3:03 PM + activity.setActivityTypeCode("UPD"); + activity.setActivityTypeDesc("Update"); + activity.setStatusCode("APP"); + activity.setStatusDesc("Approved"); + activity.setLastUpdated(LocalDateTime.now().minusHours(2)); + activity.setEntryUserid(userId); + + Sort sort = Sort.by("lastUpdated").descending(); + when(openingsActivityRepository.findAllByEntryUserid(userId, sort)) + .thenReturn(List.of(activity)); + + List dtoList = + dashboardMetricsService.getUserRecentOpeningsActions(); + + Assertions.assertFalse(dtoList.isEmpty()); + Assertions.assertEquals(1, dtoList.size()); + Assertions.assertEquals("Update", dtoList.get(0).activityType()); + Assertions.assertEquals(112233L, dtoList.get(0).openingId()); + Assertions.assertEquals("APP", dtoList.get(0).statusCode()); + Assertions.assertEquals("Approved", dtoList.get(0).statusDescription()); + Assertions.assertEquals("2 hours ago", dtoList.get(0).lastUpdatedLabel()); + } + + @Test + @DisplayName("My recent activities request table data test no data should succeed") + void getUserRecentOpeningsActions_noData_shouldSucceed() { + String userId = "TEST"; + + when(loggedUserService.getLoggedUserId()).thenReturn(userId); + + Sort sort = Sort.by("lastUpdated").descending(); + when(openingsActivityRepository.findAllByEntryUserid(userId, sort)) + .thenReturn(List.of()); + + List dtoList = + dashboardMetricsService.getUserRecentOpeningsActions(); + + Assertions.assertTrue(dtoList.isEmpty()); + } } diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningServiceTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningServiceTest.java new file mode 100644 index 00000000..7e60b157 --- /dev/null +++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningServiceTest.java @@ -0,0 +1,125 @@ +package ca.bc.gov.restapi.results.postgres.service; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ca.bc.gov.restapi.results.common.exception.UserOpeningNotFoundException; +import ca.bc.gov.restapi.results.common.security.LoggedUserService; +import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto; +import ca.bc.gov.restapi.results.postgres.entity.OpeningsActivityEntity; +import ca.bc.gov.restapi.results.postgres.entity.UserOpeningEntity; +import ca.bc.gov.restapi.results.postgres.repository.OpeningsActivityRepository; +import ca.bc.gov.restapi.results.postgres.repository.UserOpeningRepository; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class UserOpeningServiceTest { + + @Mock LoggedUserService loggedUserService; + + @Mock UserOpeningRepository userOpeningRepository; + + @Mock OpeningsActivityRepository openingsActivityRepository; + + private UserOpeningService userOpeningService; + + private static final String USER_ID = "TEST"; + + @BeforeEach + void setup() { + this.userOpeningService = + new UserOpeningService( + loggedUserService, userOpeningRepository, openingsActivityRepository); + } + + @Test + @DisplayName("Get user tracked openings happy path should succeed") + void getUserTrackedOpenings_happyPath_shouldSucceed() { + when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID); + + UserOpeningEntity entity = new UserOpeningEntity(); + entity.setUserId(USER_ID); + entity.setOpeningId(223344L); + + when(userOpeningRepository.findAllByUserId(USER_ID)).thenReturn(List.of(entity)); + + LocalDateTime now = LocalDateTime.now().minusMinutes(2); + OpeningsActivityEntity openingEntity = new OpeningsActivityEntity(); + openingEntity.setOpeningId(entity.getOpeningId()); + openingEntity.setActivityTypeCode("UPD"); + openingEntity.setActivityTypeDesc("Update"); + openingEntity.setStatusCode("APP"); + openingEntity.setStatusDesc("Approved"); + openingEntity.setLastUpdated(now); + openingEntity.setEntryUserid(USER_ID); + + when(openingsActivityRepository.findAllById(List.of(223344L))) + .thenReturn(List.of(openingEntity)); + + List openings = userOpeningService.getUserTrackedOpenings(); + + Assertions.assertFalse(openings.isEmpty()); + Assertions.assertEquals("Update", openings.get(0).activityType()); + Assertions.assertEquals(223344L, openings.get(0).openingId()); + Assertions.assertEquals("APP", openings.get(0).statusCode()); + Assertions.assertEquals("Approved", openings.get(0).statusDescription()); + Assertions.assertEquals("2 minutes ago", openings.get(0).lastUpdatedLabel()); + } + + @Test + @DisplayName("Get user tracked openings no data should succeed") + void getUserTrackedOpenings_noData_shouldSucceed() { + when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID); + + when(userOpeningRepository.findAllByUserId(USER_ID)).thenReturn(List.of()); + + List openings = userOpeningService.getUserTrackedOpenings(); + + Assertions.assertTrue(openings.isEmpty()); + } + + @Test + @DisplayName("Save opening to user happy path shoudl succeed") + void saveOpeningToUser_happyPath_shouldSucceed() { + when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID); + when(userOpeningRepository.saveAndFlush(any())).thenReturn(new UserOpeningEntity()); + userOpeningService.saveOpeningToUser(112233L); + } + + @Test + @DisplayName("Delete opening from user's favourite happy path should succeed") + void deleteOpeningFromUserFavourite_happyPath_shouldSucceed() { + when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID); + + UserOpeningEntity userEntity = new UserOpeningEntity(); + when(userOpeningRepository.findById(any())).thenReturn(Optional.of(userEntity)); + + doNothing().when(userOpeningRepository).delete(any()); + doNothing().when(userOpeningRepository).flush(); + + userOpeningService.deleteOpeningFromUserFavourite(112233L); + } + + @Test + @DisplayName("Delete opening from user's favourite not found should fail") + void deleteOpeningFromUserFavourite_notFound_shouldFail() { + when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID); + when(userOpeningRepository.findById(any())).thenReturn(Optional.empty()); + + Assertions.assertThrows( + UserOpeningNotFoundException.class, + () -> { + userOpeningService.deleteOpeningFromUserFavourite(112233L); + }); + } +} diff --git a/backend/src/test/resources/sql_scripts/OpeningsLastYearRepositoryIntegrationTest.sql b/backend/src/test/resources/sql_scripts/OpeningsLastYearRepositoryIntegrationTest.sql new file mode 100644 index 00000000..23ddd360 --- /dev/null +++ b/backend/src/test/resources/sql_scripts/OpeningsLastYearRepositoryIntegrationTest.sql @@ -0,0 +1,14 @@ +insert into openings_last_year ( + opening_id + ,opening_entry_userid + ,entry_timestamp + ,update_timestamp + ,status_code + ,org_unit_code + ,client_number +) values + (7012511, 'TEST', '2024-04-05', '2024-04-08', 'APP', 'DCR', '00012797'), + (7012512, 'TEST', '2024-02-03', '2024-03-08', 'APP', 'DCR', '00012797'), + (7012513, 'TEST', '2024-03-04', '2024-04-08', 'APP', 'DCR', '00012797'), + (7012514, 'TEST', '2024-01-15', '2024-02-15', 'APP', 'DCR', '00012797'), + (7012515, 'TEST', '2024-02-18', '2024-02-25', 'APP', 'DCR', '00012797');