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
2 changes: 2 additions & 0 deletions src/features/order/constants/order-error-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const ORDER_CHECKOUT_ERRORS = {
DUPLICATE_OPTION_ITEM: '중복된 옵션 선택입니다.',
INVALID_OPTION_ITEM: '해당 상품의 옵션이 아닙니다.',
OPTION_GROUP_RULE_VIOLATION: '옵션 그룹의 선택 규칙을 충족하지 않습니다.',
OPTION_CUSTOMIZATION_REQUIRED:
'커스텀 정보가 필요한 옵션은 아직 주문할 수 없습니다.',
PICKUP_NOT_AVAILABLE: '선택한 픽업 일시는 예약할 수 없습니다.',
ORDER_AMOUNT_OUT_OF_RANGE: '주문 금액이 처리 가능한 범위를 벗어났습니다.',
UNSUPPORTED_CURRENCY: 'KRW 상품만 주문할 수 있습니다.',
Expand Down
36 changes: 36 additions & 0 deletions src/features/order/services/order-checkout.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,42 @@ describe('OrderCheckoutService (real DB)', () => {
).rejects.toThrow(BadRequestException);
});

it('설명/이미지 필수 옵션 선택은 커스텀 확장 전까지 거절한다', async () => {
const store = await makeOpenStore();
const product = await createProduct(prisma, { store_id: store.id });
const group = await prisma.productOptionGroup.create({
data: {
product_id: product.id,
name: '레터링',
is_required: false,
min_select: 1,
max_select: 1,
option_requires_description: true,
},
});
const item = await prisma.productOptionItem.create({
data: { option_group_id: group.id, title: '문구 입력', price_delta: 0 },
});
const buyer = await makeBuyer();

await expect(
service.createOrder(
buyer.id,
baseInput({
productId: product.id.toString(),
optionItemIds: [item.id.toString()],
}),
),
).rejects.toThrow(BadRequestException);

// 해당 그룹을 선택하지 않으면 주문 가능(선택 그룹이므로)
const ok = await service.createOrder(
buyer.id,
baseInput({ productId: product.id.toString() }),
);
expect(ok.status).toBe('SUBMITTED');
});

it('없거나 비활성 상품·비활성 매장 상품은 NOT_FOUND다', async () => {
const buyer = await makeBuyer();
const inactiveStore = await createStore(prisma, { is_active: false });
Expand Down
10 changes: 10 additions & 0 deletions src/features/order/services/order-checkout.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,16 @@ export class OrderCheckoutService {
ORDER_CHECKOUT_ERRORS.OPTION_GROUP_RULE_VIOLATION,
);
}
// 설명/이미지 필수 옵션은 커스텀 입력 없이는 판매자 요구 정보가 빠진 채
// 주문된다 — 커스텀 체크아웃 확장 전까지 해당 옵션 선택은 거절한다
if (
count > 0 &&
(group.option_requires_description || group.option_requires_image)
) {
throw new BadRequestException(
ORDER_CHECKOUT_ERRORS.OPTION_CUSTOMIZATION_REQUIRED,
);
}
}
return selections;
}
Expand Down
5 changes: 5 additions & 0 deletions src/features/product/repositories/product.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export interface ProductDetailRow {
is_required: boolean;
min_select: number;
max_select: number;
// 주문 생성(checkout)의 커스텀 필수 옵션 가드용
option_requires_description: boolean;
option_requires_image: boolean;
sort_order: number;
option_items: {
id: bigint;
Expand Down Expand Up @@ -923,6 +926,8 @@ export class ProductRepository {
is_required: true,
min_select: true,
max_select: true,
option_requires_description: true,
option_requires_image: true,
sort_order: true,
option_items: {
where: { is_active: true, deleted_at: null },
Expand Down
Loading