mcpField
S
spree-taxes

spree-taxes

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

spree-taxes について

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

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

詳細

Spree 税金

Spree 6 における税金は、次の 3 つの判断で決まります。

  1. 何を販売するか → 商品/バリアントの Spree::TaxCategory(ラインアイテムにコピーされます)。
  2. どこで適用するか → 購入の税金住所(デフォルトでは配送先住所)と、その マーケット。
  3. 誰が計算するか → マーケットの 税金プロバイダー。設定されていない場合は Spree.default_tax_provider(デフォルトは Spree::TaxProvider::Internal で、Spree::TaxRate の行を読み取ります)。

計算方法にかかわらず、出力は常に同じです。カート/注文に Spree::TaxLine の行が作成され、Spree::Carts::RecalculateTotals によって included_tax_total/additional_tax_total に反映されます(spree-order-totals を参照)。

5.x のゾーンと Spree::Calculator::DefaultTax から移行する場合:税金ゾーンと税金調整はなくなりました。spree-upgrade-5-to-6 を参照してください。

税金カテゴリ

Spree::TaxCategory はストア単位です。属性は name、is_default(ストアごとにデフォルトは 1 つ。カテゴリが設定されていない商品にはこれが使われます)、tax_code(Avalara の税金コードなど、外部エンジンが認識するコード)です。商品/バリアントは税金カテゴリを 1 つ参照し、ラインアイテムは tax_category_id のスナップショットを保持します。配送方法には配送料の税金用にカテゴリを設定できます。カテゴリのない手数料にはストアのデフォルトが使われます。

税率(Internal プロバイダー)

Spree::TaxRate(tax_…、ストア単位、論理削除対応)には ゾーンがありません。ISO コードで管轄区域を指定します。

フィールド意味
amount小数の割合です。0.2 = 20%(amount_percentage は 20 を読み書きします)
country_code / state_code適用先です。国が nil の場合はすべての地域、州が nil の場合は国全体に適用されます
tax_category適用対象です
included_in_pricetrue = VAT 方式で、価格にすでに含まれています。false = 価格に上乗せされます
name / show_rate_in_label税金行にスナップショットとして保存されるラベルです("VAT 20%")
store = Spree::Current.store
standard = Spree::TaxCategory.default(store)
Spree::TaxRate.create!(name: 'VAT', amount: 0.20, country_code: 'DE', tax_category: standard, included_in_price: true)
Spree::TaxRate.create!(name: 'CA sales tax', amount: 0.0725, country_code: 'US', state_code: 'CA', tax_category: standard)

管理 API:/api/v3/admin/tax_categories、/api/v3/admin/tax_rates(adminClient.taxRates.create({ …, country_code: 'GB', tax_category_id: 'taxcat_…' }))。

照合:TaxRate.for_store(store).for_jurisdiction(country, state) — 商品のカテゴリに該当し、国/州が一致する(または nil である)税率がすべて適用されます。そのため、国全体に適用される US の税率と、US/CA の税率は、どちらもカリフォルニア州の住所に適用されます。Internal プロバイダーが unsupported_capabilities として宣言している機能は us_local_tax、reverse_charge、oss_thresholds、proportional_delivery_tax です。単純な国/州別税率を超える機能には、外部エンジンを使用してください。

税金住所

purchase.tax_address は、ストア設定 tax_using_ship_address が true(デフォルト)の場合は配送先住所、それ以外の場合は請求先住所です。purchase.tax_country は、まずマーケットのデフォルトの国にフォールバックし、次にストアのデフォルトの国にフォールバックします。これにより、住所がまだ登録されていない場合でも、内税方式のマーケットで実際の価格を表示できます。

