mcpField
S
spree-b2b

spree-b2b

github.com/spree開発10102 回インストール4 週間前に更新
リポジトリ

spree-b2b について

Spree 6 の卸売ストア構築を支援するスキルです。企業構造、カタログ価格、数量ルール、アクセス制御を扱い、B2B 設定の規則や注意点を説明します。

主な機能

  1. 01企業ツリーとメンバーシップ
  2. 02企業別カタログと価格設定
  3. 03数量ルールと注文最低額
  4. 04卸売ストアのアクセス制御

活用シーン

  1. 01Spree B2B ストアの設定計画
  2. 02企業購入者とメンバーシップの設定
  3. 03卸売価格と数量段階の設定
  4. 04カタログや価格の閲覧制限

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/spree/agent-skills --skill spree-b2b

インストール後、Claude Code で/spree-b2bを入力して読み込まれたか確認します。

詳細

Spree B2B/卸売

Spree 6 では、B2B は4 つのオープンソースの構成要素で構成されています。これらはすべて設定であり、1 つのストアフロントと 1 つのバックエンドで、小売と業務用販売を並行して提供できます。

構成要素対応する内容
Company(Spree::Company)誰が購入するか:組織、そのメンバー、住所、税務上の識別情報
Catalog(Spree::Catalog)契約内容:その対象者に何を表示するか、いくらで販売するか、最低注文数量はいくつか
チャネルによる制限(storefront_access)ゲストにカタログや価格を表示するかどうか
Freight(配送ルールと Freight レートプロバイダー)卸売貨物の配送方法:カートン、パレット、コンテナ、審査後の見積もり

最初から最後までの手順は、B2B ストアを構築するガイド(node_modules/@spree/docs/dist/developer/how-to/build-a-b2b-store.md)にあります。設定の順序やダッシュボードの画面については、このガイドに従ってください。このスキルでは、その背景にあるルールや注意点を説明します。

すべてのストアにシードされるもの: wholesale チャネル(login_required、ゲストチェックアウトは無効)、Wholesale 顧客グループ(卸売ポータルの承認マーカー)、および卸売チャネルに紐付けられた公開可能なキー。サンプルデータ(spree sample-data)には、デモ購入者 [email protected]/spree123 が含まれます。これらの認証情報は公開されているため、サンプルデータを本番環境に読み込まないでください(このタスクには環境ガードがありません)。また、数量別価格が設定された卸売価格表、2 階層の会社 Acme Industrial → Acme EMEA、および Acme Industrial に割り当てられた非アクティブな Wholesale Assortment カタログも含まれます。

Spree Enterprise のみ:会社のロールと権限(OSS のメンバーシップにある role は表示上のラベルにすぎず、各メンバーは自身の権限の範囲内ですべての操作を行えます)、注文の承認(approval_required)、利用限度額、ガバナンス監査履歴、会社のオンボーディング/承認フロー。支払い条件、掛け払い、見積もりは Enterprise のロードマップに含まれています。Enterprise のガバナンスは同じエンドポイントを通じて適用されるため、有効化しても OSS のストアフロントは変更なく動作します。

