spree-marketplace について
spree-marketplaceは、Spree 6の出品者登録、商品審査、注文分割、手数料、支払いを解説するスキルです。Spreeストアをマーケットプレイスとして設定・拡張するチーム向けに、主要なデータモデル、ワークフロー、ストア単位の設定を説明します。
主な機能
- 01出品者登録とステータスワークフロー
- 02設定可能な出品者要件チェックリスト
- 03出品者による商品提出と審査
- 04複数出品者の注文グループと決済分割
- 05手数料記録と出品者支払い台帳
活用シーン
- 01マーケットプレイスの出品者登録設定
- 02運営者向け商品審査キューの構築
- 03複数出品者にまたがるチェックアウト処理
- 04手数料と支払い業務の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-marketplaceインストール後、Claude Code で/spree-marketplaceを入力して読み込まれたか確認します。
同梱ファイルreferences/payout-provider.mdreferences/seller-api.md
詳細
Spree Marketplace(販売者、手数料、支払い)
このスキルに含まれる機能はすべて、オープンソースの Spree 6 で利用できます。販売者の登録、オンボーディング、商品審査、注文の分割、手数料、支払い台帳、Stripe Connect、販売者パネルに対応しています。「マーケットプレイスモード」の切り替えはありません。販売者がいるストアはマーケットプレイスになります。ほとんどのマーケットプレイスはコードではなく設定で実現できます。コードを記述するのは、新しい要件の種類、手数料ルール、支払いプロバイダー、または専用の販売者アプリが必要な場合だけです。
Spree Enterprise でのみ利用可能:返金時の手数料の取り戻しと決済間の相殺、プロバイダーの明細との照合、KYC 業務、販売者の税務報告(DAC7)、税金の納付を担う販売者の platform 設定(マーケットプレイス・ファシリテーター)。これらの機能が OSS で利用できると案内しないでください。
モデル
| モデル | プレフィックス | 内容 |
|---|---|---|
Spree::Seller | sel_ | ストア内の販売業者(単一ストアのリソース、パラノイド)。商品、在庫ロケーション、配送方法、梱包タイプ、ポリシーを所有し、ロールを通じて独自のチームを持ちます |
Spree::Product#seller_id | — | nil はマーケットプレイス独自の在庫を意味します。LineItem#seller_id は、商品明細の選択中にバリアント/商品からスナップショットされ、注文確定時に固定されます |
Spree::ProductSubmission | prodsub_ | 審査記録:pending / approved / rejected / withdrawn。レコードは蓄積され、最新のものが有効です |
Spree::SellerRequirement(STI) | selreq_ | 登録済みの種類のインスタンスであるチェックリストの項目 |
Spree::SellerRequirementSubmission | selsub_ | 販売者が提出した内容:pending / accepted / rejected / waived |
Spree::OrderGroup | ogrp_ | 複数の販売者にまたがるチェックアウト。顧客、住所、支払い、合計金額を保持します |
Spree::PaymentSplit | paysp_ | グループの単一の支払いにおける、販売者の注文分の取り分 |
Spree::CommissionRate / CommissionRule | crate_ / comrule_ | 課金内容と課金のタイミングを定める設定 |
Spree::CommissionLine | cline_ | 1件の課金記録(商品ごと。配送に手数料を課す場合はフルフィルメントごとの記録も含む) |
Spree::SellerTransfer | vtr_ | 台帳のレコード。kind は earning または refund_reversal |
Spree::SellerPayout | vpo_ | 通貨ごとに作成される送金トランスファーの決済バッチ |
販売者のステータス
has_status :pending, :invited, :canceled, :onboarding, :ready_for_review, :approved, :rejected, :suspended(デフォルトは pending)。ステートマシンはありません。各ステータスの変更はワークフローであり、Admin API ではそれぞれが独立したメンバーアクションとして公開されています。
pending ─invite→ invited ─(invitation accepted)→ onboarding ─submit_for_review→ ready_for_review ─approve→ approved
ready_for_review ─reopen_onboarding→ onboarding onboarding|ready_for_review ─reject→ rejected
approved ─suspend→ suspended ─approve→ approved rejected ─approve→ approved
ワークフローは Spree::Sellers::{Create, Invite, StartOnboarding, SubmitForReview, Approve, Reject, Suspend, ReopenOnboarding} にあります。それぞれが hooks :validate, :after_<verb> を宣言するため、Spree.hooks.register('sellers.approve.validate', ->(wf) { wf.reject!('…') if … }) のように拡張できます(spree-workflows を参照)。**seller.update(status: 'approved') は絶対に使用しないでください。**チェック、メール送信、フックを迂回してしまいます。販売できるのは、休業モードではない approved ステータスの販売者のみです(Spree::Seller.sellable)。販売者は holiday_mode_until を設定して販売を一時停止します。Sellers::Create は販売者の在庫ロケーションも作成します。
イベント:seller.invited、seller.onboarding_started、seller.submitted_for_review、seller.approved(ペイロードには requirements_overridden と unmet_requirements が含まれます)、seller.rejected、seller.suspended、seller.onboarding_reopened、seller_requirement_submission.{accepted,rejected,waived}、seller_payout.completed、order_group.completed に加え、ライフサイクルイベントの seller.created/updated/deleted と product_submission.*。
ストアの設定(設定 → マーケットプレイス)
これらの設定はすべて、PATCH /api/v3/admin/store(adminClient.store.update({...}))で書き込めます。
| 設定項目 | デフォルト | 備考 |
|---|---|---|
preferred_payout_provider | nil → Spree::PayoutProvider::System | Spree.payout_providers に含まれるクラス名である必要があります。含まれない場合、書き込みは拒否されます。プロバイダーがレジストリから削除されたストアでは、System にフォールバックします |
preferred_default_payouts_schedule_interval | monthly | daily/weekly/biweekly/monthly/manual。販売者ごとの設定が優先されます |
preferred_default_minimum_payout_amount | 0 | この金額を下回る残高は繰り越されます |
preferred_auto_approve_sellers | false | チェックリストの完了時に、人的な審査なしで販売者を承認します |
preferred_auto_approve_seller_products | false | 提出後、すぐに承認処理に進みます(提出レコードは自動承認済みとして記録されます) |
preferred_send_seller_transactional_emails | true | 販売者との連絡を独自に行う場合は、これをオフにしてください |
preferred_default_commission_tax_rate | 0 | 小数(0.23 = 23%)です。ダッシュボードにはパーセンテージで表示されます |
オンボーディング要件
チェックリストはストアごとに設定され、読み取り時に計算されます。販売者には保存されないため、新しい要件はすぐに全員に適用されます。要件が適用されるタイミングは正確に2回です。SubmitForReview(ブロッカーを示す422 processing_error)と、Approve(override_requirements: true がない限り拒否され、オーバーライドはイベントに記録されます)です。それ以降は参考情報として扱われます。
13種類の基本要件(Spree.seller_requirements)は、次の3グループに分かれます。
- 計算型。Spree が販売者のデータを読み取ります:
accept_terms、complete_profile、billing_address、returns_address、delivery_method、package_type、minimum_products、payout_account、required_custom_fields、policy。 - 申告型。販売者が提出します:
attestation。 - 確認型。販売者が提出し、運営者が承認または却下します:
operator_review、document。
新しいストアのデフォルトのチェックリストには、最初の7項目が含まれます。attestation、operator_review、document、policy は複数回表示でき、name が必要です。それ以外はストアごとに1つだけです。type は一度だけ書き込めます。
チェックリストのUIは常に GET /api/v3/admin/seller_requirements/types(adminClient.sellerRequirements.types())から構築してください。このエンドポイントは、各種類の preference_schema を返します。カスタムの種類が自動的に表示されるため、リストをハードコードしないでください。
カスタムの種類は STI サブクラスで、次のように登録します。
# app/models/my_app/seller_requirements/insurance_on_file.rb
module MyApp::SellerRequirements
class InsuranceOnFile < Spree::SellerRequirement
preference :minimum_cover, :integer, default: 1_000_000
def met_by_seller?(seller) # override this, never #satisfied? (waivers live there)
seller.get_custom_field('insurance.cover')&.value.to_i >= preferred_minimum_cover
end
def applicable?(seller) = true # false hides the row entirely
end
end
# config/initializers/spree.rb
Rails.application.config.after_initialize do
Spree.seller_requirements << MyApp::SellerRequirements::InsuranceOnFile
end
提出を受け付ける種類では、self.accepts_submissions?、self.reviewed_by_operator?、self.requires_file?、self.accepted_content_types の各クラスメソッドをオーバーライドします。アップロードされた書類はファイル名ではなくバイト列で検証され、非公開で保存されます。
商品の審査
マーケットプレイスでは、商品に proposed と rejected のステータスが追加されます。販売者が商品を提出し、運営者が判断します。販売者側では、sellerClient.products.submit(id)/bulkSubmit を使います。また、販売者はいつでも自分の商品を draft または archive にできます。運営者側では、adminClient.products.approve(id)/reject(id, { reason }) を使います。審査キューは products.list({ status_eq: 'proposed' }) です。却下理由は却下の呼び出し時に指定してください。理由は提出レコードに保存されます。販売者が上書きできるため、商品 metadata には保存しないでください。詳しくは spree-catalog を参照してください。
分割チェックアウト:注文グループ
カートに N 社の販売者の商品が含まれる場合、注文の確定時に、販売者ごとに 1 つの Order を持つ OrderGroup が作成されます(ファーストパーティ商品については、運営者自身の注文も作成されます)。支払いは 1 回で、PaymentSplit として按分されます。販売者が 1 社のチェックアウトでは、グループなしの通常の注文が作成されます。この分割処理は、カートのチェックアウトと管理者が作成した下書き注文の両方が通る、注文側のワークフローである Spree::Orders::Complete で行われます。そのため、次の点に注意してください。
POST /api/v3/store/carts/:id/completeは、OrderではなくOrderGroup(ogrp_…、orders[]を含む)を返す場合があります。@spree/sdkではcarts.completeの型はPromise<Order | OrderGroup>です。isOrderGroup(result)ガード(orders[]の有無を判定)で型を絞り込んでください。確認ページと注文履歴では、グループを 1 回の購入として表示してください。PATCH /api/v3/admin/orders/:id/completeもグループを返す場合があります。@spree/admin-sdkではisOrderGroup(result)を使用してください。例:const orders = isOrderGroup(r) ? r.orders : [r]- グループの合計は子注文の合計であり、分割された値ではありません。配送費と注文単位の手数料は、商品金額に応じて按分されます。
- チェックアウトデータは共有ではなく複製されます。各子注文(およびグループ)には、チェックアウト単位のフィールドである顧客、メールアドレス、会社、市場、チャネル、
customer_note、po_numberが含まれます。また、それぞれの注文にはカートのmetadataのディープコピーが含まれます(これは顧客が書き込み可能なため、承認や不正利用のフラグに使用しないでください)。一方の兄弟注文のmetadataを編集しても、他方には反映されません。 - カスタムコードで注文を確定する場合は、ステータスを直接書き換えず、
Spree.order_complete_workflowまたはSpree.carts_complete_workflowを使用してください。販売者の紐付け、コミッション、グループはすべてこの処理に依存しています。 - 購入は 1 回、確認通知も 1 回です。 各子注文は通知なしで配置されます(
order.placedのペイロードではnotify_customer: false)。顧客とストアオーナーへのメールは、Spree::OrderGroupMailer/Spree::OrderGroupEmailSubscriberを介してorder_group.completed上でグループから 1 回だけ送信されます(ペイロードには購入時の実際のnotify_customerが含まれます)。送信状況はorder_group.confirmation_delivered/store_owner_notification_deliveredで追跡されます。管理画面で任意の子注文から再送信すると(POST /api/v3/admin/orders/:id/resend_confirmation→order.resend_confirmation_email)、order_group.resend_confirmation_emailに転送され、グループのメールが再送信されます。ストアフロントまたは ERP から独自の確認通知を送信する場合は、order.placedに加えてorder_group.completedにもサブスクライブし、両方でnotify_customerを尊重してください(Ruby ではorder.grouped?で子注文かどうかを判定できます)。そうしないと、分割チェックアウトでは確認通知が届かないか、販売者ごとに 1 通ずつ届くことになります。分割されないチェックアウトに変更はありません。 - 注文数 ≠ 荷物数です。 1 つの倉庫から発送される複数の販売者の注文では、配送費を共有(注文間で分割)し、梱包も 1 つになります。「何が届くか」を確認するには、子注文の数ではなく
order_group.fulfillment_groups(Spree::FulfillmentGroup:delivery_method、cost、line_items、sellers、orders)を使用してください。 - 管理者向けの読み取り専用 API:
adminClient.orderGroups.list()/get('ogrp_…', { expand: ['orders'] })。
手数料
Spree は order.placed 時に同期的に手数料を請求します(Spree::OrderCommissionSubscriber → Spree.commissions_commission_order_service、冪等)。請求は line_item.seller_id ごとにまとめられます。販売者が設定されていない明細には手数料はかかりません。主な仕様は次のとおりです。
- レートはリストの順に評価され、最初に一致したものが適用されます。 優先度をスコアリングする仕組みはありません。ルールのないレートはすべてに一致するため、全件に適用するレートは一番下に置きます(例:
position: 99)。新しいレートは先頭に作成されます。 - 1つのレートでは、すべてのルールに一致する必要があります。 1つのルール内のIDは、いずれかに一致すればよい条件です。ルールタイプ(
Spree.commission_rules)はproduct_rule、category_rule(子孫も一致)、seller_rule(空の場合は何にも一致しません)、item_total_rule(下限は含み、上限は含みません)です。1つのレートにつき、各タイプのルールは1つまでです。更新時にrules: [...]を指定すると、ルールセット全体が置き換えられます。GET /admin/commission_rates/rule_typesでタイプを確認できます。 - 一致するレートがない場合、手数料は発生しません。 Spree がデフォルトを自動で設定することはありません。
- 手数料の基準額は、デフォルトでは税抜き(
tax_inclusive: false)で、常に割引後の金額です。include_shippingを指定すると配送にも手数料がかかりますが、fixedレートでは配送に手数料をかけられません。 - 金額は通貨ごとに指定され、換算は行われません。
amounts: { USD: '5.00' }は定額料金を設定し、1単位ごとに請求されます。amountsにない通貨の場合は、次のレートに進みます。bounds: { EUR: { min_amount, max_amount } }は、手数料率の下限と上限を設定します。上限・下限が設定されていない通貨では、手数料率は上限なしで適用されます。 - 手数料にかかる税は、販売者の管轄区域に従って決定されます。 次の順に解決されます。レートの
commission_tax_rate、販売者の請求先住所を使った税エンジン、ストアのpreferred_default_commission_tax_rate。taxability_reasonはstandard_rated/zero_rated/reverse_chargeです。 - 明細は変更不可のスナップショットで、書き込み経路はありません。 レートを編集または論理削除しても、過去の記録は書き換えられません。
- 手数料は
Spree::Feeではありません。顧客には表示されず、注文合計にも含まれません。
差し替えポイントは、Spree::Dependencies のキー commissions_resolve_rate_service、commissions_resolve_tax_rate_service、commissions_calculate_line_service、commissions_commission_order_service です。カスタムルールは Spree::CommissionRule をサブクラス化し、applicable?(context) を実装します(コンテキストから seller、order、line_item、fulfillment、currency、product、subject を参照できます)。上記の要件種別と同じパターンで、Rails.application.config.after_initialize do … end 内から Spree.commission_rules に追加します。
支払い:台帳
order.fulfilled → SellerTransfer(kind: earning) = seller's sale − commission, sale currency
refund.created → SellerTransfer(kind: refund_reversal) (never edits the earning)
sweep (schedule) → SellerPayout per seller per currency, lists the transfers it settles
complete → operator (System provider) or provider webhook → seller_payout.completed
- 販売者が収益を得るのは、支払い時ではなく注文の履行時です。デジタル注文は即時に履行されるため、収益もすぐに発生します。
- 振込と支払いは、
pending→processing→completed、failed、unresolvedのステータスを共有します。残高から差し引かれるのは、完了した支払いのみです。 作成時に完了するものはありません。unresolvedは結果が不明であることを意味するため、同じ金額を二重に支払わないよう、振込は処理済みとして保持されます。オペレーター(または Webhook)が解決します。 - 残高は通貨ごとに算出されます(
adminClient.sellers.balances(id))。adminClient.sellers.settle(id)(POST /admin/sellers/:id/payouts)で手動精算します。一部失敗した場合も 201 が返され、失敗はmeta.failuresに記載されます。支払うべき金額がない場合は 422 が返されます。System プロバイダーの場合は、adminClient.sellerPayouts.complete('vpo_…', { reference })で支払い済みとしてマークします。 - ホストアプリでは、2 つの定期ジョブをスケジュールする必要があります。 スケジュールしないと、販売者への支払いが行われないままになります。
server/config/recurring.yml(Solid Queue)または独自のスケジューラーを確認してください。
production:
sweep_due_seller_payouts:
class: Spree::SellerPayouts::SweepDueJob
schedule: at 3am every day
execute_pending_seller_transfers:
class: Spree::SellerTransfers::ExecutePendingDueJob
schedule: every hour at minute 27
どちらのジョブも、販売者がいないストアでは軽量な何もしない処理になります。販売者ごとの間隔ロジックはジョブ内で実行されるため、cron で表現しようとしないでください。
支払いプロバイダー。 プロバイダーは Spree.payout_providers に登録し、ストアごとに選択します。
Spree::PayoutProvider::Systemがデフォルトです。帳簿を管理しますが、資金は移動しません。SpreeStripe::PayoutProvider(spree_stripegem)は Stripe Connect です。販売者は Express でオンボーディングします。収益はすべて、顧客の請求をsource_transactionに設定した振込になります。支払いは Spree のスケジュールで実行され(接続アカウントは手動スケジュールを使用)、payout.paid/payout.failedはPOST /api/v3/webhooks/payouts/:payment_method_idに届きます。ローカル開発では、STRIPE_CONNECT_SIGNING_SECRETを指定してstripe listen --forward-connect-to …を使用してください。返金時には、Stripe の振込取消しは Enterprise 機能のため、取消行のみが記録されます。- カスタムプロバイダーは
Spree::PayoutProvider::Baseをサブクラス化します。契約、エラーの意味、冪等性、Webhook のパターンについては references/payout-provider.md を参照してください。
プロバイダーの requires_payout_account? が true の場合は、payout_account 要件を追加してください。追加しないと、販売者は承認されて販売できても、収益が保留状態のままになります。
販売者 API と販売者パネル
/api/v3/seller/* は、サインイン済みの販売者チーム向けの独立した API です。@spree/seller-sdk がこの API をラップします。主なポイントは次のとおりです。
- 認証方式は JWT のみで、オーディエンスは
seller_apiです(管理者用トークンとストア用トークンでは 401 が返されます)。意図的にシークレットキーはありません。すべての呼び出しでX-Spree-Seller-Idが必要です(ただし、サインイン中の本人のアカウントに対するGET/PATCH /seller/meは除きます)。リフレッシュトークンは/api/v3/seller/authをスコープとする HttpOnly Cookie です。販売者を運営していないストアスタッフは、ログイン時に 401 が返されます。 - 403 は、販売者ヘッダーがない、または不明である場合、あるいは必要な権限キーがない場合に返されます。404 は、この販売者にとって対象が見つからないことを示します。他の販売者のレコードに対しても 404 が返されるため、レコードを列挙することはできません。パスに販売者 ID を指定するエンドポイントはありません。
- 販売者は商品を申請しますが、
activeは設定できません。販売者は注文を配達済みにできません。台帳のエンドポイント(balances、transfers、payouts)は読み取り専用です。 - 販売者スタッフの権限は、販売者が所有するロールから付与されます。バックオフィスと同じフラットな権限キーを使用しますが、対象はより限定されています(
spree-auth-permissionsを参照してください)。
import { createSellerClient } from '@spree/seller-sdk'
const seller = createSellerClient({ baseUrl: 'https://marketplace.example.com' })
const { token, sellers } = await seller.auth.login({ email, password })
seller.setToken(token)
seller.setSeller(sellers[0].id)
await seller.orders.fulfillments.fulfill(orderId, fulfillmentId, { tracking: '1Z…' })
名前空間の全テーブル、オペレーター向け Admin エンドポイント、ルートについては、references/seller-api.md を参照してください。
販売者パネル。 @spree/seller-dashboard は、管理者ダッシュボードと @spree/dashboard-core および -ui を共有する React SPA です。create-spree-app が自動的にセットアップします。既存のプロジェクトでは、spree add seller-dashboard を実行すると apps/seller-dashboard/ が作成されます。開発時はポート 5174(管理者ダッシュボードは 5173)で実行され、本番環境では /sellers で配信されます。知っておくべき点は次のとおりです。
.env.localで設定するのはVITE_API_PROXY_TARGETのみです。開発時にVITE_SPREE_API_URLを設定しないでください。プロキシをバイパスしてしまい、SameSite=Laxのリフレッシュ Cookie が機能しなくなります。@spree/seller-dashboardからインポートしたdefineDashboardPluginを使い、src/plugins.tsでカスタマイズします(spree-dashboard-pluginsを参照)。利用できるスロットは 2 つだけで、seller.team.actionsとseller.team.afterです。それ以外のものには、ルートを追加してください。seller.form_*スロットは 管理者 ダッシュボード用です。- 英語、アラビア語、ドイツ語、フランス語、ポーランド語、簡体字中国語に対応しています(
src/locales/{en,ar,de,fr,pl,zh-CN}.json)。各販売者ユーザーはアカウントダイアログで言語を選択できます。選択した言語はアカウントのselected_locale(PATCH /seller/me)として保存され、どのブラウザーからサインインしても適用されます。言語を切り替えるとページが再読み込みされます。言語を追加するには、src/locales/<code>.jsonをパネルに追加します。API からではなく、バンドル内のファイルから検出されます。ブランディング設定はありません。Tailwind でスタイルを変更し、翻訳キーを上書きして文言を変更してください。 - アカウントダイアログでは、サインイン中の本人の情報(名前、写真、言語)を
PATCH /seller/meで編集します。販売者のビジネス(公開プロフィール)はPATCH /seller/profileで編集します。カスタム UI でもこの区分を維持してください。
開発環境でサンプルデータを使うには、spree rake spree:sellers:sample_data を実行します(ネイティブ環境では bin/rails spree:sellers:sample_data)。[email protected] / spree123 でサインインし、開発環境とテスト環境以外では実行されないようになっています。
注意点
- ストアフロントとバックオフィスでは、完了処理の
OrderGroupの結果を処理する必要があります。 これはマーケットプレイスで最もよくある不具合です。 - 定期ジョブの登録漏れ:スイープと保留中の送金の再試行は、
recurring.ymlまたはスケジューラーに登録する必要があります。登録されていなくても警告は表示されません。 - 包括的なコミッション率は、個別のものより優先されます:それより後にある設定はすべて適用されず、新しい率は先頭に追加されます。
- 販売者を指定しない
seller_ruleは何にも一致せず、通貨の金額が指定されていない固定レートは、その通貨ではスキップされます。 - オープンなマーケットプレイスで自動承認を有効にすると、唯一の人による確認プロセスがなくなります。
- Seller、Product、SellerPayout、Order のステータスを直接設定しないでください。ワークフローとエンドポイントを使用してください。
- 送金プロバイダーでタイムアウト時に
GatewayErrorを発生させると、送金が再実行されます。代わりにSpree::Core::AmbiguousGatewayErrorを発生させてください。 - 送金ウェブフックのハンドラーは冪等性を保ってください。
(provider, reference)で送金を特定し、金額や新しさで特定しないでください。 - 販売者スコープのカスタムエンドポイントでは、すべての検索を現在の販売者を起点に行い、他の販売者のレコードに対しては 403 ではなく 404 を返す必要があります。
spree:sellers:sample_dataの rake タスクは開発/テスト専用です。
さらに詳しく
node_modules/@spree/docs/dist/developer/core-concepts/sellers.md、…/commissions.mdnode_modules/@spree/docs/dist/developer/how-to/build-a-marketplace.md:エンドツーエンドのセットアップガイドnode_modules/@spree/docs/dist/developer/providers/payouts.md:送金プロバイダーの作成方法node_modules/@spree/docs/dist/integrations/payments/stripe-connect.mdnode_modules/@spree/docs/dist/api-reference/seller-api/{introduction,authentication,errors}.md- https://spreecommerce.org/docs/developer/core-concepts/sellers
- 関連するスキル:
spree-workflows(フック)、spree-checkout(カートの完了処理)、spree-fulfillment(販売者の配送方法、梱包タイプ)、spree-payments(Stripe)、spree-dashboard-plugins、spree-events-webhooks