mcpField
S
spree-returns

spree-returns

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

spree-returns について

spree-returnsは、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-returns

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

詳細

Spree の返品、交換、クレーム

それぞれ独自のライフサイクルを持つ3種類のレコードがあり、すべて確定済みの Spree::Order に関連付けられています。

レコードプレフィックス/番号発生した事象結果
Spree::Returnret_ / RET…顧客が商品を返送返金
Spree::Exchangeexch_ / EX…顧客が商品を返送別の商品(価格差を精算)
Spree::Claimclaim_ / CLM…到着時に破損していた、届かなかった、違う商品だった返金、交換品の送付、またはその両方 — 通常、商品は返送されません

これらは分けて扱ってください。「到着時に破損していた」ケースを、すぐに受領済みとして処理する返品としてモデル化してはいけません。破損を報告できるよう、クレームを使用してください。5.x(ReturnAuthorization → CustomerReturn → Reimbursement、ReturnItem)から移行する場合、これらは廃止されています。spree-upgrade-5-to-6 を参照してください。

モデル

Order
  ├── Return (ret_)        status, stock_location, reason (ReturnReason rar_), memo, received_at, refunded_at, created_by
  │     ├── ReturnLineItem (rli_)   fulfillment_item + line_item + variant; quantity (announced), received_quantity, resellable, pre_tax_amount
  │     ├── refunds (Spree::Refund, originator: return) / store_credits (originator: return)
  │     ├── ShippingLabel × n       prepaid return postage
  │     └── Delivery × n            inbound tracking
  ├── Exchange (exch_)     status, stock_location, reason (ReturnReason)
  │     └── ExchangeLineItem (eli_) fulfillment_item + line_item; original_variant → new_variant; price_difference
  └── Claim (claim_)       status, resolution, reason (ClaimReason clr_)
        └── ClaimLineItem (cli_)    line_item + variant; quantity, description, images, send_replacement,
                                     replacement_variant, refund_amount
  • 返品と交換はカートの明細項目ではなく、フルフィルメント項目(出荷済みの単位)を参照します。クレームは明細項目を参照します(商品が届いていない可能性があるためです)。
  • 3種類すべてがストア単位のリソース(SingleStoreResource)であり、metadata とカスタムフィールドを持ち、操作を行ったユーザー(created_by、承認者、返金担当者)を記録します。
  • 理由はストアが管理する語彙です。Spree::ReturnReason(返品と交換で使用)と Spree::ClaimReason があります。シードデータによって各ストアにデフォルトの理由が作成されます(クレーム理由には「到着時に破損」「届かなかった」「違う商品が送られた」などがあります)。マーチャントは独自の理由を追加できます。

ステータス

has_status を介した文字列の status カラムを使用します。ステートマシンはなく、遷移はワークフローです。

レコードフローキャンセル可能な状態
返品requested → approved → received → refunded、または canceledrequested、approved
交換requested → approved → received → fulfilled、または canceledrequested、approved
クレームopen → approved → resolved、または denied / canceledopen、approved(却下できるのは open の場合のみ)

ワークフロー

すべての遷移は Spree.<key> を通じて解決される Spree::Workflow です(Spree.dependencies を使って差し替えられます。spree-dependencies を参照してください)。いずれも先頭に validate フックがあります。