企業

  • 最大 5 階層のツリーです(Spree::Company::MAX_DEPTH)。ルートの kind は company でなければなりません。kind は、税務登録を保持できる法人を表す company、または保持できない組織単位を表す division です。プレフィックス:comp_。
  • メンバーシップはサブツリー全体に適用されます。「Acme Europe」のメンバーは、その配下のすべてのノードに対して操作できます。認可では常に、「この顧客はこのノードまたはその祖先のいずれかに対する権限を持っているか」を確認します。メンバーシップ(cmem_)は常に有効で、必ず実在する顧客に紐づいています。
  • メールアドレスでメンバーを追加します。 ストアフロントからの操作(client.companies.members.create(companyId, { customer_email }) → POST /api/v3/store/companies/:id/members)では、既存の顧客であっても必ず招待(cinv_)が作成されます。アドレスを入力しただけでは、意図した相手かどうか確認できず、レスポンスからそのメールアドレスにアカウントがあるかどうかもわからないためです。スタッフからの操作(ダッシュボード/POST /api/v3/admin/companies/:id/memberships、サービス Spree::Companies::AddMember)では、既存の顧客はすぐにメンバーになり(cmem_)、不明なメールアドレスには招待が送られます。ID のプレフィックスを確認してください。招待の有効期限は30日です。招待された人は、サインインせずに companyInvitations.lookup(token) / accept(token, { first_name, last_name, password }) を呼び出します。
  • 会社ごとの住所録です。 各住所にラベルが付き、配送先と請求先のデフォルト住所を設定できます。配送拠点は住所でありノードではないため、倉庫が10か所あっても会社を10社作る必要はありません。
  • 税務情報は最も近い company の祖先(company.legal_entity)を通じて解決されます。 登録情報がなくても、最初の company ノードで探索は停止するため、子会社が親会社の VAT 番号を引き継ぐことはありません。税務識別番号と免税証明書は、法人にのみ紐づきます。証明書のステータスではなく active を参照してください。active は有効期限も考慮するためです。spree-taxes を参照してください。
  • ノードを削除すると、そのサブツリー全体が削除されます。配下に注文がある場合、Spree は削除を拒否します。ブランチを移動するには、parent_id を更新します。
  • 会社向けのストアフロントでの自己登録機能はありません。Store API で /store/companies に対して提供されるのは show/update のみです。スタッフはダッシュボード、または POST /api/v3/admin/companies を通じて会社を作成します。
会社向けの購入
// Store SDK: which companies can the signed-in customer act for?
const { data: memberships } = await client.account.companies()   // m.company.ancestors = path above it

await client.carts.update(cartId, { company_id: 'comp_xxx' })     // must be a node they have standing on
const { data: orders } = await client.companies.orders.list('comp_xxx') // whole-subtree order history
// other members' orders come with gift card codes masked and no digital download links — those stay with the buyer
await client.companies.addresses.create('comp_xxx', { label: 'Northern Warehouse', /* … */ default_shipping: true })

メンバーシップが1つだけの購入者は、会社が自動的に決まるため、選択する必要はありません。カートの company_id は、住所や価格とともに注文の完了時に注文へ固定されます。カートでは company_id、company_name、po_number_required を参照できます。バックエンドでは、コードから参照するのは Spree::Purchase::Company concern の cart.b2b? / cart.resolved_company です。

カタログ:取引条件

カタログ(cat_)には、品揃え(CatalogProduct)、任意の専有価格リスト、customer_group または company への割り当て(cata_)、および取引条件があります。

  • 品揃えが空の場合、価格オーバーレイになります:対象者には、カタログの価格ですべての商品が表示されます。品揃えが空でない場合、範囲が制限されます:対象者には、その商品だけが表示されます。最初の商品を追加すると、カタログは「すべて」から「これだけ」に切り替わります。
  • 価格リストは、厳密に1つのカタログに専有され、独自の対象ルールはありません。適用されるのは、カタログが適用されるからであり、通常のルール照合の対象からは除外されます。削除する(price_list: null)かカタログを削除すると、価格リストは論理削除されます。所有者もルールもない価格リストはすべての購入者に価格を適用してしまうため、Spree は決して解放しません。無効化されたカタログの価格リストは休止状態になります。
  • 価格は、バリアントごとの明示的なprices、および/またはprice_adjustment_percentage(例:-15)で設定し、任意で数量区分ごとのprice_adjustment_tiersを指定します。パーセンテージ方式は、カタログ専有の価格リストでのみ機能します。
  • カタログは無効状態で作成されます。 公開するには、adminClient.catalogs.activate(id)(PATCH /admin/catalogs/:id/activate)を実行します。有効化は(列への書き込みではなく)ワークフロー(Spree::Catalogs::Activate)であり、チャネルのdefault_catalog_idに指定されている場合を除き、割り当てのないカタログの有効化は拒否されます(エラーno_audience)。無効化しても、後で使えるようにすべてが保持されます。削除すると、専有価格リストも削除されます。
const catalog = await adminClient.catalogs.create({
  name: 'Acme wholesale',
  price_list: { name: 'Acme pricing', price_adjustment_percentage: -15 },
  assignments: [{ assignable_type: 'company', assignable_id: 'comp_xxx' }],
  minimum_order_quantity: 6, order_multiple: 6,          // catalog-wide defaults
  order_minimums: [{ currency: 'EUR', amount: 500 }],    // per currency, never converted
})
await adminClient.catalogs.products.create(catalog.id, ['prod_a', 'prod_b'])  // → restricting mode
await adminClient.catalogs.activate(catalog.id)
解決方法:誰に何が表示され、いくら支払うのか
  1. 対象者を特定します。カタログを返す最初の情報源が優先され、情報源同士は組み合わされません。 順序は、会社ノードおよびその祖先、次に顧客のグループ、最後にチャネルのdefault_catalog_idです。
  2. 表示範囲は、該当するカタログの品揃えの和集合です。適用されるカタログに品揃えが空のものが1つでもあれば、制限はありません。
  3. 価格は、該当するカタログの専有価格リストを、最も近いノードから順に確認します。同じノードに複数のカタログが適用される場合は、購入数量で比較して、最も安い価格が適用されます。次に、ルールが一致する通常の価格リストを確認し、その後に基本価格を適用します。より近いノードの取引条件は、さらに上位にあるノードのより安い価格より優先されます。

会社を通じて購入する購入者は、会社の階層でカタログが1つでも割り当てられると、顧客グループのカタログを適用されなくなります。取引段階は会社への割り当てとしてモデル化してください(ルートにグループ全体のカタログを設定し、メンバー企業または部門に段階別カタログを設定します)。顧客グループ(cg_)は、小売ロイヤルティ会員、スタッフ、承認済みの個人卸売顧客など、会社を代表して購入しない購入者にのみ使用してください。新しい設定では、CustomerGroupRule価格リストよりもカタログを優先してください。

ストアフロントへのアクセス制限(下記)は、カタログの解決より先に確認されます。

数量ルールと注文の最小数量
  • バリアント単位のminimum_order_quantityとorder_multiple(Variant列)は、各フィールドについて、バリアントの基本値、カタログのデフォルト値、カタログと商品のオーバーライド(Spree::Catalogs::ResolveQuantityRules、結果はSpree::QuantityRule)の順に解決されます。ルールが指定されている最も詳細なレベルが優先されます。ステップは最小数量から数えます。MOQが48、倍数が24の場合は48、72、96が許可され、50は許可されません。MOQが50、倍数が24の場合は50、74、98が許可されます。制限のないバリアントは1/1を参照します。
  • 通貨ごとの注文最小金額:adminClient.catalogs.orderMinimums.create(id, { currency: 'USD', amount: '600' })(または作成時にorder_minimums)。オーバーライドはcatalogs.products.list(id, { expand: ['quantity_rule'] })で読み取れます。
  • 商品ごとのオーバーライドは、全体を一括して設定します。adminClient.catalogs.quantityRules.upsert(id, { terms: { prod_x: { minimum_order_quantity: 48, order_multiple: 24 } } })。品揃えに含まれない商品に対するルールを追加すると、その商品が品揃えに加わり、オーバーレイカタログが制限モードに切り替わります。
  • ルールは商品追加時(Carts::AddItem、UpsertItems)と完了時に再度適用されます(要件コードquantity_rule_violated)。注文最小金額は推奨要件(order_minimum_not_met)であり、満たされていない場合は完了できません。カートにはorder_minimum、order_minimum_shortfall、below_order_minimumが、バリアントにはminimum_order_quantity、order_multiple、purchase_unitが公開されるため、ステッパーで倍数に沿って数量を変更できます。
  • スタッフに紐づけられた購入は対象外です(管理者の下書き、created_byが設定されたカート)。そのため、個別に交渉した例外を記録できます。条件は都度読み込まれ、保存されません。注文確定後に再検証されることはありません。
カタログ価格表:ルール
  • 複数のモードを組み合わせられます:price_adjustment_percentage(ショップ価格からの割引率)、price_adjustment_tiers([{ min_quantity, percentage }])、明示的なprices、およびmin_quantityを指定したprices(数量割引)。
  • 数量は明細行ごとに適用されます。1つのSKUを10個購入すると10個の価格区分が適用されますが、2つのSKUをそれぞれ5個購入しても適用されません。
  • 価格区分が設定されたバリアントの価格には、その価格区分のみが適用されます。カタログのパーセンテージ割引は上乗せされません。
  • 価格区分の価格が、それより少ない数量に適用される価格を上回ることはできません(保存時に拒否されます)。また、1つのリストで、バリアントごとに通貨あたり最大10個の価格区分を設定できます(Spree::Price::MAXIMUM_BREAKS_PER_VARIANT)。
  • 通貨間で価格が換算されることはありません。購入者が取引するすべての通貨で条件を提示してください。
  • CustomerGroupRule/UserRuleによる価格ルールは、すでに使用されている箇所では引き続き機能しますが、新しいリストでは利用できません。カタログを割り当てて対象顧客を設定してください。

ゲート付きチャネルと卸売ポータル

Spree::Channel#storefront_access は public | prices_hidden | login_required のいずれかです。未設定の場合はストアの preferred_storefront_access を継承し、デフォルトは public です。Store API がこれを適用するため、ストアフロント側で制限を緩和することはできません。

  • login_required は、ゲストによる読み取りに 401 を返します(認証、パスワードのリセット、参照データは対象外です)。
  • prices_hidden は、すべての金額フィールドが null のカタログをゲストに返します。
  • guest_checkout は別個のチャネル設定です。

サインイン済みの顧客が storefront_access によって制限されることはありません。購入者の承認はアプリ側で行います。たとえば、customers/me → customer_groups から「Wholesale」顧客グループへの所属を確認できます。

リクエストで使用するチャネルは、SDK の channel オプションまたは client.setChannel('wholesale') で設定する X-Spree-Channel ヘッダー、あるいはチャネルに紐づいた公開可能キーで指定します。Spree は wholesale チャネル(login_required、ゲストチェックアウトは無効)と、それに紐づいた公開可能キーをシードします。

Next.js ストアフロントには、オプトイン方式の /wholesale ポータルが用意されています。有効にするには SPREE_WHOLESALE_CHANNEL=wholesale を設定します。未設定の場合、すべての /wholesale ルートは 404 を返します。SPREE_WHOLESALE_PUBLISHABLE_KEY は任意です。画面ごとに別の SDK クライアント、カート Cookie、キャッシュキーを使い、顧客 JWT は共有します。これは、たとえばログインを必須にする B2B ストアでメインチャネルを制限するなど、ニーズに合わせて変更できるリファレンス実装です。spree-storefront を参照してください。

PO 番号

  • po_number は、Cart と Order の通常のインデックス付きカラムです(Spree::Purchase::PurchaseOrder。仕入先調達モデルで po_ をプレフィックスに持つ Spree::PurchaseOrder ではありません)。注文完了時に注文へコピーされます。
  • 会社の Spree::Company#po_number_required を有効にします。PO 番号が設定されるまで、チェックアウトでは { step: 'address', field: 'po_number', code: 'po_number_required' } という要件が返されます。設定するには client.carts.update(id, { po_number: 'PO-4471' }) を使います。スタッフキーを使った注文は対象外です。
  • 任意の PO 書類 は POST/GET/DELETE /api/v3/store/carts/:id/po_document にあります。PDF、画像、Word ファイルに対応し、サイズは最大 10 MB です。コンテンツを検査し、非公開で保存したうえで注文にコピーします。管理者は GET /admin/orders/:id/po_document で読み取れます。

掛け売り注文

掛け取引の注文を今受け付け、後から代金を回収する方法は 2 つあります。OSS には支払条件エンジンはありません(net 30、与信限度額、デポジットは Enterprise のロードマップに含まれています)。

  • チェックアウト時に請求書払いを選択する。 組み込みの Spree::PaymentMethod::Check は、決済プロバイダーに接続せずに支払いを記録します。注文完了時に成功し、スタッフがキャプチャするまで pending のままになります。有効でストアフロントに表示されるすべての決済方法が、すべての注文で提示されるため、請求書払いを会社購入に限定するには、次のようにサブクラス化します。
    # server/app/models/spree/payment_method/invoice.rb
    class Spree::PaymentMethod::Invoice < Spree::PaymentMethod::Check
      def available_for_order?(order) = super && order.b2b?
    end
    # server/config/initializers/spree.rb
    Rails.application.config.after_initialize { Spree.payment_methods << Spree::PaymentMethod::Invoice }
    
  • スタッフキーを使った下書き注文。 adminClient.orders.create({ customer_id, company_id, channel_id, currency, po_number, use_customer_default_address: true, items: [{ variant_id, quantity, price: '8.75' }] })。company_id(作成時または更新時)を指定すると、その会社のカタログ価格と税額が適用された会社購入になります。商品項目の price は交渉済みの価格です(price_source: 'manual'。再価格設定されません。price: null を指定するとカタログ価格に戻ります)。指定できるのは注文確定前のみです。確定後に金額を変更するには手数料や割引を使います。その後、adminClient.orders.complete(id, { payment_pending: true }) で決済処理を行わずに注文を確定します(注文番号が割り当てられ、在庫が引き当てられ、order.placed が発火し、payment_status は none のままです)。請求書の支払いを受けたら、orders.payments.create(orderId, { payment_method_id, amount }) と orders.payments.capture(orderId, paymentId) を実行します。ダッシュボードの「完了」アクションでは支払いが処理されます。payment_pending は API 専用です。

