Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 할인 쿠폰 발급 #82

Merged
merged 53 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
58b40e9
[test] CouponIssuanceTest 구현
kimhyun5u Aug 15, 2024
2a9ea1d
[feat] CouponIssuance 구현
kimhyun5u Aug 15, 2024
b18a1c4
[fix] DuplicateCouponTitleException 수정
kimhyun5u Aug 15, 2024
3006357
[test] CouponIssuanceTest 수정
kimhyun5u Aug 15, 2024
d2f5b69
[feat] CouponErrorCode 수정
kimhyun5u Aug 15, 2024
7b968c5
[feat] ExpiredCouponException 구현
kimhyun5u Aug 15, 2024
3d94a01
[fix] CouponIssuanceTest 테스트 조건 수정
kimhyun5u Aug 15, 2024
a78ffb6
[feat] CouponIssuanceErrorCode 구현
kimhyun5u Aug 15, 2024
0edbd05
[fix] ExpiredCouponException 수정
kimhyun5u Aug 15, 2024
dcc1a5b
[feat] InvalidICreationIssuanceException 구현
kimhyun5u Aug 15, 2024
ab07e62
[feat] CouponIssuanceValidator 구현
kimhyun5u Aug 15, 2024
c7300a5
[fix] CouponIssuanceErrorCode 수정
kimhyun5u Aug 15, 2024
f912959
[feat] Coupon 수정
kimhyun5u Aug 15, 2024
4f5ed5b
[feat] CouponIssuance 수정
kimhyun5u Aug 15, 2024
eeaff6c
[fix] CouponErrorCode 수정
kimhyun5u Aug 15, 2024
2d0c017
[fix] TestCoupon 수정
kimhyun5u Aug 15, 2024
fa6f35a
[fix] CouponIssuanceTest 수정
kimhyun5u Aug 15, 2024
55999ee
[feat] CouponIssuanceRepository 구현
kimhyun5u Aug 15, 2024
776eedd
[fix] CustomerRepository 수정
kimhyun5u Aug 15, 2024
e2bcbf5
[test] IssueCouponServiceTest 구현
kimhyun5u Aug 15, 2024
3c542d3
[feat] TestCustomer 구현
kimhyun5u Aug 15, 2024
60e55cb
[feat] TestCouponIssuance 구현
kimhyun5u Aug 15, 2024
908f356
[feat] CustomerFixture 수정
kimhyun5u Aug 15, 2024
203b319
[feat] CouponIssuanceFixture 구현
kimhyun5u Aug 15, 2024
e4ba264
[feat] IssueCouponCommand 구현
kimhyun5u Aug 15, 2024
d44df70
[feat] IssueCouponService 구현
kimhyun5u Aug 15, 2024
afafcd5
[test] IssueCouponServiceTest 수정
kimhyun5u Aug 15, 2024
a322afb
[docs] IssueCouponService 주석 수정
kimhyun5u Aug 15, 2024
6203301
[test] IssueCouponServiceTest 수정
kimhyun5u Aug 15, 2024
601bd0d
[feat] TestCoupon 수정
kimhyun5u Aug 15, 2024
e39f161
[refactor] CouponFixture 수정
kimhyun5u Aug 15, 2024
10c0613
[feat] IssueCouponService 수정
kimhyun5u Aug 15, 2024
f05341e
[feat] InsufficientCouponQuantityException 구현
kimhyun5u Aug 15, 2024
19fae51
[feat] CouponErrorCode 수정
kimhyun5u Aug 15, 2024
fd797a8
[feat] CouponIssuance 수정
kimhyun5u Aug 15, 2024
f1ea78f
[feat] Coupon 수정
kimhyun5u Aug 15, 2024
5424807
[test] IssueCouponServiceIntegrationTest 구현
kimhyun5u Aug 15, 2024
8e1ad32
[feat] CouponFixture 수정
kimhyun5u Aug 15, 2024
035af13
[feat] CouponRepository 수정
kimhyun5u Aug 15, 2024
dba84a2
[feat] IssueCouponService 수정
kimhyun5u Aug 15, 2024
b42a7f2
[docs] CouponApiControllerTest 수정
kimhyun5u Aug 15, 2024
c148289
[fix] CouponApiControllerTest 수정
kimhyun5u Aug 15, 2024
10143d9
[test] CouponApiControllerTest 수정
kimhyun5u Aug 15, 2024
435e59d
[fix] CouponApiControllerTest 수정
kimhyun5u Aug 15, 2024
b72fbc0
[feat] IssueCouponResponse 구현
kimhyun5u Aug 15, 2024
ace0702
[feat] CouponApiController 수정
kimhyun5u Aug 15, 2024
8aff497
[feat] IssueCouponService 수정
kimhyun5u Aug 15, 2024
588a223
[docs] IssueCouponService 수정
kimhyun5u Aug 15, 2024
4df13ff
[test] CouponApiControllerTest 수정
kimhyun5u Aug 15, 2024
bb1650e
[feat] CouponExceptionHandler 수정
kimhyun5u Aug 15, 2024
f770f0d
[fix] IssueCouponServiceTest 수정
kimhyun5u Aug 15, 2024
49b9fef
[merge] remote-tracking branch 'origin/main' into feature/75_kimhyun5…
kimhyun5u Aug 15, 2024
94cfa48
[feat] CouponIssuance 수정
kimhyun5u Aug 16, 2024
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
16 changes: 16 additions & 0 deletions src/main/java/camp/woowak/lab/coupon/domain/Coupon.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import org.springframework.format.annotation.DateTimeFormat;

