数量制限の実装スコープ境界¶
- 作成日: 2026-04-02
- 更新日: 2026-06-08
- 目的: 数量制限機能を「基本制限」と「複合制限」に分類し、現在の実装範囲と未実装範囲を明文化する。
概要¶
本ドキュメントは Issue「数量制限の実装範囲とテスト不足を整理する」に対応した、実装・テスト状況の棚卸し結果です。
数量制限は以下の2階層に分類します。
| 分類 | 内容 | フェーズ |
|---|---|---|
| 基本制限 | PurchaseLimitRule の minimumQuantity / purchaseUnit / maxPerOrder | 実装済み |
| 月間購入数制限 | PurchaseLimitRule の limitQuantity(JST 暦月集計) | 実装済み |
| その他の複合制限 | 累計制限・カテゴリ制限・組み合わせ制限 | Phase 2(未実装) |
基本制限(Phase 1)¶
フィールド定義¶
購入制限の正本は PurchaseLimitRule 系 entity とする。旧 ProductVariant custom field の
minimumQuantity / purchaseUnit / maxPerOrder / periodPurchaseLimit* は migration/backfill の入力としてのみ扱い、
移行後の現行 schema・Storefront 表示・backend guard は PurchaseLimitRule だけを見る。
| フィールド | 説明 | デフォルト |
|---|---|---|
minimumQuantity |
1回の注文で購入できる最小数量 | 1 |
purchaseUnit |
購入数量の単位(例: 3個単位) | 1 |
maxPerOrder |
1回の注文で購入できる最大数量 | 制限なし(null) |
minimumQuantityとpurchaseUnitは別の制約として扱う。 注文可能な数量は、minimumQuantity以上かつpurchaseUnitの倍数を同時に満たす値とする。- 初期数量や自動補正では、上記 2 条件を最初に満たす最小有効数量を採用する。
- 例:
minimumQuantity=1, purchaseUnit=10→10, 20, 30, ... - 例:
minimumQuantity=5, purchaseUnit=3→6, 9, 12, ... - 例:
minimumQuantity=10, purchaseUnit=10→10, 20, 30, ... - Vendure Dashboard の Product Variant 直書き項目は readonly とし、編集正本にはしない。
商品共通制限は、対象顧客・対象顧客グループを空にした
PurchaseLimitRuleとして設定する。
適用箇所と実装状況¶
1. ドメイン層(packages/domain/src/rules/product.ts)¶
共通ロジックを集約しています。テスト:
packages/domain/src/rules/product.spec.ts。
resolvePurchaseConstraints(): Product/Variant レベルの優先順位を統一解決。バンドル商品は制限を無効化。sanitizeQuantity(): 入力値を、最小数量と購入単位を同時に満たす最小有効数量へサニタイズ。determineInitialQuantity(): 購入画面の初期数量を、制約を最初に満たす数量として算出。
2. Storefront UI(商品カード / 商品詳細 / クイックオーダー / カート / checkout)¶
Storefront では apps/storefront/src/lib/purchase-limit.ts を数量制限 helper とし、基本制限と月間購入数制限を同じ rule state から扱う。
商品カード・商品詳細などの「カート投入 CTA を有効にしてよいか」の最終判定は
apps/storefront/src/lib/cart-eligibility.ts を共通入口にする。
- 商品カード・商品詳細の
AddToCartButtonは、active order 内の同一 variant 数量を合算した残追加可能数を使ってmaxPerOrder/ 月間上限を事前判定する。 - quick order は SKU 解決後に同じ helper で
min / step / max/cart / max/期間を検証する。 - カート明細は、現在の line を除いた同一 variant 数量を合算して、更新可能な最大数量を再計算する。
- checkout は visible order lines 全体を再評価し、数量制限を超える商品がある場合は確定導線をブロックする。
minimumQuantity=5, purchaseUnit=3のような組み合わせでも、quick order / 商品詳細 / カート / セット商品検証が同じ判定式を使う。cart-eligibility.tsは、購入可能期間・在庫状態・Shop API 拒否後の理由・配送モード制約・数量制限を束ね、canAddToCart/reason/summary/showInfoを返す。UI component はこの結果を描画するだけにし、 surface ごとに別の購入不可理由や優先順を持たせない。- 数量上限の事前判定では
maxPerOrder/ 期間ルールに加えて、実在庫数saleableStockLevel(カート内既存数量を差引いた残数)でもAddToCartButtonの 数量ステッパー上限を丸める(在庫超過の要求を事前に防ぐ)。詳細はdocs/specifications/2026-04-product-variant-out-of-stock-threshold.mdを正本とする。 addItemToOrderがInsufficientStockErrorを返した場合は、サーバが返すquantityAvailableで数量上限を実在庫に丸めて再試行可能にする(残数 0 のときのみ 在庫切れとして CTA を無効化)。quantityAvailableを欠くレスポンスや他の拒否はresolveAddItemToOrderRejectionReason()で購入不可理由へ変換し、以後の CTA を 無効化する(安全側 fallback)。いずれも UX 上の反映であり、server-side guard の代替ではない。
3. 商品詳細・商品カードの制限説明 UI(apps/storefront/src/components/product/purchase-restriction-notice.tsx)¶
PurchaseRestrictionNotice を popover UI とし、価格内訳と同様に購入可能数制限を説明する。
- 表示項目は
PurchaseLimitRule由来のminimumQuantity(最小購入可能数)、purchaseUnit(購入単位数量)、maxPerOrder(1回のご注文上限)、limitQuantity(月間上限・購入済み数量・カート内数量・追加可能数量)を含む。 - 残数表示には、Shop API の
ProductVariant.purchaseLimitRuleStatesを使用する。 - 顧客を解決できない場合は、残数ではなく「ログイン後に残数を表示します」を表示する。
顧客・顧客グループ別の月間購入数制限¶
購入制限 rule は、顧客ターゲットなしなら商品共通制限、顧客または顧客グループターゲットありなら個別制限として扱う。limitQuantity を持つ rule は 月間購入数制限として扱う。
- 集計期間は JST の暦月に固定する。
- 開始: 当月1日 00:00:00 JST(含む)
- 終了: 翌月1日 00:00:00 JST(含まない)
- 例: 2026年6月の月間制限は
2026-06-01 00:00:00 JST以上、2026-07-01 00:00:00 JST未満の注文履歴を集計する。 - Storefront の表示期間は
2026-06-01〜2026-06-30のように「当月1日〜月末」とする。 - rule の
periodStart/periodEndは rule 自体の有効期間として使う。 月間の購入履歴集計期間を任意日付範囲へ変更するための項目ではない。 - backend guard と Storefront の残数表示は、どちらも同じ暦月集計を正本にする。
- 履歴集計に含める注文 state は「成立した購入」のみとし、
Cancelled(キャンセル)とDeclined(決済拒否=未成立)は除外する。除外集合はpackages/plugins/src/standard-extensions/inventory/order-history-states.tsのEXCLUDED_PURCHASE_HISTORY_STATESを正本とし、月間購入数制限・購入制限ルール・予約上限の 3 系統で共有する。 - 返金系 state(
Refunded/PartiallyRefunded)を月間枠へ戻すか否かは業務要件の確定待ち。 確定するまで返金注文は枠を消費したまま(集計に含めたまま)とし、要件確定後に除外集合へ反映する。
4. 商品詳細ページ(apps/storefront/src/components/product/add-to-cart-button.tsx)¶
resolvePurchaseConstraints() と purchase-limit.ts を使用して商品詳細の数量 UI に制約を適用し、
CTA の有効・無効は cart-eligibility.ts の resolveVariantCartEligibility() を使う。
PurchaseLimitRule.minimumQuantityが最低数量の正本。旧 ProductVariant custom field は migration/backfill 入力としてのみ扱う。maxPerOrderは active order 内の既存数量を合算した残追加可能数で判定する。- 月間購入数制限は server-side の履歴累計に active order 内既存数量を足した残数で判定する。
- 最小有効数量より残追加可能数が小さい場合は、CTA を事前ブロックする。
- 在庫不足など Shop API で初めて確定する拒否理由を受けた場合は、同じ
resolveVariantCartEligibility()のserverUnavailableReasonとして扱い、ボタンと数量操作を無効化する。
5. セット商品サーバー側検証(packages/plugins/src/standard-extensions/set-products/)¶
SetProductsOrderInterceptor が willAddItemToOrder / willAdjustOrderLine
で構成品の数量制限を検証。
テスト:
packages/plugins/src/standard-extensions/set-products/index.spec.ts(構成品の数量制限と月間購入数制限を検証)
検証ロジック(component-validator.ts):
validateComponentQuantityRules()
├─ maxPerOrder 超過 → "購入上限を超えています(商品番号=XXX / 上限=N)。"
├─ minimumQuantity 未満 → "最低注文数に満たない数量です(商品番号=XXX / 最低=N)。"
├─ purchaseUnit 不一致 → "購入単位に合わない数量です(商品番号=XXX / 単位=N)。"
└─ 在庫不足 → "在庫が不足しています(商品番号=XXX / 在庫=N)。"
テストカバレッジ:
| ケース | willAddItemToOrder |
willAdjustOrderLine |
|---|---|---|
| maxPerOrder 超過時にエラー | ✅ | ✅ |
| minimumQuantity 未満時にエラー | ✅ | ✅ |
| purchaseUnit 不一致時にエラー | ✅ | ✅ |
複合制限(Phase 2・一部実装)¶
月間購入数制限は PurchaseLimitRule.limitQuantity として実装済みです。その他の複合制限は未実装であり、引き続き今回のスコープ外とします。
QuantityControlRuleEntity の現在の位置づけ¶
ファイル:
packages/plugins/src/standard-extensions/inventory/entities/quantity-control-rule.entity.ts
エンティティは定義・登録されていますが、以下の制御タイプに対応するサービス側の判定処理は実装されていません。
| controlType | 内容 | 実装状況 |
|---|---|---|
MIN_QUANTITY |
最小購入数量 | ✅ PurchaseLimitRule.minimumQuantity で実装済み |
MAX_QUANTITY |
最大購入数量 | ✅ PurchaseLimitRule.maxPerOrder で実装済み |
STEP_QUANTITY |
購入単位制限 | ✅ PurchaseLimitRule.purchaseUnit で実装済み |
TOTAL_LIMIT |
累計購入制限 | ❌ 未実装 |
PERIOD_LIMIT |
月間購入数制限(JST 暦月) | ✅ 月間購入数制限として PurchaseLimitRule.limitQuantity で実装済み |
CATEGORY_LIMIT |
カテゴリ別制限 | ❌ 未実装 |
COMBINATION_LIMIT |
組み合わせ制限 | ❌ 未実装 |
QuantityControlRuleEntity は引き続き将来の横断ルール用に保持し、購入制限の現行正本にはしない。
今回実装した PERIOD_LIMIT¶
- 設定フィールド:
limitQuantityminimumQuantitypurchaseUnitmaxPerOrder- 判定導線:
- 商品追加
- カート数量変更
- 注文確定直前
- Storefront の事前表示(
ProductVariant.purchaseLimitRuleStates) - 集計単位:
- 同一顧客 × 同一 ProductVariant
- セット商品は親 SKU ではなく構成品 OrderLine を集計
- 期間境界:
- 月間購入数制限の集計は JST 暦月
- rule の
periodStart/periodEndは rule 自体の有効期間であり、月間集計範囲ではない - 自動テスト:
packages/plugins/src/standard-extensions/inventory/purchase-limit-rule.service.spec.tsapps/storefront/src/lib/purchase-limit.test.tsapps/storefront/src/components/product/purchase-restriction-notice.test.tsx
Storefront への露出方法¶
- Product Variant の raw custom field (
minimumQuantity/purchaseUnit/maxPerOrder/periodPurchaseLimit*) は旧 DB column からの migration/backfill 入力としてのみ扱い、購入制限表示の正本にはしない。 - Shop API の
ProductVariant.purchaseLimitRuleStatesが以下を返す。 labellimitminimumQuantitypurchaseUnitmaxPerOrderstartDateendDatehistoricalQuantityisCustomerResolved- Storefront はこの
historicalQuantityに active order 内の同一 variant 数量を足して、残追加可能数を算出する。
Phase 2 で実装が必要な制限¶
- 累計制限(TOTAL_LIMIT): 顧客ごとの過去注文履歴を参照し、累積購入数量を制限する
- カテゴリ別制限(CATEGORY_LIMIT): 特定カテゴリの商品を複数種類購入する際の合計数量を制限する
- 組み合わせ制限(COMBINATION_LIMIT): 特定商品の組み合わせ購入に対するルール(同時購入許可・禁止・数量上限)を適用する
Phase 2 実装時の前提条件¶
- 注文履歴参照 API の設計
QuantityControlRuleEntityとオーダーインターセプターの接続- 期間テンプレート(プリセット)の運用設計
- 管理者例外許可(
adminOverrideAllowedフラグ)の UI 連携
責務境界まとめ¶
購入制限(実装済み)
└─ PurchaseLimitRule
├─ minimumQuantity (最小購入数量)
├─ purchaseUnit (購入単位)
├─ maxPerOrder (1注文あたり最大数量)
└─ limitQuantity (JST 暦月の月間購入上限)
└─ Shop API
└─ ProductVariant.purchaseLimitRuleStates
└─ 適用箇所
├─ InventoryManagementPlugin の PurchaseLimitRuleOrderInterceptor
├─ Storefront UI(商品カード・商品詳細・クイックオーダー・カート・checkout)
└─ 注文確定前の OrderProcess
その他の複合制限(Phase 2・未実装)
└─ QuantityControlRuleEntity
├─ TOTAL_LIMIT (累計購入制限)
├─ CATEGORY_LIMIT (カテゴリ別制限)
└─ COMBINATION_LIMIT(組み合わせ制限)
関連ファイル¶
| ファイル | 役割 |
|---|---|
packages/domain/src/rules/product.ts |
数量制約の共通ロジック |
packages/domain/src/rules/product.spec.ts |
数量制約のユニットテスト |
packages/plugins/src/standard-extensions/inventory/purchase-limit-rule.service.ts |
購入制限 rule の集計・判定 |
packages/plugins/src/standard-extensions/inventory/purchase-limit-rule.resolver.ts |
Storefront 向け購入制限 rule state の Shop API |
packages/plugins/src/standard-extensions/inventory/purchase-limit-rule.service.spec.ts |
購入制限 rule の service テスト |
packages/plugins/src/standard-extensions/inventory/purchase-limit-rule.process.ts |
注文確定前の再検証 |
apps/storefront/src/lib/purchase-limit.ts |
Storefront の数量・月間上限判定 helper |
apps/storefront/src/lib/cart-eligibility.ts |
Storefront のカート投入 CTA 可否・不可理由の統一判定 |
apps/storefront/src/components/product/purchase-restriction-notice.tsx |
購入可能数制限の popover 表示 |
packages/plugins/src/standard-extensions/set-products/component-validator.ts |
セット商品構成品の数量バリデーション |
packages/plugins/src/standard-extensions/set-products/index.spec.ts |
セット商品数量制限テスト |
packages/plugins/src/standard-extensions/inventory/entities/quantity-control-rule.entity.ts |
複合制限エンティティ(Phase 2) |
apps/vendure-server/src/config/custom-fields/product-variant-custom-fields.ts |
移行元として残す readonly Variant customFields |
docs/01-requirements/shipping-and-purchase-unit-requirements.md |
購入単位マスタ要件 |