Freight

  • バリアントのカートン階層:carton_package_type_id(kind: 'carton' の PackageType)、units_per_carton、carton_weight、cartons_per_pallet、および purchase_unit(unit または carton。ストアフロントでの数量の表示方法だけが変わります)。保存される数量は常に単位数です。カートンの境界をまたぐ order_multiple は拒否されます。
  • カートと料金には freight_summary(total_units、total_cartons、total_pallets、m³ 単位の total_volume、kg 単位の total_weight、および complete)が表示されます。complete: false の場合、一部の商品は単位の寸法にフォールバックしているため、数値は最小値として表示してください。
  • 貨物運賃の階層は、ルールを持つ rate_provider: 'Spree::DeliveryRateProvider::Freight' の通常の配送方法です。volume_rule は m³ 単位の minimum_volume / maximum_volume を受け取ります。company_rule は company_orders_only を受け取ります。true の場合は企業注文のみ、false の場合は企業以外の注文のみが対象です。プロバイダーは価格未設定の料金(unpriced: true。「確認後に見積もり」と表示され、最後に並び、事前選択されません)を返します。チェックアウトは引き続き完了でき、見積もり後に貨物運賃が追加されます。spree-fulfillment を参照してください。
  • 取扱手数料や追加料金などは Spree::Fee の行として登録します(常に正の値で、デフォルトでは課税対象です)。spree-order-totals を参照してください。

注意点

  • オーバーレイカタログに商品を1つ追加すると、ほかの商品はすべて非表示になります。品揃えの対象外の商品に数量条件を設定した場合も同様です。
  • 企業の購入者について、顧客グループに階層カタログを割り当てても効果はありません。企業の階層が先に適用されるためです。企業に割り当ててください。
  • activate を忘れると、新しいカタログは非アクティブのまま機能しません。
  • カタログの価格表を切り離すと、ソフト削除されます。Spree はルールとの照合に再利用しません。
  • 部門が親の VAT ID を継承すると想定すること。最も近い企業ノードが使用されますが、そこに VAT ID がない場合があります。
  • prices_hidden が、サインイン済みの未承認購入者にも価格を非表示にすると想定すること。対象となるのはゲストのみなので、承認処理はアプリ側で行ってください。
  • UI でのみ MOQ を検証すること。API でも検証され、スタッフ向けの経路では仕様上スキップされます。
  • 企業のセルフ登録、approval_required による制御、企業ロールは OSS に含まれません。これらを前提に構築しないでください。

詳しくはこちら

  • node_modules/@spree/docs/dist/developer/how-to/build-a-b2b-store.md — エンドツーエンドガイド(https://spreecommerce.org/docs/developer/how-to/build-a-b2b-store)
  • node_modules/@spree/docs/dist/developer/core-concepts/{companies,catalogs,channels,freight,fees,customers}.md
  • node_modules/@spree/docs/dist/developer/storefront/nextjs/wholesale.md
  • node_modules/@spree/docs/dist/user/how-to/selling-to-businesses.md:取引先顧客1社のエンドツーエンド設定
  • https://spreecommerce.org/docs/developer/core-concepts/companies
  • 関連スキル:spree-pricing(価格表、数量ルール)、spree-checkout(要件レジストリ)、spree-taxes、spree-fulfillment、spree-storefront、spree-auth-permissions

コメント