import camp.woowak.lab.coupon.exception.InsufficientCouponQuantityException;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
Expand Down Expand Up @@ -47,4 +48,19 @@ public Coupon(String title, int discountAmount, int quantity, LocalDateTime expi
this.quantity = quantity;
this.expiredAt = expiredAt;
}

public boolean isExpired() {
return expiredAt.isBefore(LocalDateTime.now());
}

public boolean hasAvailableQuantity() {
return quantity > 0;
}

public void decreaseQuantity() {
if (!hasAvailableQuantity()) {
throw new InsufficientCouponQuantityException("quantity is not enough");
}
quantity--;
}
}
51 changes: 51 additions & 0 deletions src/main/java/camp/woowak/lab/coupon/domain/CouponIssuance.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package camp.woowak.lab.coupon.domain;

import java.time.LocalDateTime;

import org.springframework.data.annotation.CreatedDate;

import camp.woowak.lab.customer.domain.Customer;
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.ManyToOne;
import lombok.Getter;

@Entity
@Getter
public class CouponIssuance {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@JoinColumn(name = "coupon_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private Coupon coupon;

@JoinColumn(name = "customer_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;

@Column(nullable = false, updatable = false)
@CreatedDate
private LocalDateTime issuedAt;

@Column
private LocalDateTime usedAt;

protected CouponIssuance() {
}

public CouponIssuance(Coupon coupon, Customer customer) {
CouponIssuanceValidator.validate(customer, coupon);
// coupon 수량 감소
coupon.decreaseQuantity();
this.coupon = coupon;
this.customer = customer;
this.issuedAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package camp.woowak.lab.coupon.domain;

import camp.woowak.lab.coupon.exception.ExpiredCouponException;
import camp.woowak.lab.coupon.exception.InvalidICreationIssuanceException;
import camp.woowak.lab.customer.domain.Customer;

public class CouponIssuanceValidator {
private CouponIssuanceValidator() {
}

public static void validate(Customer customer, Coupon coupon) {
validateNotNull(customer, coupon);
validateNotExpired(coupon);

}

private static void validateNotExpired(Coupon coupon) {
if (coupon.isExpired()) {
throw new ExpiredCouponException("만료된 쿠폰입니다.");
}
}

private static void validateNotNull(Object... objects) {
for (var object : objects) {
if (object == null) {
throw new InvalidICreationIssuanceException("쿠폰 발급에 필요한 정보가 없습니다.");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
public enum CouponErrorCode implements ErrorCode {
INVALID_CREATION(HttpStatus.BAD_REQUEST, "cp_1_1", "잘못된 요청입니다."),
DUPLICATE_COUPON_TITLE(HttpStatus.CONFLICT, "cp_1_2", "중복된 쿠폰 제목입니다."),
INSUFFICIENT_QUANTITY(HttpStatus.CONFLICT, "cp_1_3", "발급 가능한 쿠폰이 부족합니다."),
;

int status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package camp.woowak.lab.coupon.exception;

import org.springframework.http.HttpStatus;

import camp.woowak.lab.common.exception.ErrorCode;

public enum CouponIssuanceErrorCode implements ErrorCode {
INVALID_ISSUANCE(HttpStatus.BAD_REQUEST, "ci_1_1", "잘못된 발급 요청입니다."),
NOT_FOUND_COUPON(HttpStatus.NOT_FOUND, "ci_1_2", "존재하지 않는 쿠폰입니다."),
EXPIRED_COUPON(HttpStatus.CONFLICT, "ci_1_3", "만료된 쿠폰입니다."),
NOT_ENOUGH_COUPON(HttpStatus.CONFLICT, "ci_1_4", "쿠폰이 부족합니다."),
;

int status;
String errorCode;
String message;

CouponIssuanceErrorCode(HttpStatus status, String errorCode, String message) {
this.status = status.value();
this.errorCode = errorCode;
this.message = message;
}

@Override
public int getStatus() {
return status;
}

@Override
public String getErrorCode() {
return errorCode;
}

@Override
public String getMessage() {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package camp.woowak.lab.coupon.exception;

import camp.woowak.lab.common.exception.BadRequestException;
import camp.woowak.lab.common.exception.ConflictException;

public class DuplicateCouponTitleException extends BadRequestException {
public class DuplicateCouponTitleException extends ConflictException {
public DuplicateCouponTitleException(String message) {
super(CouponErrorCode.DUPLICATE_COUPON_TITLE, message);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package camp.woowak.lab.coupon.exception;

import camp.woowak.lab.common.exception.ConflictException;

public class ExpiredCouponException extends ConflictException {
public ExpiredCouponException(String message) {
super(CouponIssuanceErrorCode.EXPIRED_COUPON, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package camp.woowak.lab.coupon.exception;

import camp.woowak.lab.common.exception.ConflictException;

public class InsufficientCouponQuantityException extends ConflictException {
public InsufficientCouponQuantityException(String message) {
super(CouponErrorCode.INSUFFICIENT_QUANTITY, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package camp.woowak.lab.coupon.exception;

import camp.woowak.lab.common.exception.BadRequestException;

public class InvalidICreationIssuanceException extends BadRequestException {
public InvalidICreationIssuanceException(String message) {
super(CouponIssuanceErrorCode.INVALID_ISSUANCE, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package camp.woowak.lab.coupon.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import camp.woowak.lab.coupon.domain.CouponIssuance;

public interface CouponIssuanceRepository extends JpaRepository<CouponIssuance, Long> {
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package camp.woowak.lab.coupon.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;

import camp.woowak.lab.coupon.domain.Coupon;
import jakarta.persistence.LockModeType;

public interface CouponRepository extends JpaRepository<Coupon, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT c FROM Coupon c WHERE c.id = :id")
Optional<Coupon> findByIdWithPessimisticLock(Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package camp.woowak.lab.coupon.service;

import org.springframework.stereotype.Service;

import camp.woowak.lab.coupon.domain.Coupon;
import camp.woowak.lab.coupon.domain.CouponIssuance;
import camp.woowak.lab.coupon.exception.ExpiredCouponException;
import camp.woowak.lab.coupon.exception.InsufficientCouponQuantityException;
import camp.woowak.lab.coupon.exception.InvalidICreationIssuanceException;
import camp.woowak.lab.coupon.repository.CouponIssuanceRepository;
import camp.woowak.lab.coupon.repository.CouponRepository;
import camp.woowak.lab.coupon.service.command.IssueCouponCommand;
import camp.woowak.lab.customer.domain.Customer;
import camp.woowak.lab.customer.repository.CustomerRepository;
import jakarta.transaction.Transactional;

@Service
public class IssueCouponService {
private final CouponIssuanceRepository couponIssuanceRepository;
private final CouponRepository couponRepository;
private final CustomerRepository customerRepository;

public IssueCouponService(CouponIssuanceRepository couponIssuanceRepository, CouponRepository couponRepository,
CustomerRepository customerRepository) {
this.couponIssuanceRepository = couponIssuanceRepository;
this.couponRepository = couponRepository;
this.customerRepository = customerRepository;
}

/**
*
* @throws InvalidICreationIssuanceException customer 또는 coupon이 존재하지 않을 경우
* @throws ExpiredCouponException coupon이 만료되었을 경우
* @throws InsufficientCouponQuantityException coupon 수량이 부족할 경우
*/
@Transactional
public Long issueCoupon(IssueCouponCommand cmd) {
// customer 조회
Customer targetCustomer = customerRepository.findById(cmd.customerId())
.orElseThrow(() -> new InvalidICreationIssuanceException("customer not found"));

// coupon 조회
Coupon targetCoupon = couponRepository.findByIdWithPessimisticLock(cmd.couponId())
.orElseThrow(() -> new InvalidICreationIssuanceException("coupon not found"));

// coupon 수량 확인
if (!targetCoupon.hasAvailableQuantity()) {
throw new InsufficientCouponQuantityException("quantity of coupon is insufficient");
}

// coupon issuance 생성
CouponIssuance newCouponIssuance = new CouponIssuance(targetCoupon, targetCustomer);

// coupon issuance 저장
return couponIssuanceRepository.save(newCouponIssuance).getId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package camp.woowak.lab.coupon.service.command;

import java.util.UUID;

public record IssueCouponCommand(UUID customerId, Long couponId) {
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package camp.woowak.lab.customer.repository;

import java.util.Optional;
import java.util.UUID;

import org.springframework.data.jpa.repository.JpaRepository;

import camp.woowak.lab.customer.domain.Customer;

public interface CustomerRepository extends JpaRepository<Customer, Long> {
public interface CustomerRepository extends JpaRepository<Customer, UUID> {
Optional<Customer> findByEmail(String email);
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
package camp.woowak.lab.web.api.coupon;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import camp.woowak.lab.coupon.service.CreateCouponService;
import camp.woowak.lab.coupon.service.IssueCouponService;
import camp.woowak.lab.coupon.service.command.CreateCouponCommand;
import camp.woowak.lab.coupon.service.command.IssueCouponCommand;
import camp.woowak.lab.web.authentication.LoginCustomer;
import camp.woowak.lab.web.authentication.annotation.AuthenticationPrincipal;
import camp.woowak.lab.web.dto.request.coupon.CreateCouponRequest;
import camp.woowak.lab.web.dto.response.coupon.CreateCouponResponse;
import camp.woowak.lab.web.dto.response.coupon.IssueCouponResponse;
import jakarta.validation.Valid;

@RestController
public class CouponApiController {
private final CreateCouponService createCouponService;
private final IssueCouponService issueCouponService;

public CouponApiController(CreateCouponService createCouponService) {
public CouponApiController(CreateCouponService createCouponService, IssueCouponService issueCouponService) {
this.createCouponService = createCouponService;
this.issueCouponService = issueCouponService;
}

@PostMapping("/coupons")
Expand All @@ -31,4 +39,14 @@ public CreateCouponResponse createCoupon(@Valid @RequestBody CreateCouponRequest
return new CreateCouponResponse(couponId);
}

@PostMapping("/coupons/{couponId}/issue")
@ResponseStatus(HttpStatus.CREATED)
public IssueCouponResponse issueCoupon(@AuthenticationPrincipal LoginCustomer loginCustomer,
@PathVariable Long couponId) {
IssueCouponCommand cmd = new IssueCouponCommand(loginCustomer.getId(), couponId);

issueCouponService.issueCoupon(cmd);

return new IssueCouponResponse();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import camp.woowak.lab.common.advice.DomainExceptionHandler;
import camp.woowak.lab.common.exception.HttpStatusException;
import camp.woowak.lab.coupon.exception.DuplicateCouponTitleException;
import camp.woowak.lab.coupon.exception.ExpiredCouponException;
import camp.woowak.lab.coupon.exception.InvalidICreationIssuanceException;
import lombok.extern.slf4j.Slf4j;

@Slf4j
Expand All @@ -23,6 +25,22 @@ public ProblemDetail handleDuplicateCouponTitleException(DuplicateCouponTitleExc
return getProblemDetail(e, HttpStatus.CONFLICT);
}

/**
*
* InvalidICreationIssuanceException.class 를 처리한다.
*/
@ExceptionHandler(value = InvalidICreationIssuanceException.class)
public ProblemDetail handleInvalidICreationIssuanceException(InvalidICreationIssuanceException e) {
log.warn("Bad Request", e.getMessage());
return getProblemDetail(e, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(value = ExpiredCouponException.class)
public ProblemDetail handleExpiredCouponException(ExpiredCouponException e) {
log.warn("Conflict", e.getMessage());
return getProblemDetail(e, HttpStatus.CONFLICT);
}

private ProblemDetail getProblemDetail(HttpStatusException e, HttpStatus status) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(status, e.errorCode().getMessage());
problemDetail.setProperty("errorCode", e.errorCode().getErrorCode());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package camp.woowak.lab.web.dto.response.coupon;

public record IssueCouponResponse() {
}
Loading