内税と外税

  • 税率ごとの included_in_price が計算方法を決めます。内税は税込価格から差し引かれ(pre_tax_amount は商品に保存されます)、included_tax_total に計上されます。外税は additional_tax_total に計上され、total に加算されます。
  • Spree::Market#tax_inclusive は、そのマーケットの価格が税込みで表示されることを示す宣言です。Store API のマーケットにも公開されるため、ストアフロントで価格に「VAT 込み」などのラベルを付けられます。マーケットの各国に設定する税率と整合させてください。
  • 国境を越える VAT の再計算:VAT 込み価格を別の国の顧客に表示する際、Spree::VatPriceCalculation は国内の VAT を取り除き、配送先の VAT を加算します。これは Internal プロバイダーの場合に限ります(外部エンジンでは税率行を保持しないため、代わりに地域別価格リストを使用してください。spree-pricing を参照)。
  • サマリーでは、included_tax_total を他の金額に加算しないでください。これはすでに価格に含まれています。1 つの項目で表す場合は、tax_total = 内税 + 外税とするのが安全です。

税プロバイダー

cart.tax_provider   # => market&.tax_provider_instance || Spree.default_tax_provider.new
  • Spree::Market#tax_provider はクラス名の文字列で、Spree.tax_providers に照らして検証されます(Admin API:PATCH /api/v3/admin/markets/:id { tax_provider: 'MyTax::Provider' };GET /api/v3/admin/tax_providers では、インストール済みのプロバイダーが利用可否や未対応の機能とともに一覧表示されます)。
  • Spree.default_tax_provider はクラスを返します(クラスまたは文字列を受け付けます):Spree.default_tax_provider = 'MyTax::Provider'。
  • プロバイダーはステートレスで、呼び出しごとに新しくインスタンス化され、コンストラクター引数はありません。認証情報は、プロバイダーが実行時に読み取る Spree::Integration に格納します。認証情報が必要な場合は、self.available_for_store?(store) をオーバーライドしてください。
プロバイダーの契約(Spree::TaxProvider::Base)
メソッド呼び出しタイミング必須要件
estimate(owner, items = nil, tax_date:, tax_identifier:, exemptions:, context:)凍結されていないすべての再計算時(カート、下書き注文)。注文確定後は呼び出されませんアイテムごとに全置換:アイテムの既存の税行を削除し、新しい税行を書き込みます。処理区分ごとに1行とし、金額がゼロの行も含めます。items: nil は owner.taxable_items(注文明細、フルフィルメント、関税以外の手数料)を意味します。計算できない場合は例外を発生させます。
commit(order)注文確定後、トランザクションの外(Carts::Complete の finalize)冪等にしてください。完了処理が再実行される可能性があるため、注文をキーにして申告を管理します
void(order)注文キャンセル時確定済みの取引書類を取り消します
refund(order, return_items, amount:, tax_date:)返品の返金時実際に返金された amount を超える税額を返金しないでください
service_tax_rate(address:, store:)マーケットプレイスのコミッション請求時割合、または nil(判断しない)を返します
self.unsupported_capabilitiesAdmin の市場プロバイダー組み合わせ時対応できない機能を宣言します(シンボル)
self.display_name, self.available_for_store?(store)Admin表示/利用可否の判定
作成方法
# server/app/models/my_tax/provider.rb
module MyTax
  class Provider < Spree::TaxProvider::Base
    def self.display_name = 'MyTax'
    def self.unsupported_capabilities = %i[reverse_charge]

    def estimate(owner, items = nil, tax_date: nil, tax_identifier: nil, exemptions: [], context: {})
      items ||= owner.taxable_items
      return if items.empty?

      quote = MyTax::Client.quote(owner, items, tax_date:, vat_id: tax_identifier&.value, **context)

      items.group_by(&:class).each do |klass, group|
        fk = { Spree::LineItem => :line_item_id, Spree::Fulfillment => :fulfillment_id, Spree::Fee => :fee_id }.fetch(klass)
        Spree::TaxLine.where(fk => group.map(&:id)).delete_all        # replace-all
      end

      owner_key = owner.is_a?(Spree::Order) ? :order : :cart
      quote.lines.each do |line|
        Spree::TaxLine.create!(
          owner_key => owner, line.item_fk => line.item_id,
          amount: line.tax, rate: line.rate, label: line.label,
          included: false, provider_id: 'mytax',
          taxability_reason: line.reason,                     # e.g. 'standard_rated', 'reverse_charge'
          country_code: line.country, state_code: line.state,
          data: { jurisdictions: line.breakdown }             # provider payload, free-form
        )
      end
    end

    def commit(order)
      MyTax::Client.commit(document_code: order.number)       # idempotent by order number
    end

    def void(order) = MyTax::Client.void(document_code: order.number)
  end
