Skip to content

feat(order): 주문 생성 mutation(createOrder) — 정식 API의 확정 부분집합 - #208

Merged
chanwoo7 merged 5 commits into
developfrom
feat/create-order-mutation
Aug 24, 2026
Merged

feat(order): 주문 생성 mutation(createOrder) — 정식 API의 확정 부분집합#208
chanwoo7 merged 5 commits into
developfrom
feat/create-order-mutation

Conversation

@chanwoo7

Copy link
Copy Markdown
Member

배경

커스텀 단계 UI·기획이 미확정이라 주문 생성 API가 없어, FE가 주문 흐름을 테스트할 수단이 없습니다(시드 데이터는 목록/상세 화면용일 뿐 생성 흐름은 불가).
임시 스텁 대신 정식 createOrder의 확정된 부분집합으로 구현합니다 — 이 흐름에서 확정된 것(단일 상품·옵션 일괄 제출·픽업 일시·수량·서버 가격 스냅샷)만 담고, 커스텀은 스펙 확정 후 optional 필드(예: customDraftId)로 확장합니다.
따라서 이후 계약 변경 없이 FE가 최종 계약의 부분집합에 맞춰 개발할 수 있습니다.

변경점

  • SDL order-checkout.graphql: createOrder(input: CreateOrderInput!): CreateOrderOutput!
    입력: productId + optionItemIds + pickupAt + quantity(기본 1, ≤99) + buyerName/Phone(optional). 출력은 요약(orderId·orderNumber·status·pickupAt·totalPrice), 상세는 기존 myOrder 재조회.
  • 검증(OrderCheckoutService)
    • 옵션: 활성 그룹·아이템 소속, 필수/min/max 규칙, 중복 거절. 선택 그룹은 0개 또는 min~max개(명세 외 정책).
    • 픽업: StorePickupScheduleService.isPickupSlotAvailable 신설 — 달력·슬롯과 동일 규칙에 슬롯 시작 시각 정합·capacity 잔여(기존 점유+이번 수량 ≤ capacity)를 더해 재검증. 판정 로직은 store feature에 유지하고 배럴로 공개(Refactor: 매장 픽업 가능 판정 정책 단일화 (today-pickup / pickup-schedule / 주문 생성 공용) #206 단일화 방향과 일치).
    • 주문자: input 우선, 없으면 프로필(닉네임·전화번호) fallback, 둘 다 없으면 거절.
  • 가격 서버 스냅샷: FE 제출 금액을 신뢰하지 않음. subtotal=(정가+델타)×수량, discount=(정가−판매가)×수량, total=판매가 기준. Item·옵션 스냅샷 컬럼 전부 기록.
  • 쓰기 경로: OrderRepository.createSubmittedOrder — Order+Item+옵션+상태 히스토리(null→SUBMITTED)를 중첩 create로 원자 생성. SUBMITTED는 기존 규칙대로 알림 미발송.
  • 주문번호: ORD-YYYYMMDD-XXXXXX(KST 날짜+혼동 문자 제외 랜덤 6자리, RandomService 주입) — 명세 외 정책 결정. unique 충돌(P2002) 시 3회 재시도.

검증

  • real DB 회귀 테스트 24건: 생성 스냅샷·가격·히스토리, 옵션 규칙 전 분기, buyer fallback, 픽업 판정(휴무·비정렬·리드타임·capacity 잔여·초 이하 시각), 주문번호 충돌 재시도·소진, NOT_FOUND, input 순수 검증, resolver 통합.
  • yarn validate 전체 통과 (1598 tests / 187 suites).

커스텀 단계 스펙 미확정 상태에서 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 통합
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a9adbd1f-297a-4f39-b921-5e9b7b0b695f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

🧹 knip — dead-code 리포트

요약 항목 없음
전체 리포트
(knip 출력 없음 — 이슈 0이거나 실행 실패)

청소 후보(오탐 가능) · 기준 docs/guide/architecture-conventions.md

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

🩺 NestJS Doctor — 89/100 (Good)

진단 282건 (error 0).

Category error warning info
architecture 0 0 13
correctness 0 129 0
performance 0 24 17
schema 0 0 86
security 0 13 0
architecture / security 상위 항목
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal type 'IAuditLogRepository'.
  • warning security/security/no-exposed-env-vars: Direct 'process.env.NODE_ENV' access in 'AuthController'. Use ConfigService instead.
  • warning security/security/require-guards-on-endpoints: Endpoint 'start' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'callback' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'refresh' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'logout' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerLogin' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerRefresh' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerLogout' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'devIssueToken' has no @UseGuards() at class or method level.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal module '@/features/conversation/repositories/conversation.repository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal type 'ConversationRepository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal module '@/features/order/repositories/order.repository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal type 'OrderRepository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal module '@/features/product/repositories/product.repository'.

오탐 포함 가능 · 기준 docs/guide/architecture-conventions.md

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +81 to +83
const subtotalPrice = (product.regular_price + deltaSum) * quantity;
const discountPrice = (product.regular_price - effectivePrice) * quantity;
const itemSubtotalPrice = (effectivePrice + deltaSum) * quantity;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 커밋 전 금액 검증 추가 — subtotal/discount/itemSubtotal이 음수·비안전 정수·2,147,483,647(GraphQL Int) 초과면 BadRequest. 음수 델타 언더플로·32비트 초과 회귀 테스트 2케이스 추가.

Comment on lines +65 to +69
const pickupAvailable = await this.pickupSchedule.isPickupSlotAvailable({
storeId: product.store_id,
pickupAt: input.pickupAt,
additionalQuantity: quantity,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: createSubmittedOrder를 트랜잭션으로 감싸고 capacity 행 SELECT ... FOR UPDATE 잠금 후 점유 재집계 → 초과면 생성 취소(null→BadRequest). 동시 주문 2건 중 정확히 1건만 성공하는 real DB 동시성 테스트 추가.

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 97.61% 4947/5068
🟢 Branches 93.95% 2018/2148
🟢 Functions 96.08% 955/994
🟢 Lines 98.21% 4502/4584

Test suite run success

1601 tests passing in 187 suites.

Report generated by 🧪jest coverage report action from dee7b8f

@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

- 커밋 전 금액 검증: subtotal/discount/itemSubtotal이 음수·비안전 정수·
  GraphQL Int 상한(2,147,483,647) 초과면 거절 — 저장 후 응답 직렬화
  실패로 인한 재시도 중복 주문 경로 차단, 음수 델타 언더플로 방지
- createSubmittedOrder를 트랜잭션화하고 capacity 행 FOR UPDATE 잠금 후
  점유 재집계 — 동시 주문이 마지막 잔여를 함께 차지하는 race 차단
  (초과 시 null 반환 → 서비스가 BadRequest 변환)
- 회귀 테스트 3건 추가: 32비트 초과/음수 금액 거절, 동시 주문 2건 중
  정확히 1건만 성공(real DB 동시성)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +74 to +78
const pickupAvailable = await this.pickupSchedule.isPickupSlotAvailable({
storeId: product.store_id,
pickupAt: input.pickupAt,
additionalQuantity: quantity,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: findProductDetailById에 preparation_time_minutes 로드 추가, 체크아웃에서 pickupAt ≥ now + 제작 소요시간을 매장 리드타임과 별개 조건으로 강제. 회귀 테스트 추가 (26h 제작 상품의 22h 뒤 슬롯 거절/46h 뒤 허용).

Comment on lines +121 to +123
productNameSnapshot: product.name,
regularPriceSnapshot: product.regular_price,
salePriceSnapshot: product.sale_price,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영(정책 선택): 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 상품 거절)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 체크아웃 진입 시 requireActiveUser와 동일 의미론의 활성 USER+프로필 검증 추가 (SELLER/ADMIN → Forbidden, 삭제 계정·프로필 없음 → Unauthorized). user feature는 배럴 없는 컨벤션이라 order 경로에 동일 로직 구현. 회귀 테스트 2케이스 추가.

Comment on lines +38 to +40
@IsString()
@IsNotEmpty()
@MaxLength(30)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 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 거절/프로필 없음 거절/전화 형식)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +34 to +38
@IsOptional()
@IsString()
@IsNotEmpty()
@MaxLength(100)
buyerName?: string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: resolveBuyerInfo에서 buyerName trim 후 빈 값이면 미입력으로 취급해 프로필 닉네임 fallback. 공백 입력 회귀 케이스 추가.

buyerName을 trim해 빈 값이면 프로필 닉네임 fallback으로 처리한다 —
공백 이름이 검증된 닉네임을 덮어써 판매자에게 빈 표기로 보이는 것 방지.
회귀 케이스 1건 추가.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant