-
Notifications
You must be signed in to change notification settings - Fork 78
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[1단계 - 복제 지연] 몰리(김지민) 미션 제출합니다. (#41)
* docs(STEP1.md): 요구사항 분석 * feat(Name): 쿠폰 이름 VO 구현 * feat(DiscountAmountTest): 쿠폰 할인 금액 VO 구현 * feat(MinimumOrderAmount): 최소 주문 금액 VO 구현 * refactor(DiscountAmount, Name): VO record로 변경 * feat(Category): Category Enum 구현 * feat(DiscountRate): 할인율 VO 구현 * feat(IssuancePeriod): 발급 기간 VO 구현 * feat(Coupon): Coupon 엔티티 구현 * feat(Member): Member 엔티티 구현 * feat(BaseTimeEntity): BaseTimeEntity 구현 * feat(ExpirationDate): 회원 쿠폰 만료일 VO 구현 * feat(MemberCoupon): MemberCoupon 엔티티 구현 * feat(/src): 패키지 구조 개 * refactor(/domain): 변수명을 명확하게 변경 * fix(application.yml): hbm2ddl 설정을 update로 변경 * feat(CouponService): 쿠폰 생성, 조회 구현 * chore(/datasource): DataSource 설정 * feat(CouponFixture): 테스트용 픽스처 생성 * test(CouponServiceTest): 쿠폰 복제 지연 테스트 * refactor(DataSourceConfiguration): 상수 추출 * feat(DiscountRate): 할인율을 Integer로 변경 * feat(DiscountRate): 할인율을 Integer로 변경 * refactor(DiscountRate): 할인율을 도메인의 책임으로 * feat(TransactionRouter): WriteDB로 요청을 보내는 객체 추가 * feat(CouponService): read 실패 시 write로 요청을 보내도록 * refactor(/src): 패키지 개선 * refactor(IssuancePeriod): 발급 시간 확인 로직 개선 * refactor(IssuancePeriod): @DisplayName 추가 * refactor(ExpirationDate): 만료 기간 확인 로직 개선 * refactor(DiscountAmount, MinimumOrderAmount): 금액 타입을 Long으로 일치 * fix: 물리 네이밍 방식을 카멜 케이스를 언더스코어로 변경해주는 전략으로 변경 * refactor(/domain): 금액 출력 형식을 상수와 포맷 문자열을 사용하여 개선 * refactor(/domain): 아이디 생성 전략을 IDENTITY로 변 * refactor(DataSourceConfiguration): Datasource 빈 주입 방식 변경 * refactor(/domain): 객체 타입 null 체크 추가
- Loading branch information
Showing
28 changed files
with
776 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# 요구사항 분석 | ||
- [x] 도메인 설계 | ||
- [x] 엔티티 설계 및 구현 | ||
- [x] 쿠폰 생성 구현 | ||
- [x] 쿠폰 조회 기능 구현 | ||
- [x] 복제 지연 이슈 확인 | ||
- [x] 복제 지연 해결 | ||
|
||
## 도메인 설계 | ||
|
||
### 쿠폰 | ||
|
||
회원에게 발급하는 쿠폰의 이름, 할인 금액, 최소 주문 금액 등 쿠폰의 설정에 해당하는 내용을 관리한다. | ||
|
||
- 이름 | ||
- 이름은 반드시 존재해야 한다. | ||
- 이름의 길이는 최대 30자 이하여야 한다. | ||
- 할인 금액 | ||
- 할인 금액은 1,000원 이상이어야 한다. | ||
- 할인 금액은 10,000원 이하여야 한다. | ||
- 할인 금액은 500원 단위로 설정할 수 있다. | ||
- 할인율 제약 | ||
- 할인율은 할인금액 / 최소 주문 금액 식을 사용하여 계산하며, 소수점은 버림 한다. | ||
- 예를 들어, 최소 주문 금액이 30,000원일 때 할인 금액이 1,000원이라면 쿠폰의 할인율은 3%(1000 / 30000 = 3.333...%)가 된다. | ||
- 할인율은 3% 이상이어야 한다. | ||
- 할인율은 20% 이하여야 한다. | ||
- 최소 주문 금액 | ||
- 최소 주문 금액은 5,000원 이상이어야 한다. | ||
- 최소 주문 금액은 100,000원 이하여야 한다. | ||
- 카테고리 | ||
- 서비스의 카테고리는 다음과 같이 네 종류가 있고, 이 중 하나의 카테고리만 선택할 수 있다. | ||
- 패션, 가전, 가구, 식품 | ||
- 발급 기간 | ||
- 시작일은 종료일보다 이전이어야 한다. 시작일과 종료일이 같다면, 해당 일에만 발급할 수 있는 쿠폰이 된다. | ||
- 시작일 00:00:00.000000 부터 발급할 수 있다. | ||
- 종료일 23:59:59.999999 까지 발급할 수 있다. | ||
|
||
### 회원 쿠폰 | ||
회원에게 발급된 쿠폰 한 장을 관리한다. 다음 정보를 포함한다. | ||
|
||
- 회원 쿠폰 PK | ||
- 쿠폰 PK | ||
- 회원 PK | ||
- 사용 여부 | ||
- 발급 일시 | ||
- 만료 일시 | ||
- 회원에게 발급된 쿠폰은 발급일 포함 7일 동안 사용 가능하다. | ||
- 만료 일의 23:59:59.999999 까지 사용할 수 있다. |
31 changes: 31 additions & 0 deletions
31
src/main/java/coupon/coupon/application/CouponService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package coupon.coupon.application; | ||
|
||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
import coupon.coupon.domain.Coupon; | ||
import coupon.coupon.domain.CouponRepository; | ||
import coupon.infrastructure.datasource.TransactionRouter; | ||
import lombok.AllArgsConstructor; | ||
|
||
@Service | ||
@AllArgsConstructor | ||
public class CouponService { | ||
|
||
private final CouponRepository couponRepository; | ||
private final TransactionRouter transactionRouter; | ||
|
||
public void create(final Coupon coupon) { | ||
couponRepository.save(coupon); | ||
} | ||
|
||
@Transactional(readOnly = true) | ||
public Coupon getCoupon(final Long id) { | ||
return couponRepository.findById(id) | ||
.orElseGet(() -> getCouponFromWrite(id)); | ||
} | ||
|
||
public Coupon getCouponFromWrite(final Long id) { | ||
return transactionRouter.routeWrite(() -> couponRepository.fetchById(id)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package coupon.coupon.domain; | ||
|
||
public enum Category { | ||
PASSION, | ||
HOME_APPLIANCE, | ||
FURNITURE, | ||
FOOD, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package coupon.coupon.domain; | ||
|
||
import java.time.LocalDate; | ||
|
||
import jakarta.persistence.Column; | ||
import jakarta.persistence.Embedded; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.GeneratedValue; | ||
import jakarta.persistence.GenerationType; | ||
import jakarta.persistence.Id; | ||
import jakarta.persistence.Table; | ||
|
||
import coupon.infrastructure.audit.BaseTimeEntity; | ||
import lombok.AccessLevel; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Entity | ||
@Getter | ||
@AllArgsConstructor | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
@Table(name = "coupon") | ||
public class Coupon extends BaseTimeEntity { | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
@Column(name = "id", nullable = false) | ||
private Long id; | ||
|
||
@Column(name = "name", nullable = false) | ||
private Name name; | ||
|
||
@Column(name = "discount_mount", nullable = false) | ||
@Embedded | ||
private DiscountAmount discountAmount; | ||
|
||
@Column(name = "discount_rate", nullable = false) | ||
@Embedded | ||
private DiscountRate discountRate; | ||
|
||
@Column(name = "minimum_order_amount", nullable = false) | ||
@Embedded | ||
private MinimumOrderAmount minimumOrderAmount; | ||
|
||
@Column(name = "issuance_period", nullable = false) | ||
@Embedded | ||
private IssuancePeriod issuancePeriod; | ||
|
||
public Coupon( | ||
final String name, | ||
final Long discountAmount, | ||
final Long minimumOrderAmount, | ||
final LocalDate issueStartDate, | ||
final LocalDate issueEndDate | ||
) { | ||
this.name = new Name(name); | ||
this.discountAmount = new DiscountAmount(discountAmount); | ||
this.discountRate = DiscountRate.from(discountAmount, minimumOrderAmount); | ||
this.minimumOrderAmount = new MinimumOrderAmount(minimumOrderAmount); | ||
this.issuancePeriod = new IssuancePeriod(issueStartDate, issueEndDate); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package coupon.coupon.domain; | ||
|
||
import java.util.Optional; | ||
|
||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
public interface CouponRepository extends JpaRepository<Coupon, Long> { | ||
|
||
default Coupon fetchById(Long id) { | ||
return findById(id).orElseThrow(() -> new IllegalArgumentException("해당하는 쿠폰이 없습니다. id: " + id)); | ||
} | ||
|
||
Optional<Coupon> findById(Long id); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package coupon.coupon.domain; | ||
|
||
import jakarta.persistence.Embeddable; | ||
|
||
@Embeddable | ||
public record DiscountAmount(Long discountAmount) { | ||
|
||
private static final Long MINIMUM_DISCOUNT_AMOUNT = 1_000L; | ||
private static final Long MAXIMUM_DISCOUNT_AMOUNT = 10_000L; | ||
private static final Long DISCOUNT_AMOUNT_UNIT = 500L; | ||
|
||
public DiscountAmount { | ||
validateDiscountAmountNull(discountAmount); | ||
validateDiscountAmountRange(discountAmount); | ||
validateDiscountUnit(discountAmount); | ||
} | ||
|
||
private void validateDiscountAmountNull(final Long discountAmount) { | ||
if (discountAmount == null) { | ||
throw new IllegalArgumentException("할인 금액은 null이 될 수 없습니다."); | ||
} | ||
} | ||
|
||
private void validateDiscountAmountRange(final Long discountAmount) { | ||
if (discountAmount > MAXIMUM_DISCOUNT_AMOUNT || discountAmount < MINIMUM_DISCOUNT_AMOUNT) { | ||
throw new IllegalArgumentException(String.format("할인 금액은 %,d원 이상 %,d원 이하만 가능합니다.", MINIMUM_DISCOUNT_AMOUNT, MAXIMUM_DISCOUNT_AMOUNT)); | ||
} | ||
} | ||
|
||
private void validateDiscountUnit(final Long discountAmount) { | ||
if (discountAmount % DISCOUNT_AMOUNT_UNIT != 0) { | ||
throw new IllegalArgumentException(String.format("할인 금액은 %,d원 단위로만 가능합니다.", DISCOUNT_AMOUNT_UNIT)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package coupon.coupon.domain; | ||
|
||
import jakarta.persistence.Embeddable; | ||
|
||
@Embeddable | ||
public record DiscountRate(Integer discountRate) { | ||
|
||
private static final Integer MINIMUM_DISCOUNT_RATE = 3; | ||
private static final Integer MAXIMUM_DISCOUNT_RATE = 20; | ||
|
||
public DiscountRate { | ||
validateDiscountRateNull(discountRate); | ||
validateRateRange(discountRate); | ||
} | ||
|
||
private void validateDiscountRateNull(final Integer discountRate) { | ||
if (discountRate == null) { | ||
throw new IllegalArgumentException("할인율은 null이 될 수 없습니다."); | ||
} | ||
} | ||
|
||
public static DiscountRate from(final Long discountAmount, final Long minimumOrderAmount) { | ||
return new DiscountRate((int) ((double) discountAmount / minimumOrderAmount * 100)); | ||
} | ||
|
||
public void validateRateRange(final Integer discountRate) { | ||
if (discountRate < MINIMUM_DISCOUNT_RATE || discountRate > MAXIMUM_DISCOUNT_RATE) { | ||
throw new IllegalArgumentException(String.format("할인율은 %,d%% 이상 %,d%% 이하의 값이어야 합니다.", MINIMUM_DISCOUNT_RATE, MAXIMUM_DISCOUNT_RATE)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package coupon.coupon.domain; | ||
|
||
import java.time.LocalDate; | ||
import java.time.LocalDateTime; | ||
import java.time.LocalTime; | ||
|
||
import jakarta.persistence.Embeddable; | ||
|
||
@Embeddable | ||
public record IssuancePeriod(LocalDate startDate, LocalDate endDate) { | ||
|
||
public IssuancePeriod { | ||
validateIssuancePeriodNull(startDate, endDate); | ||
} | ||
|
||
public boolean canIssue(final LocalDateTime dateTime) { | ||
LocalDateTime startDateTime = getStartDateTime(); | ||
LocalDateTime endDateTime = getEndDateTime(); | ||
return !dateTime.isBefore(startDateTime) && !dateTime.isAfter(endDateTime); | ||
} | ||
|
||
private void validateIssuancePeriodNull(final LocalDate startDate, final LocalDate endDate) { | ||
if (startDate == null || endDate == null) { | ||
throw new IllegalArgumentException("쿠폰 발행 시작일 또는 종료일은 null이 될 수 없습니다."); | ||
} | ||
} | ||
|
||
public LocalDateTime getStartDateTime() { | ||
return startDate.atStartOfDay(); | ||
} | ||
|
||
public LocalDateTime getEndDateTime() { | ||
return endDate.atTime(LocalTime.MAX); | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
src/main/java/coupon/coupon/domain/MinimumOrderAmount.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package coupon.coupon.domain; | ||
|
||
import jakarta.persistence.Embeddable; | ||
|
||
@Embeddable | ||
public record MinimumOrderAmount(Long minimumOrderAmount) { | ||
|
||
private static final Long MINIMUM_ORDER_AMOUNT = 5_000L; | ||
private static final Long MAXIMUM_ORDER_AMOUNT = 100_000L; | ||
|
||
public MinimumOrderAmount { | ||
validateMinimumOrderAmountNull(minimumOrderAmount); | ||
validateAmount(minimumOrderAmount); | ||
} | ||
|
||
private void validateMinimumOrderAmountNull(final Long minimumOrderAmount) { | ||
if (minimumOrderAmount == null) { | ||
throw new IllegalArgumentException("최소 주문 금액은 null이 될 수 없습니다."); | ||
} | ||
} | ||
|
||
private void validateAmount(final Long minimumOrderAmount) { | ||
if (minimumOrderAmount < MINIMUM_ORDER_AMOUNT || minimumOrderAmount > MAXIMUM_ORDER_AMOUNT) { | ||
throw new IllegalArgumentException(String.format("최소 주문 금액은 %,d원 이상 %,d원 이하이어야 합니다.", MINIMUM_ORDER_AMOUNT, MAXIMUM_ORDER_AMOUNT)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package coupon.coupon.domain; | ||
|
||
import jakarta.persistence.Embeddable; | ||
|
||
@Embeddable | ||
public record Name(String name) { | ||
|
||
private static final int MAX_NAME_LENGTH = 30; | ||
|
||
public Name { | ||
validateNameNull(name); | ||
validateNameLength(name); | ||
} | ||
|
||
private void validateNameNull(String name) { | ||
if (name == null || name.isEmpty()) { | ||
throw new IllegalArgumentException("이름은 null이 될 수 없습니다."); | ||
} | ||
} | ||
|
||
private void validateNameLength(final String name) { | ||
if (name.length() > MAX_NAME_LENGTH) { | ||
throw new IllegalArgumentException("이름은 최대 30자 이하여야 합니다."); | ||
} | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
src/main/java/coupon/infrastructure/audit/BaseTimeEntity.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package coupon.infrastructure.audit; | ||
|
||
import java.time.LocalDateTime; | ||
|
||
import jakarta.persistence.Column; | ||
import jakarta.persistence.EntityListeners; | ||
import jakarta.persistence.MappedSuperclass; | ||
|
||
import org.springframework.data.annotation.CreatedDate; | ||
import org.springframework.data.annotation.LastModifiedDate; | ||
import org.springframework.data.jpa.domain.support.AuditingEntityListener; | ||
|
||
import lombok.Getter; | ||
|
||
@EntityListeners(AuditingEntityListener.class) | ||
@MappedSuperclass | ||
@Getter | ||
public class BaseTimeEntity { | ||
|
||
@CreatedDate | ||
@Column(updatable = false, nullable = false) | ||
private LocalDateTime createdAt; | ||
|
||
@LastModifiedDate | ||
@Column(nullable = false) | ||
private LocalDateTime modifiedAt; | ||
} |
9 changes: 9 additions & 0 deletions
9
src/main/java/coupon/infrastructure/audit/JpaAuditingConfiguration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package coupon.infrastructure.audit; | ||
|
||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing; | ||
|
||
@Configuration | ||
@EnableJpaAuditing | ||
public class JpaAuditingConfiguration { | ||
} |
Oops, something went wrong.