end
# server/config/initializers/spree.rb
Spree.default_tax_provider = 'MyTax::Provider'  # optional fallback for markets naming none (string: no autoload at boot)

Rails.application.config.after_initialize do
  Spree.tax_providers << MyTax::Provider        # selectable per market; app/ classes can't be referenced during boot
end

TaxLine.taxability_reasons はクラス属性です(standard_rated、reduced_rated、zero_rated、reverse_charge、intra_community_supply、export、customer_exempt、product_exempt、not_collecting、not_subject_to_tax)。独自の理由は Spree::TaxLine.taxability_reasons += ['my_reason'] で追加します。

ネットワーク I/O は高速で、障害に適切に対応できるようにしてください。estimate はカートの書き込み処理中、および注文確定ロック中に実行されます。例外が発生すると、再計算は中止されます。

追加データの受け渡し:set_tax_line_context

型付き入力では渡されない情報(顧客の利用コード、チャネル、見積もり ID など)がプロバイダーに必要な場合は、コンテキストハンドラーから取得します。各ハンドラーが返すハッシュは統合され、context: として渡されます。

module MyApp
  class AvalaraEntityUseCode
    def call(workflow)
      code = workflow.cart.customer&.metadata&.dig('entity_use_code')
      code ? { entity_use_code: code } : {}
    end
  end
end

Spree.hooks.register('carts.recalculate_totals.set_tax_line_context', 'MyApp::AvalaraEntityUseCode')
# Admin draft-order edits use the twin key: 'orders.recalculate_totals.set_tax_line_context'

免税情報や購入者の VAT 番号にはコンテキストを使わないでください。これらは型付き入力です(以下を参照)。

TaxLine の行

Spree::TaxLine(tl_):所有者(カートまたは注文)、line_item/fulfillment/fee のいずれか1つ、amount、rate、label、included、tax_rate(外部プロバイダーの場合は nil)、provider_id、taxability_reason、country_code/state_code、data(JSON)。スナップショットとして保存されるため、後から税率を編集または削除しても、確定済みの注文には影響しません。

  • 書き込みを行えるのはプロバイダーのみです。Admin API の GET /api/v3/admin/orders/:id/tax_lines は読み取り専用です。
  • カートから注文への確定時に、再見積もりせず、そのままコピーされます。確定済みの注文では金額が凍結されます。
  • 配送への課税 → フルフィルメントに行を作成します。手数料への課税 → 手数料に行を作成します(fee_id)。関税はデフォルトの課税対象には含まれません。

B2B:税務識別番号と免税

