From ba92a3d331ac5e57874ada126ecfb54795e20034 Mon Sep 17 00:00:00 2001 From: VoHoangAn <63175067+VoHoangAn@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:03:16 +0700 Subject: [PATCH] Unit test for transaction (#127) * add test * add test * fix duplicate * fix in out to vietnamese * fix choose a user to vietnamese * fix validation and to vietnamese * fix validation to vietnamese * fix default is admin * fix edit modal * add statistic * fix return active category * fix checkstyle * add test * add test * fix --- .../sunrise/constraints/AfterAndUntilNow.java | 2 +- .../StringMustBeDigitWithFraction.java | 21 ++ ...tringMustBeDigitWithFractionValidator.java | 36 ++ .../controllers/TransactionController.java | 24 +- .../CreateOrUpdateTransactionRequest.java | 2 + .../dtos/responses/StatisticResponse.java | 13 + .../repositories/TransactionRepository.java | 15 + .../sunrise/services/TransactionService.java | 3 + .../services/impl/CategoryServiceImpl.java | 17 +- .../services/impl/TransactionServiceImpl.java | 56 +-- .../java/com/fjb/sunrise/utils/Constants.java | 1 + .../templates/transaction/index.html | 339 +++++++++--------- .../services/TransactionServiceTest.java | 160 ++++++++- 13 files changed, 468 insertions(+), 221 deletions(-) create mode 100644 src/main/java/com/fjb/sunrise/constraints/StringMustBeDigitWithFraction.java create mode 100644 src/main/java/com/fjb/sunrise/constraints/StringMustBeDigitWithFractionValidator.java create mode 100644 src/main/java/com/fjb/sunrise/dtos/responses/StatisticResponse.java diff --git a/src/main/java/com/fjb/sunrise/constraints/AfterAndUntilNow.java b/src/main/java/com/fjb/sunrise/constraints/AfterAndUntilNow.java index d0b7bc52..19ae75be 100644 --- a/src/main/java/com/fjb/sunrise/constraints/AfterAndUntilNow.java +++ b/src/main/java/com/fjb/sunrise/constraints/AfterAndUntilNow.java @@ -11,7 +11,7 @@ @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface AfterAndUntilNow { - String message() default "must be after {value} and before now"; + String message() default "Thời gian tạo phải sau 01/01/2020 và trước hiện tại"; Class[] groups() default {}; Class[] payload() default {}; String value(); diff --git a/src/main/java/com/fjb/sunrise/constraints/StringMustBeDigitWithFraction.java b/src/main/java/com/fjb/sunrise/constraints/StringMustBeDigitWithFraction.java new file mode 100644 index 00000000..11da82c2 --- /dev/null +++ b/src/main/java/com/fjb/sunrise/constraints/StringMustBeDigitWithFraction.java @@ -0,0 +1,21 @@ +package com.fjb.sunrise.constraints; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Constraint(validatedBy = StringMustBeDigitWithFractionValidator.class) +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface StringMustBeDigitWithFraction { + String message() default "Số tiền phải là chữ số với số thập phân là {fraction}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + int fraction(); +} diff --git a/src/main/java/com/fjb/sunrise/constraints/StringMustBeDigitWithFractionValidator.java b/src/main/java/com/fjb/sunrise/constraints/StringMustBeDigitWithFractionValidator.java new file mode 100644 index 00000000..80ab204d --- /dev/null +++ b/src/main/java/com/fjb/sunrise/constraints/StringMustBeDigitWithFractionValidator.java @@ -0,0 +1,36 @@ +package com.fjb.sunrise.constraints; + + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.springframework.util.StringUtils; + +public class StringMustBeDigitWithFractionValidator + implements ConstraintValidator { + private int fraction = 0; + + @Override + public void initialize(StringMustBeDigitWithFraction constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + fraction = constraintAnnotation.fraction(); + } + + @Override + public boolean isValid(String string, ConstraintValidatorContext constraintValidatorContext) { + String ignoreComma = string.replaceAll(",", ""); + String ignoreDot = ignoreComma.replace(".", ""); + if (ignoreDot.chars().allMatch(Character::isDigit)) { + if (fraction > 0) { + int startOfFraction = ignoreComma.indexOf("."); + if (startOfFraction == -1) { + return true; + } + if (ignoreComma.length() - startOfFraction - 1 > fraction) { + return false; + } + } + return true; + } + return false; + } +} diff --git a/src/main/java/com/fjb/sunrise/controllers/TransactionController.java b/src/main/java/com/fjb/sunrise/controllers/TransactionController.java index 641b580d..6aa6645d 100644 --- a/src/main/java/com/fjb/sunrise/controllers/TransactionController.java +++ b/src/main/java/com/fjb/sunrise/controllers/TransactionController.java @@ -1,9 +1,5 @@ package com.fjb.sunrise.controllers; -import static com.fjb.sunrise.utils.Constants.ApiConstant.CATEGORIES; -import static com.fjb.sunrise.utils.Constants.ApiConstant.TRANSACTION_INDEX; -import static com.fjb.sunrise.utils.Constants.ApiConstant.USERS; - import com.fjb.sunrise.dtos.base.DataTableInputDTO; import com.fjb.sunrise.dtos.requests.CreateOrUpdateTransactionRequest; import com.fjb.sunrise.dtos.responses.TransactionFullPageResponse; @@ -13,6 +9,7 @@ import com.fjb.sunrise.services.CategoryService; import com.fjb.sunrise.services.TransactionService; import com.fjb.sunrise.services.UserService; +import com.fjb.sunrise.utils.Constants; import jakarta.validation.Valid; import java.text.ParseException; import lombok.RequiredArgsConstructor; @@ -42,21 +39,24 @@ public class TransactionController { @GetMapping("/create") public String getCreate(@ModelAttribute("request") CreateOrUpdateTransactionRequest request, Model model) { - model.addAttribute(CATEGORIES, categoryService.findCategoryByAdminAndUser()); - model.addAttribute(USERS, userService.findAllNormalUser()); - return TRANSACTION_INDEX; + model.addAttribute(Constants.ApiConstant.CATEGORIES, categoryService.findCategoryByAdminAndUser()); + model.addAttribute(Constants.ApiConstant.USERS, userService.findAllNormalUser()); + model.addAttribute(Constants.ApiConstant.STATISTIC, transactionService.statistic()); + return Constants.ApiConstant.TRANSACTION_INDEX; } @PostMapping("/create") public String postCreate(@ModelAttribute("request") @Valid CreateOrUpdateTransactionRequest request, BindingResult result, Model model) throws ParseException { - model.addAttribute(CATEGORIES, categoryService.findCategoryByAdminAndUser()); - model.addAttribute(USERS, userService.findAllNormalUser()); + model.addAttribute(Constants.ApiConstant.CATEGORIES, categoryService.findCategoryByAdminAndUser()); + model.addAttribute(Constants.ApiConstant.USERS, userService.findAllNormalUser()); + model.addAttribute(Constants.ApiConstant.STATISTIC, transactionService.statistic()); if (result.hasErrors()) { - model.addAttribute(CATEGORIES, categoryService.findCategoryByAdminAndUser()); - model.addAttribute(USERS, userService.findAllNormalUser()); - return TRANSACTION_INDEX; + model.addAttribute(Constants.ApiConstant.CATEGORIES, categoryService.findCategoryByAdminAndUser()); + model.addAttribute(Constants.ApiConstant.USERS, userService.findAllNormalUser()); + model.addAttribute(Constants.ApiConstant.STATISTIC, transactionService.statistic()); + return Constants.ApiConstant.TRANSACTION_INDEX; } Transaction transaction = transactionService.create(request); return "redirect:/transaction/create"; diff --git a/src/main/java/com/fjb/sunrise/dtos/requests/CreateOrUpdateTransactionRequest.java b/src/main/java/com/fjb/sunrise/dtos/requests/CreateOrUpdateTransactionRequest.java index b4b625cc..32fdedfa 100644 --- a/src/main/java/com/fjb/sunrise/dtos/requests/CreateOrUpdateTransactionRequest.java +++ b/src/main/java/com/fjb/sunrise/dtos/requests/CreateOrUpdateTransactionRequest.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fjb.sunrise.constraints.AfterAndUntilNow; +import com.fjb.sunrise.constraints.StringMustBeDigitWithFraction; import com.fjb.sunrise.enums.ETrans; import jakarta.validation.constraints.NotBlank; import java.io.Serializable; @@ -16,6 +17,7 @@ public class CreateOrUpdateTransactionRequest implements Serializable { private Long id; @NotBlank(message = "Amount must not be blank") + @StringMustBeDigitWithFraction(fraction = 2) private String amount; private ETrans transactionType; diff --git a/src/main/java/com/fjb/sunrise/dtos/responses/StatisticResponse.java b/src/main/java/com/fjb/sunrise/dtos/responses/StatisticResponse.java new file mode 100644 index 00000000..04907d59 --- /dev/null +++ b/src/main/java/com/fjb/sunrise/dtos/responses/StatisticResponse.java @@ -0,0 +1,13 @@ +package com.fjb.sunrise.dtos.responses; + +import java.io.Serializable; +import lombok.Data; + + + +@Data +public class StatisticResponse implements Serializable { + private String totalThisYear; + private String totalInputThisYear; + private String totalThisMonth; +} diff --git a/src/main/java/com/fjb/sunrise/repositories/TransactionRepository.java b/src/main/java/com/fjb/sunrise/repositories/TransactionRepository.java index 2fcc85b5..c158d12d 100644 --- a/src/main/java/com/fjb/sunrise/repositories/TransactionRepository.java +++ b/src/main/java/com/fjb/sunrise/repositories/TransactionRepository.java @@ -1,13 +1,28 @@ package com.fjb.sunrise.repositories; + +import com.fjb.sunrise.enums.ETrans; import com.fjb.sunrise.models.Transaction; +import java.time.LocalDateTime; 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.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; + + public interface TransactionRepository extends JpaRepository, JpaSpecificationExecutor { Page findAll(Pageable pageable); + + @Query("select sum(t.amount) from Transaction t " + + "where t.updatedAt between ?1 and ?2") + Double sumAmountInRange(LocalDateTime start, LocalDateTime end); + + @Query("select sum(t.amount) from Transaction t " + + "where t.transactionType = ?1 and t.updatedAt between ?2 and ?3 ") + Double sumTransactionTypeINInThisYear(ETrans transactionType, LocalDateTime start, LocalDateTime end); + } diff --git a/src/main/java/com/fjb/sunrise/services/TransactionService.java b/src/main/java/com/fjb/sunrise/services/TransactionService.java index 7765677f..dfd07a51 100644 --- a/src/main/java/com/fjb/sunrise/services/TransactionService.java +++ b/src/main/java/com/fjb/sunrise/services/TransactionService.java @@ -2,6 +2,7 @@ import com.fjb.sunrise.dtos.base.DataTableInputDTO; import com.fjb.sunrise.dtos.requests.CreateOrUpdateTransactionRequest; +import com.fjb.sunrise.dtos.responses.StatisticResponse; import com.fjb.sunrise.models.Transaction; import java.text.ParseException; import org.springframework.data.domain.Page; @@ -12,4 +13,6 @@ public interface TransactionService { Page getTransactionList(DataTableInputDTO payload, String email); Transaction update(CreateOrUpdateTransactionRequest request) throws ParseException; + + StatisticResponse statistic(); } diff --git a/src/main/java/com/fjb/sunrise/services/impl/CategoryServiceImpl.java b/src/main/java/com/fjb/sunrise/services/impl/CategoryServiceImpl.java index 7f1dc135..2df3197f 100644 --- a/src/main/java/com/fjb/sunrise/services/impl/CategoryServiceImpl.java +++ b/src/main/java/com/fjb/sunrise/services/impl/CategoryServiceImpl.java @@ -148,7 +148,7 @@ public List findCategoryByAdminAndUser() { List orders = new ArrayList<>(); orders.add(Sort.Order.asc("owner.role")); Sort sortOpt = Sort.by(orders); - Specification specs = findAllByUser(); + Specification specs = findAllForTransaction(); return categoryRepository.findAll(specs, sortOpt); } @@ -169,6 +169,21 @@ private Specification findAllByUser() { }); } + private Specification findAllForTransaction() { + return Specification.where((root, query, builder) -> { + Join userJoin = root.join("owner"); + User dbUser = userRepository.findById(getCurrentUserId()).orElseThrow(); + if (dbUser.getRole() == ERole.ADMIN) { + return builder.equal(root.get("status"), EStatus.ACTIVE); + } + Predicate hasRoleAdmin = builder.equal(userJoin.get("role"), "ADMIN"); + Predicate isOwner = builder.equal(userJoin.get("id"), getCurrentUserId()); + Predicate active = builder.equal(root.get("status"), EStatus.ACTIVE); + Predicate or = builder.or(hasRoleAdmin, isOwner); + return builder.and(or, active); + }); + } + public Long getCurrentUserId() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); org.springframework.security.core.userdetails.User securityUser = diff --git a/src/main/java/com/fjb/sunrise/services/impl/TransactionServiceImpl.java b/src/main/java/com/fjb/sunrise/services/impl/TransactionServiceImpl.java index 3f541439..122b556b 100644 --- a/src/main/java/com/fjb/sunrise/services/impl/TransactionServiceImpl.java +++ b/src/main/java/com/fjb/sunrise/services/impl/TransactionServiceImpl.java @@ -2,6 +2,7 @@ import com.fjb.sunrise.dtos.base.DataTableInputDTO; import com.fjb.sunrise.dtos.requests.CreateOrUpdateTransactionRequest; +import com.fjb.sunrise.dtos.responses.StatisticResponse; import com.fjb.sunrise.enums.ETrans; import com.fjb.sunrise.models.Category; import com.fjb.sunrise.models.Transaction; @@ -11,8 +12,6 @@ import com.fjb.sunrise.services.TransactionService; import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.Root; -import jakarta.persistence.criteria.Subquery; import jakarta.transaction.Transactional; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; @@ -21,16 +20,7 @@ import java.time.LocalDateTime; import java.time.Month; import java.time.Year; -import java.time.ZoneId; import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.logging.log4j.util.Strings; @@ -65,12 +55,6 @@ public Transaction create(CreateOrUpdateTransactionRequest request) throws Parse return transactionRepository.save(transaction); } - public Transaction findById(Long id) { - return transactionRepository.findById(id).orElseThrow( - () -> new IllegalArgumentException("Invalid transaction id") - ); - } - @Override public Page getTransactionList(DataTableInputDTO payload, String email) { Sort sortOpt = Sort.by(Sort.Direction.ASC, "id"); @@ -100,9 +84,15 @@ public Page getTransactionList(DataTableInputDTO payload, String em // "%" + keyword + "%" specs = specs.and(((root, query, builder) -> { Join categoryJoin = root.join("category"); + String search = payload.getSearch().getOrDefault("value", "").toLowerCase(); + if (search.equals("thu")) { + search = "IN"; + } else if (search.equals("chi")) { + search = "OUT"; + } Predicate predictTransactionType = builder.like(builder.lower(root.get("transactionType")), String.format("%%%s%%", - payload.getSearch().getOrDefault("value", "").toLowerCase())); + search.toLowerCase())); Predicate predictCategory = builder.like(builder.lower(categoryJoin.get("name")), String.format("%%%s%%", payload.getSearch().getOrDefault("value", "").toLowerCase() @@ -121,7 +111,6 @@ public Transaction update(CreateOrUpdateTransactionRequest request) throws Parse transaction.setUpdatedAt(request.getCreatedAt()); transaction.setTransactionType(request.getTransactionType()); transaction.setAmount(convertMoneyStringWithCommaToDouble(request.getAmount())); - transaction.setUser(userRepository.findByEmailOrPhone(getCurrentUserName())); transaction.setCategory(categoryRepository.findById(request.getCategory()).orElse(null)); log.error("update: {}", transaction.toString()); @@ -130,14 +119,27 @@ public Transaction update(CreateOrUpdateTransactionRequest request) throws Parse return transaction1; } - private String changeFormatFromFullDateToMonthDate(LocalDateTime dateTime) throws ParseException { - SimpleDateFormat monthDate = new SimpleDateFormat("dd-MM", Locale.ENGLISH); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); - - - Date date = sdf.parse(dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); - monthDate.format(date); - return monthDate.format(date); + @Override + public StatisticResponse statistic() { + StatisticResponse response = new StatisticResponse(); + final LocalDateTime firstDay = getFirstOrLastDateOfThisYear(false); + final LocalDateTime lastDay = getFirstOrLastDateOfThisYear(true); + final LocalDateTime firstDayOfThisMonth = getFirstDayOfThisMonth(); + + + response.setTotalThisMonth( + convertDoubleWithScientificNotationToDouble( + transactionRepository.sumAmountInRange(firstDayOfThisMonth, lastDay) + )); + + response.setTotalThisYear( + convertDoubleWithScientificNotationToDouble( + transactionRepository.sumAmountInRange(firstDay, lastDay) + )); + response.setTotalInputThisYear(convertDoubleWithScientificNotationToDouble( + transactionRepository.sumTransactionTypeINInThisYear(ETrans.IN, firstDay, lastDay) + )); + return response; } private String convertDoubleWithScientificNotationToDouble(Double amount) { diff --git a/src/main/java/com/fjb/sunrise/utils/Constants.java b/src/main/java/com/fjb/sunrise/utils/Constants.java index 7b1a18b8..e7ff7c80 100644 --- a/src/main/java/com/fjb/sunrise/utils/Constants.java +++ b/src/main/java/com/fjb/sunrise/utils/Constants.java @@ -36,6 +36,7 @@ private ApiConstant() {} public static final String TRANSACTION_INDEX = "transaction/index"; public static final String CATEGORIES = "categories"; public static final String USERS = "users"; + public static final String STATISTIC = "statistic"; public static final String AUTH_REDIRECT_LOGIN = "redirect:/auth/login"; public static final String AUTH_VIEW = "auth/loginAndRegister"; diff --git a/src/main/resources/templates/transaction/index.html b/src/main/resources/templates/transaction/index.html index a5667990..0633a0f0 100644 --- a/src/main/resources/templates/transaction/index.html +++ b/src/main/resources/templates/transaction/index.html @@ -51,10 +51,11 @@

Tổng quan

- +
@@ -64,172 +65,165 @@

Tổng quan

-
- - -
-
-
-
-
-
- Thu nhập (Tháng Này) +
+
+ +
+
+
+
+
+
+ Thu nhập (Tháng Này) +
+
+
+
+
-
20,000,000 VNĐ
-
-
-
-
- -
-
-
-
-
-
- Thu nhập (Năm nay) + +
+
+
+
+
+
+ Thu nhập (Năm nay) +
+
+
+
+
-
240,000,000 VNĐ
-
-
-
-
- -
-
-
-
-
-
Tổng chi - (Tháng này) -
-
-
-
12,000,000 - VNĐ -
+ +
+
+
+
+
+
+ Tổng chi (Tháng này)
- +
+
+
+
-
-
-
-
- - -
-
-
-
-
-
- Tổng nợ (Tháng này) + +
+
+
+
+
+
+ Tổng nợ (Tháng này) +
+
0 VNĐ
+
+
+
-
0 VNĐ
-
-
-
-
+
-
-
- -
-
Biểu đồ thu nhập (Tháng này)
- -
- -
-
- -
-
-
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + -
-
- -
-
Thu nhập theo danh mục (Tháng này) -
- -
- -
-
- -
-
- - Lương - - - Chứng khoán - - - Bitcoin - - - Khác - -
-
-
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@@ -318,23 +312,24 @@
-
+