Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import com.sparta.tdd.domain.payment.entity.Payment;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
Expand All @@ -18,4 +20,8 @@ void bulkSoftDeleteByUserId(
@Param("deletedAt") LocalDateTime deletedAt,
@Param("deletedBy") Long deletedBy
);

@SuppressWarnings("NullableProblems")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 경고 무시 어노테이션은 어떤 문제떄문에 작성해주신걸까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

order,user가 null 이 발생할 수 있다고 경고가 떠서 제외 시켰습니다!

결제 완료 된 경우는 order, user이 null 발생 할 수 없다고 생각하여 제외 시켰습니다.

@EntityGraph(attributePaths = {"order", "user"})
Optional<Payment> findById(UUID id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.sparta.tdd.domain.payment.enums.PaymentStatus;
import com.sparta.tdd.global.exception.BusinessException;
import com.sparta.tdd.global.exception.ErrorCode;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

Expand Down Expand Up @@ -38,17 +39,16 @@ private void processApproved(Payment payment, Order order) {

// 주문 상태 변경 (PENDING -> DELIVERED)
order.nextStatus();

// TODO: 포인트 적립
}

private void processCancelled(Payment payment, Order order) {
if (LocalDateTime.now().minusMinutes(5).isAfter(payment.getCreatedAt())) {
throw new BusinessException(ErrorCode.PAYMENT_CANCEL_TIME_EXPIRED);
}
// 주문 상태를 PENDING으로 복구
order.changeOrderStatus(OrderStatus.PENDING);

// 환불처리는 진행 된 것으로 가정하겠습니다.

// TODO 재고 복구, 적립된 포인트 회수 등
}

private void processFailed(Payment payment, Order order) {
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/com/sparta/tdd/domain/point/dto/PointRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.sparta.tdd.domain.point.dto;

import com.sparta.tdd.domain.point.enums.PointType;
import com.sparta.tdd.domain.user.entity.User;
import java.util.UUID;
import lombok.Builder;

@Builder
public record PointRequest(
User user,
UUID referenceId,
PointType type,
Long amount,
String description
) {

public static PointRequest forPayment(User user, UUID orderId, Long amount,
String description) {
return PointRequest.builder()
.user(user)
.referenceId(orderId)
.amount(amount)
.type(PointType.PAYMENT_EARNED)
.description(description)
.build();
}

public static PointRequest forReview(User user, UUID reviewId, Long amount,
String description) {
return PointRequest.builder()
.user(user)
.referenceId(reviewId)
.amount(amount)
.type(PointType.REVIEW_EARNED)
.description(description)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.sparta.tdd.domain.point.entity;

import com.sparta.tdd.domain.point.dto.PointRequest;
import com.sparta.tdd.domain.point.enums.PointType;
import com.sparta.tdd.global.model.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Table(name = "p_point_history")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PointHistory extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "point_history_id")
private UUID id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "point_wallet_id")
private PointWallet wallet;

@Column(name = "reference_id")
private UUID referenceId;

@Column(name = "amount")
private Long amount;

@Column(name = "type")
@Enumerated(EnumType.STRING)
private PointType type;

@Column(name = "description")
private String description;

@Column(name = "expire_at")
private LocalDateTime expireAt;

@Builder
private PointHistory(PointWallet wallet, UUID referenceId, Long amount,
PointType type, String description, LocalDateTime expireAt) {
this.wallet = wallet;
this.referenceId = referenceId;
this.amount = amount;
this.type = type;
this.description = description;
this.expireAt = expireAt;
}

public static PointHistory create(PointWallet wallet, PointRequest request,
LocalDateTime expireAt) {
return PointHistory.builder()
.wallet(wallet)
.referenceId(request.referenceId())
.amount(request.amount())
.type(request.type())
.description(request.description())
.expireAt(expireAt)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.sparta.tdd.domain.point.entity;

import com.sparta.tdd.domain.user.entity.User;
import com.sparta.tdd.global.model.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Table(name = "p_point_wallet")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PointWallet extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "point_wallet_id", nullable = false, updatable = false)
private UUID id;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

@Column(name = "balance")
private Long balance;

@Builder
public PointWallet(User user) {
this.user = user;
this.balance = 0L;
}

public void addBalance(Long earnAmount) {
this.balance += earnAmount;
}

public Long subtractBalance(Long amount) {
if (this.balance < amount) {
Long currentBalance = this.balance;
this.balance = 0L;
return amount - currentBalance;
}
this.balance -= amount;
return amount;
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/sparta/tdd/domain/point/enums/PointType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.sparta.tdd.domain.point.enums;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum PointType {
PAYMENT_EARNED("결제 적립"),
REVIEW_EARNED("리뷰 적립"),
PAYMENT_CANCELLED("결제 취소"),
USED("사용"),
EXPIRED("만료");

private final String description;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.sparta.tdd.domain.point.repository;

import com.sparta.tdd.domain.point.entity.PointHistory;
import com.sparta.tdd.domain.point.enums.PointType;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PointHistoryRepository extends JpaRepository<PointHistory, Long> {

boolean existsByReferenceIdAndTypeAndDeletedAtIsNull(UUID referenceId, PointType type);

Optional<PointHistory> findByReferenceIdAndTypeAndDeletedAtIsNull(UUID paymentId,
PointType pointType);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.sparta.tdd.domain.point.repository;

import com.sparta.tdd.domain.point.entity.PointWallet;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PointWalletRepository extends JpaRepository<PointWallet, Long> {

Optional<PointWallet> findByUserId(Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.sparta.tdd.domain.point.service;

import com.sparta.tdd.domain.point.dto.PointRequest;
import com.sparta.tdd.domain.point.entity.PointHistory;
import com.sparta.tdd.domain.point.entity.PointWallet;
import com.sparta.tdd.domain.point.enums.PointType;
import com.sparta.tdd.domain.point.repository.PointHistoryRepository;
import com.sparta.tdd.domain.point.repository.PointWalletRepository;
import com.sparta.tdd.domain.user.entity.User;
import com.sparta.tdd.global.exception.BusinessException;
import com.sparta.tdd.global.exception.ErrorCode;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class PointService {

private final PointWalletRepository walletRepository;
private final PointHistoryRepository historyRepository;

@Transactional
public void earnPoints(PointRequest request) {
if (isDuplicateEarn(request.referenceId(), request.type())) {
return;
}

PointWallet wallet = getOrCreateWallet(request.user());
wallet.addBalance(request.amount());

PointHistory pointHistory = PointHistory.create(wallet, request,
LocalDateTime.now().plusYears(1));

historyRepository.save(pointHistory);
}

@Transactional
public void losePoints(User user, UUID paymentId) {
PointWallet wallet = findWalletById(user.getId());
PointHistory pointHistory = findPointHistory(paymentId);
Long usedAmount = wallet.subtractBalance(pointHistory.getAmount());

PointHistory cancelledHistory = PointHistory.builder()
.wallet(wallet)
.referenceId(paymentId)
.amount(usedAmount)
.type(PointType.PAYMENT_CANCELLED)
.description("결제 취소 (결제번호: " + paymentId + ")")
.expireAt(LocalDateTime.now())
.build();

historyRepository.save(cancelledHistory);
}

private PointWallet findWalletById(Long userId) {
return walletRepository.findByUserId(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
}

private PointHistory findPointHistory(UUID paymentId) {
return historyRepository.findByReferenceIdAndTypeAndDeletedAtIsNull(paymentId,
PointType.PAYMENT_EARNED)
.orElseThrow(() -> new BusinessException(ErrorCode.PAYMENT_NOT_FOUND));
}


private PointWallet getOrCreateWallet(User user) {
return walletRepository.findByUserId(user.getId())
.orElseGet(() -> walletRepository.save(PointWallet.builder()
.user(user)
.build()));
}

private boolean isDuplicateEarn(UUID referenceId, PointType type) {
if (referenceId == null || type == null) {
return false;
}
return historyRepository.existsByReferenceIdAndTypeAndDeletedAtIsNull(referenceId, type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ void bulkSoftDeleteByUserId(
@Query("SELECT r.id FROM Review r WHERE r.store.id IN :storeIds AND r.deletedAt IS NULL")
List<UUID> findReviewIdsByStoreIds(@Param("storeIds") List<UUID> storeIds);

@Query("SELECT COUNT(r) > 0 FROM Review r WHERE r.order.id = :orderId AND r.deletedAt IS NULL")
boolean existsByOrderId(@Param("orderId") UUID orderId);

@Query("SELECT COUNT(r) FROM Review r WHERE r.store.id = :storeId AND r.deletedAt IS NULL")
Long countByStoreIdAndNotDeleted(@Param("storeId") UUID storeId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public ReviewResponseDto createReview(Long userId, UUID orderId, ReviewRequestDt
Store store = findStoreById(request.storeId());
Order order = findOrderById(orderId);

existsByOrderId(orderId);

Review review = request.toEntity(user, store, order);

Review savedReview = reviewRepository.save(review);
Expand Down Expand Up @@ -165,4 +167,10 @@ private Order findOrderById(UUID orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new BusinessException(ErrorCode.ORDER_NOT_FOUND));
}

private void existsByOrderId(UUID orderId) {
if (reviewRepository.existsByOrderId(orderId)) {
throw new BusinessException(ErrorCode.DUPLICATE_REVIEW);
}
}
}
Loading