レコードワークフロー(DI キー)引数フック
返品return_create_workflow → Spree::Returns::Createorder:, items: [{ fulfillment_item:, quantity: }], stock_location:, reason:, memo:, created_by:validate, after_create
return_approve_workflow → Returns::Approvereturn_record:, approver:validate, after_approve
return_receive_workflow → Returns::Receivereturn_record:, items: [{ return_line_item:, quantity:, resellable: }], received_by:validate, before_restock, after_receive
return_refund_workflow → Returns::Refundreturn_record:, amount:, refund_method: 'original_payment', refunder:validate, before_refund, after_refund
return_cancel_workflow → Returns::Cancelreturn_record:, reason:validate, after_cancel
return_purchase_label_workflow → Returns::PurchaseLabelreturn_record:validate, after_purchase_label
交換exchange_create_workflow → Exchanges::Createorder:, items: [{ fulfillment_item:, new_variant:, quantity: }], …validate, after_create
exchange_approve_workflow、exchange_receive_workflow、exchange_cancel_workflow返品と同様validate、after_*(受領時は before_restock も)
exchange_fulfill_workflow → Exchanges::Fulfillexchange:, refund_method: 'store_credit', refunder:validate, before_settle, after_fulfill
クレームclaim_create_workflow → Claims::Createorder:, items: [{ line_item:, quantity:, description:, send_replacement:, replacement_variant:, refund_amount: }], reason:, memo:, created_by:validate, after_create
claim_approve_workflow、claim_deny_workflow、claim_cancel_workflowvalidate、after_*
claim_resolve_workflow → Claims::Resolveclaim:, resolution:, refund_method: 'store_credit', amount:, replacement_line_item_ids:, resolver:validate, before_settle, after_resolve

フックキーはクラス名に従います。例:returns.create.validate、returns.receive.before_restock、exchanges.fulfill.before_settle、claims.resolve.after_resolve など。

result = Spree.return_create_workflow.call(
  order: order,
  items: [{ fulfillment_item: order.fulfillment_items.find_by_prefix_id!('fi_…'), quantity: 1 }],
  reason: store.return_reasons.first, # or nil
  created_by: current_admin           # nil = customer self-service (window enforced)
)
result.success? ? result.value : result.error

作成時のガード(3種類すべて):注文は完了済みで、キャンセルされていない必要があります。また、アイテムは空であってはなりません。返品と交換では、まだ返品可能な数量を超える数量も拒否されます。先行するキャンセルされていない返品 および 交換の数量は、互いに差し引かれます(Spree::Returns::ReturnableQuantity)。同じリクエスト内で二重に指定された数量も同様です。クレームも各ラインアイテムについて同様に上限が設けられています。open/approved/resolved のクレームですでに対象となっている数量は、再度クレームできません(拒否またはキャンセルされたクレーム分は再び請求可能になります)。

返品ポリシー:期間、例外、カスタムルール

コアには、返品ルールとして Spree::Returns::EligibilityValidator が1つだけ含まれており、returns.create.validate と exchanges.create.validate の両方に登録されています。

  • 期間は market.preferred_return_window_days(デフォルトは30日、nil は無制限)です。注文にマーケットがない場合は、Spree::Market のデフォルト値が使用されます。期間は配送日ではなく、order.completed_at(注文確定日時)を起点に計算されます。
  • created_by が存在する場合、チェックはスキップされます。返品を作成するスタッフ(Admin API および Seller API)は、created_by によって追跡可能な例外として扱われます。Store API(顧客)からの返品は、常にチェックされます。
  • マーケットの withdrawal_period_days(デフォルトは14日、EU の最低基準)は、別個の法定通知期間です。返品の作成可否は制限しません。
  • レガシー設定の Spree::Config.return_eligibility_number_of_days は非推奨であり、どこからも読み込まれません。

独自のハンドラーでルールを置き換えるか、拡張してください。コアはアプリケーションのイニシャライザーより前にバリデーターを登録するため、config/initializers/spree.rb のトップレベルで置き換えられます。

# app/services/my_store/return_policy.rb
module MyStore
  class ReturnPolicy
    def call(workflow) # Spree::Returns::Create or Spree::Exchanges::Create
      return if workflow.created_by.present? # keep the staff override

      order = workflow.order
      window = order.market&.preferred_return_window_days || 30
      delivered_at = order.fulfillments.maximum(:delivered_at) || order.completed_at
      workflow.reject!("Returns are accepted for #{window} days after delivery") if delivered_at < window.days.ago

      final_sale = workflow.items.any? do |item|
        item[:fulfillment_item].variant.product.get_custom_field('policy.final_sale')&.value == 'true'
      end
      workflow.reject!('Final-sale items cannot be returned') if final_sale
    end
  end
end

