diff --git a/build.gradle b/build.gradle index 82badb15..9783e62f 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,7 @@ dependencies { // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.11.0' // Querydsl implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' @@ -92,6 +93,9 @@ dependencies { // S3 implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.2") implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3' + + // webflux + implementation 'org.springframework.boot:spring-boot-starter-webflux' } tasks.named('test') { diff --git a/src/docs/asciidoc/coupon.adoc b/src/docs/asciidoc/coupon.adoc index 6e98b9e4..4f424964 100644 --- a/src/docs/asciidoc/coupon.adoc +++ b/src/docs/asciidoc/coupon.adoc @@ -85,6 +85,15 @@ include::{snippets}/coupons/http-response.adoc[] 사용자가 자신의 보관함에 있는 쿠폰들을 조회합니다. +==== 요청 + +include::{snippets}/my-coupons/couponId/http-request.adoc[] + +[discrete] +==== 응답 + +include::{snippets}/my-coupons/couponId/http-response.adoc[] + --- === 쿠폰 사용 (진행 중) diff --git a/src/main/java/com/moabam/api/application/bug/BugMapper.java b/src/main/java/com/moabam/api/application/bug/BugMapper.java index 9a3bb497..5099351f 100644 --- a/src/main/java/com/moabam/api/application/bug/BugMapper.java +++ b/src/main/java/com/moabam/api/application/bug/BugMapper.java @@ -40,8 +40,8 @@ public static BugHistoryItemResponse toBugHistoryItemResponse(BugHistoryDto dto) BugHistoryItemResponse.PaymentResponse payment = BugHistoryItemResponse.PaymentResponse.builder() .id(dto.payment().getId()) .orderName(dto.payment().getOrder().getName()) - .discountAmount(dto.payment().getCoupon().getPoint()) - .totalAmount(dto.payment().getAmount()) + .discountAmount(dto.payment().getDiscountAmount()) + .totalAmount(dto.payment().getTotalAmount()) .build(); return BugHistoryItemResponse.builder() diff --git a/src/main/java/com/moabam/api/application/bug/BugService.java b/src/main/java/com/moabam/api/application/bug/BugService.java index 3fb80281..abf7aa70 100644 --- a/src/main/java/com/moabam/api/application/bug/BugService.java +++ b/src/main/java/com/moabam/api/application/bug/BugService.java @@ -13,10 +13,10 @@ import com.moabam.api.application.member.MemberService; import com.moabam.api.application.payment.PaymentMapper; import com.moabam.api.application.product.ProductMapper; +import com.moabam.api.domain.bug.Bug; import com.moabam.api.domain.bug.repository.BugHistorySearchRepository; import com.moabam.api.domain.coupon.Coupon; import com.moabam.api.domain.item.repository.BugHistoryDto; -import com.moabam.api.domain.member.Member; import com.moabam.api.domain.payment.Payment; import com.moabam.api.domain.payment.repository.PaymentRepository; import com.moabam.api.domain.product.Product; @@ -42,9 +42,9 @@ public class BugService { private final PaymentRepository paymentRepository; public BugResponse getBug(Long memberId) { - Member member = memberService.getById(memberId); + Bug bug = memberService.getById(memberId).getBug(); - return BugMapper.toBugResponse(member.getBug()); + return BugMapper.toBugResponse(bug); } public BugHistoryResponse getBugHistory(Long memberId) { @@ -73,6 +73,12 @@ public PurchaseProductResponse purchaseBugProduct(Long memberId, Long productId, return ProductMapper.toPurchaseProductResponse(payment); } + @Transactional + public void charge(Long memberId, Product bugProduct) { + Bug bug = memberService.getById(memberId).getBug(); + bug.charge(bugProduct.getQuantity()); + } + private Product getById(Long productId) { return productRepository.findById(productId) .orElseThrow(() -> new NotFoundException(PRODUCT_NOT_FOUND)); diff --git a/src/main/java/com/moabam/api/application/coupon/CouponManageService.java b/src/main/java/com/moabam/api/application/coupon/CouponManageService.java index 3fd13b1b..f4d1ef92 100644 --- a/src/main/java/com/moabam/api/application/coupon/CouponManageService.java +++ b/src/main/java/com/moabam/api/application/coupon/CouponManageService.java @@ -35,7 +35,7 @@ public class CouponManageService { @Scheduled(fixedDelay = 1000) public void issue() { - LocalDate now = LocalDate.from(clockHolder.times()); + LocalDate now = clockHolder.date(); Optional isCoupon = couponRepository.findByStartAt(now); if (!canIssue(isCoupon)) { @@ -69,7 +69,7 @@ public void deleteCouponManage(String couponName) { } private void validateRegister(String couponName) { - LocalDate now = LocalDate.from(clockHolder.times()); + LocalDate now = clockHolder.date(); Optional coupon = couponRepository.findByStartAt(now); if (coupon.isEmpty() || !coupon.get().getName().equals(couponName)) { diff --git a/src/main/java/com/moabam/api/application/coupon/CouponMapper.java b/src/main/java/com/moabam/api/application/coupon/CouponMapper.java index d38dbb77..ef8eb15a 100644 --- a/src/main/java/com/moabam/api/application/coupon/CouponMapper.java +++ b/src/main/java/com/moabam/api/application/coupon/CouponMapper.java @@ -1,9 +1,14 @@ package com.moabam.api.application.coupon; +import java.util.List; + import com.moabam.api.domain.coupon.Coupon; import com.moabam.api.domain.coupon.CouponType; +import com.moabam.api.domain.coupon.CouponWallet; import com.moabam.api.dto.coupon.CouponResponse; import com.moabam.api.dto.coupon.CreateCouponRequest; +import com.moabam.api.dto.coupon.MyCouponResponse; +import com.moabam.global.common.util.StreamUtils; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -25,7 +30,7 @@ public static Coupon toEntity(Long adminId, CreateCouponRequest coupon) { } // TODO : Admin Table 생성 시, 관리자 명 추가할 예정 - public static CouponResponse toDto(Coupon coupon) { + public static CouponResponse toResponse(Coupon coupon) { return CouponResponse.builder() .id(coupon.getId()) .adminName(coupon.getAdminId() + "admin") @@ -38,4 +43,24 @@ public static CouponResponse toDto(Coupon coupon) { .openAt(coupon.getOpenAt()) .build(); } + + public static List toResponses(List coupons) { + return StreamUtils.map(coupons, CouponMapper::toResponse); + } + + public static MyCouponResponse toMyResponse(CouponWallet couponWallet) { + Coupon coupon = couponWallet.getCoupon(); + + return MyCouponResponse.builder() + .id(coupon.getId()) + .name(coupon.getName()) + .description(coupon.getDescription()) + .point(coupon.getPoint()) + .type(coupon.getType()) + .build(); + } + + public static List toMyResponses(List couponWallets) { + return StreamUtils.map(couponWallets, CouponMapper::toMyResponse); + } } diff --git a/src/main/java/com/moabam/api/application/coupon/CouponService.java b/src/main/java/com/moabam/api/application/coupon/CouponService.java index b8310e7d..fb153e08 100644 --- a/src/main/java/com/moabam/api/application/coupon/CouponService.java +++ b/src/main/java/com/moabam/api/application/coupon/CouponService.java @@ -7,13 +7,16 @@ import org.springframework.transaction.annotation.Transactional; import com.moabam.api.domain.coupon.Coupon; +import com.moabam.api.domain.coupon.CouponWallet; import com.moabam.api.domain.coupon.repository.CouponRepository; import com.moabam.api.domain.coupon.repository.CouponSearchRepository; +import com.moabam.api.domain.coupon.repository.CouponWalletRepository; import com.moabam.api.domain.coupon.repository.CouponWalletSearchRepository; import com.moabam.api.domain.member.Role; import com.moabam.api.dto.coupon.CouponResponse; import com.moabam.api.dto.coupon.CouponStatusRequest; import com.moabam.api.dto.coupon.CreateCouponRequest; +import com.moabam.api.dto.coupon.MyCouponResponse; import com.moabam.global.auth.model.AuthMember; import com.moabam.global.common.util.ClockHolder; import com.moabam.global.error.exception.BadRequestException; @@ -30,9 +33,9 @@ public class CouponService { private final ClockHolder clockHolder; private final CouponManageService couponManageService; - private final CouponRepository couponRepository; private final CouponSearchRepository couponSearchRepository; + private final CouponWalletRepository couponWalletRepository; private final CouponWalletSearchRepository couponWalletSearchRepository; @Transactional @@ -55,30 +58,47 @@ public void delete(AuthMember admin, Long couponId) { couponManageService.deleteCouponManage(coupon.getName()); } + @Transactional + public void use(Long memberId, Long couponWalletId) { + Coupon coupon = getByWallet(memberId, couponWalletId); + couponRepository.delete(coupon); + } + public CouponResponse getById(Long couponId) { Coupon coupon = couponRepository.findById(couponId) .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON)); - return CouponMapper.toDto(coupon); + return CouponMapper.toResponse(coupon); } - public Coupon getByWalletIdAndMemberId(Long couponWalletId, Long memberId) { - return couponWalletSearchRepository.findByIdAndMemberId(couponWalletId, memberId) + public Coupon getByWallet(Long memberId, Long couponWalletId) { + return couponWalletRepository.findByIdAndMemberId(couponWalletId, memberId) .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON_WALLET)) .getCoupon(); } public List getAllByStatus(CouponStatusRequest request) { - LocalDate now = LocalDate.from(clockHolder.times()); + LocalDate now = clockHolder.date(); List coupons = couponSearchRepository.findAllByStatus(now, request); - return coupons.stream() - .map(CouponMapper::toDto) - .toList(); + return CouponMapper.toResponses(coupons); + } + + public List getWallet(Long couponId, AuthMember authMember) { + List couponWallets = + couponWalletSearchRepository.findAllByCouponIdAndMemberId(couponId, authMember.id()); + + return CouponMapper.toMyResponses(couponWallets); + } + + public Coupon getByWalletIdAndMemberId(Long couponWalletId, Long memberId) { + return couponWalletSearchRepository.findByIdAndMemberId(couponWalletId, memberId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON_WALLET)) + .getCoupon(); } private void validatePeriod(LocalDate startAt, LocalDate openAt) { - LocalDate now = LocalDate.from(clockHolder.times()); + LocalDate now = clockHolder.date(); if (!now.isBefore(startAt)) { throw new BadRequestException(ErrorMessage.INVALID_COUPON_START_AT_PERIOD); diff --git a/src/main/java/com/moabam/api/application/notification/NotificationService.java b/src/main/java/com/moabam/api/application/notification/NotificationService.java index 11f408e4..bcbec2af 100644 --- a/src/main/java/com/moabam/api/application/notification/NotificationService.java +++ b/src/main/java/com/moabam/api/application/notification/NotificationService.java @@ -30,24 +30,22 @@ public class NotificationService { private static final String KNOCK_BODY = "%s님이 콕 찔렀습니다."; private static final String CERTIFY_TIME_BODY = "%s방 인증 시간입니다."; - private static final String KNOCK_KEY = "room_%s_member_%s_knocks_%s"; + private final ClockHolder clockHolder; private final FcmService fcmService; private final RoomService roomService; + private final NotificationRepository notificationRepository; private final ParticipantSearchRepository participantSearchRepository; - private final ClockHolder clockHolder; @Transactional public void sendKnock(AuthMember member, Long targetId, Long roomId) { roomService.validateRoomById(roomId); - - String knockKey = generateKnockKey(member.id(), targetId, roomId); - validateConflictKnock(knockKey); + validateConflictKnock(member.id(), targetId, roomId); String fcmToken = fcmService.findTokenByMemberId(targetId); fcmService.sendAsync(fcmToken, String.format(KNOCK_BODY, member.nickname())); - notificationRepository.saveKnock(knockKey); + notificationRepository.saveKnock(member.id(), targetId, roomId); } @Scheduled(cron = "0 50 * * * *") @@ -68,8 +66,8 @@ public List getMyKnockStatusInRoom(Long memberId, Long roomId, List !participant.getMemberId().equals(memberId)) .toList(); - Predicate knockPredicate = targetId - -> notificationRepository.existsKnockByKey(generateKnockKey(memberId, targetId, roomId)); + Predicate knockPredicate = targetId -> + notificationRepository.existsKnockByKey(memberId, targetId, roomId); Map> knockStatus = filteredParticipants.stream() .map(Participant::getMemberId) @@ -78,13 +76,9 @@ public List getMyKnockStatusInRoom(Long memberId, Long roomId, List new NotFoundException(PAYMENT_NOT_FOUND)); } + + private Payment getByOrderId(String orderId) { + return paymentSearchRepository.findByOrderId(orderId) + .orElseThrow(() -> new NotFoundException(PAYMENT_NOT_FOUND)); + } } diff --git a/src/main/java/com/moabam/api/application/product/ProductMapper.java b/src/main/java/com/moabam/api/application/product/ProductMapper.java index 32369eb2..b847d9c1 100644 --- a/src/main/java/com/moabam/api/application/product/ProductMapper.java +++ b/src/main/java/com/moabam/api/application/product/ProductMapper.java @@ -35,7 +35,7 @@ public static PurchaseProductResponse toPurchaseProductResponse(Payment payment) return PurchaseProductResponse.builder() .paymentId(payment.getId()) .orderName(payment.getOrder().getName()) - .price(payment.getAmount()) + .price(payment.getTotalAmount()) .build(); } } diff --git a/src/main/java/com/moabam/api/domain/bug/Bug.java b/src/main/java/com/moabam/api/domain/bug/Bug.java index 246844cf..e9c61edd 100644 --- a/src/main/java/com/moabam/api/domain/bug/Bug.java +++ b/src/main/java/com/moabam/api/domain/bug/Bug.java @@ -74,10 +74,17 @@ private void decreaseBug(BugType bugType, int bug) { } public void increaseBug(BugType bugType, int bug) { - switch (bugType) { - case MORNING -> this.morningBug += bug; - case NIGHT -> this.nightBug += bug; - case GOLDEN -> this.goldenBug += bug; + if (bugType.equals(BugType.MORNING)) { + this.morningBug += bug; + return; + } + + if (bugType.equals(BugType.NIGHT)) { + this.nightBug += bug; } } + + public void charge(int quantity) { + this.goldenBug += quantity; + } } diff --git a/src/main/java/com/moabam/api/domain/coupon/repository/CouponManageRepository.java b/src/main/java/com/moabam/api/domain/coupon/repository/CouponManageRepository.java index b2aa3814..22f7df8f 100644 --- a/src/main/java/com/moabam/api/domain/coupon/repository/CouponManageRepository.java +++ b/src/main/java/com/moabam/api/domain/coupon/repository/CouponManageRepository.java @@ -7,7 +7,7 @@ import org.springframework.stereotype.Repository; -import com.moabam.api.infrastructure.redis.StringRedisRepository; +import com.moabam.api.infrastructure.redis.ValueRedisRepository; import com.moabam.api.infrastructure.redis.ZSetRedisRepository; import lombok.RequiredArgsConstructor; @@ -19,7 +19,7 @@ public class CouponManageRepository { private static final String STOCK_KEY = "%s_INCR"; private final ZSetRedisRepository zSetRedisRepository; - private final StringRedisRepository stringRedisRepository; + private final ValueRedisRepository valueRedisRepository; public void addIfAbsentQueue(String couponName, Long memberId, double registerTime) { zSetRedisRepository.addIfAbsent(requireNonNull(couponName), requireNonNull(memberId), registerTime); @@ -34,20 +34,20 @@ public Set popMinQueue(String couponName, long count) { } public void deleteQueue(String couponName) { - stringRedisRepository.delete(requireNonNull(couponName)); + valueRedisRepository.delete(requireNonNull(couponName)); } public int increaseIssuedStock(String couponName) { String stockKey = String.format(STOCK_KEY, requireNonNull(couponName)); - return stringRedisRepository + return valueRedisRepository .increment(requireNonNull(stockKey)) .intValue(); } public int getIssuedStock(String couponName) { String stockKey = String.format(STOCK_KEY, requireNonNull(couponName)); - String stockValue = stringRedisRepository.get(requireNonNull(stockKey)); + String stockValue = valueRedisRepository.get(requireNonNull(stockKey)); if (stockValue == null) { return 0; @@ -58,6 +58,6 @@ public int getIssuedStock(String couponName) { public void deleteIssuedStock(String couponName) { String stockKey = String.format(STOCK_KEY, requireNonNull(couponName)); - stringRedisRepository.delete(requireNonNull(stockKey)); + valueRedisRepository.delete(requireNonNull(stockKey)); } } diff --git a/src/main/java/com/moabam/api/domain/coupon/repository/CouponWalletRepository.java b/src/main/java/com/moabam/api/domain/coupon/repository/CouponWalletRepository.java index 626512dc..48da8ca1 100644 --- a/src/main/java/com/moabam/api/domain/coupon/repository/CouponWalletRepository.java +++ b/src/main/java/com/moabam/api/domain/coupon/repository/CouponWalletRepository.java @@ -1,9 +1,12 @@ package com.moabam.api.domain.coupon.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import com.moabam.api.domain.coupon.CouponWallet; public interface CouponWalletRepository extends JpaRepository { + Optional findByIdAndMemberId(Long id, Long memberId); } diff --git a/src/main/java/com/moabam/api/domain/coupon/repository/CouponWalletSearchRepository.java b/src/main/java/com/moabam/api/domain/coupon/repository/CouponWalletSearchRepository.java index abd93f5c..afcdbf16 100644 --- a/src/main/java/com/moabam/api/domain/coupon/repository/CouponWalletSearchRepository.java +++ b/src/main/java/com/moabam/api/domain/coupon/repository/CouponWalletSearchRepository.java @@ -3,11 +3,13 @@ import static com.moabam.api.domain.coupon.QCoupon.*; import static com.moabam.api.domain.coupon.QCouponWallet.*; +import java.util.List; import java.util.Optional; import org.springframework.stereotype.Repository; import com.moabam.api.domain.coupon.CouponWallet; +import com.moabam.global.common.util.DynamicQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; @@ -18,6 +20,17 @@ public class CouponWalletSearchRepository { private final JPAQueryFactory jpaQueryFactory; + public List findAllByCouponIdAndMemberId(Long couponId, Long memberId) { + return jpaQueryFactory + .selectFrom(couponWallet) + .join(couponWallet.coupon, coupon).fetchJoin() + .where( + DynamicQuery.generateEq(couponId, couponWallet.coupon.id::eq), + DynamicQuery.generateEq(memberId, couponWallet.memberId::eq) + ) + .fetch(); + } + public Optional findByIdAndMemberId(Long id, Long memberId) { return Optional.ofNullable(jpaQueryFactory .selectFrom(couponWallet) diff --git a/src/main/java/com/moabam/api/domain/item/repository/InventorySearchRepository.java b/src/main/java/com/moabam/api/domain/item/repository/InventorySearchRepository.java index 431cf898..0141d2e4 100644 --- a/src/main/java/com/moabam/api/domain/item/repository/InventorySearchRepository.java +++ b/src/main/java/com/moabam/api/domain/item/repository/InventorySearchRepository.java @@ -23,23 +23,23 @@ public class InventorySearchRepository { private final JPAQueryFactory jpaQueryFactory; public Optional findOne(Long memberId, Long itemId) { - return Optional.ofNullable( - jpaQueryFactory.selectFrom(inventory) - .where( - DynamicQuery.generateEq(memberId, inventory.memberId::eq), - DynamicQuery.generateEq(itemId, inventory.item.id::eq)) - .fetchOne() + return Optional.ofNullable(jpaQueryFactory + .selectFrom(inventory) + .where( + DynamicQuery.generateEq(memberId, inventory.memberId::eq), + DynamicQuery.generateEq(itemId, inventory.item.id::eq)) + .fetchOne() ); } public Optional findDefault(Long memberId, ItemType type) { - return Optional.ofNullable( - jpaQueryFactory.selectFrom(inventory) - .where( - DynamicQuery.generateEq(memberId, inventory.memberId::eq), - DynamicQuery.generateEq(type, inventory.item.type::eq), - inventory.isDefault.isTrue()) - .fetchOne() + return Optional.ofNullable(jpaQueryFactory + .selectFrom(inventory) + .where( + DynamicQuery.generateEq(memberId, inventory.memberId::eq), + DynamicQuery.generateEq(type, inventory.item.type::eq), + inventory.isDefault.isTrue()) + .fetchOne() ); } diff --git a/src/main/java/com/moabam/api/domain/notification/repository/NotificationRepository.java b/src/main/java/com/moabam/api/domain/notification/repository/NotificationRepository.java index aaffa123..30cdd5d3 100644 --- a/src/main/java/com/moabam/api/domain/notification/repository/NotificationRepository.java +++ b/src/main/java/com/moabam/api/domain/notification/repository/NotificationRepository.java @@ -7,7 +7,7 @@ import org.springframework.stereotype.Repository; -import com.moabam.api.infrastructure.redis.StringRedisRepository; +import com.moabam.api.infrastructure.redis.ValueRedisRepository; import lombok.RequiredArgsConstructor; @@ -15,19 +15,22 @@ @RequiredArgsConstructor public class NotificationRepository { + private static final String KNOCK_KEY = "room_%s_member_%s_knocks_%s"; private static final long EXPIRE_KNOCK = 12; - private final StringRedisRepository stringRedisRepository; + private final ValueRedisRepository valueRedisRepository; - public void saveKnock(String key) { - stringRedisRepository.save( - requireNonNull(key), - BLANK, - Duration.ofHours(EXPIRE_KNOCK) - ); + public void saveKnock(Long memberId, Long targetId, Long roomId) { + String knockKey = + String.format(KNOCK_KEY, requireNonNull(roomId), requireNonNull(memberId), requireNonNull(targetId)); + + valueRedisRepository.save(knockKey, BLANK, Duration.ofHours(EXPIRE_KNOCK)); } - public boolean existsKnockByKey(String key) { - return stringRedisRepository.hasKey(requireNonNull(key)); + public boolean existsKnockByKey(Long memberId, Long targetId, Long roomId) { + String knockKey = + String.format(KNOCK_KEY, requireNonNull(roomId), requireNonNull(memberId), requireNonNull(targetId)); + + return valueRedisRepository.hasKey(requireNonNull(knockKey)); } } diff --git a/src/main/java/com/moabam/api/domain/payment/Payment.java b/src/main/java/com/moabam/api/domain/payment/Payment.java index cf55b607..9c1f4dea 100644 --- a/src/main/java/com/moabam/api/domain/payment/Payment.java +++ b/src/main/java/com/moabam/api/domain/payment/Payment.java @@ -1,7 +1,6 @@ package com.moabam.api.domain.payment; import static com.moabam.global.error.model.ErrorMessage.*; -import static java.lang.Math.*; import static java.util.Objects.*; import java.time.LocalDateTime; @@ -53,18 +52,17 @@ public class Payment { @JoinColumn(name = "product_id", updatable = false, nullable = false) private Product product; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "coupon_id") - private Coupon coupon; - @Column(name = "coupon_wallet_id") private Long couponWalletId; @Embedded private Order order; - @Column(name = "amount", nullable = false) - private int amount; + @Column(name = "total_amount", nullable = false) + private int totalAmount; + + @Column(name = "discount_amount", nullable = false) + private int discountAmount; @Column(name = "payment_key") private String paymentKey; @@ -84,11 +82,14 @@ public class Payment { private LocalDateTime approvedAt; @Builder - public Payment(Long memberId, Product product, Order order, int amount, PaymentStatus status) { + public Payment(Long memberId, Product product, Long couponWalletId, Order order, int totalAmount, + int discountAmount, PaymentStatus status) { this.memberId = requireNonNull(memberId); this.product = requireNonNull(product); + this.couponWalletId = couponWalletId; this.order = requireNonNull(order); - this.amount = validateAmount(amount); + this.totalAmount = validateAmount(totalAmount); + this.discountAmount = validateAmount(discountAmount); this.status = requireNonNullElse(status, PaymentStatus.READY); } @@ -100,20 +101,46 @@ private int validateAmount(int amount) { return amount; } + public void validateInfo(Long memberId, int amount) { + validateByMember(memberId); + validateByTotalAmount(amount); + } + public void validateByMember(Long memberId) { if (!this.memberId.equals(memberId)) { throw new BadRequestException(INVALID_MEMBER_PAYMENT); } } + private void validateByTotalAmount(int amount) { + if (this.totalAmount != amount) { + throw new BadRequestException(INVALID_PAYMENT_INFO); + } + } + + public boolean isCouponApplied() { + return !isNull(this.couponWalletId); + } + public void applyCoupon(Coupon coupon, Long couponWalletId) { - this.coupon = coupon; this.couponWalletId = couponWalletId; - this.amount = max(MIN_AMOUNT, this.amount - coupon.getPoint()); + this.discountAmount = coupon.getPoint(); + this.totalAmount = Math.max(MIN_AMOUNT, this.totalAmount - coupon.getPoint()); } public void request(String orderId) { this.order.updateId(orderId); this.requestedAt = LocalDateTime.now(); } + + public void confirm(String paymentKey, LocalDateTime approvedAt) { + this.paymentKey = paymentKey; + this.approvedAt = approvedAt; + this.status = PaymentStatus.DONE; + } + + public void fail(String paymentKey) { + this.paymentKey = paymentKey; + this.status = PaymentStatus.ABORTED; + } } diff --git a/src/main/java/com/moabam/api/domain/payment/repository/PaymentSearchRepository.java b/src/main/java/com/moabam/api/domain/payment/repository/PaymentSearchRepository.java new file mode 100644 index 00000000..dafb6925 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/payment/repository/PaymentSearchRepository.java @@ -0,0 +1,27 @@ +package com.moabam.api.domain.payment.repository; + +import static com.moabam.api.domain.payment.QPayment.*; + +import java.util.Optional; + +import org.springframework.stereotype.Repository; + +import com.moabam.api.domain.payment.Payment; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class PaymentSearchRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public Optional findByOrderId(String orderId) { + return Optional.ofNullable(jpaQueryFactory + .selectFrom(payment) + .where(payment.order.id.eq(orderId)) + .fetchOne() + ); + } +} diff --git a/src/main/java/com/moabam/api/dto/coupon/MyCouponResponse.java b/src/main/java/com/moabam/api/dto/coupon/MyCouponResponse.java new file mode 100644 index 00000000..a0201ebb --- /dev/null +++ b/src/main/java/com/moabam/api/dto/coupon/MyCouponResponse.java @@ -0,0 +1,16 @@ +package com.moabam.api.dto.coupon; + +import com.moabam.api.domain.coupon.CouponType; + +import lombok.Builder; + +@Builder +public record MyCouponResponse( + Long id, + String name, + String description, + int point, + CouponType type +) { + +} diff --git a/src/main/java/com/moabam/api/dto/payment/ConfirmPaymentRequest.java b/src/main/java/com/moabam/api/dto/payment/ConfirmPaymentRequest.java new file mode 100644 index 00000000..ee5a4d03 --- /dev/null +++ b/src/main/java/com/moabam/api/dto/payment/ConfirmPaymentRequest.java @@ -0,0 +1,15 @@ +package com.moabam.api.dto.payment; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; + +@Builder +public record ConfirmPaymentRequest( + @NotBlank String paymentKey, + @NotBlank String orderId, + @NotNull @Min(0) int amount +) { + +} diff --git a/src/main/java/com/moabam/api/dto/payment/ConfirmTossPaymentRequest.java b/src/main/java/com/moabam/api/dto/payment/ConfirmTossPaymentRequest.java new file mode 100644 index 00000000..3527f148 --- /dev/null +++ b/src/main/java/com/moabam/api/dto/payment/ConfirmTossPaymentRequest.java @@ -0,0 +1,12 @@ +package com.moabam.api.dto.payment; + +import lombok.Builder; + +@Builder +public record ConfirmTossPaymentRequest( + String paymentKey, + String orderId, + int amount +) { + +} diff --git a/src/main/java/com/moabam/api/dto/payment/ConfirmTossPaymentResponse.java b/src/main/java/com/moabam/api/dto/payment/ConfirmTossPaymentResponse.java new file mode 100644 index 00000000..ee32917a --- /dev/null +++ b/src/main/java/com/moabam/api/dto/payment/ConfirmTossPaymentResponse.java @@ -0,0 +1,21 @@ +package com.moabam.api.dto.payment; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.moabam.api.domain.payment.PaymentStatus; + +import lombok.Builder; + +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConfirmTossPaymentResponse( + String paymentKey, + String orderId, + String orderName, + PaymentStatus status, + int totalAmount, + LocalDateTime approvedAt +) { + +} diff --git a/src/main/java/com/moabam/api/dto/product/PurchaseProductRequest.java b/src/main/java/com/moabam/api/dto/product/PurchaseProductRequest.java index a14a00c7..6e7eda86 100644 --- a/src/main/java/com/moabam/api/dto/product/PurchaseProductRequest.java +++ b/src/main/java/com/moabam/api/dto/product/PurchaseProductRequest.java @@ -1,6 +1,6 @@ package com.moabam.api.dto.product; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; public record PurchaseProductRequest( @Nullable Long couponWalletId diff --git a/src/main/java/com/moabam/api/infrastructure/fcm/FcmRepository.java b/src/main/java/com/moabam/api/infrastructure/fcm/FcmRepository.java index eaa5c55b..39b97666 100644 --- a/src/main/java/com/moabam/api/infrastructure/fcm/FcmRepository.java +++ b/src/main/java/com/moabam/api/infrastructure/fcm/FcmRepository.java @@ -6,7 +6,7 @@ import org.springframework.stereotype.Repository; -import com.moabam.api.infrastructure.redis.StringRedisRepository; +import com.moabam.api.infrastructure.redis.ValueRedisRepository; import lombok.RequiredArgsConstructor; @@ -16,10 +16,10 @@ public class FcmRepository { private static final long EXPIRE_FCM_TOKEN = 60; - private final StringRedisRepository stringRedisRepository; + private final ValueRedisRepository valueRedisRepository; public void saveToken(Long memberId, String fcmToken) { - stringRedisRepository.save( + valueRedisRepository.save( String.valueOf(requireNonNull(memberId)), requireNonNull(fcmToken), Duration.ofDays(EXPIRE_FCM_TOKEN) @@ -27,14 +27,14 @@ public void saveToken(Long memberId, String fcmToken) { } public void deleteTokenByMemberId(Long memberId) { - stringRedisRepository.delete(String.valueOf(requireNonNull(memberId))); + valueRedisRepository.delete(String.valueOf(requireNonNull(memberId))); } public String findTokenByMemberId(Long memberId) { - return stringRedisRepository.get(String.valueOf(requireNonNull(memberId))); + return valueRedisRepository.get(String.valueOf(requireNonNull(memberId))); } public boolean existsTokenByMemberId(Long memberId) { - return stringRedisRepository.hasKey(String.valueOf(requireNonNull(memberId))); + return valueRedisRepository.hasKey(String.valueOf(requireNonNull(memberId))); } } diff --git a/src/main/java/com/moabam/api/infrastructure/payment/TossPaymentMapper.java b/src/main/java/com/moabam/api/infrastructure/payment/TossPaymentMapper.java new file mode 100644 index 00000000..146036eb --- /dev/null +++ b/src/main/java/com/moabam/api/infrastructure/payment/TossPaymentMapper.java @@ -0,0 +1,18 @@ +package com.moabam.api.infrastructure.payment; + +import com.moabam.api.dto.payment.ConfirmTossPaymentRequest; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class TossPaymentMapper { + + public static ConfirmTossPaymentRequest toConfirmRequest(String paymentKey, String orderId, int amount) { + return ConfirmTossPaymentRequest.builder() + .paymentKey(paymentKey) + .orderId(orderId) + .amount(amount) + .build(); + } +} diff --git a/src/main/java/com/moabam/api/infrastructure/payment/TossPaymentService.java b/src/main/java/com/moabam/api/infrastructure/payment/TossPaymentService.java new file mode 100644 index 00000000..2389746e --- /dev/null +++ b/src/main/java/com/moabam/api/infrastructure/payment/TossPaymentService.java @@ -0,0 +1,50 @@ +package com.moabam.api.infrastructure.payment; + +import java.util.Base64; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; + +import com.moabam.api.dto.payment.ConfirmTossPaymentRequest; +import com.moabam.api.dto.payment.ConfirmTossPaymentResponse; +import com.moabam.global.config.TossPaymentConfig; +import com.moabam.global.error.exception.MoabamException; +import com.moabam.global.error.model.ErrorResponse; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Mono; + +@Service +@RequiredArgsConstructor +public class TossPaymentService { + + private final TossPaymentConfig config; + private WebClient webClient; + + @PostConstruct + public void init() { + this.webClient = WebClient.builder() + .baseUrl(config.baseUrl()) + .defaultHeaders(headers -> { + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + headers.setBearerAuth(Base64.getEncoder().encodeToString(config.secretKey().getBytes())); + }) + .build(); + } + + public ConfirmTossPaymentResponse confirm(ConfirmTossPaymentRequest request) { + return webClient.post() + .uri("/v1/payments/confirm") + .body(BodyInserters.fromValue(request)) + .retrieve() + .onStatus(HttpStatusCode::isError, response -> response.bodyToMono(ErrorResponse.class) + .flatMap(error -> Mono.error(new MoabamException(error.message())))) + .bodyToMono(ConfirmTossPaymentResponse.class) + .block(); + } +} diff --git a/src/main/java/com/moabam/api/infrastructure/redis/StringRedisRepository.java b/src/main/java/com/moabam/api/infrastructure/redis/ValueRedisRepository.java similarity index 95% rename from src/main/java/com/moabam/api/infrastructure/redis/StringRedisRepository.java rename to src/main/java/com/moabam/api/infrastructure/redis/ValueRedisRepository.java index c5d4d9df..9f456e9a 100644 --- a/src/main/java/com/moabam/api/infrastructure/redis/StringRedisRepository.java +++ b/src/main/java/com/moabam/api/infrastructure/redis/ValueRedisRepository.java @@ -9,7 +9,7 @@ @Repository @RequiredArgsConstructor -public class StringRedisRepository { +public class ValueRedisRepository { private final RedisTemplate redisTemplate; diff --git a/src/main/java/com/moabam/api/presentation/CouponController.java b/src/main/java/com/moabam/api/presentation/CouponController.java index e5b03746..e61b5c6b 100644 --- a/src/main/java/com/moabam/api/presentation/CouponController.java +++ b/src/main/java/com/moabam/api/presentation/CouponController.java @@ -17,6 +17,7 @@ import com.moabam.api.dto.coupon.CouponResponse; import com.moabam.api.dto.coupon.CouponStatusRequest; import com.moabam.api.dto.coupon.CreateCouponRequest; +import com.moabam.api.dto.coupon.MyCouponResponse; import com.moabam.global.auth.annotation.Auth; import com.moabam.global.auth.model.AuthMember; @@ -58,4 +59,10 @@ public List getAllByStatus(@Valid @RequestBody CouponStatusReque public void registerQueue(@Auth AuthMember authMember, @RequestParam("couponName") String couponName) { couponManageService.register(authMember, couponName); } + + @GetMapping({"/my-coupons", "/my-coupons/{couponId}"}) + public List getWallet(@Auth AuthMember authMember, + @PathVariable(value = "couponId", required = false) Long couponId) { + return couponService.getWallet(couponId, authMember); + } } diff --git a/src/main/java/com/moabam/api/presentation/PaymentController.java b/src/main/java/com/moabam/api/presentation/PaymentController.java index 715fac64..e294f6fc 100644 --- a/src/main/java/com/moabam/api/presentation/PaymentController.java +++ b/src/main/java/com/moabam/api/presentation/PaymentController.java @@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.RestController; import com.moabam.api.application.payment.PaymentService; +import com.moabam.api.dto.payment.ConfirmPaymentRequest; import com.moabam.api.dto.payment.PaymentRequest; import com.moabam.global.auth.annotation.Auth; import com.moabam.global.auth.model.AuthMember; @@ -25,9 +26,14 @@ public class PaymentController { @PostMapping("/{paymentId}") @ResponseStatus(HttpStatus.OK) - public void requestPayment(@Auth AuthMember member, - @PathVariable Long paymentId, + public void request(@Auth AuthMember member, @PathVariable Long paymentId, @Valid @RequestBody PaymentRequest request) { paymentService.request(member.id(), paymentId, request); } + + @PostMapping("/confirm") + @ResponseStatus(HttpStatus.OK) + public void confirm(@Auth AuthMember member, @Valid @RequestBody ConfirmPaymentRequest request) { + paymentService.confirm(member.id(), request); + } } diff --git a/src/main/java/com/moabam/global/config/TossPaymentConfig.java b/src/main/java/com/moabam/global/config/TossPaymentConfig.java new file mode 100644 index 00000000..e3f2bcca --- /dev/null +++ b/src/main/java/com/moabam/global/config/TossPaymentConfig.java @@ -0,0 +1,11 @@ +package com.moabam.global.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "payment.toss") +public record TossPaymentConfig( + String baseUrl, + String secretKey +) { + +} diff --git a/src/main/resources/config b/src/main/resources/config index 2a1a59a1..ea25d857 160000 --- a/src/main/resources/config +++ b/src/main/resources/config @@ -1 +1 @@ -Subproject commit 2a1a59a16d8e868185c125a58aec0682f3c53f0d +Subproject commit ea25d85744c2e6fcedbdb66b34c08837d382814d diff --git a/src/main/resources/static/docs/coupon.html b/src/main/resources/static/docs/coupon.html index 5678c54e..4b9cc040 100644 --- a/src/main/resources/static/docs/coupon.html +++ b/src/main/resources/static/docs/coupon.html @@ -461,7 +461,7 @@

요청

POST /admins/coupons HTTP/1.1
 Content-Type: application/json;charset=UTF-8
-Content-Length: 183
+Content-Length: 175
 Host: localhost:8080
 
 {
@@ -479,9 +479,11 @@ 

응답

HTTP/1.1 201 Created
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
+Access-Control-Allow-Origin: +Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH +Access-Control-Allow-Headers: Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers, X-Requested-With,Content-Type, Referer +Access-Control-Allow-Credentials: true +Access-Control-Max-Age: 3600

@@ -496,7 +498,7 @@

쿠폰 삭제

요청

-
DELETE /admins/coupons/1 HTTP/1.1
+
DELETE /admins/coupons/33 HTTP/1.1
 Host: localhost:8080
@@ -504,9 +506,11 @@

응답

HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
+Access-Control-Allow-Origin: +Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH +Access-Control-Allow-Headers: Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers, X-Requested-With,Content-Type, Referer +Access-Control-Allow-Credentials: true +Access-Control-Max-Age: 3600

@@ -522,7 +526,7 @@

특정 쿠폰 조회

요청

-
GET /coupons/26 HTTP/1.1
+
GET /coupons/22 HTTP/1.1
 Host: localhost:8080
@@ -530,14 +534,16 @@

응답

HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
+Access-Control-Allow-Origin:
+Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
+Access-Control-Allow-Headers: Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers, X-Requested-With,Content-Type, Referer
+Access-Control-Allow-Credentials: true
+Access-Control-Max-Age: 3600
 Content-Type: application/json
-Content-Length: 215
+Content-Length: 205
 
 {
-  "id" : 16,
+  "id" : 22,
   "adminName" : "1admin",
   "name" : "couponName",
   "description" : "",
@@ -565,7 +571,7 @@ 

요청

POST /coupons/search HTTP/1.1
 Content-Type: application/json;charset=UTF-8
-Content-Length: 44
+Content-Length: 41
 Host: localhost:8080
 
 {
@@ -578,14 +584,16 @@ 

응답

HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
+Access-Control-Allow-Origin:
+Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
+Access-Control-Allow-Headers: Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers, X-Requested-With,Content-Type, Referer
+Access-Control-Allow-Credentials: true
+Access-Control-Max-Age: 3600
 Content-Type: application/json
-Content-Length: 216
+Content-Length: 206
 
 [ {
-  "id" : 17,
+  "id" : 23,
   "adminName" : "1admin",
   "name" : "coupon1",
   "description" : "",
@@ -623,9 +631,11 @@ 

응답

HTTP/1.1 400 Bad Request
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
+Access-Control-Allow-Origin:
+Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
+Access-Control-Allow-Headers: Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers, X-Requested-With,Content-Type, Referer
+Access-Control-Allow-Credentials: true
+Access-Control-Max-Age: 3600
 Content-Type: application/json
 Content-Length: 64
 
@@ -644,8 +654,62 @@ 

특정 사용자의 쿠
사용자가 자신의 보관함에 있는 쿠폰들을 조회합니다.

+
+

요청

+
+
+
GET /my-coupons HTTP/1.1
+Host: localhost:8080
+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Access-Control-Allow-Origin:
+Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
+Access-Control-Allow-Headers: Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers, X-Requested-With,Content-Type, Referer
+Access-Control-Allow-Credentials: true
+Access-Control-Max-Age: 3600
+Content-Type: application/json
+Content-Length: 507
+
+[ {
+  "id" : 17,
+  "name" : "c1",
+  "description" : "",
+  "point" : 10,
+  "type" : "MORNING_COUPON"
+}, {
+  "id" : 18,
+  "name" : "c2",
+  "description" : "",
+  "point" : 10,
+  "type" : "MORNING_COUPON"
+}, {
+  "id" : 19,
+  "name" : "c3",
+  "description" : "",
+  "point" : 10,
+  "type" : "MORNING_COUPON"
+}, {
+  "id" : 20,
+  "name" : "c4",
+  "description" : "",
+  "point" : 10,
+  "type" : "MORNING_COUPON"
+}, {
+  "id" : 21,
+  "name" : "c5",
+  "description" : "",
+  "point" : 10,
+  "type" : "MORNING_COUPON"
+} ]
+
+

+
@@ -494,18 +490,20 @@

요청

POST /notifications HTTP/1.1
 Content-Type: application/x-www-form-urlencoded
 Host: localhost:8080
-Content-Length: 9
+Content-Length: 18
 
-fcmToken=
+fcmToken=FCM-TOKEN

응답

HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
+Access-Control-Allow-Origin: +Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH +Access-Control-Allow-Headers: Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers, X-Requested-With,Content-Type, Referer +Access-Control-Allow-Credentials: true +Access-Control-Max-Age: 3600
@@ -515,7 +513,7 @@

응답

diff --git a/src/test/java/com/moabam/api/application/coupon/CouponManageServiceTest.java b/src/test/java/com/moabam/api/application/coupon/CouponManageServiceTest.java index 214ca0ab..e347aba6 100644 --- a/src/test/java/com/moabam/api/application/coupon/CouponManageServiceTest.java +++ b/src/test/java/com/moabam/api/application/coupon/CouponManageServiceTest.java @@ -5,7 +5,6 @@ import static org.mockito.BDDMockito.*; import java.time.LocalDate; -import java.time.LocalDateTime; import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -56,7 +55,7 @@ void issue_all_success() { Coupon coupon = CouponFixture.coupon(1000, 100); Set membersId = new HashSet<>(Set.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L)); - given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(clockHolder.date()).willReturn(LocalDate.now()); given(couponRepository.findByStartAt(any(LocalDate.class))).willReturn(Optional.of(coupon)); given(couponManageRepository.getIssuedStock(any(String.class))).willReturn(10); given(couponManageRepository.popMinQueue(any(String.class), any(long.class))).willReturn(membersId); @@ -73,7 +72,7 @@ void issue_all_success() { @Test void issue_notStartAt() { // Given - given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(clockHolder.date()).willReturn(LocalDate.now()); given(couponRepository.findByStartAt(any(LocalDate.class))).willReturn(Optional.empty()); // When @@ -92,7 +91,7 @@ void issue_stockEnd() { // Given Coupon coupon = CouponFixture.coupon(1000, 100); - given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(clockHolder.date()).willReturn(LocalDate.now()); given(couponRepository.findByStartAt(any(LocalDate.class))).willReturn(Optional.of(coupon)); given(couponManageRepository.getIssuedStock(any(String.class))).willReturn(coupon.getStock()); @@ -112,7 +111,7 @@ void issue_queue_stockENd() { Coupon coupon = CouponFixture.coupon(1000, 100); Set membersId = new HashSet<>(Set.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L)); - given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(clockHolder.date()).willReturn(LocalDate.now()); given(couponRepository.findByStartAt(any(LocalDate.class))).willReturn(Optional.of(coupon)); given(couponManageRepository.getIssuedStock(any(String.class))).willReturn(10); given(couponManageRepository.popMinQueue(any(String.class), any(long.class))).willReturn(membersId); @@ -133,7 +132,7 @@ void register_success() { AuthMember member = AuthorizationThreadLocal.getAuthMember(); Coupon coupon = CouponFixture.coupon(); - given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(clockHolder.date()).willReturn(LocalDate.now()); given(couponRepository.findByStartAt(any(LocalDate.class))).willReturn(Optional.of(coupon)); // When @@ -150,7 +149,7 @@ void register_StartAt_BadRequestException() { // Given AuthMember member = AuthorizationThreadLocal.getAuthMember(); - given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(clockHolder.date()).willReturn(LocalDate.now()); given(couponRepository.findByStartAt(any(LocalDate.class))).willReturn(Optional.empty()); // When & Then @@ -167,7 +166,7 @@ void register_Name_BadRequestException() { AuthMember member = AuthorizationThreadLocal.getAuthMember(); Coupon coupon = CouponFixture.coupon(); - given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(clockHolder.date()).willReturn(LocalDate.now()); given(couponRepository.findByStartAt(any(LocalDate.class))).willReturn(Optional.of(coupon)); // When & Then diff --git a/src/test/java/com/moabam/api/application/coupon/CouponServiceTest.java b/src/test/java/com/moabam/api/application/coupon/CouponServiceTest.java index d929f46b..a8e19049 100644 --- a/src/test/java/com/moabam/api/application/coupon/CouponServiceTest.java +++ b/src/test/java/com/moabam/api/application/coupon/CouponServiceTest.java @@ -1,10 +1,10 @@ package com.moabam.api.application.coupon; +import static com.moabam.support.fixture.CouponFixture.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.*; import java.time.LocalDate; -import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -19,12 +19,15 @@ import com.moabam.api.domain.coupon.Coupon; import com.moabam.api.domain.coupon.CouponType; +import com.moabam.api.domain.coupon.CouponWallet; import com.moabam.api.domain.coupon.repository.CouponRepository; import com.moabam.api.domain.coupon.repository.CouponSearchRepository; +import com.moabam.api.domain.coupon.repository.CouponWalletSearchRepository; import com.moabam.api.domain.member.Role; import com.moabam.api.dto.coupon.CouponResponse; import com.moabam.api.dto.coupon.CouponStatusRequest; import com.moabam.api.dto.coupon.CreateCouponRequest; +import com.moabam.api.dto.coupon.MyCouponResponse; import com.moabam.global.auth.model.AuthMember; import com.moabam.global.auth.model.AuthorizationThreadLocal; import com.moabam.global.common.util.ClockHolder; @@ -51,6 +54,9 @@ class CouponServiceTest { @Mock CouponSearchRepository couponSearchRepository; + @Mock + CouponWalletSearchRepository couponWalletSearchRepository; + @Mock ClockHolder clockHolder; @@ -63,7 +69,7 @@ void create_success() { CreateCouponRequest request = CouponFixture.createCouponRequest(); given(couponRepository.existsByName(any(String.class))).willReturn(false); - given(clockHolder.times()).willReturn(LocalDateTime.of(2022, 1, 1, 1, 1)); + given(clockHolder.date()).willReturn(LocalDate.of(2022, 1, 1)); // When couponService.create(admin, request); @@ -95,7 +101,7 @@ void create_Type_NotFoundException() { CreateCouponRequest request = CouponFixture.createCouponRequest("UNKNOWN", 2, 1); given(couponRepository.existsByName(any(String.class))).willReturn(false); - given(clockHolder.times()).willReturn(LocalDateTime.of(2022, 1, 1, 1, 1)); + given(clockHolder.date()).willReturn(LocalDate.of(2022, 1, 1)); // When & Then assertThatThrownBy(() -> couponService.create(admin, request)) @@ -144,7 +150,7 @@ void create_StartAt_BadRequestException() { AuthMember admin = AuthorizationThreadLocal.getAuthMember(); CreateCouponRequest request = CouponFixture.createCouponRequest(); - given(clockHolder.times()).willReturn(LocalDateTime.of(2025, 1, 1, 1, 1)); + given(clockHolder.date()).willReturn(LocalDate.of(2025, 1, 1)); given(couponRepository.existsByName(any(String.class))).willReturn(false); given(couponRepository.existsByStartAt(any(LocalDate.class))).willReturn(false); @@ -165,7 +171,7 @@ void create_OpenAt_BadRequestException() { given(couponRepository.existsByName(any(String.class))).willReturn(false); given(couponRepository.existsByStartAt(any(LocalDate.class))).willReturn(false); - given(clockHolder.times()).willReturn(LocalDateTime.of(2022, 1, 1, 1, 1)); + given(clockHolder.date()).willReturn(LocalDate.of(2022, 1, 1)); // When & Then assertThatThrownBy(() -> couponService.create(admin, request)) @@ -180,6 +186,7 @@ void delete_success() { // Given AuthMember admin = AuthorizationThreadLocal.getAuthMember(); Coupon coupon = CouponFixture.coupon(10, 100); + given(couponRepository.findById(any(Long.class))).willReturn(Optional.of(coupon)); // When @@ -209,6 +216,7 @@ void delete_Admin_NotFoundException() { void delete_NotFoundException() { // Given AuthMember admin = AuthorizationThreadLocal.getAuthMember(); + given(couponRepository.findById(any(Long.class))).willReturn(Optional.empty()); // When & Then @@ -222,6 +230,7 @@ void delete_NotFoundException() { void getById_success() { // Given Coupon coupon = CouponFixture.coupon(10, 100); + given(couponRepository.findById(any(Long.class))).willReturn(Optional.of(coupon)); // When @@ -251,7 +260,7 @@ void getAllByStatus_success(List coupons) { // Given CouponStatusRequest request = CouponFixture.couponStatusRequest(false, false); - given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(clockHolder.date()).willReturn(LocalDate.now()); given(couponSearchRepository.findAllByStatus(any(LocalDate.class), any(CouponStatusRequest.class))) .willReturn(coupons); @@ -261,4 +270,39 @@ void getAllByStatus_success(List coupons) { // Then assertThat(actual).hasSize(coupons.size()); } + + @WithMember + @DisplayName("나의 쿠폰함을 성공적으로 조회한다.") + @MethodSource("com.moabam.support.fixture.CouponWalletFixture#provideCouponWalletByCouponId1_total5") + @ParameterizedTest + void getWallet_success(List couponWallets) { + // Given + AuthMember authMember = AuthorizationThreadLocal.getAuthMember(); + + given(couponWalletSearchRepository.findAllByCouponIdAndMemberId(isNull(), any(Long.class))) + .willReturn(couponWallets); + + // When + List actual = couponService.getWallet(null, authMember); + + // Then + assertThat(actual).hasSize(couponWallets.size()); + } + + @WithMember + @DisplayName("지갑에서 특정 쿠폰을 성공적으로 조회한다.") + @Test + void getByWalletIdAndMemberId_success() { + // Given + CouponWallet couponWallet = CouponWallet.create(1L, coupon()); + + given(couponWalletSearchRepository.findByIdAndMemberId(any(Long.class), any(Long.class))) + .willReturn(Optional.of(couponWallet)); + + // When + Coupon actual = couponService.getByWalletIdAndMemberId(1L, 1L); + + // Then + assertThat(actual.getName()).isEqualTo(couponWallet.getCoupon().getName()); + } } diff --git a/src/test/java/com/moabam/api/application/notification/NotificationServiceTest.java b/src/test/java/com/moabam/api/application/notification/NotificationServiceTest.java index 60e7e65f..a85c0fc9 100644 --- a/src/test/java/com/moabam/api/application/notification/NotificationServiceTest.java +++ b/src/test/java/com/moabam/api/application/notification/NotificationServiceTest.java @@ -19,7 +19,6 @@ import com.moabam.api.domain.notification.repository.NotificationRepository; import com.moabam.api.domain.room.Participant; import com.moabam.api.domain.room.repository.ParticipantSearchRepository; -import com.moabam.api.infrastructure.fcm.FcmRepository; import com.moabam.api.infrastructure.fcm.FcmService; import com.moabam.global.auth.model.AuthMember; import com.moabam.global.auth.model.AuthorizationThreadLocal; @@ -34,43 +33,41 @@ class NotificationServiceTest { @InjectMocks - private NotificationService notificationService; + NotificationService notificationService; @Mock - private RoomService roomService; + RoomService roomService; @Mock - private FcmRepository fcmRepository; + FcmService fcmService; @Mock - private FcmService fcmService; + NotificationRepository notificationRepository; @Mock - private NotificationRepository notificationRepository; + ParticipantSearchRepository participantSearchRepository; @Mock - private ParticipantSearchRepository participantSearchRepository; - - @Mock - private ClockHolder clockHolder; + ClockHolder clockHolder; @WithMember - @DisplayName("성공적으로 상대에게 콕 알림을 보낸다. - Void") + @DisplayName("상대에게 콕 알림을 성공적으로 보낸다. - Void") @Test - void sendKnock() { + void sendKnock_success() { // Given AuthMember member = AuthorizationThreadLocal.getAuthMember(); willDoNothing().given(roomService).validateRoomById(any(Long.class)); - given(notificationRepository.existsKnockByKey(any(String.class))).willReturn(false); given(fcmService.findTokenByMemberId(any(Long.class))).willReturn("FCM-TOKEN"); + given(notificationRepository.existsKnockByKey(any(Long.class), any(Long.class), any(Long.class))) + .willReturn(false); // When notificationService.sendKnock(member, 2L, 1L); // Then verify(fcmService).sendAsync(any(String.class), any(String.class)); - verify(notificationRepository).saveKnock(any(String.class)); + verify(notificationRepository).saveKnock(any(Long.class), any(Long.class), any(Long.class)); } @WithMember @@ -79,6 +76,7 @@ void sendKnock() { void sendKnock_Room_NotFoundException() { // Given AuthMember member = AuthorizationThreadLocal.getAuthMember(); + willThrow(NotFoundException.class).given(roomService).validateRoomById(any(Long.class)); // When & Then @@ -94,7 +92,8 @@ void sendKnock_FcmToken_NotFoundException() { AuthMember member = AuthorizationThreadLocal.getAuthMember(); willDoNothing().given(roomService).validateRoomById(any(Long.class)); - given(notificationRepository.existsKnockByKey(any(String.class))).willReturn(false); + given(notificationRepository.existsKnockByKey(any(Long.class), any(Long.class), any(Long.class))) + .willReturn(false); given(fcmService.findTokenByMemberId(any(Long.class))) .willThrow(new NotFoundException(ErrorMessage.NOT_FOUND_FCM_TOKEN)); @@ -112,7 +111,8 @@ void sendKnock_ConflictException() { AuthMember member = AuthorizationThreadLocal.getAuthMember(); willDoNothing().given(roomService).validateRoomById(any(Long.class)); - given(notificationRepository.existsKnockByKey(any(String.class))).willReturn(true); + given(notificationRepository.existsKnockByKey(any(Long.class), any(Long.class), any(Long.class))) + .willReturn(true); // When & Then assertThatThrownBy(() -> notificationService.sendKnock(member, 1L, 1L)) @@ -123,7 +123,7 @@ void sendKnock_ConflictException() { @DisplayName("특정 인증 시간에 해당하는 방 사용자들에게 알림을 성공적으로 보낸다. - Void") @MethodSource("com.moabam.support.fixture.ParticipantFixture#provideParticipants") @ParameterizedTest - void sendCertificationTime(List participants) { + void sendCertificationTime_success(List participants) { // Given given(participantSearchRepository.findAllByRoomCertifyTime(any(Integer.class))).willReturn(participants); given(fcmService.findTokenByMemberId(any(Long.class))).willReturn("FCM-TOKEN"); @@ -161,7 +161,8 @@ void getMyKnockStatusInRoom_knocked(List participants) { // Given AuthMember member = AuthorizationThreadLocal.getAuthMember(); - given(notificationRepository.existsKnockByKey(any(String.class))).willReturn(true); + given(notificationRepository.existsKnockByKey(any(Long.class), any(Long.class), any(Long.class))) + .willReturn(true); // When List actual = notificationService.getMyKnockStatusInRoom(member.id(), 1L, participants); @@ -178,8 +179,8 @@ void getMyKnockStatusInRoom_notKnocked(List participants) { // Given AuthMember member = AuthorizationThreadLocal.getAuthMember(); - // given - given(notificationRepository.existsKnockByKey(any(String.class))).willReturn(false); + given(notificationRepository.existsKnockByKey(any(Long.class), any(Long.class), any(Long.class))) + .willReturn(false); // When List actual = notificationService.getMyKnockStatusInRoom(member.id(), 1L, participants); diff --git a/src/test/java/com/moabam/api/application/payment/PaymentServiceTest.java b/src/test/java/com/moabam/api/application/payment/PaymentServiceTest.java index f5599a33..c56e95bf 100644 --- a/src/test/java/com/moabam/api/application/payment/PaymentServiceTest.java +++ b/src/test/java/com/moabam/api/application/payment/PaymentServiceTest.java @@ -1,6 +1,8 @@ package com.moabam.api.application.payment; +import static com.moabam.support.fixture.CouponFixture.*; import static com.moabam.support.fixture.PaymentFixture.*; +import static com.moabam.support.fixture.ProductFixture.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.*; @@ -14,9 +16,16 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import com.moabam.api.application.bug.BugService; +import com.moabam.api.application.coupon.CouponService; +import com.moabam.api.domain.payment.Payment; +import com.moabam.api.domain.payment.PaymentStatus; import com.moabam.api.domain.payment.repository.PaymentRepository; -import com.moabam.api.domain.product.repository.ProductRepository; +import com.moabam.api.domain.payment.repository.PaymentSearchRepository; +import com.moabam.api.dto.payment.ConfirmPaymentRequest; import com.moabam.api.dto.payment.PaymentRequest; +import com.moabam.api.infrastructure.payment.TossPaymentService; +import com.moabam.global.error.exception.MoabamException; import com.moabam.global.error.exception.NotFoundException; @ExtendWith(MockitoExtension.class) @@ -26,18 +35,27 @@ class PaymentServiceTest { PaymentService paymentService; @Mock - ProductRepository productRepository; + BugService bugService; + + @Mock + CouponService couponService; + + @Mock + TossPaymentService tossPaymentService; @Mock PaymentRepository paymentRepository; + @Mock + PaymentSearchRepository paymentSearchRepository; + @DisplayName("결제를 요청한다.") @Nested - class RequestPayment { + class Request { @DisplayName("해당 결제 정보가 존재하지 않으면 예외가 발생한다.") @Test - void payment_not_found_exception() { + void not_found_exception() { // given Long memberId = 1L; Long paymentId = 1L; @@ -50,4 +68,60 @@ void payment_not_found_exception() { .hasMessage("존재하지 않는 결제 정보입니다."); } } + + @DisplayName("결제를 승인한다.") + @Nested + class Confirm { + + @DisplayName("해당 결제 정보가 존재하지 않으면 예외가 발생한다.") + @Test + void not_found_exception() { + // given + Long memberId = 1L; + ConfirmPaymentRequest request = confirmPaymentRequest(); + given(paymentSearchRepository.findByOrderId(request.orderId())).willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> paymentService.confirm(memberId, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 결제 정보입니다."); + } + + @DisplayName("쿠폰을 적용한 경우 쿠폰을 차감한 후 벌레를 충전한다.") + @Test + void use_coupon_success() { + // given + Long memberId = 1L; + Long couponWalletId = 1L; + Payment payment = paymentWithCoupon(bugProduct(), discount1000Coupon(), couponWalletId); + ConfirmPaymentRequest request = confirmPaymentRequest(); + given(paymentSearchRepository.findByOrderId(request.orderId())).willReturn(Optional.of(payment)); + given(tossPaymentService.confirm(confirmTossPaymentRequest())).willReturn(confirmTossPaymentResponse()); + + // when + paymentService.confirm(memberId, request); + + // then + verify(couponService, times(1)).use(memberId, couponWalletId); + verify(bugService, times(1)).charge(memberId, payment.getProduct()); + } + + @DisplayName("실패한다.") + @Test + void fail() { + // given + Long memberId = 1L; + Long couponWalletId = 1L; + Payment payment = paymentWithCoupon(bugProduct(), discount1000Coupon(), couponWalletId); + ConfirmPaymentRequest request = confirmPaymentRequest(); + given(paymentSearchRepository.findByOrderId(request.orderId())).willReturn(Optional.of(payment)); + given(tossPaymentService.confirm(any())).willThrow(MoabamException.class); + + // when + paymentService.confirm(memberId, request); + + // then + assertThat(payment.getStatus()).isEqualTo(PaymentStatus.ABORTED); + } + } } diff --git a/src/test/java/com/moabam/api/domain/bug/BugTest.java b/src/test/java/com/moabam/api/domain/bug/BugTest.java index 7e1981f0..53c50f30 100644 --- a/src/test/java/com/moabam/api/domain/bug/BugTest.java +++ b/src/test/java/com/moabam/api/domain/bug/BugTest.java @@ -70,11 +70,9 @@ void increase_bug_success() { // when bug.increaseBug(BugType.MORNING, 5); bug.increaseBug(BugType.NIGHT, 5); - bug.increaseBug(BugType.GOLDEN, 5); // then assertThat(bug.getMorningBug()).isEqualTo(MORNING_BUG + 5); assertThat(bug.getNightBug()).isEqualTo(NIGHT_BUG + 5); - assertThat(bug.getGoldenBug()).isEqualTo(GOLDEN_BUG + 5); } } diff --git a/src/test/java/com/moabam/api/domain/coupon/CouponTest.java b/src/test/java/com/moabam/api/domain/coupon/CouponTest.java index 2ab0e438..e1e146e3 100644 --- a/src/test/java/com/moabam/api/domain/coupon/CouponTest.java +++ b/src/test/java/com/moabam/api/domain/coupon/CouponTest.java @@ -13,9 +13,9 @@ class CouponTest { - @DisplayName("쿠폰이 정상적으로 생성된다. - Coupon") + @DisplayName("쿠폰이 성공적으로 생성된다. - Coupon") @Test - void coupon() { + void coupon_success() { // Given LocalDate startAt = LocalDate.of(2023, 2, 1); LocalDate openAt = LocalDate.of(2023, 1, 1); @@ -44,7 +44,7 @@ void coupon() { @DisplayName("쿠폰 보너스 포인트가 1보다 작다. - BadRequestException") @Test - void coupon_validatePoint_Point_BadRequestException() { + void validatePoint_BadRequestException() { // When& Then assertThatThrownBy(() -> CouponFixture.coupon(0, 1)) .isInstanceOf(BadRequestException.class) @@ -53,7 +53,7 @@ void coupon_validatePoint_Point_BadRequestException() { @DisplayName("쿠폰 재고가 1보다 작다. - BadRequestException") @Test - void coupon_validatePoint_Stock_BadRequestException() { + void validateStock_BadRequestException() { // When& Then assertThatThrownBy(() -> CouponFixture.coupon(1, 0)) .isInstanceOf(BadRequestException.class) diff --git a/src/test/java/com/moabam/api/domain/coupon/CouponTypeTest.java b/src/test/java/com/moabam/api/domain/coupon/CouponTypeTest.java index 406bc061..960df900 100644 --- a/src/test/java/com/moabam/api/domain/coupon/CouponTypeTest.java +++ b/src/test/java/com/moabam/api/domain/coupon/CouponTypeTest.java @@ -10,9 +10,9 @@ class CouponTypeTest { - @DisplayName("존재하는 쿠폰을 가져온다. - CouponType") + @DisplayName("존재하는 쿠폰을 성공적으로 가져온다. - CouponType") @Test - void couponType_from() { + void from_success() { // When CouponType actual = CouponType.from(CouponType.GOLDEN_COUPON.getName()); @@ -22,7 +22,7 @@ void couponType_from() { @DisplayName("존재하지 않는 쿠폰을 가져온다. - NotFoundException") @Test - void couponType_from_NotFoundException() { + void from_NotFoundException() { // When & Then assertThatThrownBy(() -> CouponType.from("Not-Coupon")) .isInstanceOf(NotFoundException.class) diff --git a/src/test/java/com/moabam/api/domain/coupon/CouponWalletTest.java b/src/test/java/com/moabam/api/domain/coupon/CouponWalletTest.java index 059a9838..13bcd9e5 100644 --- a/src/test/java/com/moabam/api/domain/coupon/CouponWalletTest.java +++ b/src/test/java/com/moabam/api/domain/coupon/CouponWalletTest.java @@ -9,9 +9,9 @@ class CouponWalletTest { - @DisplayName("쿠폰 지갑 엔티티를 생성한다. - Void") + @DisplayName("쿠폰 지갑 엔티티를 성공적으로 생성한다. - Void") @Test - void couponWallet() { + void couponWallet_success() { // Given Coupon coupon = CouponFixture.coupon("CouponName", 1, 2); diff --git a/src/test/java/com/moabam/api/domain/coupon/repository/CouponManageRepositoryTest.java b/src/test/java/com/moabam/api/domain/coupon/repository/CouponManageRepositoryTest.java index 03c7cf49..2dd66862 100644 --- a/src/test/java/com/moabam/api/domain/coupon/repository/CouponManageRepositoryTest.java +++ b/src/test/java/com/moabam/api/domain/coupon/repository/CouponManageRepositoryTest.java @@ -16,7 +16,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.redis.core.ZSetOperations.TypedTuple; -import com.moabam.api.infrastructure.redis.StringRedisRepository; +import com.moabam.api.infrastructure.redis.ValueRedisRepository; import com.moabam.api.infrastructure.redis.ZSetRedisRepository; @ExtendWith(MockitoExtension.class) @@ -29,7 +29,7 @@ class CouponManageRepositoryTest { ZSetRedisRepository zSetRedisRepository; @Mock - StringRedisRepository stringRedisRepository; + ValueRedisRepository valueRedisRepository; @DisplayName("쿠폰 대기열에 사용자가 성공적으로 등록된다. - Void") @Test @@ -57,7 +57,7 @@ void addIfAbsentQueue_memberId_NullPointerException() { .isInstanceOf(NullPointerException.class); } - @DisplayName("쿠폰 대기열에서 10명을 꺼내고 삭제한다.") + @DisplayName("쿠폰 대기열에서 성공적으로 10명을 꺼내고 삭제한다.") @MethodSource("com.moabam.support.fixture.CouponFixture#provideTypedTuples") @ParameterizedTest void popMinQueue_success(Set> tuples) { @@ -86,7 +86,7 @@ void deleteQueue_success() { couponManageRepository.deleteQueue("couponName"); // Then - verify(stringRedisRepository).delete(any(String.class)); + verify(valueRedisRepository).delete(any(String.class)); } @DisplayName("쿠폰명이 Null인 대기열을 삭제한다. - NullPointerException") @@ -101,7 +101,7 @@ void deleteQueue_NullPointerException() { @Test void increaseIssuedStock_success() { // Given - given(stringRedisRepository.increment(any(String.class))).willReturn(77L); + given(valueRedisRepository.increment(any(String.class))).willReturn(77L); // When int actual = couponManageRepository.increaseIssuedStock("couponName"); @@ -122,7 +122,7 @@ void increaseIssuedStock_NullPointerException() { @Test void getIssuedStock_success() { // Given - given(stringRedisRepository.get(any(String.class))).willReturn("1"); + given(valueRedisRepository.get(any(String.class))).willReturn("1"); // When int actual = couponManageRepository.getIssuedStock("couponName"); @@ -135,7 +135,7 @@ void getIssuedStock_success() { @Test void getIssuedStock_zero() { // Given - given(stringRedisRepository.get(any(String.class))).willReturn(null); + given(valueRedisRepository.get(any(String.class))).willReturn(null); // When int actual = couponManageRepository.getIssuedStock("couponName"); @@ -159,7 +159,7 @@ void deleteIssuedStock_success() { couponManageRepository.deleteIssuedStock("couponName"); // Then - verify(stringRedisRepository).delete(any(String.class)); + verify(valueRedisRepository).delete(any(String.class)); } @DisplayName("쿠폰명이 Null인 할당된 쿠폰 재고를 삭제한다. - NullPointerException") diff --git a/src/test/java/com/moabam/api/domain/coupon/repository/CouponSearchRepositoryTest.java b/src/test/java/com/moabam/api/domain/coupon/repository/CouponSearchRepositoryTest.java index 22deb4da..67fdf033 100644 --- a/src/test/java/com/moabam/api/domain/coupon/repository/CouponSearchRepositoryTest.java +++ b/src/test/java/com/moabam/api/domain/coupon/repository/CouponSearchRepositoryTest.java @@ -22,15 +22,15 @@ class CouponSearchRepositoryTest { @Autowired - private CouponRepository couponRepository; + CouponRepository couponRepository; @Autowired - private CouponSearchRepository couponSearchRepository; + CouponSearchRepository couponSearchRepository; - @DisplayName("발급 가능한 쿠폰을 조회한다. - List") + @DisplayName("발급 가능한 쿠폰을 성공적으로 조회한다. - List") @MethodSource("com.moabam.support.fixture.CouponFixture#provideCoupons") @ParameterizedTest - void findAllByStatus(List coupons) { + void findAllByStatus_success(List coupons) { // Given CouponStatusRequest request = CouponFixture.couponStatusRequest(false, false); LocalDate now = LocalDate.of(2023, 7, 1); diff --git a/src/test/java/com/moabam/api/domain/coupon/repository/CouponWalletSearchRepositoryTest.java b/src/test/java/com/moabam/api/domain/coupon/repository/CouponWalletSearchRepositoryTest.java index db5176c7..1d8d6526 100644 --- a/src/test/java/com/moabam/api/domain/coupon/repository/CouponWalletSearchRepositoryTest.java +++ b/src/test/java/com/moabam/api/domain/coupon/repository/CouponWalletSearchRepositoryTest.java @@ -3,13 +3,20 @@ import static com.moabam.support.fixture.CouponFixture.*; import static org.assertj.core.api.Assertions.*; +import java.util.List; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import com.moabam.api.domain.coupon.Coupon; import com.moabam.api.domain.coupon.CouponWallet; +import com.moabam.global.error.exception.NotFoundException; +import com.moabam.global.error.model.ErrorMessage; import com.moabam.support.annotation.QuerydslRepositoryTest; +import com.moabam.support.fixture.CouponFixture; @QuerydslRepositoryTest class CouponWalletSearchRepositoryTest { @@ -23,17 +30,83 @@ class CouponWalletSearchRepositoryTest { @Autowired private CouponWalletSearchRepository couponWalletSearchRepository; - @DisplayName("회원의 특정 쿠폰 지갑을 조회한다.") + @DisplayName("나의 쿠폰함의 특정 쿠폰을 조회한다.. - List") + @Test + void findAllByCouponIdAndMemberId_success() { + // Given + Coupon coupon = couponRepository.save(CouponFixture.coupon()); + CouponWallet couponWallet = couponWalletRepository.save(CouponWallet.create(1L, coupon)); + + // When + List actual = couponWalletSearchRepository.findAllByCouponIdAndMemberId(coupon.getId(), 1L); + + // Then + assertThat(actual).hasSize(1); + assertThat(actual.get(0).getCoupon().getName()).isEqualTo(coupon.getName()); + assertThat(actual.get(0).getMemberId()).isEqualTo(couponWallet.getMemberId()); + } + + @DisplayName("ID가 1인 회원은 쿠폰 1개를 가지고 있다. - List") + @MethodSource("com.moabam.support.fixture.CouponWalletFixture#provideCouponWalletAll") + @ParameterizedTest + void findAllByCouponIdAndMemberId_Id1_success(List couponWallets) { + // Given + couponWallets.forEach(couponWallet -> { + Coupon coupon = couponRepository.save(couponWallet.getCoupon()); + couponWalletRepository.save(CouponWallet.create(couponWallet.getMemberId(), coupon)); + }); + + // When + List actual = couponWalletSearchRepository.findAllByCouponIdAndMemberId(null, 1L); + + // Then + assertThat(actual).hasSize(1); + } + + @DisplayName("ID가 2인 회원은 쿠폰 ID가 777인 쿠폰을 가지고 있지 않다. - List") + @MethodSource("com.moabam.support.fixture.CouponWalletFixture#provideCouponWalletAll") + @ParameterizedTest + void findAllByCouponIdAndMemberId_Id2_notCouponId777(List couponWallets) { + // Given + couponWallets.forEach(couponWallet -> { + Coupon coupon = couponRepository.save(couponWallet.getCoupon()); + couponWalletRepository.save(CouponWallet.create(couponWallet.getMemberId(), coupon)); + }); + + // When + List actual = couponWalletSearchRepository.findAllByCouponIdAndMemberId(777L, 2L); + + // Then + assertThat(actual).isEmpty(); + } + + @DisplayName("ID가 3인 회원은 쿠폰 3개를 가지고 있다. - List") + @MethodSource("com.moabam.support.fixture.CouponWalletFixture#provideCouponWalletAll") + @ParameterizedTest + void findAllByCouponIdAndMemberId_Id3_success(List couponWallets) { + // Given + couponWallets.forEach(couponWallet -> { + Coupon coupon = couponRepository.save(couponWallet.getCoupon()); + couponWalletRepository.save(CouponWallet.create(couponWallet.getMemberId(), coupon)); + }); + + // When + List actual = couponWalletSearchRepository.findAllByCouponIdAndMemberId(null, 3L); + + // Then + assertThat(actual).hasSize(3); + } + + @DisplayName("회원의 특정 쿠폰 지갑을 성공적으로 조회한다.") @Test - void find_by_id_and_member_id() { + void findByIdAndMemberId_success() { // given - Long id = 1L; - Long memberId = 1L; Coupon coupon = couponRepository.save(discount1000Coupon()); - couponWalletRepository.save(CouponWallet.create(memberId, coupon)); + CouponWallet couponWallet = couponWalletRepository.save(CouponWallet.create(1L, coupon)); // when - CouponWallet actual = couponWalletSearchRepository.findByIdAndMemberId(id, memberId).orElseThrow(); + CouponWallet actual = couponWalletSearchRepository.findByIdAndMemberId(couponWallet.getId(), 1L) + .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON_WALLET)); // then assertThat(actual.getCoupon()).isEqualTo(coupon); diff --git a/src/test/java/com/moabam/api/domain/notification/repository/NotificationRepositoryTest.java b/src/test/java/com/moabam/api/domain/notification/repository/NotificationRepositoryTest.java index 6a9a376d..681a9aa5 100644 --- a/src/test/java/com/moabam/api/domain/notification/repository/NotificationRepositoryTest.java +++ b/src/test/java/com/moabam/api/domain/notification/repository/NotificationRepositoryTest.java @@ -12,50 +12,82 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import com.moabam.api.infrastructure.redis.StringRedisRepository; +import com.moabam.api.infrastructure.redis.ValueRedisRepository; @ExtendWith(MockitoExtension.class) class NotificationRepositoryTest { @InjectMocks - private NotificationRepository notificationRepository; + NotificationRepository notificationRepository; @Mock - private StringRedisRepository stringRedisRepository; + ValueRedisRepository valueRedisRepository; @DisplayName("콕 알림이 성공적으로 저장된다. - Void") @Test - void notificationRepository_saveKnockNotification() { + void saveKnock_success() { // When - notificationRepository.saveKnock("knockKey"); + notificationRepository.saveKnock(1L, 1L, 1L); // Then - verify(stringRedisRepository).save(any(String.class), any(String.class), any(Duration.class)); + verify(valueRedisRepository).save(any(String.class), any(String.class), any(Duration.class)); } - @DisplayName("콕 알림 저장 시, 필요한 값이 NULL 이다. - NullPointerException") + @DisplayName("콕 찌르는 사용자의 ID가 Null인 콕 알림을 저장한다. - NullPointerException") @Test - void notificationRepository_saveKnockNotification_NullPointerException() { + void saveKnock_MemberId_NullPointerException() { // When & Then - assertThatThrownBy(() -> notificationRepository.saveKnock(null)) + assertThatThrownBy(() -> notificationRepository.saveKnock(null, 1L, 1L)) .isInstanceOf(NullPointerException.class); } - @DisplayName("콕 알림 여부 체크를 정상적으로 확인한다. - Boolean") + @DisplayName("콕 찌를 대상의 ID가 Null인 콕 알림을 저장한다. - NullPointerException") @Test - void notificationRepository_existsKnockByMemberId() { + void saveKnock_TargetId_NullPointerException() { + // When & Then + assertThatThrownBy(() -> notificationRepository.saveKnock(1L, null, 1L)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("방 ID가 Null인 콕 알림을 저장한다. - NullPointerException") + @Test + void saveKnock_RoomId_NullPointerException() { + // When & Then + assertThatThrownBy(() -> notificationRepository.saveKnock(1L, 2L, null)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("콕 알림 여부 체크를 성공적으로 확인한다. - Boolean") + @Test + void existsKnockByKey_success() { // When - notificationRepository.existsKnockByKey("knock key"); + notificationRepository.existsKnockByKey(1L, 1L, 1L); // Then - verify(stringRedisRepository).hasKey(any(String.class)); + verify(valueRedisRepository).hasKey(any(String.class)); + } + + @DisplayName("콕 찌르는 사용자의 ID가 Null인 콕 알림 여부를 체크한다. - NullPointerException") + @Test + void existsKnockByKey_MemberId_NullPointerException() { + // When & Then + assertThatThrownBy(() -> notificationRepository.existsKnockByKey(null, 1L, 1L)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("콕 찌를 상대 ID가 Null인 콕 알림 여부를 체크한다. - NullPointerException") + @Test + void existsKnockByKey_TargetId_NullPointerException() { + // When & Then + assertThatThrownBy(() -> notificationRepository.existsKnockByKey(1L, null, 1L)) + .isInstanceOf(NullPointerException.class); } - @DisplayName("콕 알림 여부 체크 시, 필요한 값이 NULL 이다. - NullPointerException") + @DisplayName("방 ID가 Null인 콕 알림 여부를 체크한다. - NullPointerException") @Test - void notificationRepository_existsKnockByMemberId_NullPointerException() { + void existsKnockByKey_RoomId_NullPointerException() { // When & Then - assertThatThrownBy(() -> notificationRepository.existsKnockByKey(null)) + assertThatThrownBy(() -> notificationRepository.existsKnockByKey(1L, 2L, null)) .isInstanceOf(NullPointerException.class); } } diff --git a/src/test/java/com/moabam/api/domain/payment/PaymentTest.java b/src/test/java/com/moabam/api/domain/payment/PaymentTest.java index 8a2dfffb..b9ca3d9c 100644 --- a/src/test/java/com/moabam/api/domain/payment/PaymentTest.java +++ b/src/test/java/com/moabam/api/domain/payment/PaymentTest.java @@ -21,7 +21,7 @@ void validate_amount_exception() { .memberId(1L) .product(bugProduct()) .order(order(bugProduct())) - .amount(-1000); + .totalAmount(-1000); assertThatThrownBy(paymentBuilder::build) .isInstanceOf(BadRequestException.class) @@ -44,8 +44,8 @@ void success() { payment.applyCoupon(coupon, couponWalletId); // then - assertThat(payment.getAmount()).isEqualTo(BUG_PRODUCT_PRICE - 1000); - assertThat(payment.getCoupon()).isEqualTo(coupon); + assertThat(payment.getTotalAmount()).isEqualTo(BUG_PRODUCT_PRICE - 1000); + assertThat(payment.getDiscountAmount()).isEqualTo(coupon.getPoint()); assertThat(payment.getCouponWalletId()).isEqualTo(couponWalletId); } @@ -61,7 +61,7 @@ void discount_amount_greater() { payment.applyCoupon(coupon, couponWalletId); // then - assertThat(payment.getAmount()).isZero(); + assertThat(payment.getTotalAmount()).isZero(); } } diff --git a/src/test/java/com/moabam/api/dto/coupon/CreateCouponRequestTest.java b/src/test/java/com/moabam/api/dto/coupon/CreateCouponRequestTest.java index 5fcf7561..3454611b 100644 --- a/src/test/java/com/moabam/api/dto/coupon/CreateCouponRequestTest.java +++ b/src/test/java/com/moabam/api/dto/coupon/CreateCouponRequestTest.java @@ -15,7 +15,7 @@ class CreateCouponRequestTest { @DisplayName("쿠폰 발급 가능 시작 날짜가 올바른 형식으로 입력된다. - yyyy-MM-dd") @Test - void createCouponRequest_StartAt() throws JsonProcessingException { + void startAt_success() throws JsonProcessingException { // Given ObjectMapper objectMapper = new ObjectMapper(); objectMapper.registerModule(new JavaTimeModule()); diff --git a/src/test/java/com/moabam/api/infrastructure/fcm/FcmRepositoryTest.java b/src/test/java/com/moabam/api/infrastructure/fcm/FcmRepositoryTest.java index 4c6f4367..83f2e887 100644 --- a/src/test/java/com/moabam/api/infrastructure/fcm/FcmRepositoryTest.java +++ b/src/test/java/com/moabam/api/infrastructure/fcm/FcmRepositoryTest.java @@ -13,46 +13,54 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import com.moabam.api.infrastructure.redis.StringRedisRepository; +import com.moabam.api.infrastructure.redis.ValueRedisRepository; @ExtendWith(MockitoExtension.class) class FcmRepositoryTest { @InjectMocks - private FcmRepository fcmRepository; + FcmRepository fcmRepository; @Mock - private StringRedisRepository stringRedisRepository; + ValueRedisRepository valueRedisRepository; @DisplayName("FCM 토큰이 성공적으로 저장된다. - Void") @Test - void saveToken() { + void saveToken_success() { // When fcmRepository.saveToken(1L, "value1"); // Then - verify(stringRedisRepository).save(any(String.class), any(String.class), any(Duration.class)); + verify(valueRedisRepository).save(any(String.class), any(String.class), any(Duration.class)); } - @DisplayName("FCM 토큰 저장 시, 필요한 값이 NULL 이다. - NullPointerException") + @DisplayName("ID가 Null인 사용자가 FCM 토큰을 저장한다. - NullPointerException") @Test - void saveToken_NullPointerException() { + void saveToken_MemberId_NullPointerException() { // When & Then assertThatThrownBy(() -> fcmRepository.saveToken(null, "value")) .isInstanceOf(NullPointerException.class); } + @DisplayName("토큰이 Null인 FCM 토큰을 저장한다. - NullPointerException") + @Test + void saveToken_FcmToken_NullPointerException() { + // When & Then + assertThatThrownBy(() -> fcmRepository.saveToken(1L, null)) + .isInstanceOf(NullPointerException.class); + } + @DisplayName("FCM 토큰이 성공적으로 삭제된다. - Void") @Test - void deleteTokenByMemberId() { + void deleteTokenByMemberId_success() { // When fcmRepository.deleteTokenByMemberId(1L); // Then - verify(stringRedisRepository).delete(any(String.class)); + verify(valueRedisRepository).delete(any(String.class)); } - @DisplayName("FCM 토큰 삭제 시, 필요한 값이 NULL 이다. - NullPointerException") + @DisplayName("ID가 Null인 사용자가 FCM 토큰을 삭제한다.. - NullPointerException") @Test void deleteTokenByMemberId_NullPointerException() { // When & Then @@ -62,15 +70,15 @@ void deleteTokenByMemberId_NullPointerException() { @DisplayName("FCM 토큰을 성공적으로 조회된다. - (String) FCM TOKEN") @Test - void findTokenByMemberId() { + void findTokenByMemberId_success() { // When fcmRepository.findTokenByMemberId(1L); // Then - verify(stringRedisRepository).get(any(String.class)); + verify(valueRedisRepository).get(any(String.class)); } - @DisplayName("FCM 토큰 조회 시, 필요한 값이 NULL 이다. - NullPointerException") + @DisplayName("ID가 Null인 사용자가 FCM 토큰을 조회한다. - NullPointerException") @Test void findTokenByMemberId_NullPointerException() { // When & Then @@ -80,15 +88,15 @@ void findTokenByMemberId_NullPointerException() { @DisplayName("FCM 토큰 존재 여부를 성공적으로 확인한다. - Boolean") @Test - void existsTokenByMemberId() { + void existsTokenByMemberId_success() { // When fcmRepository.existsTokenByMemberId(1L); // Then - verify(stringRedisRepository).hasKey(any(String.class)); + verify(valueRedisRepository).hasKey(any(String.class)); } - @DisplayName("FCM 토큰 존재 여부 체크 시, 필요한 값이 NULL 이다. - NullPointerException") + @DisplayName("ID가 Null인 사용자가 FCM 토큰 존재 여부를 확인한다. - NullPointerException") @Test void existsTokenByMemberId_NullPointerException() { // When & Then diff --git a/src/test/java/com/moabam/api/infrastructure/fcm/FcmServiceTest.java b/src/test/java/com/moabam/api/infrastructure/fcm/FcmServiceTest.java index 218e4ef7..c4b7cc09 100644 --- a/src/test/java/com/moabam/api/infrastructure/fcm/FcmServiceTest.java +++ b/src/test/java/com/moabam/api/infrastructure/fcm/FcmServiceTest.java @@ -20,18 +20,18 @@ class FcmServiceTest extends WithoutFilterSupporter { @Autowired - private FcmService fcmService; + FcmService fcmService; @MockBean - private FirebaseMessaging firebaseMessaging; + FirebaseMessaging firebaseMessaging; @MockBean - private FcmRepository fcmRepository; + FcmRepository fcmRepository; @WithMember @DisplayName("FCM 토큰이 성공적으로 저장된다. - Void") @Test - void saveToken() { + void saveToken_success() { // Given AuthMember authMember = AuthorizationThreadLocal.getAuthMember(); @@ -72,7 +72,7 @@ void saveToken_Null() { @DisplayName("FCM 토큰이 성공적으로 삭제된다. - Void") @Test - void deleteTokenByMemberId() { + void deleteTokenByMemberId_success() { // When fcmRepository.deleteTokenByMemberId(1L); @@ -82,7 +82,7 @@ void deleteTokenByMemberId() { @DisplayName("FCM 토큰을 성공적으로 조회된다. - (String) FCM TOKEN") @Test - void findTokenByMemberId() { + void findTokenByMemberId_success() { // When fcmRepository.findTokenByMemberId(1L); @@ -92,7 +92,7 @@ void findTokenByMemberId() { @DisplayName("비동기 FCM 알림을 성공적으로 보낸다. - Void") @Test - void sendAsync() { + void sendAsync_success() { // When fcmService.sendAsync("FCM-TOKEN", "알림"); diff --git a/src/test/java/com/moabam/api/infrastructure/payment/TossPaymentServiceTest.java b/src/test/java/com/moabam/api/infrastructure/payment/TossPaymentServiceTest.java new file mode 100644 index 00000000..e9a57d0e --- /dev/null +++ b/src/test/java/com/moabam/api/infrastructure/payment/TossPaymentServiceTest.java @@ -0,0 +1,92 @@ +package com.moabam.api.infrastructure.payment; + +import static com.moabam.support.fixture.PaymentFixture.*; +import static org.assertj.core.api.Assertions.*; + +import java.io.IOException; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.moabam.api.dto.payment.ConfirmTossPaymentRequest; +import com.moabam.api.dto.payment.ConfirmTossPaymentResponse; +import com.moabam.global.config.TossPaymentConfig; +import com.moabam.global.error.exception.MoabamException; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; + +@SpringBootTest +@ActiveProfiles("test") +class TossPaymentServiceTest { + + @Autowired + TossPaymentConfig config; + + @Autowired + ObjectMapper objectMapper; + + TossPaymentService tossPaymentService; + MockWebServer mockWebServer; + + @BeforeEach + public void setup() { + mockWebServer = new MockWebServer(); + tossPaymentService = new TossPaymentService( + new TossPaymentConfig(mockWebServer.url("/").toString(), config.secretKey()) + ); + tossPaymentService.init(); + } + + @AfterEach + public void tearDown() throws IOException { + mockWebServer.shutdown(); + } + + @DisplayName("결제 승인을 요청한다.") + @Nested + class Confirm { + + @DisplayName("성공한다.") + @Test + void success() throws Exception { + // given + ConfirmTossPaymentRequest request = confirmTossPaymentRequest(); + ConfirmTossPaymentResponse expected = confirmTossPaymentResponse(); + mockWebServer.enqueue(new MockResponse() + .setResponseCode(200) + .setBody(objectMapper.writeValueAsString(expected)) + .addHeader("Content-Type", "application/json")); + + // when + ConfirmTossPaymentResponse actual = tossPaymentService.confirm(request); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("예외가 발생한다.") + @Test + void exception() { + // given + ConfirmTossPaymentRequest request = confirmTossPaymentRequest(); + String jsonString = "{\"code\":\"NOT_FOUND_PAYMENT\",\"message\":\"존재하지 않는 결제 입니다.\"}"; + mockWebServer.enqueue(new MockResponse() + .setResponseCode(404) + .setBody(jsonString) + .addHeader("Content-Type", "application/json")); + + // when, then + assertThatThrownBy(() -> tossPaymentService.confirm(request)) + .isInstanceOf(MoabamException.class) + .hasMessage("존재하지 않는 결제 입니다."); + } + } +} diff --git a/src/test/java/com/moabam/api/infrastructure/redis/StringRedisRepositoryTest.java b/src/test/java/com/moabam/api/infrastructure/redis/ValueRedisRepositoryTest.java similarity index 64% rename from src/test/java/com/moabam/api/infrastructure/redis/StringRedisRepositoryTest.java rename to src/test/java/com/moabam/api/infrastructure/redis/ValueRedisRepositoryTest.java index 83b9d44a..c0566697 100644 --- a/src/test/java/com/moabam/api/infrastructure/redis/StringRedisRepositoryTest.java +++ b/src/test/java/com/moabam/api/infrastructure/redis/ValueRedisRepositoryTest.java @@ -13,11 +13,11 @@ import com.moabam.global.config.EmbeddedRedisConfig; -@SpringBootTest(classes = {EmbeddedRedisConfig.class, StringRedisRepository.class}) -class StringRedisRepositoryTest { +@SpringBootTest(classes = {EmbeddedRedisConfig.class, ValueRedisRepository.class}) +class ValueRedisRepositoryTest { @Autowired - StringRedisRepository stringRedisRepository; + ValueRedisRepository valueRedisRepository; String key = "key"; String value = "value"; @@ -26,17 +26,17 @@ class StringRedisRepositoryTest { @BeforeEach void setUp() { - stringRedisRepository.save(key, value, duration); + valueRedisRepository.save(key, value, duration); } @AfterEach void setDown() { - if (stringRedisRepository.hasKey(key)) { - stringRedisRepository.delete(key); + if (valueRedisRepository.hasKey(key)) { + valueRedisRepository.delete(key); } - if (stringRedisRepository.hasKey(stockKey)) { - stringRedisRepository.delete(stockKey); + if (valueRedisRepository.hasKey(stockKey)) { + valueRedisRepository.delete(stockKey); } } @@ -44,41 +44,41 @@ void setDown() { @Test void save_success() { // Then - assertThat(stringRedisRepository.get(key)).isEqualTo(value); + assertThat(valueRedisRepository.get(key)).isEqualTo(value); } @DisplayName("레디스의 특정 데이터가 성공적으로 조회된다. - String(Value)") @Test void get_success() { // When - String actual = stringRedisRepository.get(key); + String actual = valueRedisRepository.get(key); // Then - assertThat(actual).isEqualTo(stringRedisRepository.get(key)); + assertThat(actual).isEqualTo(valueRedisRepository.get(key)); } @DisplayName("레디스의 특정 데이터 존재 여부를 성공적으로 체크한다. - Boolean") @Test void hasKey_success() { // When & Then - assertThat(stringRedisRepository.hasKey("not found key")).isFalse(); + assertThat(valueRedisRepository.hasKey("not found key")).isFalse(); } @DisplayName("레디스의 특정 데이터가 성공적으로 삭제된다. - Void") @Test void delete_success() { // When - stringRedisRepository.delete(key); + valueRedisRepository.delete(key); // Then - assertThat(stringRedisRepository.hasKey(key)).isFalse(); + assertThat(valueRedisRepository.hasKey(key)).isFalse(); } @DisplayName("레디스의 특정 데이터의 값이 1 증가한다.") @Test void increment_success() { // When - Long actual = stringRedisRepository.increment(stockKey); + Long actual = valueRedisRepository.increment(stockKey); // Then assertThat(actual).isEqualTo(1L); diff --git a/src/test/java/com/moabam/api/infrastructure/redis/ZSetRedisRepositoryTest.java b/src/test/java/com/moabam/api/infrastructure/redis/ZSetRedisRepositoryTest.java index 5b94879d..cfc29bb4 100644 --- a/src/test/java/com/moabam/api/infrastructure/redis/ZSetRedisRepositoryTest.java +++ b/src/test/java/com/moabam/api/infrastructure/redis/ZSetRedisRepositoryTest.java @@ -15,14 +15,14 @@ import com.moabam.global.config.EmbeddedRedisConfig; -@SpringBootTest(classes = {EmbeddedRedisConfig.class, ZSetRedisRepository.class, StringRedisRepository.class}) +@SpringBootTest(classes = {EmbeddedRedisConfig.class, ZSetRedisRepository.class, ValueRedisRepository.class}) class ZSetRedisRepositoryTest { @Autowired ZSetRedisRepository zSetRedisRepository; @Autowired - StringRedisRepository stringRedisRepository; + ValueRedisRepository valueRedisRepository; @Autowired RedisTemplate redisTemplate; @@ -32,8 +32,8 @@ class ZSetRedisRepositoryTest { @AfterEach void afterEach() { - if (stringRedisRepository.hasKey(key)) { - stringRedisRepository.delete(key); + if (valueRedisRepository.hasKey(key)) { + valueRedisRepository.delete(key); } } @@ -44,7 +44,7 @@ void addIfAbsent_success() { zSetRedisRepository.addIfAbsent(key, value, 1); // Then - assertThat(stringRedisRepository.hasKey(key)).isTrue(); + assertThat(valueRedisRepository.hasKey(key)).isTrue(); } @DisplayName("이미 존재하는 값을 한 번 더 저장을 시도한다. - Void") @@ -72,7 +72,7 @@ void popMin_same_success() { // Then assertThat(actual).hasSize(3); - assertThat(stringRedisRepository.hasKey(key)).isFalse(); + assertThat(valueRedisRepository.hasKey(key)).isFalse(); } @Disabled @@ -106,6 +106,6 @@ void popMin_less_success() { // Then assertThat(actual).hasSize(3); - assertThat(stringRedisRepository.hasKey(key)).isTrue(); + assertThat(valueRedisRepository.hasKey(key)).isTrue(); } } diff --git a/src/test/java/com/moabam/api/presentation/CouponControllerTest.java b/src/test/java/com/moabam/api/presentation/CouponControllerTest.java index d9fa0bfa..880962fb 100644 --- a/src/test/java/com/moabam/api/presentation/CouponControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/CouponControllerTest.java @@ -8,7 +8,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -import java.time.LocalDateTime; +import java.time.LocalDate; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -28,7 +28,9 @@ import com.moabam.api.application.coupon.CouponMapper; import com.moabam.api.domain.coupon.Coupon; import com.moabam.api.domain.coupon.CouponType; +import com.moabam.api.domain.coupon.CouponWallet; import com.moabam.api.domain.coupon.repository.CouponRepository; +import com.moabam.api.domain.coupon.repository.CouponWalletRepository; import com.moabam.api.domain.member.Role; import com.moabam.api.dto.coupon.CouponStatusRequest; import com.moabam.api.dto.coupon.CreateCouponRequest; @@ -37,8 +39,9 @@ import com.moabam.support.annotation.WithMember; import com.moabam.support.common.WithoutFilterSupporter; import com.moabam.support.fixture.CouponFixture; -import com.moabam.support.fixture.CouponSnippetFixture; -import com.moabam.support.fixture.ErrorSnippetFixture; +import com.moabam.support.snippet.CouponSnippet; +import com.moabam.support.snippet.CouponWalletSnippet; +import com.moabam.support.snippet.ErrorSnippet; @Transactional @SpringBootTest @@ -55,6 +58,9 @@ class CouponControllerTest extends WithoutFilterSupporter { @Autowired CouponRepository couponRepository; + @Autowired + CouponWalletRepository couponWalletRepository; + @MockBean ClockHolder clockHolder; @@ -64,7 +70,8 @@ class CouponControllerTest extends WithoutFilterSupporter { void create_Coupon_success() throws Exception { // Given CreateCouponRequest request = CouponFixture.createCouponRequest(); - given(clockHolder.times()).willReturn(LocalDateTime.of(2022, 1, 1, 1, 1)); + + given(clockHolder.date()).willReturn(LocalDate.of(2022, 1, 1)); // When & Then mockMvc.perform(post("/admins/coupons") @@ -74,7 +81,7 @@ void create_Coupon_success() throws Exception { .andDo(document("admins/coupons", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - CouponSnippetFixture.CREATE_COUPON_REQUEST)) + CouponSnippet.CREATE_COUPON_REQUEST)) .andExpect(status().isCreated()); } @@ -85,7 +92,7 @@ void create_Coupon_StartAt_BadRequestException() throws Exception { // Given CreateCouponRequest request = CouponFixture.createCouponRequest(); - given(clockHolder.times()).willReturn(LocalDateTime.of(2025, 1, 1, 1, 1)); + given(clockHolder.date()).willReturn(LocalDate.of(2025, 1, 1)); // When & Then mockMvc.perform(post("/admins/coupons") @@ -95,8 +102,8 @@ void create_Coupon_StartAt_BadRequestException() throws Exception { .andDo(document("admins/coupons", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - CouponSnippetFixture.CREATE_COUPON_REQUEST, - ErrorSnippetFixture.ERROR_MESSAGE_RESPONSE)) + CouponSnippet.CREATE_COUPON_REQUEST, + ErrorSnippet.ERROR_MESSAGE_RESPONSE)) .andExpect(status().isBadRequest()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.message").value(ErrorMessage.INVALID_COUPON_START_AT_PERIOD.getMessage())); @@ -110,7 +117,7 @@ void create_Coupon_OpenAt_BadRequestException() throws Exception { String couponType = CouponType.GOLDEN_COUPON.getName(); CreateCouponRequest request = CouponFixture.createCouponRequest(couponType, 1, 1); - given(clockHolder.times()).willReturn(LocalDateTime.of(2022, 1, 1, 1, 1)); + given(clockHolder.date()).willReturn(LocalDate.of(2022, 1, 1)); // When & Then mockMvc.perform(post("/admins/coupons") @@ -120,8 +127,8 @@ void create_Coupon_OpenAt_BadRequestException() throws Exception { .andDo(document("admins/coupons", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - CouponSnippetFixture.CREATE_COUPON_REQUEST, - ErrorSnippetFixture.ERROR_MESSAGE_RESPONSE)) + CouponSnippet.CREATE_COUPON_REQUEST, + ErrorSnippet.ERROR_MESSAGE_RESPONSE)) .andExpect(status().isBadRequest()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.message").value(ErrorMessage.INVALID_COUPON_OPEN_AT_PERIOD.getMessage())); @@ -143,8 +150,8 @@ void create_Coupon_Name_ConflictException() throws Exception { .andDo(document("admins/coupons", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - CouponSnippetFixture.CREATE_COUPON_REQUEST, - ErrorSnippetFixture.ERROR_MESSAGE_RESPONSE)) + CouponSnippet.CREATE_COUPON_REQUEST, + ErrorSnippet.ERROR_MESSAGE_RESPONSE)) .andExpect(status().isConflict()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.message").value(ErrorMessage.CONFLICT_COUPON_NAME.getMessage())); @@ -167,8 +174,8 @@ void create_Coupon_StartAt_ConflictException() throws Exception { .andDo(document("admins/coupons", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - CouponSnippetFixture.CREATE_COUPON_REQUEST, - ErrorSnippetFixture.ERROR_MESSAGE_RESPONSE)) + CouponSnippet.CREATE_COUPON_REQUEST, + ErrorSnippet.ERROR_MESSAGE_RESPONSE)) .andExpect(status().isConflict()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.message").value(ErrorMessage.CONFLICT_COUPON_START_AT.getMessage())); @@ -200,7 +207,7 @@ void delete_Coupon_NotFoundException() throws Exception { .andDo(document("admins/coupons/couponId", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - ErrorSnippetFixture.ERROR_MESSAGE_RESPONSE)) + ErrorSnippet.ERROR_MESSAGE_RESPONSE)) .andExpect(status().isNotFound()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.message").value(ErrorMessage.NOT_FOUND_COUPON.getMessage())); @@ -218,7 +225,7 @@ void getById_Coupon_success() throws Exception { .andDo(document("coupons/couponId", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - CouponSnippetFixture.COUPON_RESPONSE)) + CouponSnippet.COUPON_RESPONSE)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.id").value(coupon.getId())); @@ -233,7 +240,7 @@ void getById_Coupon_NotFoundException() throws Exception { .andDo(document("coupons/couponId", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - ErrorSnippetFixture.ERROR_MESSAGE_RESPONSE)) + ErrorSnippet.ERROR_MESSAGE_RESPONSE)) .andExpect(status().isNotFound()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.message").value(ErrorMessage.NOT_FOUND_COUPON.getMessage())); @@ -247,7 +254,7 @@ void getAllByStatus_Coupons_success(List coupons) throws Exception { CouponStatusRequest request = CouponFixture.couponStatusRequest(true, true); List coupon = couponRepository.saveAll(coupons); - given(clockHolder.times()).willReturn(LocalDateTime.of(2022, 1, 1, 1, 1)); + given(clockHolder.date()).willReturn(LocalDate.of(2022, 1, 1)); // When & Then mockMvc.perform(post("/coupons/search") @@ -257,8 +264,8 @@ void getAllByStatus_Coupons_success(List coupons) throws Exception { .andDo(document("coupons/search", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - CouponSnippetFixture.COUPON_STATUS_REQUEST, - CouponSnippetFixture.COUPON_STATUS_RESPONSE)) + CouponSnippet.COUPON_STATUS_REQUEST, + CouponSnippet.COUPON_STATUS_RESPONSE)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$", hasSize(coupon.size()))); @@ -272,7 +279,7 @@ void getAllByStatus_Coupon_success(List coupons) throws Exception { CouponStatusRequest request = CouponFixture.couponStatusRequest(false, false); couponRepository.saveAll(coupons); - given(clockHolder.times()).willReturn(LocalDateTime.of(2023, 3, 1, 1, 1)); + given(clockHolder.date()).willReturn(LocalDate.of(2023, 3, 1)); // When & Then mockMvc.perform(post("/coupons/search") @@ -282,7 +289,7 @@ void getAllByStatus_Coupon_success(List coupons) throws Exception { .andDo(document("coupons/search", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - CouponSnippetFixture.COUPON_STATUS_REQUEST)) + CouponSnippet.COUPON_STATUS_REQUEST)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$", hasSize(1))); @@ -296,7 +303,7 @@ void registerQueue_success() throws Exception { Coupon couponFixture = CouponFixture.coupon(); Coupon coupon = couponRepository.save(couponFixture); - given(clockHolder.times()).willReturn(LocalDateTime.of(2023, 2, 1, 1, 1)); + given(clockHolder.date()).willReturn(LocalDate.of(2023, 2, 1)); // When & Then mockMvc.perform(post("/coupons") @@ -316,7 +323,7 @@ void registerQueue_Zero_StartAt_BadRequestException() throws Exception { Coupon couponFixture = CouponFixture.coupon(); Coupon coupon = couponRepository.save(couponFixture); - given(clockHolder.times()).willReturn(LocalDateTime.of(2022, 1, 1, 1, 1)); + given(clockHolder.date()).willReturn(LocalDate.of(2022, 1, 1)); // When & Then mockMvc.perform(post("/coupons") @@ -325,7 +332,7 @@ void registerQueue_Zero_StartAt_BadRequestException() throws Exception { .andDo(document("coupons", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - ErrorSnippetFixture.ERROR_MESSAGE_RESPONSE)) + ErrorSnippet.ERROR_MESSAGE_RESPONSE)) .andExpect(status().isBadRequest()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.message").value(ErrorMessage.INVALID_COUPON_PERIOD.getMessage())); @@ -339,7 +346,7 @@ void registerQueue_Not_StartAt_BadRequestException() throws Exception { Coupon couponFixture = CouponFixture.coupon(); couponRepository.save(couponFixture); - given(clockHolder.times()).willReturn(LocalDateTime.of(2022, 2, 1, 1, 1)); + given(clockHolder.date()).willReturn(LocalDate.of(2022, 2, 1)); // When & Then mockMvc.perform(post("/coupons") @@ -348,7 +355,7 @@ void registerQueue_Not_StartAt_BadRequestException() throws Exception { .andDo(document("coupons", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - ErrorSnippetFixture.ERROR_MESSAGE_RESPONSE)) + ErrorSnippet.ERROR_MESSAGE_RESPONSE)) .andExpect(status().isBadRequest()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.message").value(ErrorMessage.INVALID_COUPON_PERIOD.getMessage())); @@ -361,7 +368,7 @@ void registerQueue_NotFoundException() throws Exception { // Given Coupon coupon = CouponFixture.coupon("Not found couponName", 2, 1); - given(clockHolder.times()).willReturn(LocalDateTime.of(2023, 2, 1, 1, 1)); + given(clockHolder.date()).willReturn(LocalDate.of(2023, 2, 1)); // When & Then mockMvc.perform(post("/coupons") @@ -370,9 +377,69 @@ void registerQueue_NotFoundException() throws Exception { .andDo(document("coupons", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - ErrorSnippetFixture.ERROR_MESSAGE_RESPONSE)) + ErrorSnippet.ERROR_MESSAGE_RESPONSE)) .andExpect(status().isBadRequest()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.message").value(ErrorMessage.INVALID_COUPON_PERIOD.getMessage())); } + + @WithMember + @DisplayName("GET - 나의 쿠폰함에서 특정 쿠폰을 조회한다. - List") + @Test + void getWallet_success() throws Exception { + // Given + Coupon coupon = couponRepository.save(CouponFixture.coupon()); + couponWalletRepository.save(CouponWallet.create(1L, coupon)); + + // When & Then + mockMvc.perform(get("/my-coupons/" + coupon.getId())) + .andDo(print()) + .andDo(document("my-coupons/couponId", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + CouponWalletSnippet.COUPON_WALLET_RESPONSE)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].id").value(coupon.getId())) + .andExpect(jsonPath("$[0].name").value(coupon.getName())); + } + + @WithMember + @DisplayName("GET - 나의 쿠폰 보관함에 있는 모든 쿠폰을 조회한다. - List") + @MethodSource("com.moabam.support.fixture.CouponWalletFixture#provideCouponWalletByCouponId1_total5") + @ParameterizedTest + void getWallet_all_success(List couponWallets) throws Exception { + // Given + couponWallets.forEach(couponWallet -> { + Coupon coupon = couponRepository.save(couponWallet.getCoupon()); + couponWalletRepository.save(CouponWallet.create(1L, coupon)); + }); + + // When & Then + mockMvc.perform(get("/my-coupons")) + .andDo(print()) + .andDo(document("my-coupons/couponId", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + CouponWalletSnippet.COUPON_WALLET_RESPONSE)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$", hasSize(couponWallets.size()))); + } + + @WithMember + @DisplayName("GET - 쿠폰이 없는 사용자의 쿠폰함을 조회한다. - List") + @Test + void getWallet_no_coupon() throws Exception { + // When & Then + mockMvc.perform(get("/my-coupons")) + .andDo(print()) + .andDo(document("my-coupons/couponId", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()))) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$", hasSize(0))); + } } diff --git a/src/test/java/com/moabam/api/presentation/NotificationControllerTest.java b/src/test/java/com/moabam/api/presentation/NotificationControllerTest.java index a1d00129..95581ddf 100644 --- a/src/test/java/com/moabam/api/presentation/NotificationControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/NotificationControllerTest.java @@ -29,13 +29,13 @@ import com.moabam.api.domain.room.repository.RoomRepository; import com.moabam.api.infrastructure.fcm.FcmRepository; import com.moabam.api.infrastructure.fcm.FcmService; -import com.moabam.api.infrastructure.redis.StringRedisRepository; +import com.moabam.api.infrastructure.redis.ValueRedisRepository; import com.moabam.global.error.model.ErrorMessage; import com.moabam.support.annotation.WithMember; import com.moabam.support.common.WithoutFilterSupporter; -import com.moabam.support.fixture.ErrorSnippetFixture; import com.moabam.support.fixture.MemberFixture; import com.moabam.support.fixture.RoomFixture; +import com.moabam.support.snippet.ErrorSnippet; @Transactional @SpringBootTest @@ -43,41 +43,39 @@ @AutoConfigureRestDocs class NotificationControllerTest extends WithoutFilterSupporter { - private static final String KNOCK_KEY = "room_%s_member_%s_knocks_%s"; - @Autowired - private MockMvc mockMvc; + MockMvc mockMvc; @Autowired - private MemberRepository memberRepository; + MemberRepository memberRepository; @Autowired - private RoomRepository roomRepository; + RoomRepository roomRepository; @Autowired - private NotificationRepository notificationRepository; + NotificationRepository notificationRepository; @Autowired - private StringRedisRepository stringRedisRepository; + ValueRedisRepository valueRedisRepository; @Autowired - private FcmService fcmService; + FcmService fcmService; @Autowired - private FcmRepository fcmRepository; + FcmRepository fcmRepository; @MockBean - private FirebaseMessaging firebaseMessaging; + FirebaseMessaging firebaseMessaging; - private Member target; - private Room room; - private String knockKey; + Member target; + Room room; + String knockKey; @BeforeEach void setUp() { target = memberRepository.save(MemberFixture.member("123", "targetName")); room = roomRepository.save(RoomFixture.room()); - knockKey = String.format(KNOCK_KEY, room.getId(), 1, target.getId()); + knockKey = String.format("room_%s_member_%s_knocks_%s", room.getId(), 1, target.getId()); willReturn(null) .given(firebaseMessaging) @@ -87,13 +85,13 @@ void setUp() { @AfterEach void setDown() { fcmService.deleteTokenByMemberId(target.getId()); - stringRedisRepository.delete(knockKey); + valueRedisRepository.delete(knockKey); } @WithMember @DisplayName("POST - 성공적으로 FCM Token을 저장한다. - Void") @Test - void createFcmToken() throws Exception { + void createFcmToken_success() throws Exception { // When & Then mockMvc.perform(post("/notifications") .param("fcmToken", "FCM-TOKEN")) @@ -119,9 +117,9 @@ void createFcmToken_blank() throws Exception { } @WithMember - @DisplayName("GET - 성공적으로 상대에게 콕 알림을 보낸다. - Void") + @DisplayName("GET - 상대에게 콕 알림을 성공적으로 보낸다. - Void") @Test - void sendKnock() throws Exception { + void sendKnock_success() throws Exception { // Given fcmRepository.saveToken(target.getId(), "FCM_TOKEN"); @@ -144,7 +142,7 @@ void sendKnock_NotFoundException() throws Exception { .andDo(document("notifications/rooms/roomId/members/memberId", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - ErrorSnippetFixture.ERROR_MESSAGE_RESPONSE)) + ErrorSnippet.ERROR_MESSAGE_RESPONSE)) .andExpect(status().isNotFound()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.message").value(ErrorMessage.NOT_FOUND_FCM_TOKEN.getMessage())); @@ -156,7 +154,7 @@ void sendKnock_NotFoundException() throws Exception { void sendKnock_ConflictException() throws Exception { // Given fcmRepository.saveToken(target.getId(), "FCM_TOKEN"); - notificationRepository.saveKnock(knockKey); + notificationRepository.saveKnock(1L, target.getId(), room.getId()); // When & Then mockMvc.perform(get("/notifications/rooms/" + room.getId() + "/members/" + target.getId())) @@ -164,7 +162,7 @@ void sendKnock_ConflictException() throws Exception { .andDo(document("notifications/rooms/roomId/members/memberId", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - ErrorSnippetFixture.ERROR_MESSAGE_RESPONSE)) + ErrorSnippet.ERROR_MESSAGE_RESPONSE)) .andExpect(status().isConflict()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.message").value(ErrorMessage.CONFLICT_KNOCK.getMessage())); diff --git a/src/test/java/com/moabam/api/presentation/PaymentControllerTest.java b/src/test/java/com/moabam/api/presentation/PaymentControllerTest.java index 536b4ab4..ee2080b9 100644 --- a/src/test/java/com/moabam/api/presentation/PaymentControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/PaymentControllerTest.java @@ -1,8 +1,11 @@ package com.moabam.api.presentation; +import static com.moabam.global.auth.model.AuthorizationThreadLocal.*; +import static com.moabam.support.fixture.MemberFixture.*; import static com.moabam.support.fixture.PaymentFixture.*; import static com.moabam.support.fixture.ProductFixture.*; import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; import static org.springframework.http.MediaType.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; @@ -12,19 +15,25 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; import com.fasterxml.jackson.databind.ObjectMapper; +import com.moabam.api.application.member.MemberService; import com.moabam.api.domain.payment.Payment; +import com.moabam.api.domain.payment.PaymentStatus; import com.moabam.api.domain.payment.repository.PaymentRepository; import com.moabam.api.domain.product.Product; import com.moabam.api.domain.product.repository.ProductRepository; +import com.moabam.api.dto.payment.ConfirmPaymentRequest; import com.moabam.api.dto.payment.PaymentRequest; +import com.moabam.api.infrastructure.payment.TossPaymentService; import com.moabam.support.annotation.WithMember; import com.moabam.support.common.WithoutFilterSupporter; @@ -39,6 +48,12 @@ class PaymentControllerTest extends WithoutFilterSupporter { @Autowired ObjectMapper objectMapper; + @MockBean + MemberService memberService; + + @MockBean + TossPaymentService tossPaymentService; + @Autowired PaymentRepository paymentRepository; @@ -86,4 +101,53 @@ void bad_request_body_exception(String orderId) throws Exception { .andDo(print()); } } + + @Nested + @DisplayName("결제를 승인한다.") + class Confirm { + + @DisplayName("성공한다.") + @WithMember + @Test + void success() throws Exception { + // given + Long memberId = getAuthMember().id(); + Product product = productRepository.save(bugProduct()); + Payment payment = paymentRepository.save(payment(product)); + payment.request(ORDER_ID); + ConfirmPaymentRequest request = confirmPaymentRequest(); + given(tossPaymentService.confirm(confirmTossPaymentRequest())).willReturn(confirmTossPaymentResponse()); + given(memberService.getById(memberId)).willReturn(member()); + + // expected + mockMvc.perform(post("/payments/confirm") + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andDo(print()); + Payment actual = paymentRepository.findById(payment.getId()).orElseThrow(); + assertThat(actual.getStatus()).isEqualTo(PaymentStatus.DONE); + } + + @DisplayName("결제 승인 요청 바디가 유효하지 않으면 예외가 발생한다.") + @WithMember + @ParameterizedTest + @CsvSource(value = { + ", random_order_id_123, 2000", + "payment_key_123, , 2000", + "payment_key_123, random_order_id_123, -1000", + }) + void bad_request_body_exception(String paymentKey, String orderId, int amount) throws Exception { + // given + ConfirmPaymentRequest request = new ConfirmPaymentRequest(paymentKey, orderId, amount); + + // expected + mockMvc.perform(post("/payments/confirm") + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("올바른 요청 정보가 아닙니다.")) + .andDo(print()); + } + } } diff --git a/src/test/java/com/moabam/support/fixture/CouponFixture.java b/src/test/java/com/moabam/support/fixture/CouponFixture.java index 647eecaa..1c67661c 100644 --- a/src/test/java/com/moabam/support/fixture/CouponFixture.java +++ b/src/test/java/com/moabam/support/fixture/CouponFixture.java @@ -32,6 +32,18 @@ public static Coupon coupon() { .build(); } + public static Coupon coupon(String name, int startAt) { + return Coupon.builder() + .name(name) + .point(10) + .type(CouponType.MORNING_COUPON) + .stock(100) + .startAt(LocalDate.of(2023, startAt, 1)) + .openAt(LocalDate.of(2023, 1, 1)) + .adminId(1L) + .build(); + } + public static Coupon coupon(int point, int stock) { return Coupon.builder() .name("couponName") diff --git a/src/test/java/com/moabam/support/fixture/CouponWalletFixture.java b/src/test/java/com/moabam/support/fixture/CouponWalletFixture.java new file mode 100644 index 00000000..7fa78e1f --- /dev/null +++ b/src/test/java/com/moabam/support/fixture/CouponWalletFixture.java @@ -0,0 +1,38 @@ +package com.moabam.support.fixture; + +import static com.moabam.support.fixture.CouponFixture.*; + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.params.provider.Arguments; + +import com.moabam.api.domain.coupon.CouponWallet; + +public final class CouponWalletFixture { + + public static Stream provideCouponWalletByCouponId1_total5() { + return Stream.of(Arguments.of( + List.of( + CouponWallet.create(1L, coupon("c1", 1)), + CouponWallet.create(1L, coupon("c2", 2)), + CouponWallet.create(1L, coupon("c3", 3)), + CouponWallet.create(1L, coupon("c4", 4)), + CouponWallet.create(1L, coupon("c5", 5)) + )) + ); + } + + public static Stream provideCouponWalletAll() { + return Stream.of(Arguments.of( + List.of( + CouponWallet.create(1L, coupon("c2", 2)), + CouponWallet.create(2L, coupon("c3", 3)), + CouponWallet.create(2L, coupon("c4", 4)), + CouponWallet.create(3L, coupon("c5", 5)), + CouponWallet.create(3L, coupon("c6", 6)), + CouponWallet.create(3L, coupon("c7", 7)) + )) + ); + } +} diff --git a/src/test/java/com/moabam/support/fixture/PaymentFixture.java b/src/test/java/com/moabam/support/fixture/PaymentFixture.java index a47080ae..a46e4b7b 100644 --- a/src/test/java/com/moabam/support/fixture/PaymentFixture.java +++ b/src/test/java/com/moabam/support/fixture/PaymentFixture.java @@ -1,19 +1,41 @@ package com.moabam.support.fixture; +import static com.moabam.support.fixture.ProductFixture.*; + +import java.time.LocalDateTime; + +import com.moabam.api.domain.coupon.Coupon; import com.moabam.api.domain.payment.Order; import com.moabam.api.domain.payment.Payment; +import com.moabam.api.domain.payment.PaymentStatus; import com.moabam.api.domain.product.Product; +import com.moabam.api.dto.payment.ConfirmPaymentRequest; +import com.moabam.api.dto.payment.ConfirmTossPaymentRequest; +import com.moabam.api.dto.payment.ConfirmTossPaymentResponse; public final class PaymentFixture { + public static final String PAYMENT_KEY = "payment_key_123"; public static final String ORDER_ID = "random_order_id_123"; + public static final int AMOUNT = 3000; public static Payment payment(Product product) { return Payment.builder() .memberId(1L) .product(product) .order(order(product)) - .amount(product.getPrice()) + .totalAmount(product.getPrice()) + .build(); + } + + public static Payment paymentWithCoupon(Product product, Coupon coupon, Long couponWalletId) { + return Payment.builder() + .memberId(1L) + .product(product) + .couponWalletId(couponWalletId) + .order(order(product)) + .totalAmount(product.getPrice()) + .discountAmount(coupon.getPoint()) .build(); } @@ -22,4 +44,31 @@ public static Order order(Product product) { .name(product.getName()) .build(); } + + public static ConfirmPaymentRequest confirmPaymentRequest() { + return ConfirmPaymentRequest.builder() + .paymentKey(PAYMENT_KEY) + .orderId(ORDER_ID) + .amount(AMOUNT) + .build(); + } + + public static ConfirmTossPaymentRequest confirmTossPaymentRequest() { + return ConfirmTossPaymentRequest.builder() + .paymentKey(PAYMENT_KEY) + .orderId(ORDER_ID) + .amount(AMOUNT) + .build(); + } + + public static ConfirmTossPaymentResponse confirmTossPaymentResponse() { + return ConfirmTossPaymentResponse.builder() + .paymentKey(PAYMENT_KEY) + .orderId(ORDER_ID) + .orderName(BUG_PRODUCT_NAME) + .status(PaymentStatus.DONE) + .totalAmount(AMOUNT) + .approvedAt(LocalDateTime.of(2023, 1, 1, 1, 1)) + .build(); + } } diff --git a/src/test/java/com/moabam/support/fixture/CouponSnippetFixture.java b/src/test/java/com/moabam/support/snippet/CouponSnippet.java similarity index 97% rename from src/test/java/com/moabam/support/fixture/CouponSnippetFixture.java rename to src/test/java/com/moabam/support/snippet/CouponSnippet.java index 40c7c63a..e1e4697f 100644 --- a/src/test/java/com/moabam/support/fixture/CouponSnippetFixture.java +++ b/src/test/java/com/moabam/support/snippet/CouponSnippet.java @@ -1,4 +1,4 @@ -package com.moabam.support.fixture; +package com.moabam.support.snippet; import static org.springframework.restdocs.payload.JsonFieldType.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; @@ -7,7 +7,7 @@ import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.restdocs.snippet.Snippet; -public final class CouponSnippetFixture { +public final class CouponSnippet { public static final RequestFieldsSnippet CREATE_COUPON_REQUEST = requestFields( fieldWithPath("name").type(STRING).description("쿠폰명"), diff --git a/src/test/java/com/moabam/support/snippet/CouponWalletSnippet.java b/src/test/java/com/moabam/support/snippet/CouponWalletSnippet.java new file mode 100644 index 00000000..f6d260be --- /dev/null +++ b/src/test/java/com/moabam/support/snippet/CouponWalletSnippet.java @@ -0,0 +1,18 @@ +package com.moabam.support.snippet; + +import static org.springframework.restdocs.payload.JsonFieldType.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; + +import org.springframework.restdocs.payload.ResponseFieldsSnippet; + +public final class CouponWalletSnippet { + + public static final ResponseFieldsSnippet COUPON_WALLET_RESPONSE = responseFields( + fieldWithPath("[].id").type(NUMBER).description("쿠폰 ID"), + fieldWithPath("[].name").type(STRING).description("쿠폰명"), + fieldWithPath("[].description").type(STRING).description("쿠폰에 대한 간단 소개 (NULL 가능)"), + fieldWithPath("[].point").type(NUMBER).description("쿠폰 사용 시, 제공하는 포인트량"), + fieldWithPath("[].type").type(STRING) + .description("쿠폰 종류 (MORNING_COUPON, NIGHT_COUPON, GOLDEN_COUPON, DISCOUNT_COUPON)") + ); +} diff --git a/src/test/java/com/moabam/support/fixture/ErrorSnippetFixture.java b/src/test/java/com/moabam/support/snippet/ErrorSnippet.java similarity index 83% rename from src/test/java/com/moabam/support/fixture/ErrorSnippetFixture.java rename to src/test/java/com/moabam/support/snippet/ErrorSnippet.java index 4c41f198..69e2c5d3 100644 --- a/src/test/java/com/moabam/support/fixture/ErrorSnippetFixture.java +++ b/src/test/java/com/moabam/support/snippet/ErrorSnippet.java @@ -1,11 +1,11 @@ -package com.moabam.support.fixture; +package com.moabam.support.snippet; import static org.springframework.restdocs.payload.JsonFieldType.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; import org.springframework.restdocs.snippet.Snippet; -public class ErrorSnippetFixture { +public class ErrorSnippet { public static final Snippet ERROR_MESSAGE_RESPONSE = responseFields( fieldWithPath("message").type(STRING).description("에러 메시지") diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 3497c521..32bd980a 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -62,3 +62,9 @@ token: secret-key: testestestestestestestestestesttestestestestestestestestestest allow: "" + +# Payment +payment: + toss: + base-url: "https://api.tosspayments.com" + secret-key: "test_sk_4yKeq5bgrpWk4XYdDoBxVGX0lzW6:"