Skip to content

Commit 7f29fdc

Browse files
committed
feature: 쿠폰 도메인 설계 및 단위 테스트 작성
1 parent abd2f63 commit 7f29fdc

3 files changed

Lines changed: 183 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.loopers.domain.coupon;
2+
3+
import com.loopers.domain.BaseEntity;
4+
import com.loopers.support.error.CoreException;
5+
import com.loopers.support.error.ErrorType;
6+
import jakarta.persistence.Column;
7+
import jakarta.persistence.Entity;
8+
import jakarta.persistence.EnumType;
9+
import jakarta.persistence.Enumerated;
10+
import jakarta.persistence.Table;
11+
import lombok.AccessLevel;
12+
import lombok.NoArgsConstructor;
13+
14+
@Entity
15+
@Table(name = "coupon")
16+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
17+
public class Coupon extends BaseEntity {
18+
19+
@Column(name = "ref_user_id", nullable = false)
20+
private long userId;
21+
22+
@Enumerated(EnumType.STRING)
23+
@Column(nullable = false)
24+
private CouponType type;
25+
26+
private long discountValue;
27+
28+
private boolean used;
29+
30+
public Coupon(long userId, CouponType type, long discountValue) {
31+
if (type == CouponType.PERCENTAGE && (discountValue < 0 || discountValue > 100)) {
32+
throw new CoreException(ErrorType.BAD_REQUEST, "할인율은 0~100% 사이여야 합니다.");
33+
}
34+
this.userId = userId;
35+
this.type = type;
36+
this.discountValue = discountValue;
37+
this.used = false;
38+
}
39+
40+
public boolean isUsed() {
41+
return used;
42+
}
43+
44+
public long calculateDiscountAmount(long totalOrderAmount) {
45+
return this.type.calculate(totalOrderAmount, this.discountValue);
46+
}
47+
48+
public void use() {
49+
if (this.used) {
50+
throw new CoreException(ErrorType.BAD_REQUEST, "이미 사용된 쿠폰입니다.");
51+
}
52+
this.used = true;
53+
}
54+
55+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.loopers.domain.coupon;
2+
3+
public enum CouponType {
4+
FIXED_AMOUNT {
5+
@Override
6+
public long calculate(long totalAmount, long discountValue) {
7+
return Math.min(totalAmount, discountValue);
8+
}
9+
},
10+
PERCENTAGE {
11+
@Override
12+
public long calculate(long totalAmount, long discountValue) {
13+
return (long) (totalAmount * (discountValue / 100.0));
14+
}
15+
};
16+
17+
public abstract long calculate(long totalAmount, long discountValue);
18+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package com.loopers.domain.coupon;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5+
import static org.junit.jupiter.api.Assertions.*;
6+
7+
import com.loopers.support.error.CoreException;
8+
import com.loopers.support.error.ErrorType;
9+
import org.junit.jupiter.api.DisplayName;
10+
import org.junit.jupiter.api.Nested;
11+
import org.junit.jupiter.api.Test;
12+
13+
class CouponTest {
14+
@Nested
15+
@DisplayName("쿠폰 할인 금액 계산 테스트")
16+
class CalculateDiscount {
17+
18+
@Test
19+
@DisplayName("정액 쿠폰: 주문 금액에서 할인 금액만큼 차감된 값을 계산한다.")
20+
void fixed_amount_discount() {
21+
// given
22+
long orderAmount = 10000L;
23+
long discountAmount = 1000L;
24+
Coupon coupon = new Coupon(1l, CouponType.FIXED_AMOUNT, discountAmount);
25+
26+
// when
27+
long result = coupon.calculateDiscountAmount(orderAmount);
28+
29+
// then
30+
assertThat(result).isEqualTo(1000L);
31+
}
32+
33+
@Test
34+
@DisplayName("정액 쿠폰: 주문 금액보다 할인 금액이 크면, 주문 금액만큼만 할인된다(결제금액 0원 보장).")
35+
void fixed_amount_discount_capped() {
36+
// given
37+
long orderAmount = 500L;
38+
long discountAmount = 1000L;
39+
Coupon coupon = new Coupon(1l, CouponType.FIXED_AMOUNT, discountAmount);
40+
41+
// when
42+
long result = coupon.calculateDiscountAmount(orderAmount);
43+
44+
// then
45+
assertThat(result).isEqualTo(500L);
46+
}
47+
48+
@Test
49+
@DisplayName("정률 쿠폰: 주문 금액의 비율만큼 할인 금액이 계산된다.")
50+
void percentage_discount() {
51+
// given
52+
long orderAmount = 20000L;
53+
long discountRate = 10L; // 10%
54+
Coupon coupon = new Coupon(1l, CouponType.PERCENTAGE, discountRate);
55+
56+
// when
57+
long result = coupon.calculateDiscountAmount(orderAmount);
58+
59+
// then
60+
assertThat(result).isEqualTo(2000L);
61+
}
62+
}
63+
64+
@Nested
65+
@DisplayName("쿠폰 생성 및 유효성 검증 테스트")
66+
class Validation {
67+
68+
@Test
69+
@DisplayName("정률 쿠폰 생성 시 할인율이 100을 초과하면 예외가 발생한다.")
70+
void create_percentage_coupon_fail_over_100() {
71+
assertThatThrownBy(() ->
72+
new Coupon(1l, CouponType.PERCENTAGE, 101L)
73+
)
74+
.isInstanceOf(CoreException.class)
75+
.hasMessage("할인율은 0~100% 사이여야 합니다.");
76+
}
77+
}
78+
79+
@Nested
80+
@DisplayName("쿠폰 사용 처리 테스트")
81+
class UseCoupon {
82+
83+
@Test
84+
@DisplayName("쿠폰을 사용하면 used 상태가 true로 변경된다.")
85+
void use_coupon_success() {
86+
// given
87+
Coupon coupon = new Coupon(1l, CouponType.FIXED_AMOUNT, 1000L);
88+
89+
// when
90+
coupon.use();
91+
92+
// then
93+
assertThat(coupon.isUsed()).isTrue();
94+
}
95+
96+
@Test
97+
@DisplayName("이미 사용된 쿠폰을 다시 사용하려고 하면 예외가 발생한다.")
98+
void use_coupon_fail_already_used() {
99+
// given
100+
Coupon coupon = new Coupon(1l, CouponType.FIXED_AMOUNT, 1000L);
101+
coupon.use();
102+
103+
// when & then
104+
assertThatThrownBy(coupon::use)
105+
.isInstanceOf(CoreException.class)
106+
.hasFieldOrPropertyWithValue("errorType", ErrorType.BAD_REQUEST)
107+
.hasMessage("이미 사용된 쿠폰입니다.");
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)