# config/initializers/spree.rb
%w[returns.create.validate exchanges.create.validate].each do |key|
  Spree.hooks.unregister(key, 'Spree::Returns::EligibilityValidator')
  Spree.hooks.register(key, 'MyStore::ReturnPolicy')
end

ハンドラーはクラス名の文字列として登録され(リロードに安全)、引数なしでインスタンス化され、ワークフローを渡して呼び出されます。workflow.reject!(message) はベースエラーを追加してワークフローを失敗させます。API はこれを、モデルのバリデーションエラーと同様に422として返します。同じ方法で後続のステップを制御できます。たとえば、一定額を超える場合にマネージャーの承認を必須にする returns.refund.validate や、claim_line_items.images に写真を必須とする claims.resolve.validate などです。「返品可能だが手動レビューが必要」という状態はありません。そのような状態は、独自のステータス(to_prepare 内で Spree::Return.add_status を使用)またはメタデータで表現してください。

返品商品の受け入れと再入庫

Returns::Receive(および Exchanges::Receive)には、倉庫で実際に数えた数量を指定します。items を省略すると、申告どおりの全商品を販売可能な状態で受け入れます。各明細では received_quantity が記録され、再入庫されるのは再販売可能な商品だけです。stock_location.restock(variant, qty, return) は、返品の返品先在庫ロケーションに、返品を原因とする received 在庫移動を記録します。再販売できない商品は記録されますが、在庫には戻りません。破損品を別の場所に振り分けたり、再販売可否を変更したりするには、before_restock を使用してください。

商品が戻る場所は、返品の stock_location です。明示的に指定された場合(stock_location_id、管理者のみ)はその場所になります。指定されていない場合は、商品を発送した在庫ロケーション(返品を受け入れる設定の場合)、販売者の返品先ロケーション、store.returns_location、商品の発送元、ストアのデフォルトの順で使用されます。

返品の返送用配送に到着を報告する配送記録があっても、返品商品の受け入れは行われません。到着は検品ではありません。

返金と精算

  • Returns::Refund にはステータスが received である必要があります。amount はデフォルトで refundable_total と同額になり、上限も refundable_total です。実際に到着した数量分のみが返金対象になります(refund_total - refunded_total)。refund_method は 'original_payment'(返品の場合のデフォルト)または 'store_credit'(Spree::RefundMethods::METHODS)です。
  • 返品または申告対象の数量の価値は、定価ではなく、顧客が割引適用後に支払った金額です。LineItem#discounted_amount_for(units)(明細の discounted_amount を按分し、通貨単位に丸めた金額で、負の値にはなりません)が、ReturnLineItem#default_pre_tax_amount と ClaimLineItem#paid_amount の基準になります。明細に適用されたクーポン、プロモーション、手動割引の金額は返金されません。また、返品された無料ギフトの価値はゼロです。
  • 返金対象額がない返品(refundable_total がゼロ。たとえば、無料ギフトのみが返送された場合)は、金額を指定せず(または 0 を指定して)Returns::Refund を呼び出すことで完了します。返金行、ストアクレジット、税額クレジット、返金メールは発生せずに refunded に移行しますが、return.refunded は引き続き発行されます。返金対象額が残っている返品でゼロを指定すると refund_amount_required、負の金額を指定すると refund_amount_negative、返金対象額を超えると refund_exceeds_balance で失敗します。
  • ストアクレジットはトランザクション内で台帳に記録されます(Spree::StoreCredit、originator = 返品)。ゲートウェイへの返金は、その後 external_step として実行され、注文の返金可能な決済に配分されます(Spree::Refund 行、originator = 返品)。税額はその後、税プロバイダーを通じてクレジットされます(refund_tax)。
  • 注文ごとに返金可能残高は1つです。 すべての返金処理(返品、交換、申告。ゲートウェイ経由またはストアクレジット)は Spree::Refunds::OrderPayments を通り、注文をロックしたうえで、決済から返金可能な残額から、その注文に対してすでに発行されたストアクレジット分(refunded_order)を差し引いた金額を上限とします。この金額を超えると refund_exceeds_paid で失敗するため、1つの注文に対する販売後の2つの記録で、それぞれ全額を返金することはできません。
  • refunded_total は返金とストアクレジットの両方を合算します。注文の payment_status も同様です。返品、交換、または申告の返金で発行されたストアクレジットには refunded_order_id が設定され、ゲートウェイ返金と同じように注文は partially_refunded / refunded に移行します。
  • 交換の場合、Exchanges::Fulfill は(Spree::Stock::Coordinator を介して)交換品のフルフィルメントを作成し、在庫を割り当てます。交換品の価格が安い場合(price_difference が負)、要求された数量ではなく、実際に受け取った数量分の差額をクレジットします。デフォルトは 'store_credit' で、'original_payment' も指定できます。交換品の価格が高い場合、差額は加盟店が回収できるように未精算のまま残ります。コアは保存済みのカードに対して、通知なしに請求することはありません。
  • 申告の場合、Claims::Resolve は resolution に refund、replacement、refund_and_replacement のいずれか(Spree::Claim::RESOLUTIONS)を指定し、ステータスが approved である必要があります。返金方法のデフォルトは 'store_credit' で、申告の合計額(明細の refund_amount の合計)を返金します。ただし、明細の paid_amount の合計が上限です(API では申告明細項目に公開されます。これを超えると refund_exceeds_paid で失敗します)。交換品は元の注文に新しいフルフィルメントとして追加されます(顧客が2つ目の注文を行う必要はありません)。解決時に指定する replacement_line_item_ids は、作成時に指定された交換対象を上書きします。
  • 返品と申告による注文の payment_status の更新は Spree::Orders::UpdateStatuses が行います。直接変更しないでください。

