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: user recent openings backend #451

Merged
merged 4 commits into from
Nov 8, 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
Expand Up @@ -9,6 +9,7 @@ public class SilvaOracleConstants {
public static final String ORG_UNIT = "orgUnit";
public static final String CATEGORY = "category";
public static final String STATUS_LIST = "statusList";
public static final String OPENING_IDS = "openingIds";
public static final String MY_OPENINGS = "myOpenings";
public static final String SUBMITTED_TO_FRPA = "submittedToFrpa";
public static final String DISTURBANCE_DATE_START = "disturbanceDateStart";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class OpeningSearchFiltersDto {

@Setter
private String requestUserId;
private List<String> openingIds;

/** Creates an instance of the search opening filter dto. */
public OpeningSearchFiltersDto(
Expand Down Expand Up @@ -68,6 +69,7 @@ public OpeningSearchFiltersDto(
.toList());
}
this.statusList = new ArrayList<>();
this.openingIds = new ArrayList<>();
if (!Objects.isNull(statusList)) {
this.statusList.addAll(statusList.stream().map(s -> String.format("'%s'", s)).toList());
}
Expand All @@ -92,6 +94,28 @@ public OpeningSearchFiltersDto(
Objects.isNull(mainSearchTerm) ? null : mainSearchTerm.toUpperCase().trim();
}

// Create a constructor with only the List<String> openingIds
public OpeningSearchFiltersDto(
List<String> openingIds) {
this.orgUnit = new ArrayList<>();
this.category = new ArrayList<>();
this.statusList = new ArrayList<>();
this.openingIds = openingIds;
this.myOpenings = null;
this.submittedToFrpa = null;
this.disturbanceDateStart = null;
this.disturbanceDateEnd = null;
this.regenDelayDateStart = null;
this.regenDelayDateEnd = null;
this.freeGrowingDateStart = null;
this.freeGrowingDateEnd = null;
this.updateDateStart = null;
this.updateDateEnd = null;
this.cuttingPermitId = null;
this.cutBlockId = null;
this.timberMark = null;
this.mainSearchTerm = null;
}
/**
* Define if a property has value.
*
Expand All @@ -103,6 +127,7 @@ public boolean hasValue(String prop) {
case SilvaOracleConstants.ORG_UNIT -> !this.orgUnit.isEmpty();
case SilvaOracleConstants.CATEGORY -> !this.category.isEmpty();
case SilvaOracleConstants.STATUS_LIST -> !this.statusList.isEmpty();
case SilvaOracleConstants.OPENING_IDS -> !this.openingIds.isEmpty();
case SilvaOracleConstants.MY_OPENINGS -> !Objects.isNull(this.myOpenings);
case SilvaOracleConstants.SUBMITTED_TO_FRPA -> !Objects.isNull(this.submittedToFrpa);
case SilvaOracleConstants.DISTURBANCE_DATE_START ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ public class OpeningSearchResponseDto {
private Boolean submittedToFrpa;
private String forestFileId;
private Long silvaReliefAppId;
private LocalDateTime lastViewDate;
}
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@ private Query setQueryParameters(OpeningSearchFiltersDto filtersDto, String nati
log.info("Setting statusList filter values");
// No need to set value since the query already dit it. Didn't work set through named param
}
// similarly for openingIds
if (filtersDto.hasValue(SilvaOracleConstants.OPENING_IDS)) {
log.info("Setting openingIds filter values");
// No need to set value since the query already dit it. Didn't work set through
// named param
}
// 4. User entry id
if (filtersDto.hasValue(SilvaOracleConstants.MY_OPENINGS)) {
log.info("Setting myOpenings filter value");
Expand Down Expand Up @@ -390,6 +396,12 @@ private String createNativeSqlQuery(OpeningSearchFiltersDto filtersDto) {
builder.append("WHERE 1=1 ");

/* Filters */
// List of openings from the openingIds of the filterDto object for the recent openings
if (filtersDto.hasValue(SilvaOracleConstants.OPENING_IDS)) {
String openingIds = String.join(",", filtersDto.getOpeningIds());
log.info("Filter for openingIds detected! openingIds={}", openingIds);
builder.append(String.format("AND o.OPENING_ID IN (%s) ", openingIds));
}
// 0. Main number filter [opening_id, opening_number, timber_mark, file_id]
// if it's a number, filter by openingId or fileId, otherwise filter by timber mark and opening
// number
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ca.bc.gov.restapi.results.postgres.dto;

import java.time.LocalDateTime;
import lombok.Builder;
import lombok.With;

@With
@Builder
public record UserRecentOpeningDto(
String userId,
String openingId,
LocalDateTime lastViewed
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package ca.bc.gov.restapi.results.postgres.endpoint;

import ca.bc.gov.restapi.results.common.pagination.PaginatedResult;
import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchResponseDto;
import ca.bc.gov.restapi.results.postgres.service.UserRecentOpeningService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/openings/recent")
public class UserRecentOpeningEndpoint {

private final UserRecentOpeningService userRecentOpeningService;

/**
* Retrieves a list of recent openings viewed by the user, limited by the number of results.
*
* @param limit The maximum number of results to return.
* @return A list of opening IDs viewed by the user.
*/
@GetMapping
public ResponseEntity<PaginatedResult<OpeningSearchResponseDto>> getUserRecentOpenings(
@RequestParam(defaultValue = "10") int limit) {
// Fetch recent openings for the logged-in user with the specified limit
return ResponseEntity.ok(userRecentOpeningService.getAllRecentOpeningsForUser(limit));
}

/**
* Records the opening viewed by the user based on the provided opening ID.
*
* @param openingId The ID of the opening viewed by the user.
* @return A simple confirmation message or the HTTP code 204-No Content.
*/
@PutMapping("/{openingId}")
@ResponseStatus(HttpStatus.ACCEPTED)
public void recordUserViewedOpening(
@PathVariable String openingId) {
// Store the opening and return the DTO
userRecentOpeningService.storeViewedOpening(openingId);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package ca.bc.gov.restapi.results.postgres.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.With;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
@With
@Builder
@Entity
@Table(schema = "silva", name = "user_recent_openings")
public class UserRecentOpeningEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

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

@Column(name = "opening_id", nullable = false)
private String openingId;

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

import ca.bc.gov.restapi.results.postgres.entity.UserRecentOpeningEntity;
import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRecentOpeningRepository extends JpaRepository<UserRecentOpeningEntity, Long> {
UserRecentOpeningEntity findByUserIdAndOpeningId(String userId, String openingId);
// Add a method to fetch recent openings for a user with a limit and sorting by last_viewed in descending order
Page<UserRecentOpeningEntity> findByUserIdOrderByLastViewedDesc(String userId, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package ca.bc.gov.restapi.results.postgres.service;

import ca.bc.gov.restapi.results.common.pagination.PaginatedResult;
import ca.bc.gov.restapi.results.common.pagination.PaginationParameters;
import ca.bc.gov.restapi.results.common.security.LoggedUserService;
import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchFiltersDto;
import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchResponseDto;
import ca.bc.gov.restapi.results.oracle.service.OpeningService;
import ca.bc.gov.restapi.results.postgres.dto.UserRecentOpeningDto;
import ca.bc.gov.restapi.results.postgres.entity.UserRecentOpeningEntity;
import ca.bc.gov.restapi.results.postgres.repository.UserRecentOpeningRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class UserRecentOpeningService {

private final LoggedUserService loggedUserService;
private final UserRecentOpeningRepository userRecentOpeningRepository;
private final OpeningService openingService;

/**
* Stores the opening viewed by the user and returns the DTO.
*
* @param openingId The ID of the opening viewed by the user.
* @return A DTO with userId, openingId, and lastViewed timestamp.
*/
public UserRecentOpeningDto storeViewedOpening(String openingId) {
String userId = loggedUserService.getLoggedUserId();
LocalDateTime lastViewed = LocalDateTime.now();

// Verify that the openingId String contains numbers only and no spaces
if (!openingId.matches("^[0-9]*$")) {
throw new IllegalArgumentException("Opening ID must contain numbers only!");
}

// Check if the user has already viewed this opening
UserRecentOpeningEntity existingEntity = userRecentOpeningRepository.findByUserIdAndOpeningId(userId, openingId);

if (existingEntity != null) {
// Update the last viewed timestamp for the existing record
existingEntity.setLastViewed(lastViewed);
userRecentOpeningRepository.save(existingEntity); // Save the updated entity
} else {
// Create a new entity if this openingId is being viewed for the first time
UserRecentOpeningEntity newEntity = new UserRecentOpeningEntity(null, userId, openingId, lastViewed);
userRecentOpeningRepository.save(newEntity); // Save the new entity
}

// Return the DTO
return new UserRecentOpeningDto(userId, openingId, lastViewed);
}

/**
* Retrieves the recent openings viewed by the logged-in user, limited by the provided limit.
*
* @param limit The maximum number of recent openings to retrieve.
* @return A list of opening IDs the user has viewed, sorted by last viewed in descending order.
*/
public PaginatedResult<OpeningSearchResponseDto> getAllRecentOpeningsForUser(int limit) {
String userId = loggedUserService.getLoggedUserId();
Pageable pageable = PageRequest.of(0, limit); // PageRequest object to apply limit

// Fetch recent openings for the user
Page<UserRecentOpeningEntity> recentOpenings = userRecentOpeningRepository
.findByUserIdOrderByLastViewedDesc(userId, pageable);

// Extract opening IDs as String
Map<String,LocalDateTime> openingIds = recentOpenings.getContent().stream()
.collect(Collectors.toMap(UserRecentOpeningEntity::getOpeningId, UserRecentOpeningEntity::getLastViewed));
log.info("User with the userId {} has the following openindIds {}", userId, openingIds);
if (openingIds.isEmpty()) {
return new PaginatedResult<>();
}

PaginatedResult<OpeningSearchResponseDto> pageResult =
openingService
.openingSearch(
new OpeningSearchFiltersDto(new ArrayList<>(openingIds.keySet())),
new PaginationParameters(0, 10)
);

return pageResult
.withData(
pageResult
.getData()
.stream()
.peek(result -> result.setLastViewDate(openingIds.get(result.getOpeningId().toString())))
.sorted(Comparator.comparing(OpeningSearchResponseDto::getLastViewDate).reversed())
.collect(Collectors.toList())
);
}

}
Loading
Loading