mcpField
S
spree-marketplace

spree-marketplace

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

spree-marketplace について

spree-marketplaceは、Spree 6の出品者登録、商品審査、注文分割、手数料、支払いを解説するスキルです。Spreeストアをマーケットプレイスとして設定・拡張するチーム向けに、主要なデータモデル、ワークフロー、ストア単位の設定を説明します。

主な機能

  1. 01出品者登録とステータスワークフロー
  2. 02設定可能な出品者要件チェックリスト
  3. 03出品者による商品提出と審査
  4. 04複数出品者の注文グループと決済分割
  5. 05手数料記録と出品者支払い台帳

活用シーン

  1. 01マーケットプレイスの出品者登録設定
  2. 02運営者向け商品審査キューの構築
  3. 03複数出品者にまたがるチェックアウト処理
  4. 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::Sellersel_ストア内の販売業者(単一ストアのリソース、パラノイド)。商品、在庫ロケーション、配送方法、梱包タイプ、ポリシーを所有し、ロールを通じて独自のチームを持ちます
Spree::Product#seller_id—nil はマーケットプレイス独自の在庫を意味します。LineItem#seller_id は、商品明細の選択中にバリアント/商品からスナップショットされ、注文確定時に固定されます
Spree::ProductSubmissionprodsub_審査記録:pending / approved / rejected / withdrawn。レコードは蓄積され、最新のものが有効です
Spree::SellerRequirement(STI)selreq_登録済みの種類のインスタンスであるチェックリストの項目
Spree::SellerRequirementSubmissionselsub_販売者が提出した内容:pending / accepted / rejected / waived
Spree::OrderGroupogrp_複数の販売者にまたがるチェックアウト。顧客、住所、支払い、合計金額を保持します
Spree::PaymentSplitpaysp_グループの単一の支払いにおける、販売者の注文分の取り分
Spree::CommissionRate / CommissionRulecrate_ / comrule_課金内容と課金のタイミングを定める設定
Spree::CommissionLinecline_1件の課金記録(商品ごと。配送に手数料を課す場合はフルフィルメントごとの記録も含む)
Spree::SellerTransfervtr_台帳のレコード。kind は earning または refund_reversal
Spree::SellerPayoutvpo_通貨ごとに作成される送金トランスファーの決済バッチ
販売者のステータス

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_providernil → Spree::PayoutProvider::SystemSpree.payout_providers に含まれるクラス名である必要があります。含まれない場合、書き込みは拒否されます。プロバイダーがレジストリから削除されたストアでは、System にフォールバックします
preferred_default_payouts_schedule_intervalmonthlydaily/weekly/biweekly/monthly/manual。販売者ごとの設定が優先されます
preferred_default_minimum_payout_amount0この金額を下回る残高は繰り越されます
preferred_auto_approve_sellersfalseチェックリストの完了時に、人的な審査なしで販売者を承認します
preferred_auto_approve_seller_productsfalse提出後、すぐに承認処理に進みます(提出レコードは自動承認済みとして記録されます)
preferred_send_seller_transactional_emailstrue販売者との連絡を独自に行う場合は、これをオフにしてください
preferred_default_commission_tax_rate0小数(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_stripe gem)は 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.md
  • node_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.md
  • node_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

コメント