税務識別番号(Spree::TaxIdentifier、txi_、kind + value、例:eu_vat / NL123456789B01)は、顧客、会社(法人のみ)、またはカート(チェックアウト時の上書き)に紐付けられます。解決順序:cart.resolved_tax_identifier → カートの上書き → 会社(最も近い法人の祖先。親から引き継ぐことはありません)→ 顧客。それぞれの中では、確認済みのものが最新のものより優先されます。完了時に解決された識別番号が注文にコピーされ(source:override/company/customer)、読み取り専用になります。識別番号がない場合は、消費者向け販売として扱われます(安全なデフォルト)。

  • Store API:GET/PUT/DELETE /api/v3/store/carts/:cart_id/tax_identifier(PUT には kind と value の両方が必要です。カートの再価格計算が行われます)。SDK ヘルパーはまだありません。client.request('PUT', \/carts/${id}/tax_identifier`, { body: { kind, value }, spreeToken })` を使用してください。
  • 管理画面:/api/v3/admin/companies/:id/tax_identifiers(および validate)、/api/v3/admin/customers/:id/tax_identifiers。
  • 種類ごとのバリデーター:Spree.tax_identifier_validators['au_abn'] = 'MyApp::AbnValidator'(Spree::TaxIdentifiers::Validator::Base のサブクラス。保存時の self.valid_format?、Spree::TaxIdentifiers::ValidateJob での登記情報チェック用 call(tax_identifier:))。組み込みの eu_vat バリデーターは形式のみを検証します。検証が必要な場合は、VIES/登記情報のバリデーターを組み込んでください。ステータス:pending/verified/unverified/unavailable/unsupported。
  • VAT ID によるリバースチャージはプロバイダーの機能です。Internal プロバイダーは tax_identifier を無視します。

免税:会社の法人に設定する Spree::TaxExemptionCertificate は、country_code/state_code の範囲に限定され、active(確認済みかつ有効期限内)の場合のみカウントされます。Spree::Tax::ResolveExemptions(Spree.tax_resolve_exemptions_service)は、証明書を Spree::TaxExemption 値オブジェクト(reason_code、certificate_number、管轄区域、item_overrides)に変換し、exemptions: として渡します。免税対象の商品には、taxability_reason: 'customer_exempt' を持つ金額ゼロの明細が設定されます。証明書を別の場所(例:Avalara の証明書ストア)から読み込むよう、サービスを差し替えられます。spree-b2b を参照してください。

Avalara およびその他のエンジン

税エンジンは、Spree.tax_providers に登録され、マーケットごとに選択されるプロバイダークラスとして組み込まれます。Avalara については、node_modules/@spree/docs/dist/integrations/tax/avalara.md を参照してください。管理画面での手順に進む前に、お使いの Spree バージョンでプロバイダーベースの設定が説明されていることを確認してください。TaxCategory#tax_code をエンジンの商品コードにマッピングし、配送方法のカテゴリーに運賃コードを設定してください。

注意点

  • 税額がゼロになる → 該当する TaxRate がありません。ストアが違う、カテゴリーが一致しない(商品が税率のないカテゴリーを使用している。カテゴリーがない商品は、ストアの is_default カテゴリーを使用します)、または追加されたマーケットに税務住所がまだない可能性があります。Spree::TaxRate.for_store(cart.store).for_jurisdiction(cart.tax_country&.iso, cart.tax_address&.state_code) を確認してください。
  • 管理者が確定済み注文を編集しても税額が更新されない — 正しい動作です。確定済み注文の再見積もりは行われません。
  • TaxLine を手動で作成・編集する — 次回の見積もり時に上書きされます。税率、カテゴリー、またはプロバイダーを変更してください。
  • マーケットの tax_provider は登録済みクラス名である必要があります。gem のプロバイダーを Spree.tax_providers に追加しないと、マーケットの検証に失敗します。
  • commit が冪等でない → 完了処理が再実行されたときにドキュメントが重複します。
  • ジョブ/コンソールでは Spree::Current.store を設定してください。税率とデフォルトカテゴリーはストアごとに異なります。

詳細情報

  • node_modules/@spree/docs/dist/developer/core-concepts/taxes.md、companies.md、markets.md、order-totals.md
  • node_modules/@spree/docs/dist/developer/customization/workflows.md — コンテキストフック
  • 関連スキル:spree-order-totals、spree-b2b、spree-providers、spree-pricing、spree-checkout
  • ソース:spree/core/app/models/spree/tax_provider/{base,internal}.rb、spree/core/app/models/concerns/spree/purchase/taxation.rb

コメント