Skip to content

Commit 564ea5b

Browse files
committed
refactor: cart_validation을 검증 항목별로 분리
1 parent 246da7e commit 564ea5b

8 files changed

Lines changed: 680 additions & 606 deletions

File tree

app/shop/serializers/cart_validation.py

Lines changed: 0 additions & 606 deletions
This file was deleted.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""cart_validation 패키지 — 도메인별 파일 분리. 기존 `from shop.serializers.cart_validation import X` 경로 유지."""
2+
3+
from shop.serializers.cart_validation._base import CustomerInfoCheckSerializer, OrderableCheckSerializerMode
4+
from shop.serializers.cart_validation.cart import CartOrderableCheckSerializer
5+
from shop.serializers.cart_validation.option import (
6+
OptionOrderableCheckSerializer,
7+
OptionOrderableCheckTypedDict,
8+
)
9+
from shop.serializers.cart_validation.product import (
10+
ProductOrderableCheckAfterValidationDataType,
11+
ProductOrderableCheckBeforeValidationDataType,
12+
ProductOrderableCheckSerializer,
13+
)
14+
from shop.serializers.cart_validation.single_product_cart import (
15+
CustomerInfoType,
16+
SingleProductCartOrderableCheckDataType,
17+
SingleProductCartOrderableCheckSerializer,
18+
)
19+
from shop.serializers.cart_validation.tag import TagOrderableCheckSerializer
20+
21+
__all__ = [
22+
"CartOrderableCheckSerializer",
23+
"CustomerInfoCheckSerializer",
24+
"CustomerInfoType",
25+
"OptionOrderableCheckSerializer",
26+
"OptionOrderableCheckTypedDict",
27+
"OrderableCheckSerializerMode",
28+
"ProductOrderableCheckAfterValidationDataType",
29+
"ProductOrderableCheckBeforeValidationDataType",
30+
"ProductOrderableCheckSerializer",
31+
"SingleProductCartOrderableCheckDataType",
32+
"SingleProductCartOrderableCheckSerializer",
33+
"TagOrderableCheckSerializer",
34+
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import enum
2+
3+
from core.const.regex import ALLOW_ALL_PATTERN, PHONE_PATTERN
4+
from rest_framework import serializers
5+
from shop.order.models import CustomerInfo
6+
7+
8+
class CustomerInfoCheckSerializer(serializers.ModelSerializer):
9+
"""고객 정보가 Regex에 맞는지 확인합니다."""
10+
11+
name = serializers.RegexField(ALLOW_ALL_PATTERN, required=True, allow_null=False, allow_blank=False)
12+
phone = serializers.RegexField(PHONE_PATTERN, required=True, allow_null=False, allow_blank=False)
13+
email = serializers.EmailField(required=True, allow_null=False, allow_blank=False)
14+
organization = serializers.RegexField(ALLOW_ALL_PATTERN, required=True, allow_null=False, allow_blank=True)
15+
16+
class Meta:
17+
model = CustomerInfo
18+
fields = ("name", "phone", "email", "organization")
19+
20+
21+
class OrderableCheckSerializerMode(str, enum.Enum):
22+
ADD_SINGLE_PRODUCT_TO_CART = enum.auto()
23+
CHECKOUT_SINGLE_PRODUCT = enum.auto()
24+
CHECKOUT_CART = enum.auto()
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import typing
2+
3+
from core.const.shop_error_messages import CartNotOrderableErrorMessages
4+
from rest_framework import serializers
5+
from shop.order.models import Order, OrderProductRelation
6+
from shop.payment_history.models import PaymentHistoryStatus
7+
from user.models import UserExt
8+
9+
10+
class CartOrderableCheckSerializer(serializers.Serializer):
11+
"""
12+
장바구니가 주문 가능한지 확인합니다.
13+
아래 조건에 해당될 경우 주문 불가능합니다.
14+
- 이미 결제한 장바구니인 경우 주문 불가능
15+
- 장바구니 내에 이미 결제한 상품이 있는 경우 주문 불가능
16+
- 장바구니 내의 상품들 주문 금액 합계가 0원 미만이거나 100만원 이상인 경우 주문 불가능
17+
"""
18+
19+
cart = serializers.PrimaryKeyRelatedField(queryset=Order.objects.filter_has_no_payment_histories(), required=True)
20+
21+
class Meta:
22+
fields = ("cart",)
23+
24+
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
25+
super().__init__(*args, **kwargs)
26+
# 타인의 미결제 cart 로 결제 흐름 트리거를 방어 — request.user 의 cart 로만 lookup 한정.
27+
request = self.context.get("request")
28+
user = request.user if request is not None else None
29+
self.fields["cart"].queryset = (
30+
Order.objects.filter_has_no_payment_histories().filter(user=user)
31+
if isinstance(user, UserExt)
32+
else Order.objects.none()
33+
)
34+
35+
def validate(self, data: dict) -> dict:
36+
cart: Order = data["cart"]
37+
38+
# 이미 결제한 장바구니인 경우 주문 불가능
39+
if cart.current_status != PaymentHistoryStatus.pending or cart.payment_histories.exists():
40+
raise serializers.ValidationError(CartNotOrderableErrorMessages.ALREADY_ORDERED)
41+
42+
# 장바구니 내에 이미 결제한 상품이 있는 경우 주문 불가능
43+
if cart.products.exclude(status=OrderProductRelation.OrderProductStatus.pending).exists():
44+
raise serializers.ValidationError(CartNotOrderableErrorMessages.CONTAINS_PAID_PRODUCT)
45+
46+
# 장바구니 내의 상품들 주문 금액 합계가 0원 이하거나 100만원 이상인 경우 주문 불가능
47+
if cart.first_paid_price <= 0:
48+
raise serializers.ValidationError(CartNotOrderableErrorMessages.CART_PRICE_TOO_LOW)
49+
if cart.first_paid_price >= 1_000_000:
50+
raise serializers.ValidationError(CartNotOrderableErrorMessages.CART_PRICE_TOO_HIGH)
51+
52+
return data
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import re
2+
import typing
3+
import uuid
4+
5+
from core.const.shop_error_messages import (
6+
OptionGroupNotOrderableErrorMessages,
7+
OptionNotOrderableErrorMessages,
8+
SignInErrorMessages,
9+
)
10+
from core.serializer.nested_model_serializer import InstanceListSerializer
11+
from django.contrib.auth.models import AnonymousUser
12+
from rest_framework import request, serializers
13+
from shop.product.models import Option, OptionGroup, Product
14+
from shop.serializers.cart_validation._base import OrderableCheckSerializerMode
15+
from user.models import UserExt
16+
17+
18+
class OptionOrderableCheckTypedDict(typing.TypedDict):
19+
product_option_group: OptionGroup
20+
product_option: Option | None
21+
custom_response: str | None
22+
23+
24+
class OptionOrderableCheckSerializer(serializers.Serializer):
25+
"""
26+
장바구니에 담긴 상품의 옵션이 주문 가능한지 확인합니다.
27+
아래 조건에 해당될 경우 주문 불가능합니다.
28+
29+
==================== 상품 옵션 그룹 (OptionGroup) ====================
30+
- 상품 옵션 중 필수 옵션이 매진된 경우 주문 불가능
31+
- 옵션 그룹이 custom_response를 받는 경우, custom_response가 올바른 형식으로 입력되지 않은 경우 주문 불가능
32+
- 옵션 그룹이 custom_response를 받지 않는 경우, option이 선택되지 않았거나 옵션 그룹에 속하지 않은 경우 주문 불가능
33+
34+
==================== 상품 옵션 (Option) ====================
35+
- 옵션의 재고가 없는 경우 주문 불가능
36+
- 옵션의 최대 구매 수량이 정해져있으면, 해당 사용자가 이미 구매한 수량이 최대 구매 수량을 초과하는 경우 주문 불가능
37+
- 고객이 옵션의 재고를 초과하여 옵션을 장바구니에 담으면 주문 불가능
38+
"""
39+
40+
product_option_group = serializers.PrimaryKeyRelatedField(
41+
queryset=OptionGroup.objects.filter(deleted_at__isnull=True),
42+
required=True,
43+
allow_null=False,
44+
)
45+
product_option = serializers.PrimaryKeyRelatedField(
46+
queryset=Option.objects.filter(deleted_at__isnull=True),
47+
required=True,
48+
allow_null=True,
49+
)
50+
custom_response = serializers.CharField(required=True, allow_null=True, allow_blank=True)
51+
52+
class Meta:
53+
list_serializer_class = InstanceListSerializer
54+
fields = "__all__"
55+
56+
@property
57+
def validation_mode(self) -> OrderableCheckSerializerMode:
58+
if not (mode := self.context.get("mode")):
59+
return OrderableCheckSerializerMode.ADD_SINGLE_PRODUCT_TO_CART
60+
return mode
61+
62+
@property
63+
def group(self) -> OptionGroup | None:
64+
group: OptionGroup | str | uuid.UUID | None = self.initial_data.get("product_option_group")
65+
if not group:
66+
return None
67+
if isinstance(group, (str, uuid.UUID)):
68+
return OptionGroup.objects.filter(pk=group).first()
69+
return group
70+
71+
def validate_product_option_group(self, group: OptionGroup) -> OptionGroup:
72+
# 상품 옵션 중 필수 옵션이 매진된 경우 주문 불가능
73+
if not group.is_group_stock_available():
74+
raise serializers.ValidationError(
75+
OptionGroupNotOrderableErrorMessages.SOLDOUT.format(group.product.name, group.name)
76+
)
77+
78+
return group
79+
80+
def validate_product_option(self, option: Option | None) -> Option | None:
81+
user: UserExt | AnonymousUser = typing.cast(request.Request, self.context["request"]).user
82+
if not isinstance(user, UserExt):
83+
raise serializers.ValidationError(SignInErrorMessages.USER_NOT_SIGNED_IN)
84+
85+
if not self.group or self.group.is_custom_response:
86+
return None
87+
88+
# 옵션 그룹이 custom_response를 받지 않는 경우, option이 선택되지 않았거나 옵션 그룹에 속하지 않은 경우 주문 불가능
89+
if not (option and option.group_id == self.group.id):
90+
raise serializers.ValidationError(OptionGroupNotOrderableErrorMessages.OPTION_NOT_SELECTED)
91+
92+
product: Product = option.group.product
93+
if option.leftover_stock is not None:
94+
# 옵션의 재고가 없는 경우 주문 불가능
95+
if option.leftover_stock <= 0:
96+
raise serializers.ValidationError(
97+
OptionNotOrderableErrorMessages.SOLDOUT.format(product.name, option.name)
98+
)
99+
100+
# 고객이 옵션의 재고를 초과하여 옵션을 장바구니에 담으면 주문 불가능
101+
user_option_cart_included_count = option.get_user_taken_stock_count(
102+
user=user,
103+
include_cart=True,
104+
include_purchased=False,
105+
)
106+
match self.validation_mode:
107+
case OrderableCheckSerializerMode.ADD_SINGLE_PRODUCT_TO_CART:
108+
user_option_cart_included_count += 1
109+
case OrderableCheckSerializerMode.CHECKOUT_SINGLE_PRODUCT:
110+
# 이미 재고 체크를 위에서 했으므로, 단일 주문의 경우 확인할 필요가 없습니다.
111+
user_option_cart_included_count = 0
112+
case OrderableCheckSerializerMode.CHECKOUT_CART:
113+
pass
114+
case _:
115+
raise ValueError("Invalid validation mode")
116+
117+
if user_option_cart_included_count > option.leftover_stock:
118+
raise serializers.ValidationError(
119+
OptionNotOrderableErrorMessages.TOO_MUCH_CART_OPTION.format(product.name, option.name)
120+
)
121+
122+
# 옵션의 최대 구매 수량이 정해져있으면, 해당 사용자가 이미 구매한 수량이 최대 구매 수량을 초과하는 경우 주문 불가능
123+
if option.max_quantity_per_user > 0: # 0 = 무제한 sentinel
124+
match self.validation_mode:
125+
case OrderableCheckSerializerMode.ADD_SINGLE_PRODUCT_TO_CART:
126+
user_option_taken_count = (
127+
option.get_user_taken_stock_count(
128+
user=user,
129+
include_cart=True,
130+
include_purchased=True,
131+
)
132+
+ 1
133+
)
134+
case OrderableCheckSerializerMode.CHECKOUT_SINGLE_PRODUCT:
135+
user_option_taken_count = (
136+
option.get_user_taken_stock_count(
137+
user=user,
138+
include_cart=False,
139+
include_purchased=True,
140+
)
141+
+ 1
142+
)
143+
case OrderableCheckSerializerMode.CHECKOUT_CART:
144+
user_option_taken_count = option.get_user_taken_stock_count(
145+
user=user,
146+
include_cart=True,
147+
include_purchased=True,
148+
)
149+
case _:
150+
raise ValueError("Invalid validation mode")
151+
152+
if user_option_taken_count > option.max_quantity_per_user:
153+
raise serializers.ValidationError(
154+
OptionNotOrderableErrorMessages.ALREADY_ORDERED_TOO_MUCH.format(product.name, option.name)
155+
)
156+
157+
return option
158+
159+
def validate_custom_response(self, custom_response: str | None) -> str | None:
160+
if not (self.group and self.group.is_custom_response):
161+
return None
162+
163+
# 옵션 그룹이 custom_response를 받는 경우, custom_response가 올바른 형식으로 입력되지 않은 경우 주문 불가능
164+
if self.group.custom_response_pattern and not re.match(
165+
self.group.custom_response_pattern, custom_response or ""
166+
):
167+
raise serializers.ValidationError(OptionGroupNotOrderableErrorMessages.CUSTOM_RESPONSE_PATTERN_MISMATCH)
168+
169+
return custom_response

0 commit comments

Comments
 (0)