コンテンツにスキップ

価格 (掛率) ルールにおける Facet ターゲティングと isDefaultRate (補足仕様)

位置づけ

本ドキュメントは既存仕様書を補完する追加合意事項 (正本補足) です。CommercialRule の policy primitive を拡張し、価格 (掛率) ルールを 「顧客 (group) × 商品グループ」の組 で表現できることを確定します。

合意日

  • 2026年5月20日

参照元

  • docs/specifications/2026-03-collection-facet-boundary.md — Brand=Facet / Collection=ナビゲーション の責務分離
  • Issue: arieal/pj-ritsubi-ecommerce#681

原則

  • 掛率は商品軸単独で持たない。常に「顧客 (group) × 商品グループ」で決まる軸で表現する。 商品側に markupRate 等の固有掛率 custom field は載せない。
  • default 顧客向け掛率は CommercialRule.isDefaultRate = true で表現する。 顧客 group 個別ルールは isDefaultRate = false
  • ルール解決順序: 同一 line に対し isDefaultRate=false のルールが pricing action を適用したら、後続の isDefaultRate=true (default) ルールは同 line への pricing action をスキップする。 各層内の優先順位は既存の priority (desc) → updatedAt (desc) を継続使用。

商品軸 — どこに紐付けられるか

CommercialRule.conditions.targets以下のいずれか、または組み合わせ を指定できる:

フィールド 評価 用途
個別商品 / variant productVariantIds OR (anyIn) 特定 SKU 限定の例外ルール
Collection collectionIds OR (anyIn) カタログ階層単位の値引
Facet facetValueIds AND (allIn) brand=mesoceutical AND product-type=retailproduct-type=gift 等のグルーピング

商流ルールは pricing/campaign の自己完結条件として保存する。表示制御の旧参照フィールド (resourceSetIds) は商流ルールでは使用せず、保存・正規化時に拒否する。

Collection は顧客向けカタログ専用とする原則 (2026-03-collection-facet-boundary.md) は維持しつつ、価格ルールでは Collection も適切なターゲティング手段として併用してよい。

顧客軸 — どこに紐付けられるか

CommercialRule以下のいずれか、または組み合わせ で顧客側を絞れる:

フィールド 用途
個別顧客 conditions.customer.customerIds 特定顧客向け契約掛率
顧客グループ conditions.customer.customerGroupIds Vendure CustomerGroup 単位

複数指定時は AND 評価 (全条件を満たす顧客のみ対象)。表示制御の旧参照フィールド (subjectScope / subjectSetId) は商流ルールでは使用しない。

実装ポイント

Domain

  • PolicyConditions.targets.facetValueIds?: string[] (AND 評価) を追加。 product 側 context productFacetValueIds: string[]facetValueIds全てを含む ときマッチ。 OR は conditions.any で表現する。
  • Product / ProductVariant の FacetValue を評価 context に投入し、商流ルールの対象商品条件として使える。
  • CommercialRule.isDefaultRate: boolean は必須。未指定を default 扱いにせず、rule 正規化時に明示値を要求する。

Plugin (@ritsubi/plugins/rule-engine/commercial)

  • ProductVariant 解決時に variant.facetValues + variant.product.facetValues の id 集合を context productFacetValueIds に投入。
  • simulateCommercialStateCommercialRule.conditions / tiers[].conditions に保存済みの inline 条件だけを評価する。表示制御の条件モデルは読み込まない。
  • 価格計算の失敗時方針は 経路で分ける(判断基準: 「誤価格を表示する害」と「商品が表示されない害」のどちらが大きいか)。
  • 注文行 (charged, calculateUnitPrice): fail-closed。CommercialRule / SMILE 単価マスタ / 直送加算の評価で例外が出ても base price に戻さず、例外を返して運用上検知する。誤単価での課金は財務事故になるため。
  • カタログ表示 (catalog, calculate。商品一覧 / 詳細 / 注文履歴 hydrate で共用): inputPrice(標準売上単価) へ fallback し、pricing.catalog.calculation_failed / pricing.catalog.missing_focus_lineERROR ログで必ず観測する。catalog で throw すると GetFavorites / 商品一覧 / 注文詳細が丸ごと 500 になり、1 商品の価格失敗が画面全体を落とすため。誤価格(例: 上代)を一時表示し得るトレードオフは受容し、恒常発生時は ERROR ログから pricing rule / SMILE 単価欠落を是正する。
  • 実装・経路別の詳細は docs/03-implementation/pricing/price-calculation.md §6.0 を正本とする。
  • 評価ループは sortRulesisDefaultRate=false (顧客 group 個別) を default rule より先に並べる
  • 非 default rule が pricing action (set_unit_price / multiply_unit_price / add_unit_amount) を適用した line index を priceOverriddenLineIndexes に蓄積。
  • 後続の default rule 適用時は applySimulationRule({ skipPricingForLineIndexes }) で同 line への pricing action をスキップ (gift / order amount などの非 pricing action は通常通り適用)。

Money 単位

  • Money 単位の正本は Vendure Money 単位運用 とする。Vendure の CurrencyCode.JPY は通貨コードを示すが、Ritsubi の標準構成では DefaultMoneyStrategy.precision = 2 のため、Vendure Money 値は JPY でも円の 100 倍整数として扱う。
  • CommercialRule.tiers[].actions[].value のうち set_unit_price / add_unit_amount / add_order_amount は Vendure Money 値で保存・評価する。
  • React Dashboard の価格・調整額入力は業務運用に合わせて円単位とし、保存時に Vendure Money へ変換、既存値の復元時に円へ戻す。
  • conditions.order.matchedSubtotalNet / cartSubtotalNet も保存・評価は Vendure Money、Dashboard 入力は円単位とする。
  • multiply_unit_price は倍率なので Money 変換しない。

Contract / GraphQL

  • CommercialRule.isDefaultRate: Boolean!CreateCommercialRuleInput / UpdateCommercialRuleInput / 出力型に露出。
  • conditions: JSON は変更なし (facetValueIds は JSON 内で運ぶ)。

Seeder

  • apps/vendure-server/src/data/seeders/commercial-rules.ts:
  • brand=mesoceutical AND product-type=retailmultiply_unit_price 0.65 (isDefaultRate=true)
  • brand=mesoceutical AND (product-type=professional OR product-type=promotion)multiply_unit_price 1.0 (isDefaultRate=true)
  • brand=mesoceutical を対象とする mesoceutical-customer-group-placeholderenabled=false / isDefaultRate=false で構造保証用に seed
  • 必要な Facet 値が存在しない環境では gracefully skip する。

Out of Scope

  • Dashboard rule 編集 UI の Facet picker / isDefaultRate チェックボックス
  • 顧客 group 別の実掛率 (マスク低掛率 group / 業務割引 group)
  • マスク類 / RCODE 等の新規 Facet 値 (product-category 等) と商品への付与 migration

受け入れ観点

  • targets.facetValueIds で複数 Facet 値を AND 指定したルールが、その全 Facet を持つ variant のみマッチする。
  • 既存ルール (facetValueIds 未指定) は挙動変化しない。
  • メソシューティカル product-type=retail variant の価格が 65% に変わる (default 顧客向け)。
  • メソシューティカル product-type=professional / promotion variant の価格が 100% (no-op trace 残る)。
  • 顧客 group 個別ルールを後続で追加した際、該当 line では default 掛率がスキップされる。
  • RCODE verif-rcode-quantity-tiers の 24個 75% / 24未満 80% tier が mesoceutical-retail-default に上書きされない (priority と product-type=retail vs RCODE SKU の targeting で区別)。