前払い返品ラベル

本文なしで POST /api/v3/admin/orders/:order_id/returns/:id/labels を呼び出すと、出荷時に使用された配送業者アカウントを通じて送料を購入します(Returns::PurchaseLabel → フルフィルメントプロバイダーの purchase_label(return))。荷物は顧客から返品先の在庫ロケーションへ送られるよう手配され、入荷用の Spree::Delivery が作成されます。file(署名付き blob ID)と tracking_number を指定すると、別途購入したラベルを記録できます。顧客は GET /api/v3/store/orders/:order_id/returns/:id/label からラベルをダウンロードできます。返品をキャンセルすると、未使用の前払いラベルの料金が返金されます。

API

ストア API(注文の所有者 — 顧客 JWT または注文トークン)。セルフサービスでの作成と閲覧のみです。

POST /api/v3/store/orders/:order_id/returns   { "items": [{ "fulfillment_item_id": "fi_…", "quantity": 1 }], "reason_id": "rar_…", "memo": "Too small" }
GET  /api/v3/store/orders/:order_id/returns[/:id][/label]
POST /api/v3/store/orders/:order_id/claims    { "items": [{ "line_item_id": "li_…", "quantity": 1, "description": "Arrived cracked" }], "reason_id": "clr_…" }
GET  /api/v3/store/orders/:order_id/claims[/:id]

TypeScript:client.orders.returns.create(orderId, …)、client.orders.claims.create(orderId, …)。

管理 API — read_orders / write_orders で制限されます(返品は注文権限リソースの一部です)。

エンドポイント備考
…/admin/orders/:order_id/returns(index/show/create/update)作成:items[{fulfillment_item_id, quantity}]、reason_id、stock_location_id、memo。更新では memo、reason_id、stock_location_id、metadata のみ編集できます
PATCH …/returns/:id/approve · /receive · /refund · /cancel受領:items[{return_line_item_id, quantity, resellable}]。返金:amount、refund_method。キャンセル:reason
…/returns/:id/labels(および download、refund)送料支払い済みの配送ラベル
…/admin/orders/:order_id/exchanges + approve · receive · fulfill · cancel作成時の項目:fulfillment_item_id、new_variant_id、quantity。フルフィルメント:refund_method
…/admin/orders/:order_id/claims + approve · resolve · deny · cancel作成時の項目:line_item_id、quantity、description、send_replacement、replacement_variant_id、refund_amount。解決:resolution、refund_method、amount、replacement_line_item_ids
GET /api/v3/admin/returns、/exchanges、/claims複数の注文をまたぐ一覧。例:Ransack の q[status_eq]=approved、q[created_at_gt]=…
/api/v3/admin/return_reasons、/claim_reasonsCRUD

