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: openings per year api #263

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package ca.bc.gov.restapi.results.postgres.dto;

import io.swagger.v3.oas.annotations.media.Schema;

/** This record represents a record for the "Opening submission trends" chart. */
@Schema(
description = "This record represents a record for the \"Opening submission trends\" chart.")
public record OpeningsPerYearDto(
@Schema(description = "The `x` value with the month number.", example = "3") Integer month,
@Schema(description = "The `x` value with the month name.", example = "Mar") String monthName,
@Schema(description = "The `y` value with the month value.", example = "70") Integer amount) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ca.bc.gov.restapi.results.postgres.dto;

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) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package ca.bc.gov.restapi.results.postgres.endpoint;

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;
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.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;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/** 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")
@RequiredArgsConstructor
public class DashboardMetricsEndpoint {

private final DashboardMetricsService dashboardMetricsService;

/**
* Get data for the Submission Trends Chart, Openings per Year.
*
* @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.
* @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.",
responses = {
@ApiResponse(
responseCode = "200",
description = "An array with twelve objects for the last 12 months."),
@ApiResponse(
responseCode = "204",
description = "No data found in the dable. No response body."),
@ApiResponse(
responseCode = "401",
description = "Access token is missing or invalid",
content = @Content(schema = @Schema(implementation = Void.class)))
})
public ResponseEntity<List<OpeningsPerYearDto>> getOpeningsSubmissionTrends(
@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 = "statusCode", required = false)
@Parameter(
name = "statusCode",
in = ParameterIn.QUERY,
description = "The Openins Status code to filter",
required = false,
example = "APP")
String statusCode,
@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) {
LocalDateTime entryDateStartDate = null;
LocalDateTime entryDateEndDate = null;
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");

if (!Objects.isNull(entryDateStart)) {
LocalDate entryDateStartLd = LocalDate.parse(entryDateStart, fmt);
entryDateStartDate = entryDateStartLd.atStartOfDay();
}

if (!Objects.isNull(entryDateEnd)) {
LocalDate entryDateEndLd = LocalDate.parse(entryDateEnd, fmt);
entryDateEndDate = entryDateEndLd.atStartOfDay();
}

OpeningsPerYearFiltersDto filtersDto =
new OpeningsPerYearFiltersDto(
orgUnitCode, statusCode, entryDateStartDate, entryDateEndDate);

List<OpeningsPerYearDto> resultList =
dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto);

if (resultList.isEmpty()) {
return ResponseEntity.noContent().build();
}

return ResponseEntity.ok(resultList);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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_last_year table. */
@Getter
@Setter
@Entity
@Table(name = "openings_last_year")
public class OpeningsLastYearEntity {

@Id
@Column(name = "opening_id")
private String openingId;

@Column(name = "opening_entry_userid", nullable = false)
private String userId;

@Column(name = "entry_timestamp", nullable = false)
private LocalDateTime entryTimestamp;

@Column(name = "update_timestamp", nullable = false)
private LocalDateTime updateTimestamp;

@Column(name = "status_code", nullable = false)
private String status;

@Column(name = "org_unit_code", nullable = false)
private String orgUnitCode;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ca.bc.gov.restapi.results.postgres.repository;

import ca.bc.gov.restapi.results.postgres.entity.OpeningsLastYearEntity;
import org.springframework.data.jpa.repository.JpaRepository;

/** This interface provides access to the database for the OpeningsLastYearEntity entity. */
public interface OpeningsLastYearRepository extends JpaRepository<OpeningsLastYearEntity, String> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package ca.bc.gov.restapi.results.postgres.service;

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.OpeningsLastYearEntity;
import ca.bc.gov.restapi.results.postgres.repository.OpeningsLastYearRepository;
import java.time.Month;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

/** This class contains methods for gathering and grouping data for the dashboard metrics screen. */
@Slf4j
@Service
@RequiredArgsConstructor
public class DashboardMetricsService {

private final OpeningsLastYearRepository openingsLastYearRepository;

/**
* Get openings submission trends data for the opening per year chart.
*
* @param filters Possible filter, see {@link OpeningsPerYearFiltersDto} for more.
* @return A list of {@link OpeningsPerYearDto} for the opening chart.
*/
public List<OpeningsPerYearDto> getOpeningsSubmissionTrends(OpeningsPerYearFiltersDto filters) {
log.info("Getting Opening Submission Trends with filters {}", filters.toString());

List<OpeningsLastYearEntity> entities =
openingsLastYearRepository.findAll(Sort.by("entryTimestamp").ascending());

if (entities.isEmpty()) {
log.info("No Opening Submission Trends data found!");
return List.of();
}

Map<Integer, List<OpeningsLastYearEntity>> resultMap = new LinkedHashMap<>();
Map<Integer, String> monthNamesMap = new HashMap<>();

// Fill with 12 months
Integer monthValue = entities.get(0).getEntryTimestamp().getMonthValue();
log.info("First month: {}", monthValue);
while (resultMap.size() < 12) {
resultMap.put(monthValue, new ArrayList<>());

String monthName = Month.of(monthValue).name().toLowerCase();
monthName = monthName.substring(0, 1).toUpperCase() + monthName.substring(1, 3);
monthNamesMap.put(monthValue, monthName);

monthValue += 1;
if (monthValue == 13) {
monthValue = 1;
}
}

for (OpeningsLastYearEntity entity : entities) {
// Org Unit filter - District
if (!Objects.isNull(filters.orgUnit())
&& !entity.getOrgUnitCode().equals(filters.orgUnit())) {
continue;
}

// Status filter
if (!Objects.isNull(filters.status()) && !entity.getStatus().equals(filters.status())) {
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;
}

resultMap.get(entity.getEntryTimestamp().getMonthValue()).add(entity);
}

List<OpeningsPerYearDto> chartData = new ArrayList<>();
for (Integer monthKey : resultMap.keySet()) {
List<OpeningsLastYearEntity> monthDataList = resultMap.get(monthKey);
String monthName = monthNamesMap.get(monthKey);
log.info("Month: {}", monthName);
chartData.add(new OpeningsPerYearDto(monthKey, monthName, monthDataList.size()));
}

return chartData;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
alter table silva.openings_last_year
add column status_code VARCHAR(3) NOT NULL,
add column org_unit_code VARCHAR(6) NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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.OpeningsPerYearDto;
import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearFiltersDto;
import ca.bc.gov.restapi.results.postgres.service.DashboardMetricsService;
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(DashboardMetricsEndpoint.class)
@WithMockUser
class DashboardMetricsEndpointTest {

@Autowired private MockMvc mockMvc;

@MockBean private DashboardMetricsService dashboardMetricsService;

@Test
@DisplayName("Opening submission trends with no filters should succeed")
void getOpeningsSubmissionTrends_noFilters_shouldSucceed() throws Exception {
OpeningsPerYearFiltersDto filtersDto = new OpeningsPerYearFiltersDto(null, null, null, null);

OpeningsPerYearDto dto = new OpeningsPerYearDto(1, "Jan", 70);
when(dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto)).thenReturn(List.of(dto));

mockMvc
.perform(
get("/api/dashboard-metrics/submission-trends")
.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].month").value("1"))
.andExpect(jsonPath("$[0].monthName").value("Jan"))
.andExpect(jsonPath("$[0].amount").value("70"))
.andReturn();
}

@Test
@DisplayName("Opening submission trends with no data should succeed")
void getOpeningsSubmissionTrends_orgUnitFilter_shouldSucceed() throws Exception {
OpeningsPerYearFiltersDto filtersDto = new OpeningsPerYearFiltersDto("DCR", null, null, null);

when(dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto)).thenReturn(List.of());

mockMvc
.perform(
get("/api/dashboard-metrics/submission-trends")
.with(csrf().asHeader())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNoContent())
.andReturn();
}
}
Loading