feat(order): 주문 생성 mutation(createOrder) — 정식 API의 확정 부분집합 - #208
Conversation
커스텀 단계 스펙 미확정 상태에서 FE가 주문 생성 흐름을 테스트할 수 있도록, 확정된 부분(단일 상품·옵션 선택·픽업 일시·수량)만으로 정식 주문 생성 경로를 구현한다. 커스텀 입력은 스펙 확정 후 optional 필드로 확장한다 (임시 스텁이 아니므로 이후 계약 변경 없음 — 사용자 확정 방향). - SDL order-checkout.graphql: createOrder(input) → 요약(CreateOrderOutput), 상세는 기존 myOrder 재조회 전제 - OrderCheckoutService: 옵션 그룹 규칙(필수/min/max/중복/타 상품) 검증, 가격 서버 스냅샷(subtotal 정가·discount 정가-판매가 차액·total 판매가 기준), 주문자 정보 input 우선 + 프로필(닉네임·전화) fallback, 둘 다 없으면 거절 - StorePickupScheduleService.isPickupSlotAvailable 신설: 달력·슬롯과 동일 규칙 + 슬롯 시작 시각 정합 + capacity 잔여(기존 점유+수량 ≤ capacity) 재검증 — 판정 로직은 store feature에 유지(배럴 export) - OrderRepository.createSubmittedOrder: Order+Item+옵션 스냅샷+상태 히스토리 중첩 create 원자 생성, SUBMITTED는 기존 규칙대로 알림 미발송 - 주문번호 ORD-YYYYMMDD-XXXXXX(KST 날짜+혼동 문자 제외 랜덤 6자리, RandomService 주입) — 명세 외 정책 결정. P2002 충돌 시 3회 재시도 회귀 테스트 24건: 생성 스냅샷·가격·히스토리 / 옵션 규칙 전 분기 / buyer fallback / 픽업 판정(휴무·정렬·리드타임·capacity 잔여·초 이하) / 주문번호 충돌 재시도·소진 / NOT_FOUND / input 검증 / resolver 통합
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🧹 knip — dead-code 리포트전체 리포트
|
🩺 NestJS Doctor — 89/100 (Good)진단 282건 (error 0).
architecture / security 상위 항목
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d46cfe3f49
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const subtotalPrice = (product.regular_price + deltaSum) * quantity; | ||
| const discountPrice = (product.regular_price - effectivePrice) * quantity; | ||
| const itemSubtotalPrice = (effectivePrice + deltaSum) * quantity; |
There was a problem hiding this comment.
Constrain totals before committing the order
A valid product may cost up to 1_000_000_000, and this input accepts quantities through 99, but the computed totals are neither bounded nor checked against the Int! output range. For example, a 1-billion-won product with quantity 3 persists successfully in the unsigned MySQL columns, then GraphQL rejects totalPrice = 3_000_000_000 after the order has committed because GraphQL Int is signed 32-bit; the buyer receives an error and may create duplicate orders by retrying. Larger totals fail during persistence instead, and sufficiently negative seller-supported option deltas can underflow the unsigned columns, so validate the computed amounts or use compatible storage/output scalars before creating the order.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
반영: 커밋 전 금액 검증 추가 — subtotal/discount/itemSubtotal이 음수·비안전 정수·2,147,483,647(GraphQL Int) 초과면 BadRequest. 음수 델타 언더플로·32비트 초과 회귀 테스트 2케이스 추가.
| const pickupAvailable = await this.pickupSchedule.isPickupSlotAvailable({ | ||
| storeId: product.store_id, | ||
| pickupAt: input.pickupAt, | ||
| additionalQuantity: quantity, | ||
| }); |
There was a problem hiding this comment.
Reserve daily capacity atomically with order creation
When concurrent buyers request the last available daily capacity, both calls can complete this availability query before either order is inserted, observe the same booked quantity, and then both create SUBMITTED orders. The capacity read and order write are separate operations with no lock, constraint, or shared transaction, so the advertised daily capacity can be exceeded under ordinary concurrent checkout traffic; recheck and reserve capacity atomically with the order insertion.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
반영: createSubmittedOrder를 트랜잭션으로 감싸고 capacity 행 SELECT ... FOR UPDATE 잠금 후 점유 재집계 → 초과면 생성 취소(null→BadRequest). 동시 주문 2건 중 정확히 1건만 성공하는 real DB 동시성 테스트 추가.
Coverage report
Test suite run success1601 tests passing in 187 suites. Report generated by 🧪jest coverage report action from dee7b8f |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
- 커밋 전 금액 검증: subtotal/discount/itemSubtotal이 음수·비안전 정수· GraphQL Int 상한(2,147,483,647) 초과면 거절 — 저장 후 응답 직렬화 실패로 인한 재시도 중복 주문 경로 차단, 음수 델타 언더플로 방지 - createSubmittedOrder를 트랜잭션화하고 capacity 행 FOR UPDATE 잠금 후 점유 재집계 — 동시 주문이 마지막 잔여를 함께 차지하는 race 차단 (초과 시 null 반환 → 서비스가 BadRequest 변환) - 회귀 테스트 3건 추가: 32비트 초과/음수 금액 거절, 동시 주문 2건 중 정확히 1건만 성공(real DB 동시성)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bcb59eca25
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const pickupAvailable = await this.pickupSchedule.isPickupSlotAvailable({ | ||
| storeId: product.store_id, | ||
| pickupAt: input.pickupAt, | ||
| additionalQuantity: quantity, | ||
| }); |
There was a problem hiding this comment.
Enforce each product's preparation time
When a product requires more preparation time than its store's minimum lead time—for example, the default preparationTimeMinutes is 180 while the store lead time is 30—this call accepts a same-day slot only 30 minutes away. ProductDetailRow does not load preparation_time_minutes, and StorePickupScheduleService.buildDaySlots checks only store.min_lead_time_minutes, so the new checkout path can commit orders before the seller-configured product preparation window; load and enforce the product requirement when validating the selected slot.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
반영: findProductDetailById에 preparation_time_minutes 로드 추가, 체크아웃에서 pickupAt ≥ now + 제작 소요시간을 매장 리드타임과 별개 조건으로 강제. 회귀 테스트 추가 (26h 제작 상품의 22h 뒤 슬롯 거절/46h 뒤 허용).
| productNameSnapshot: product.name, | ||
| regularPriceSnapshot: product.regular_price, | ||
| salePriceSnapshot: product.sale_price, |
There was a problem hiding this comment.
Preserve or reject the product currency at checkout
For a seller-configured non-KRW product, checkout copies the numeric prices but drops product.currency. The seller API accepts arbitrary three-letter currencies and buyer-facing product details expose that currency, while neither Order nor OrderItem has a currency snapshot; after creation, myOrder therefore returns an unlabeled amount that can be interpreted as KRW and cannot recover the currency if the product changes. Persist the currency with the price snapshot, or reject unsupported currencies before creating the order.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
반영(정책 선택): Order/OrderItem에 통화 스냅샷 컬럼이 없어 이번 PR에서는 KRW 외 통화 상품 주문을 거절한다(UNSUPPORTED_CURRENCY). 통화 스냅샷 컬럼 추가는 다국통화 도입 시 스키마 설계와 함께 후속 이슈로 추적 예정.
- findProductDetailById에 preparation_time_minutes·currency 활용 추가, 체크아웃에서 pickupAt ≥ now + 제작 소요시간을 매장 리드타임과 별개로 강제 - Order/OrderItem에 통화 스냅샷 컬럼이 없어 비 KRW 상품은 주문 거절 (명세 외 정책 결정 — 다국통화 스냅샷은 후속 이슈로 추적) - 상품 팩토리에 currency·preparation_time_minutes override 추가 (테스트 기본 제작시간 0분 — 스키마 기본 180분은 당일 픽업 테스트 방해) - 회귀 테스트 2건 추가 (제작시간 미달 거절/충족 허용, USD 상품 거절)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc8ec27329
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| @CurrentUser() user: JwtUser, | ||
| @Args('input') input: CreateOrderInput, | ||
| ): Promise<CreateOrderOutput> { | ||
| return this.checkoutService.createOrder(parseAccountId(user), input); |
There was a problem hiding this comment.
Require a USER account before creating orders
Any authenticated SELLER or ADMIN can call this buyer-only mutation because the resolver discards user.accountType, and supplying both buyer fields bypasses the only profile lookup in resolveBuyer. The order foreign key accepts those accounts, so a seller can create orders for their own store, advance them through the seller status API, and contaminate capacity and order-based rankings; enforce the active-USER invariant used by UserBaseService.requireActiveUser before checkout.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
반영: 체크아웃 진입 시 requireActiveUser와 동일 의미론의 활성 USER+프로필 검증 추가 (SELLER/ADMIN → Forbidden, 삭제 계정·프로필 없음 → Unauthorized). user feature는 배럴 없는 컨벤션이라 order 경로에 동일 로직 구현. 회귀 테스트 2케이스 추가.
| @IsString() | ||
| @IsNotEmpty() | ||
| @MaxLength(30) |
There was a problem hiding this comment.
Validate overridden phone numbers with the profile policy
When a caller supplies buyerPhone, values such as whitespace or "abc" pass these decorators and are preferred over the validated profile phone, leaving the seller with an unusable contact number on a committed order. Profile phone numbers are normalized against the repository's fixed 010-XXXX-XXXX policy, so checkout overrides should be trimmed and validated by the same rule before persistence.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
반영: buyerPhone에 프로필과 동일 정책(010-XXXX-XXXX 고정) @matches 강제. 정책 상수는 출처 주석과 함께 order 상수로 복제(user는 배럴 없는 feature). input 검증 테스트 추가.
- 체크아웃 진입 시 활성 USER 계정+활성 프로필을 강제한다 (requireActiveUser와 동일 의미론 — SELLER/ADMIN이 구매자 mutation으로 주문을 만들어 capacity·랭킹을 오염시키는 경로 차단) - buyerPhone에 프로필 전화번호와 동일한 010-XXXX-XXXX 고정 형식 강제 (임의 문자열이 검증된 프로필 값을 덮어쓰지 못하게) - 프로필 닉네임이 NOT NULL 최종 fallback이라 도달 불가가 된 BUYER_NAME_REQUIRED 분기 제거 - 회귀 테스트 3건 추가 (SELLER 거절/프로필 없음 거절/전화 형식)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb21992ff9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| @IsOptional() | ||
| @IsString() | ||
| @IsNotEmpty() | ||
| @MaxLength(100) | ||
| buyerName?: string; |
There was a problem hiding this comment.
Reject whitespace-only buyer names
When buyerName consists only of whitespace, @IsNotEmpty() accepts it because the string is technically nonempty, and resolveBuyerInfo then prefers it over the valid profile nickname. This commits an order whose buyer name appears blank to the seller; trim the override before validation and either reject an empty result or fall back to the profile nickname.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
반영: resolveBuyerInfo에서 buyerName trim 후 빈 값이면 미입력으로 취급해 프로필 닉네임 fallback. 공백 입력 회귀 케이스 추가.
buyerName을 trim해 빈 값이면 프로필 닉네임 fallback으로 처리한다 — 공백 이름이 검증된 닉네임을 덮어써 판매자에게 빈 표기로 보이는 것 방지. 회귀 케이스 1건 추가.
배경
커스텀 단계 UI·기획이 미확정이라 주문 생성 API가 없어, FE가 주문 흐름을 테스트할 수단이 없습니다(시드 데이터는 목록/상세 화면용일 뿐 생성 흐름은 불가).
임시 스텁 대신 정식
createOrder의 확정된 부분집합으로 구현합니다 — 이 흐름에서 확정된 것(단일 상품·옵션 일괄 제출·픽업 일시·수량·서버 가격 스냅샷)만 담고, 커스텀은 스펙 확정 후 optional 필드(예:customDraftId)로 확장합니다.따라서 이후 계약 변경 없이 FE가 최종 계약의 부분집합에 맞춰 개발할 수 있습니다.
변경점
order-checkout.graphql:createOrder(input: CreateOrderInput!): CreateOrderOutput!입력:
productId + optionItemIds + pickupAt + quantity(기본 1, ≤99) + buyerName/Phone(optional). 출력은 요약(orderId·orderNumber·status·pickupAt·totalPrice), 상세는 기존myOrder재조회.StorePickupScheduleService.isPickupSlotAvailable신설 — 달력·슬롯과 동일 규칙에 슬롯 시작 시각 정합·capacity 잔여(기존 점유+이번 수량 ≤ capacity)를 더해 재검증. 판정 로직은 store feature에 유지하고 배럴로 공개(Refactor: 매장 픽업 가능 판정 정책 단일화 (today-pickup / pickup-schedule / 주문 생성 공용) #206 단일화 방향과 일치).OrderRepository.createSubmittedOrder— Order+Item+옵션+상태 히스토리(null→SUBMITTED)를 중첩 create로 원자 생성. SUBMITTED는 기존 규칙대로 알림 미발송.ORD-YYYYMMDD-XXXXXX(KST 날짜+혼동 문자 제외 랜덤 6자리, RandomService 주입) — 명세 외 정책 결정. unique 충돌(P2002) 시 3회 재시도.검증
yarn validate전체 통과 (1598 tests / 187 suites).