spree-pricing について
Spree 6の価格設定を解説する開発者向けスキルです。通貨別の基本価格、価格リスト、ルール、価格コンテキストを扱い、ストアフロントでの価格決定とAPI管理を説明します。
主な機能
- 01通貨別の基本価格
- 02条件付き価格リストとルール
- 03コンテキストに基づく価格決定
- 04数量別価格と数量段階
- 05基本価格の履歴記録
活用シーン
- 01通貨ごとに異なる価格を設定
- 02期間限定の価格リストを予約
- 03数量に応じた価格を適用
- 04購入者に表示される価格の根拠を調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-pricingインストール後、Claude Code で/spree-pricingを入力して読み込まれたか確認します。
詳細
Spree の価格設定
Spree 6 では、バリアントの価格は常に 金額と通貨の組み合わせであり、暗黙の「その」価格はありません。基本価格に加えて、価格リスト(条件付きの上書き)があり、リクエストごとに価格設定コンテキストから解決されます。
Variant ── Price (price_) × n
├── base prices: price_list_id = nil, one per currency (what everyone pays by default)
└── list prices: price_list_id = pl_…, per currency, optional min_quantity (quantity breaks)
PriceList (pl_) ── PriceRule × n (STI: MarketRule, ChannelRule, VolumeRule, …)
└── optionally owned by one Catalog (cat_) → audience = the catalog's assignments
Price ── PriceHistory × n (base-price changes, for EU Omnibus)
税を含む表示と税抜き表示、および VAT の再表示は spree-taxes で扱います。
基本価格:price_in / amount_in / set_price
variant.set_price('USD', 39.99) # upsert the USD base price (saves immediately if the variant is persisted)
variant.set_price('USD', 39.99, 49.99) # 3rd arg = compare_at_amount; omitting it CLEARS compare-at
variant.set_price('EUR', nil) # blank amount deletes that currency's base price
variant.price_in('USD') # => Spree::Price (unsaved blank Price if none — check .persisted? / .amount)
variant.amount_in('USD') # => BigDecimal or nil
variant.compare_at_amount_in('USD')
variant.price_in('USD').display_amount # "$39.99"
product.price_in('USD') # delegates to product.default_variant
variant.prices.base_prices.exists?(currency: 'EUR')
variant.price、price=、default_price、display_price、currencyは存在しません。対象の通貨を渡してください。price_inは基本価格のみを返します。買い物客が実際に支払う価格(価格リスト適用後)には、以下のvariant.price_for(...)を使用してください。- 為替換算はありません。ストアで販売する各通貨に、それぞれ価格が必要です。リクエストの通貨で基本価格が設定されていない商品は、Store API の一覧に表示されません。
Spree::Price#amount=はローカライズされた数値を解析します('1,599.99'が使用できます)。- 入れ子になった書き込み:
variant.prices = [{ currency:, amount:, compare_at_amount: }]は、指定した通貨をアップサートし、指定されていない通貨の基本価格を削除します([]はすべてクリアします)。product.prices = [...]はデフォルトバリアントに転送されます。
API 経由
await admin.prices.create({ variant_id: 'variant_xxx', currency: 'USD', amount: '15.99' })
// Upsert many (matches on variant + currency + price_list_id + min_quantity)
await admin.prices.bulkUpsert({
prices: [
{ variant_id: 'variant_xxx', currency: 'USD', amount: '15.99', compare_at_amount: '19.99' },
{ variant_id: 'variant_xxx', currency: 'EUR', amount: '14.99' },
],
})
await admin.prices.bulkDestroy({ ids: ['price_xxx'] })
spree api post /prices/bulk_upsert -d '{"prices":[{"variant_id":"variant_xxx","currency":"USD","amount":"15.99"}]}'
/api/v3/admin/prices は基本価格および価格リストの上書き(price_list_id: 'pl_…')を扱います。Store API のレスポンスには、リクエストの通貨、マーケット、顧客に対して解決済みの price / original_price が含まれます。
価格リスト
| 属性 | 備考 |
|---|---|
status | draft(デフォルト)、active、scheduled、inactive — ワークフローで状態を遷移させます(Spree.price_list_activate_workflow / price_list_deactivate_workflow、API PATCH /price_lists/:id/activate)。starts_at が未来の価格リストを有効化すると、scheduled としてマークされます。 |
starts_at / ends_at | 時間枠(ストアのタイムゾーン)。スケジュール済みの価格リストはこの期間内に自動適用されます。ブラックフライデーセールの実施には、この方法を使います。 |
match_policy | ルールに対する all / any。 |
position | 単独の価格リスト間の優先順位 — 数値が小さいものが先です。バリアントに価格を設定する最初の価格リストが適用されます。 |
price_adjustment_percentage + price_adjustment_tiers | カタログ所有の価格リストのみ:基本価格 × (1 + pct/100) で価格を算出し、必要に応じて数量ごとの段階を設定します。価格リストに明示的な価格行がある場合は、そちらが優先されます。 |
const pl = await admin.priceLists.create({
name: 'EU Black Friday',
starts_at: '2026-11-27T00:00:00Z',
ends_at: '2026-11-30T23:59:00Z',
match_policy: 'all',
rules: [{ type: 'market_rule', preferences: { market_ids: ['mkt_eu'] } }],
prices: [{ variant_id: 'variant_xxx', currency: 'EUR', amount: '19.99' }],
})
await admin.priceLists.activate(pl.id) // lists are born draft
GET /api/v3/admin/price_lists/price_rule_types は、登録済みのルールタイプとその設定スキーマを一覧表示します。商品メンバーシップ(バリアント × 通貨ごとのプレースホルダー行)は admin.priceLists.products を通じて設定します。
価格ルール — どこで/何を通じて/どれだけ
タイプ(api_type) | クラス | 一致条件 |
|---|---|---|
market_rule | Spree::PriceRules::MarketRule | コンテキストのマーケットが market_ids に含まれること(地域ルール — ゾーンルールではありません) |
channel_rule | Spree::PriceRules::ChannelRule | コンテキストのチャネルが channel_ids に含まれること(空の場合はすべて) |
volume_rule | Spree::PriceRules::VolumeRule | min_quantity ≤ 明細数量 ≤ max_quantity(null 許容)— コンテキスト依存 |
user_rule、customer_group_rule | UserRule、CustomerGroupRule | 互換性維持のための既存ルール(superseded? = true):既存のルールは引き続き一致しますが、選択 UI には表示されなくなりました |
価格を適用する対象者はルールではなく、カタログで決まります。価格リストを、顧客グループまたは企業に割り当てられた Spree::Catalog に紐付けます(チャネルのデフォルトカタログは default_catalog_id で指定します)。spree-b2b を参照してください。
カスタムルールを作成するには、Spree::PriceRule をサブクラス化して preference を追加し、applicable?(context) を実装して、config/initializers/spree.rb の Rails.application.config.after_initialize { Spree.pricing.rules << MyApp::PriceRules::LoyaltyTierRule } で登録します。購入に関する条件(数量など)を判定する場合は self.contextual? をオーバーライドして true を返します。これにより、カタログ所有の価格リストでも適用されます。
価格の解決方法
variant.price_for(context_or_hash) → Spree::Pricing::PriceResolution → ストアの価格プロバイダー(デフォルトは Spree::PricingProvider::Internal):
- カタログ所有の価格リスト。最も近いノードから順に確認 — 購入者の会社ノード、次にその祖先ノード。該当するものがない場合は顧客グループのカタログ、さらに該当するものがなければチャネルのデフォルトカタログを確認します。同じノード内では、(購入数量に対して)最も安い価格が適用されます。所有価格リストでは対象者ルールをスキップしますが、コンテキストルール(例:
VolumeRule)、ステータス、日付は引き続き確認されます。 - ルールが一致する単独の価格リストを
positionの順に確認します。バリアントに価格を設定している最初のリストが適用されます。 - 基本価格(
price_list_id: nil)。
ctx = Spree::Pricing::Context.new(
variant: variant, currency: 'EUR',
market: market, channel: channel, user: customer, company: company, quantity: 24
) # store/market/channel/country default from Spree::Current; date defaults to now
variant.price_for(ctx) # => Spree::Price or nil
variant.price_for(currency: 'EUR', user: customer, quantity: 24) # hash form
Spree::Pricing::Context.from_order(variant, cart) # uses the cart's own market/channel/customer
Store API は X-Spree-Currency / X-Spree-Country、API キーのチャネル、認証済みの顧客からこのコンテキストを構築するため、手作業は不要です。プロセス内の価格キャッシュはありません。公開キャッシュ可能なのはゲスト向けレスポンスのみです(Vary: Accept, x-spree-currency, x-spree-locale, x-spree-channel)。認証済みのレスポンスは private, no-store です。国は Vary に含まれませんが、販売市場の選択に使われるため、CDN/アプリのキャッシュキーに追加してください(spree-i18n を参照)。
外部価格設定(ERP、契約価格): 価格プロバイダー(Spree::PricingProvider::Base#price_for(context)、handles?、cache_ttl)を設定します。プロバイダーはカタログの読み取りパスで実行されるため、cache_ttl を宣言し、handles? で匿名コンテキストを対象外にしてください。spree-providers を参照してください。
数量別価格と価格段階
方法は2つあります。価格の形状に応じて選択してください。
- 1つのリストに数量別価格を設定 —
min_quantity > 1の価格行を使います(price_list_idが必要です)。適用される価格は、明細の数量以下で最も大きいmin_quantityを持つ行のものです。リストに段階価格が設定されているバリアントには、その段階価格のみが適用されます。await admin.prices.bulkUpsert({ prices: [ { variant_id: 'variant_xxx', currency: 'USD', price_list_id: 'pl_trade', amount: '10.00' }, { variant_id: 'variant_xxx', currency: 'USD', price_list_id: 'pl_trade', min_quantity: 24, amount: '8.50' }, ]}) volume_rule(min_quantity/max_quantity)を設定したティアごとの価格リスト — 品揃え全体やその他のルールがティアごとに異なる場合に使います。ティアが重複しないようにしてください。
quantity のないコンテキスト(商品一覧、エクスポート)には、最も低い段階の価格が適用されます。
EU オムニバス指令:価格履歴と prior_price
基本価格の金額を変更するたびに、Spree::PriceHistory の行が記録されます(価格リストの価格は追跡されません)。price.prior_price は、過去30日間に記録された最安金額を返します(設定直後の金額も含みます)。
- ストアごとの設定:
preferred_track_price_history(デフォルトはtrue)、preferred_price_history_retention_days(デフォルトは30)。グローバル設定Spree::Config[:track_price_history]は非推奨です。ストアに設定してください。await admin.store.update({ preferred_track_price_history: false }) // non-EU store - Store API:商品/バリアントに
?expand=prior_priceを指定すると、{ amount, currency, display_amount, recorded_at }が返されます。 - Rake:
spree rake spree:price_history:seed(初回のみの基準値を設定します。すでに履歴がある価格はスキップします)、spree rake spree:price_history:prune(Solid Queue の定期タスクなどでスケジュールしてください)。
一括更新
update_all はコールバックをスキップするため、価格履歴が記録されず、コレクションの再マッチングも行われません。Admin API の bulk_upsert を使うか、次のように反復処理してください。
Spree::Price.base_prices.where(currency: 'USD').where.not(amount: nil).find_in_batches(batch_size: 500) do |batch|
Spree::Price.transaction { batch.each { |p| p.update!(amount: (p.amount * 1.1).round(2)) } }
end
「顧客に誤った価格が表示される」
- 通貨 —
Spree::Current.currencyまたはカートの通貨は何ですか?その通貨でバリアントに基本価格が設定されていますか? - どのリストが適用されたか —
variant.price_for(currency:, user:, quantity:)&.price_listを確認し、その購入者に適用されるカタログ(会社ノード、顧客グループ、チャネルのデフォルトの順)と、position順の単独リストを確認します。Spree::Current.price_listsには、このリクエストで候補となる単独リストが表示されます。 - ステータス/期間 — 下書きのリストは適用されません。予約済みのリストは
starts_at..ends_atの期間内のみ適用されます。無効化されたカタログが所有するリストは休止状態になります。 - 数量 — ボリュームルールと数量別価格は、コンテキストに数量がある場合にのみ適用されます(カートの明細にはありますが、商品一覧にはありません)。
- カートと商品詳細ページ(PDP) — 明細には、価格設定時の価格が保持されます。カートは、重要な変更(商品、住所/販売市場)があった場合に
Spree::Carts::Recalculateを通じて価格を再計算します。注文確定後は価格が固定されます。 - 税表示 — 税込価格と税抜価格の扱いは、販売市場/税の設定に関するものです(
spree-taxes)。
注意点
- 3 番目の引数を指定せずに
set_price(currency, amount)を呼び出すと、compare_at_amountが消去されます。 price_inは nil ではなく、未保存の空のSpree::Priceを返します。.amountまたは.persisted?をテストしてください。- 読み取り・変更・書き込み時の落とし穴:
?expand=variants.pricesは、ベース価格、価格リストの価格、数量別価格の各行をすべて返し、それぞれにprice_list_id(nullはベース価格)が含まれます。バリアントや商品にpricesを書き戻すと、すべての項目がベース価格として扱われ(省略した通貨は削除されます)。そのため、展開した配列をそのまま POST すると、ショップ価格がリスト価格や段階価格で上書きされ、金額が null の会員用プレースホルダーによって通貨が削除されます。送信前にprice_list_id === nullでフィルターしてください。リスト価格は/api/v3/admin/pricesを通じて書き込みます。(ダッシュボードの商品フォームではこのフィルタリングを行いますが、独自のクライアントでも同様に処理する必要があります。) price_list_idがない状態でmin_quantity > 1を指定することはできません。パーセンテージによる調整は、カタログ所有のリストでのみ許可されます。- カタログの価格設定を削除する(
price_list: nullを指定する)か、カタログを削除すると、所有するリストはソフト削除されます。全員に一致するよう「解放」されることはありません。 - Store API の商品の価格範囲フィルター(
products.filters→price_range)は、ベース価格のみから算出されます。会社の契約価格、下書き中または予約済みのリストなど、価格リストの価格は対象外です。また、チャネルが呼び出し元に価格を表示しない設定の場合、完全に省略されます。B2B 購入者のリスト価格が反映されるとは限りません。 - 価格リストへの書き込み(カタログのインラインリストを含む
Spree::PriceLists::Update)では、別のストアに属するバリアントが黙って除外されます。 - チェックアウト時の割引やクーポンコードにはプロモーション(
spree-promotions)を使用し、値下げ価格を商品ページに表示する必要がある場合は価格リストを使用してください。
詳細情報
node_modules/@spree/docs/dist/developer/core-concepts/pricing.mdnode_modules/@spree/docs/dist/developer/core-concepts/catalogs.mdnode_modules/@spree/docs/dist/user/how-to/volume-pricing.md、.../schedule-sale-pricing.md、.../wholesale-pricing.md- ソース:
spree_core内のSpree::Price、Spree::PriceList、Spree::PriceRule、Spree::Pricing::Context、Spree::PricingProvider::Internal - 関連スキル:
spree-catalog、spree-b2b、spree-taxes、spree-providers、spree-promotions