管理 SDK:adminClient.orders.returns.{create,approve,receive,refund,cancel}、.returns.labels.*、adminClient.orders.exchanges.{approve,receive,fulfill,cancel}、adminClient.orders.claims.{approve,resolve,deny,cancel}、adminClient.returns.list(...)。

イベント

return.requested、return.approved、return.received、return.refunded、return.canceled、exchange.requested/.approved/.received/.fulfilled/.canceled、claim.opened/.approved/.resolved/.denied/.canceled。さらに、各リソースのライフサイクルイベント *.created/.updated/.deleted があります。これらはすべて Webhook に送信されます。オプションの spree_emails gem は Spree::ReturnMailer のメール(例:return.refunded 時の返金メール)を送信します。削除済み:reimbursement.*、return_authorization.*、return_item.*、customer_return.*。

注意点

  • update!(status: …) で状態を遷移させないでください。再入庫、返金、イベント、フックがスキップされます。必ずワークフローを呼び出してください。
  • 返品可能期間は注文の作成日を起点に計算されます。ポリシーで配達日を起点にする場合(一般的で、EU の撤回権もこれに該当します)は、独自のバリデーター(上記)を作成してください。fulfillment.delivered_at は配送業者の Webhook または mark_delivered によって設定されます。
  • スタッフまたは販売者が作成する返品は、仕様上、返品可能期間の制限を受けません。スタッフにもポリシーを適用する必要がある場合、ハンドラーで created_by を理由に早期リターンしないでください。
  • Returns::Refund は received の状態からのみ実行され、返品は一度だけ返金されます。分割決済は、その一度の返金処理内で処理され(決済間に分配されます)、返金処理を二度呼び出すことはありません。
  • 顧客が支払う交換差額は自動請求されません。独自に回収してください(例:exchanges.fulfill.after_fulfill フックまたは手動決済)。
  • 交換品のフルフィルメント、その梱包明細書、ダッシュボードの Exchanges カードには、元のバリアントではなく 交換後の バリアント(new_variant)が表示されます。
  • 交換またはクレームで送付する商品は、元の明細項目の FulfillmentItem#replacement = true として記録されます(Spree::Fulfillments::Replacements で構築されます)。注文編集で対象になるのは購入済みの商品だけなので、その明細項目の数量を変更すると、有料商品が追加または削除され、交換品はそのままです。6.0 の以前のベータ版で発送された交換品にはフラグが付けられておらず、遡及して設定されることもありません。
  • クレームでは再入庫は行われません。商品は返送されません。商品が返送される場合は、返品も作成してください。
  • ルックアップは必ず注文またはストアを通じてスコープしてください(order.fulfillment_items.find_by_prefix_id!)。Spree::FulfillmentItem.find(params[:id]) は決して使用しないでください。

テスト

ファクトリー::return、:approved_return、:received_return、:return_line_item、:exchange、:approved_exchange、:received_exchange、:claim、:approved_claim、:return_reason、:claim_reason、さらに :completed_order_with_totals / :fulfillment。ポリシーハンドラーをテストするには、ワークフローを呼び出し、result.failure? と result.error のメッセージを検証してください。ハンドラーを単独で呼び出すだけでは不十分です。リクエスト以外のスペックでは、Spree::Current.store を設定してください。

さらに詳しく読むには

  • node_modules/@spree/docs/dist/developer/core-concepts/returns-exchanges-claims.md
  • node_modules/@spree/docs/dist/developer/customization/workflows.md(フック、reject!)
  • node_modules/@spree/docs/dist/developer/core-concepts/store-credits-gift-cards.md、payments.md
  • 関連スキル:spree-workflows、spree-fulfillment(ラベル、配送、交換品)、spree-inventory(再入庫の在庫移動)、spree-payments(返金)、spree-events-webhooks、spree-upgrade-5-to-6。

コメント