diff --git a/src/main/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherController.java b/src/main/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherController.java index cd82fb24..16a40acf 100644 --- a/src/main/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherController.java +++ b/src/main/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherController.java @@ -1,31 +1,19 @@ package org.swmaestro.repl.gifthub.vouchers.controller; -import java.io.IOException; -import java.util.List; - +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.swmaestro.repl.gifthub.util.JwtProvider; -import org.swmaestro.repl.gifthub.vouchers.dto.S3FileDto; -import org.swmaestro.repl.gifthub.vouchers.dto.VoucherReadResponseDto; -import org.swmaestro.repl.gifthub.vouchers.dto.VoucherSaveRequestDto; -import org.swmaestro.repl.gifthub.vouchers.dto.VoucherSaveResponseDto; -import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUpdateRequestDto; +import org.swmaestro.repl.gifthub.vouchers.dto.*; import org.swmaestro.repl.gifthub.vouchers.service.StorageService; import org.swmaestro.repl.gifthub.vouchers.service.VoucherService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; +import java.io.IOException; +import java.util.List; @RestController @RequestMapping("/vouchers") @@ -47,8 +35,8 @@ public S3FileDto saveVoucherImage(@RequestPart("image_file") MultipartFile image @PostMapping @Operation(summary = "Voucher 등록 메서드", description = "클라이언트에서 요청한 기프티콘 정보를 저장하기 위한 메서드입니다.") public VoucherSaveResponseDto saveVoucher(HttpServletRequest request, - @RequestBody VoucherSaveRequestDto voucherSaveRequestDto) throws - IOException { + @RequestBody VoucherSaveRequestDto voucherSaveRequestDto) throws + IOException { String username = jwtProvider.getUsername(jwtProvider.resolveToken(request).substring(7)); return voucherService.save(username, voucherSaveRequestDto); } @@ -69,7 +57,15 @@ public List listVoucher(HttpServletRequest request) { @PatchMapping("/{voucherId}") @Operation(summary = "Voucher 수정 메서드", description = "클라이언트에서 요청한 기프티콘 정보를 수정하기 위한 메서드입니다.") public VoucherSaveResponseDto updateVoucher(@PathVariable Long voucherId, - @RequestBody VoucherUpdateRequestDto voucherUpdateRequestDto) throws IOException { + @RequestBody VoucherUpdateRequestDto voucherUpdateRequestDto) throws IOException { return voucherService.update(voucherId, voucherUpdateRequestDto); } + + @PostMapping("/{voucherId}/usage") + @Operation(summary = "Voucher 사용 메서드", description = "클라이언트에서 요청한 기프티콘 사용 정보를 저장하기 위한 메서드입니다.") + public VoucherUseResponseDto useVoucher(HttpServletRequest request, @PathVariable Long voucherId, + @RequestBody VoucherUseRequestDto voucherUseRequestDto) throws IOException { + String username = jwtProvider.getUsername(jwtProvider.resolveToken(request).substring(7)); + return voucherService.use(username, voucherId, voucherUseRequestDto); + } } \ No newline at end of file diff --git a/src/main/java/org/swmaestro/repl/gifthub/vouchers/dto/VoucherUseRequestDto.java b/src/main/java/org/swmaestro/repl/gifthub/vouchers/dto/VoucherUseRequestDto.java new file mode 100644 index 00000000..9f5c1dca --- /dev/null +++ b/src/main/java/org/swmaestro/repl/gifthub/vouchers/dto/VoucherUseRequestDto.java @@ -0,0 +1,25 @@ +package org.swmaestro.repl.gifthub.vouchers.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +public class VoucherUseRequestDto { + private Long id; + private int amount; + private String place; + + @Builder + public VoucherUseRequestDto(Long id, int amount, String place) { + this.id = id; + this.amount = amount; + this.place = place; + } +} \ No newline at end of file diff --git a/src/main/java/org/swmaestro/repl/gifthub/vouchers/dto/VoucherUseResponseDto.java b/src/main/java/org/swmaestro/repl/gifthub/vouchers/dto/VoucherUseResponseDto.java new file mode 100644 index 00000000..ec63f016 --- /dev/null +++ b/src/main/java/org/swmaestro/repl/gifthub/vouchers/dto/VoucherUseResponseDto.java @@ -0,0 +1,24 @@ +package org.swmaestro.repl.gifthub.vouchers.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +public class VoucherUseResponseDto { + private Long usageId; + private Long voucherId; + private int balance; + + @Builder + public VoucherUseResponseDto(Long usageId, Long voucherId, int balance) { + this.usageId = usageId; + this.voucherId = voucherId; + this.balance = balance; + } +} \ No newline at end of file diff --git a/src/main/java/org/swmaestro/repl/gifthub/vouchers/repository/VoucherRepository.java b/src/main/java/org/swmaestro/repl/gifthub/vouchers/repository/VoucherRepository.java index ca44366e..b6f1f581 100644 --- a/src/main/java/org/swmaestro/repl/gifthub/vouchers/repository/VoucherRepository.java +++ b/src/main/java/org/swmaestro/repl/gifthub/vouchers/repository/VoucherRepository.java @@ -6,8 +6,6 @@ import java.util.List; public interface VoucherRepository extends JpaRepository { - List findByMemberUsername(String username); - - List findByMemberUsernameAndBrandName(String username, String brandName); + List findAllByMemberUsername(String username); } diff --git a/src/main/java/org/swmaestro/repl/gifthub/vouchers/repository/VoucherUsageHistoryRepository.java b/src/main/java/org/swmaestro/repl/gifthub/vouchers/repository/VoucherUsageHistoryRepository.java index a4a66d03..3a31d9b4 100644 --- a/src/main/java/org/swmaestro/repl/gifthub/vouchers/repository/VoucherUsageHistoryRepository.java +++ b/src/main/java/org/swmaestro/repl/gifthub/vouchers/repository/VoucherUsageHistoryRepository.java @@ -3,5 +3,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.swmaestro.repl.gifthub.vouchers.entity.VoucherUsageHistory; +import java.util.List; + public interface VoucherUsageHistoryRepository extends JpaRepository { + List findAllByVoucherId(Long voucherId); } diff --git a/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/VoucherService.java b/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/VoucherService.java index 07e142d8..25e4c0f0 100644 --- a/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/VoucherService.java +++ b/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/VoucherService.java @@ -1,24 +1,24 @@ package org.swmaestro.repl.gifthub.vouchers.service; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.swmaestro.repl.gifthub.auth.service.MemberService; import org.swmaestro.repl.gifthub.exception.BusinessException; import org.swmaestro.repl.gifthub.exception.ErrorCode; import org.swmaestro.repl.gifthub.util.DateConverter; -import org.swmaestro.repl.gifthub.vouchers.dto.VoucherReadResponseDto; -import org.swmaestro.repl.gifthub.vouchers.dto.VoucherSaveRequestDto; -import org.swmaestro.repl.gifthub.vouchers.dto.VoucherSaveResponseDto; -import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUpdateRequestDto; +import org.swmaestro.repl.gifthub.vouchers.dto.*; import org.swmaestro.repl.gifthub.vouchers.entity.Voucher; +import org.swmaestro.repl.gifthub.vouchers.entity.VoucherUsageHistory; import org.swmaestro.repl.gifthub.vouchers.repository.VoucherRepository; +import org.swmaestro.repl.gifthub.vouchers.repository.VoucherUsageHistoryRepository; -import lombok.RequiredArgsConstructor; +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; @Service @RequiredArgsConstructor @@ -30,24 +30,25 @@ public class VoucherService { private final ProductService productService; private final VoucherRepository voucherRepository; private final MemberService memberService; + private final VoucherUsageHistoryRepository voucherUsageHistoryRepository; /* 기프티콘 저장 메서드 */ public VoucherSaveResponseDto save(String username, VoucherSaveRequestDto voucherSaveRequestDto) throws - IOException { + IOException { Voucher voucher = Voucher.builder() - .brand(brandService.read(voucherSaveRequestDto.getBrandName())) - .product(productService.read(voucherSaveRequestDto.getProductName())) - .barcode(voucherSaveRequestDto.getBarcode()) - .expiresAt(DateConverter.stringToLocalDate(voucherSaveRequestDto.getExpiresAt())) - .imageUrl(storageService.getBucketAddress(voucherDirName) + voucherSaveRequestDto.getImageUrl()) - .member(memberService.read(username)) - .build(); + .brand(brandService.read(voucherSaveRequestDto.getBrandName())) + .product(productService.read(voucherSaveRequestDto.getProductName())) + .barcode(voucherSaveRequestDto.getBarcode()) + .expiresAt(DateConverter.stringToLocalDate(voucherSaveRequestDto.getExpiresAt())) + .imageUrl(storageService.getBucketAddress(voucherDirName) + voucherSaveRequestDto.getImageUrl()) + .member(memberService.read(username)) + .build(); return VoucherSaveResponseDto.builder() - .id(voucherRepository.save(voucher).getId()) - .build(); + .id(voucherRepository.save(voucher).getId()) + .build(); } /* @@ -67,7 +68,7 @@ public VoucherReadResponseDto read(Long id) { 사용자 별 기프티콘 목록 조회 메서드 */ public List list(String username) { - List vouchers = voucherRepository.findByMemberUsername(username); + List vouchers = voucherRepository.findAllByMemberUsername(username); if (vouchers == null) { throw new BusinessException("존재하지 않는 사용자 입니다.", ErrorCode.NOT_FOUND_RESOURCE); } @@ -83,20 +84,73 @@ public List list(String username) { */ public VoucherSaveResponseDto update(Long voucherId, VoucherUpdateRequestDto voucherUpdateRequestDto) { Voucher voucher = voucherRepository.findById(voucherId) - .orElseThrow(() -> new BusinessException("존재하지 않는 상품권 입니다.", ErrorCode.NOT_FOUND_RESOURCE)); + .orElseThrow(() -> new BusinessException("존재하지 않는 상품권 입니다.", ErrorCode.NOT_FOUND_RESOURCE)); voucher.setBarcode( - voucherUpdateRequestDto.getBarcode() == null ? voucher.getBarcode() : voucherUpdateRequestDto.getBarcode()); + voucherUpdateRequestDto.getBarcode() == null ? voucher.getBarcode() : voucherUpdateRequestDto.getBarcode()); voucher.setBrand(voucherUpdateRequestDto.getBrandName() == null ? voucher.getBrand() : - brandService.read(voucherUpdateRequestDto.getBrandName())); + brandService.read(voucherUpdateRequestDto.getBrandName())); voucher.setProduct(voucherUpdateRequestDto.getProductName() == null ? voucher.getProduct() : - productService.read(voucherUpdateRequestDto.getProductName())); + productService.read(voucherUpdateRequestDto.getProductName())); voucher.setExpiresAt(voucherUpdateRequestDto.getExpiresAt() == null ? voucher.getExpiresAt() : - DateConverter.stringToLocalDate(voucherUpdateRequestDto.getExpiresAt())); + DateConverter.stringToLocalDate(voucherUpdateRequestDto.getExpiresAt())); return VoucherSaveResponseDto.builder() - .id(voucherId) - .build(); + .id(voucherId) + .build(); + } + + /* + 기프티콘 사용 등록 메서드 + */ + public VoucherUseResponseDto use(String username, Long voucherId, VoucherUseRequestDto voucherUseRequestDto) { + Optional voucher = voucherRepository.findById(voucherId); + List vouchers = voucherRepository.findAllByMemberUsername(username); + List voucherUsageHistories = voucherUsageHistoryRepository.findAllByVoucherId(voucherId); + + if (voucher.isEmpty()) { + throw new BusinessException("존재하지 않는 상품권 입니다.", ErrorCode.NOT_FOUND_RESOURCE); + } + if (!vouchers.contains(voucher.get())) { + throw new BusinessException("상품권을 사용할 권한이 없습니다.", ErrorCode.ACCESS_DENIED); + } + int totalUsageAmount = voucherUsageHistories.stream() + .mapToInt(VoucherUsageHistory::getAmount) + .sum(); + + totalUsageAmount = Math.max(totalUsageAmount, 0); + + if (totalUsageAmount == voucher.get().getBalance()) { + throw new BusinessException("이미 사용된 상품권 입니다.", ErrorCode.NOT_FOUND_RESOURCE); + } + + int remainingBalance = voucher.get().getBalance() - totalUsageAmount; + int requestedAmount = voucherUseRequestDto.getAmount(); + + if (requestedAmount > remainingBalance) { + throw new BusinessException("잔액이 부족합니다.", ErrorCode.EXIST_RESOURCE); + } + + if (voucher.get().getExpiresAt().isBefore(LocalDate.now())) { + throw new BusinessException("유효기간이 만료된 상품권 입니다.", ErrorCode.EXIST_RESOURCE); + } + + + VoucherUsageHistory voucherUsageHistory = VoucherUsageHistory.builder() + .member(memberService.read(username)) + .voucher(voucher.get()) + .amount(voucherUseRequestDto.getAmount()) + .place(voucherUseRequestDto.getPlace()) + .createdAt(LocalDateTime.now()) + .build(); + + voucherUsageHistoryRepository.save(voucherUsageHistory); + + return VoucherUseResponseDto.builder() + .usageId(voucherUsageHistory.getId()) + .voucherId(voucherId) + .balance(remainingBalance - requestedAmount) + .build(); } /* @@ -104,12 +158,12 @@ public VoucherSaveResponseDto update(Long voucherId, VoucherUpdateRequestDto vou */ public VoucherReadResponseDto mapToDto(Voucher voucher) { VoucherReadResponseDto voucherReadResponseDto = VoucherReadResponseDto.builder() - .id(voucher.getId()) - .barcode(voucher.getBarcode()) - .expiresAt(voucher.getExpiresAt().toString()) - .product(voucher.getProduct()) - .username(voucher.getMember().getUsername()) - .build(); + .id(voucher.getId()) + .barcode(voucher.getBarcode()) + .expiresAt(voucher.getExpiresAt().toString()) + .product(voucher.getProduct()) + .username(voucher.getMember().getUsername()) + .build(); return voucherReadResponseDto; } } diff --git a/src/test/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherControllerTest.java b/src/test/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherControllerTest.java index 1544fb8c..9a9f17f2 100644 --- a/src/test/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherControllerTest.java +++ b/src/test/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherControllerTest.java @@ -1,13 +1,6 @@ package org.swmaestro.repl.gifthub.vouchers.controller; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -import java.util.ArrayList; -import java.util.List; - +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -17,13 +10,16 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; import org.swmaestro.repl.gifthub.util.JwtProvider; -import org.swmaestro.repl.gifthub.vouchers.dto.VoucherReadResponseDto; -import org.swmaestro.repl.gifthub.vouchers.dto.VoucherSaveRequestDto; -import org.swmaestro.repl.gifthub.vouchers.dto.VoucherSaveResponseDto; -import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUpdateRequestDto; +import org.swmaestro.repl.gifthub.vouchers.dto.*; import org.swmaestro.repl.gifthub.vouchers.service.VoucherService; -import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc @@ -45,16 +41,16 @@ class VoucherControllerTest { void saveVoucher() throws Exception { // given VoucherSaveRequestDto voucher = VoucherSaveRequestDto.builder() - .brandName("스타벅스") - .productName("아이스 아메리카노 T") - .barcode("012345678910") - .expiresAt("2023-06-15") - .imageUrl("https://s3.ap-northeast-2.amazonaws.com/gifthub-voucher/1623777600000_스타벅스_아이스아메리카노T.png") - .build(); + .brandName("스타벅스") + .productName("아이스 아메리카노 T") + .barcode("012345678910") + .expiresAt("2023-06-15") + .imageUrl("https://s3.ap-northeast-2.amazonaws.com/gifthub-voucher/1623777600000_스타벅스_아이스아메리카노T.png") + .build(); VoucherSaveResponseDto voucherSaveResponseDto = VoucherSaveResponseDto.builder() - .id(1L) - .build(); + .id(1L) + .build(); // when when(jwtProvider.resolveToken(any())).thenReturn("my_awesome_access_token"); @@ -63,10 +59,10 @@ void saveVoucher() throws Exception { // then mockMvc.perform(post("/vouchers") - .header("Authorization", "Bearer my_awesome_access_token") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(voucher))) - .andExpect(status().isOk()); + .header("Authorization", "Bearer my_awesome_access_token") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(voucher))) + .andExpect(status().isOk()); } /* @@ -77,10 +73,10 @@ void readVoucherTest() throws Exception { // given Long voucherId = 1L; VoucherReadResponseDto voucherReadResponseDto = VoucherReadResponseDto.builder() - .id(1L) - .barcode("012345678910") - .expiresAt("2023-06-15") - .build(); + .id(1L) + .barcode("012345678910") + .expiresAt("2023-06-15") + .build(); when(voucherService.read(voucherId)).thenReturn(voucherReadResponseDto); //when @@ -101,24 +97,24 @@ void listVoucherTest() throws Exception { List voucherReadResponseDtos = new ArrayList<>(); voucherReadResponseDtos.add(VoucherReadResponseDto.builder() - .id(1L) - .barcode("012345678910") - .expiresAt("2023-06-15") - .build()); + .id(1L) + .barcode("012345678910") + .expiresAt("2023-06-15") + .build()); voucherReadResponseDtos.add(VoucherReadResponseDto.builder() - .id(2L) - .barcode("012345678911") - .expiresAt("2023-06-16") - .build()); + .id(2L) + .barcode("012345678911") + .expiresAt("2023-06-16") + .build()); when(jwtProvider.resolveToken(any())).thenReturn(accessToken); when(jwtProvider.getUsername(anyString())).thenReturn(username); when(voucherService.list(username)).thenReturn(voucherReadResponseDtos); mockMvc.perform(get("/vouchers") - .header("Authorization", "Bearer " + accessToken)) - .andExpect(status().isOk()); + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()); } /* @@ -129,15 +125,15 @@ void listVoucherTest() throws Exception { void voucherUpdateTest() throws Exception { // given VoucherUpdateRequestDto voucherUpdateRequestDto = VoucherUpdateRequestDto.builder() - .brandName("스타벅스") - .productName("아이스 아메리카노 T") - .barcode("012345678910") - .expiresAt("2023-06-15") - .build(); + .brandName("스타벅스") + .productName("아이스 아메리카노 T") + .barcode("012345678910") + .expiresAt("2023-06-15") + .build(); VoucherSaveResponseDto voucherSaveResponseDto = VoucherSaveResponseDto.builder() - .id(1L) - .build(); + .id(1L) + .build(); // when when(jwtProvider.resolveToken(any())).thenReturn("my_awesome_access_token"); @@ -146,9 +142,43 @@ void voucherUpdateTest() throws Exception { // then mockMvc.perform(patch("/vouchers/1") - .header("Authorization", "Bearer my_awesome_access_token") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(voucherSaveResponseDto))) - .andExpect(status().isOk()); + .header("Authorization", "Bearer my_awesome_access_token") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(voucherSaveResponseDto))) + .andExpect(status().isOk()); + } + + /* + 기프티콘 사용 등록 테스트 + */ + @Test + @WithMockUser(username = "이진우", roles = "USER") + void useVoucherTest() throws Exception { + // given + Long voucherId = 1L; + VoucherUseRequestDto voucherUseRequestDto = VoucherUseRequestDto.builder() + .id(1L) + .amount(5000) + .place("스타벅스 아남타워점") + .build(); + + VoucherUseResponseDto voucherUseResponseDto = VoucherUseResponseDto.builder() + .usageId(1L) + .voucherId(1L) + .balance(20000) + .build(); + + // when + when(jwtProvider.resolveToken(any())).thenReturn("my_awesome_access_token"); + when(jwtProvider.getUsername(anyString())).thenReturn("이진우"); + when(voucherService.use(anyString(), eq(voucherId), any(VoucherUseRequestDto.class))) + .thenReturn(voucherUseResponseDto); + + // then + mockMvc.perform(post("/vouchers/1/usage") + .header("Authorization", "Bearer my_awesome_access_token") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(voucherUseRequestDto))) + .andExpect(status().isOk()); } -} \ No newline at end of file +}