価格 (掛率) ルールにおける 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=retail、product-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 側 contextproductFacetValueIds: 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 集合を contextproductFacetValueIdsに投入。 simulateCommercialStateはCommercialRule.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_lineの ERROR ログで必ず観測する。catalog で throw するとGetFavorites/ 商品一覧 / 注文詳細が丸ごと 500 になり、1 商品の価格失敗が画面全体を落とすため。誤価格(例: 上代)を一時表示し得るトレードオフは受容し、恒常発生時は ERROR ログから pricing rule / SMILE 単価欠落を是正する。 - 実装・経路別の詳細は
docs/03-implementation/pricing/price-calculation.md§6.0 を正本とする。 - 評価ループは
sortRulesでisDefaultRate=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=retail→multiply_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-placeholderをenabled=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=retailvariant の価格が 65% に変わる (default 顧客向け)。 - メソシューティカル
product-type=professional/promotionvariant の価格が 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 で区別)。