Skip to content
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
14 changes: 14 additions & 0 deletions backend/exence/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ dependencies {
implementation(libs.liquibase.core)
runtimeOnly(libs.postgresql)

// QueryDSL
implementation(libs.querydsl.jpa) {
artifact {
classifier = "jakarta"
}
}
annotationProcessor(libs.querydsl.apt) {
artifact {
classifier = "jakarta"
}
}
annotationProcessor("jakarta.annotation:jakarta.annotation-api")
annotationProcessor("jakarta.persistence:jakarta.persistence-api")

// Development tools
developmentOnly(libs.spring.boot.devtools)

Expand Down
3 changes: 3 additions & 0 deletions backend/exence/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ emoji-java = "5.1.1"
postgresql = "42.7.4"
mockito = "5.5.0"
bouncycastle = "1.70"
querydsl = "5.1.0"

[libraries]
spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa" }
Expand All @@ -39,6 +40,8 @@ emoji-java = { module = "com.vdurmont:emoji-java", version.ref = "emoji-java" }
postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
bouncycastle = { module = "org.bouncycastle:bcprov-jdk15on", version.ref = "bouncycastle" }
querydsl-jpa = { module = "com.querydsl:querydsl-jpa", version.ref = "querydsl" }
querydsl-apt = { module = "com.querydsl:querydsl-apt", version.ref = "querydsl" }

[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
Expand Down Expand Up @@ -40,9 +41,12 @@ public ResponseEntity<TransactionDTO> getTransactionById(@PathVariable("id") Lon
return ResponseFactory.ok(transactionDTO);
}

// TODO: EX-241: Pageable annotation refactor
@GetMapping()
public ResponseEntity<PageResponse<TransactionDTO>> getTransactions(@Valid @ModelAttribute TransactionFilter filter,
@PageableDefault(size = 20) Pageable pageable) {
@PageableDefault(sort = "date",
direction = Sort.Direction.DESC,
size = 20) Pageable pageable) {
Page<TransactionDTO> page = transactionService.getTransactions(filter, pageable);
return ResponseFactory.page(page);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.exence.finance.modules.transaction.repository;

import com.exence.finance.modules.transaction.dto.TransactionType;
import com.exence.finance.modules.transaction.dto.request.TransactionFilter;
import com.exence.finance.modules.transaction.entity.QTransaction;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.time.Instant;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class TransactionPredicateBuilder {

private static final QTransaction qTransaction = QTransaction.transaction;

public static Predicate buildPredicate(TransactionFilter filter) {
if (filter == null) {
return null;
}

BooleanBuilder builder = new BooleanBuilder();

addKeywordFilter(builder, filter.getKeyword());
addCategoryFilter(builder, filter.getCategoryId());
addTypeFilter(builder, filter.getType());
addDateRangeFilter(builder, filter.getDateFrom(), filter.getDateTo());
addAmountRangeFilter(builder, filter.getAmountFrom(), filter.getAmountTo());
addRecurringFilter(builder, filter.getRecurring());

return builder.hasValue() ? builder : null;
}

private static void addKeywordFilter(BooleanBuilder builder, String keyword) {
if (StringUtils.hasText(keyword)) {
String lowerKeyword = keyword.toLowerCase();
builder.and(
qTransaction.title.lower().contains(lowerKeyword)
.or(qTransaction.note.lower().contains(lowerKeyword))
);
}
}

private static void addCategoryFilter(BooleanBuilder builder, Long categoryId) {
if (categoryId != null) {
builder.and(qTransaction.category.id.eq(categoryId));
}
}

private static void addTypeFilter(BooleanBuilder builder, TransactionType type) {
if (type != null) {
builder.and(qTransaction.type.eq(type));
}
}

private static void addDateRangeFilter(BooleanBuilder builder, Instant dateFrom, Instant dateTo) {
if (dateFrom != null) {
builder.and(qTransaction.date.goe(dateFrom));
}
if (dateTo != null) {
builder.and(qTransaction.date.loe(dateTo));
}
}

private static void addAmountRangeFilter(BooleanBuilder builder, BigDecimal amountFrom, BigDecimal amountTo) {
if (amountFrom != null) {
builder.and(qTransaction.amount.goe(amountFrom));
}
if (amountTo != null) {
builder.and(qTransaction.amount.loe(amountTo));
}
}

private static void addRecurringFilter(BooleanBuilder builder, Boolean recurring) {
if (recurring != null) {
builder.and(qTransaction.recurring.eq(recurring));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,41 +1,24 @@
package com.exence.finance.modules.transaction.repository;

import com.exence.finance.modules.transaction.dto.TransactionType;
import com.exence.finance.modules.transaction.dto.request.TransactionFilter;
import com.exence.finance.modules.transaction.entity.Transaction;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.math.BigDecimal;
import java.util.Optional;

@Repository
public interface TransactionRepository extends JpaRepository<Transaction, Long> {
public interface TransactionRepository extends JpaRepository<Transaction, Long>, QuerydslPredicateExecutor<Transaction> {

@Query("SELECT t FROM Transaction t WHERE t.id = :id")
Optional<Transaction> find(@Param("id") Long id);

@Query("SELECT t FROM Transaction t WHERE " +
"(:#{#filter.keyword} IS NULL OR :#{#filter.keyword} = '' OR " +
"LOWER(t.title) LIKE %:#{#filter.keyword?.toLowerCase()}% OR " +
"LOWER(t.note) LIKE %:#{#filter.keyword?.toLowerCase()}%) " +
"AND (:#{#filter.categoryId} IS NULL OR t.category.id = :#{#filter.categoryId}) " +
"AND (:#{#filter.type} IS NULL OR t.type = :#{#filter.type}) " +
"AND (COALESCE(:#{#filter.dateFrom}, t.date) IS NULL OR t.date >= COALESCE(:#{#filter.dateFrom}, t.date)) " +
"AND (COALESCE(:#{#filter.dateTo}, t.date) IS NULL OR t.date <= COALESCE(:#{#filter.dateTo}, t.date)) " +
"AND (:#{#filter.amountFrom} IS NULL OR t.amount >= :#{#filter.amountFrom}) " +
"AND (:#{#filter.amountTo} IS NULL OR t.amount <= :#{#filter.amountTo}) " +
"AND (:#{#filter.recurring} IS NULL OR t.recurring = :#{#filter.recurring}) " +
"ORDER BY t.date DESC")
Page<Transaction> findWithFilter(
@Param("filter") TransactionFilter filter,
Pageable pageable
);

@Query("SELECT t FROM Transaction t WHERE " +
"t.recurring = true " +
"AND t.type = :type " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
import com.exence.finance.modules.transaction.dto.response.TransactionTotalsResponse;
import com.exence.finance.modules.transaction.entity.Transaction;
import com.exence.finance.modules.transaction.mapper.TransactionMapper;
import com.exence.finance.modules.transaction.repository.TransactionPredicateBuilder;
import com.exence.finance.modules.transaction.repository.TransactionRepository;
import com.exence.finance.modules.transaction.service.TransactionService;
import com.querydsl.core.types.Predicate;
import java.math.BigDecimal;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
Expand Down Expand Up @@ -44,17 +46,12 @@ public TransactionDTO getTransactionById(Long id) {

public Page<TransactionDTO> getTransactions(TransactionFilter filter, Pageable pageable){
Page<Transaction> transactions;
Predicate predicate = TransactionPredicateBuilder.buildPredicate(filter);

if (filter == null || filter.hasActiveFilter()) {
transactions = transactionRepository.findWithFilter(filter, pageable);
if (predicate != null) {
transactions = transactionRepository.findAll(predicate, pageable);
} else {
Sort sortByDateDesc = Sort.by(Sort.Direction.DESC, "date");
Pageable sortedByDate = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
pageable.getSort().and(sortByDateDesc)
);
transactions = transactionRepository.findAll(sortedByDate);
transactions = transactionRepository.findAll(pageable);
}

return transactions.map(transactionMapper::mapToTransactionDTO);
Expand Down
Loading