0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Salesforce】B2B コマースで日本の消費税(10%・8%)を1つのカートで分ける方法

0
Last updated at Posted at 2026-10-30

はじめに

B2B コマース(拡張 LWR のストア)で日本向けの販売サイトを作ると、早い段階で次の壁に当たります。

  • 標準税率 10% の商品(文房具など)と、軽減税率 8% の商品(飲食料品など)を、1つのカートに入れたい
  • ところが標準の「手動の税」を設定すると、カートの全商品が同じ率で計算される

この記事では、この問題を 税コード + 税の拡張(Apex) で解決する手順を、実際に動かして確かめた結果と一緒にまとめます。円の端数で引っかかる点も最後に書きます。

対象読者

  • 拡張 LWR の B2B コマースのストアで、日本の消費税を扱う必要がある管理者・開発者
  • Apex の基本(クラスを書いて反映できる)と、ストアの基本設定は分かる方

対象外

  • Aura のストア(Spring '23 より前にコマースを有効にした org だけで作れる、古い型のストアです。税はチェックアウトのフローの中で、税の連携の Apex クラス(sfdc_checkout.CartTaxCalculations を実装したもの)を呼ぶ別の仕組みです。この記事では扱いません)
  • Stripe 経由の Salesforce Tax Solution を使う構成
  • インボイス制度(適格請求書)の記載要件、税務上の判断(端数処理の方法は事業者が決めることです)

検証の環境

Developer Edition から作ったスクラッチ org で、Salesforce CLI と Claude Code を道具に使って検証しました。数値は、手計算した期待値と実際の画面・データを突き合わせたものです。

検証は2回に分けています。1回目は通貨が米ドルの org、2回目は通貨が円の org です。結果の節もこの2つに分けて書きます。

なぜ「手動の税」ではできないのか

B2B コマースの税には道がいくつかあります。公式ヘルプ「Set Up Taxes in B2B Commerce」では、大きく次の3つです。

  1. Salesforce Tax Solution(Stripe を使う自動計算)
  2. 税の拡張・計算サービス(Avalara や Vertex などを Apex の拡張でつなぐ)
  3. 手動の Salesforce Tax Solution

自前で用意しやすいのは「手動」ですが、これには次の制約があります。

  • カスタムのチェックアウトのストアだけ
  • 総額の課税(税込み価格 = gross)だけ
  • 国・州の単位の税率

さらに、商品ごとに別の税の決まり(Tax Policy)を付けられるのは、Stripe か拡張を使うストアだけです(ヘルプ「Assign a Tax Policy to a Product」)。手動のときは、ストアの既定の決まりが全商品に使われます。

実際に確かめた

軽減税率の材料(Tax Policy・Tax Treatment・税コード・8% の Tax Rate)を作り、お茶の Product2.TaxPolicyId に付けました。手動の税のままカートに入れると、結果は次のとおりでした。

  • 商品の TaxPolicyId は保存できる(エラーにならない)
  • でも計算では使われない。ペンもお茶も、ストアの既定の「標準 10%」で計算された

円の org で確かめると、ペン10本(1,200円)とお茶10本(2,160円)のカートは、全部 10% で税 305 円になりました(8% が効いていれば 269 円のはずです)。エラーが出ないので、気づかずに公開してしまう危険があります。

公式の道: 税コード + 税の拡張

開発者ガイド「Salesforce Commerce Tax API」には、日本の消費税(JCT)の説明があります。

  • 税の区分(tax class)で 10% と 8%(一部の食品・飲み物・新聞)を分ける
  • 丸めは商品ごとではなく、区分ごとの合計で行う

これを踏まえると、日本向けの道は次の形になります。

役割 何で持つか
商品ごとの税の区分 Product2.TaxPolicyId(Tax Policy → 既定の Tax Treatment → 税コード)
税コードごとの率 TaxRate(10 や 8)
計算 Apex の税の拡張

税の拡張は2種類あります(開発者ガイド「Available Extensions」)。

  • commercestoretax.TaxService(拡張ポイント Commerce_Domain_Tax_Service)
  • CartExtension.TaxCartCalculator(拡張ポイント Commerce_Domain_Tax_CartCalculator)

公式の見本(GitHub の forcedotcom/commerce-extensibility、commerce/domain/tax/)にも「まず TaxService で足りるかを確かめる」とあります。今回も TaxService で足りました。見本は「税コード → 率」を引く形なので、そのまま日本の消費税に合います。

手順とコード

1. 軽減税率の材料を作る

手動の税で標準 10% を作る手順と同じものを、もう1組作ります。順番は次のとおりです。

  1. Tax Policy を Draft で作る
  2. Tax Treatment を作る(税コード、例 JCT8。Legal Entity と Tax Engine は標準の側と同じにする)
  3. Tax Rate を作る(税コード・州・率 8)
  4. Tax Policy を Active にする
  5. 軽減税率の商品の Product2.TaxPolicyId にこの Tax Policy を付ける

2. Apex の拡張を書く

commercestoretax.TaxService を継承し、processCalculateTaxes を上書きします。全体は長いので、要点だけ抜粋します(クラス名などは記事用に短くしています)。

public virtual with sharing class JapanTaxService extends commercestoretax.TaxService {

    public override commercestoretax.CalculateTaxesResponse processCalculateTaxes(
            commercestoretax.CalculateTaxesRequest request) {
        String storeId = request.getWebstoreId();
        Boolean gross = isGross(storeId); // ストアの DefaultTaxLocaleType が Gross か
        commercestoretax.CalculateTaxesResponse response =
            new commercestoretax.CalculateTaxesResponse(
                gross ? commercestoretax.TaxLocaleType.Gross : commercestoretax.TaxLocaleType.Net);
        // 明細を税コードごとに分け、率は TaxRate から引く(Apex に 10 や 8 を書かない)
        // 区分ごとの合計で丸め、ずれは区分の中で一番大きい明細に寄せる
        // ...
        for (/* 明細 */) {
            commercestoretax.CalculateTaxesResponseLineItem out =
                new commercestoretax.CalculateTaxesResponseLineItem();
            out.setLineId(li.getLineId());
            out.setProductId(li.getProductId());
            out.setTotalLineTaxAmount(t);
            out.setTotalPriceTaxAmount(priceTax);
            out.setTotalAdjustmentTaxAmount(t - priceTax);
            out.setTotalTieredAdjustmentTaxAmount(0);
            out.addTaxInfo(new commercestoretax.TaxInfo(
                ship.getCountry(), ship.getState(), 1, (Double) rate,
                'JCT ' + rate.stripTrailingZeros().toPlainString() + '%', code, t));
            response.addCalculateTaxesResponseLineItem(out);
        }
        response.setTotalTaxAmount(grandTax);
        return response;
    }

    // 内税: 税込み × 率 / (100 + 率)、外税: 税抜き × 率 / 100(丸める前)
    private static Decimal taxOf(Decimal amount, Decimal rate, Boolean gross) {
        return gross ? amount * rate / (100 + rate) : amount * rate / 100;
    }
}

実際に動かして分かった、効いたポイントを挙げます。

  • 税コードの決め方: 明細に税コードが入っていればそれを使い、無ければ「商品の Tax Policy → 既定の Tax Treatment の税コード」、それも無ければ「ストアの既定の Tax Policy の税コード」の順にしました。
  • 率が無い税コードは例外にする: 黙って 0 円にしないためです。例外を投げると、支払いが ValidationError になり、注文は作られませんでした。
  • TaxRate は without sharing の内部クラスで読む: 画面では購入者のユーザーで拡張が動きます。with sharing のクラスで TaxRate を読むと購入者には 0 件に見え、No tax rate for tax code が購入者の画面に出ました。管理者の API だけで確かめていると見つかりません。
  • 内税と外税は要求に入っていない: request.getWebstoreId() で WebStore.DefaultTaxLocaleType(Gross / Net)を引いて切り替えます。ストアの値を変えるだけで、外税の合計は「税抜き + 税」になりました。
  • 値引きのあとの額に課税する: 明細の getTotalPrice() は値引き前で、getAdjustments() の各 getAmount() はマイナスの値で入ります。足してから課税しました(ヘルプ「Taxing and Promotion Adjustments」に沿う)。
  • setTotalTieredAdjustmentTaxAmount(0) を入れる: 税の計算 API(/commerce/webstores/{id}/taxes/actions/calculate-taxes)を叩くときだけ、この欄が無いと UNKNOWN_EXCEPTION になりました。カート・チェックアウトの画面では気づかない落とし穴です。

3. テストを書く

公式(開発者ガイド「Write Tests for an Extension Provider」「Mock the Base Apex Class」)に従い、次のようにしました。

  • カバー率は 75% 以上にする
  • 別の名前空間のクラスは mock できないので、照会(WebStore や TaxRate の SOQL)を仮想メソッドに分け、テストの内側のクラスで上書きする
  • DML が 0 件であることを確かめる

今回のクラスは 9 本のテストで全て Pass、カバー率は 95% でした。

4. 登録してストアにつなぐ

登録は RegisteredExternalService、ストアへのつなぎは StoreIntegratedService です。画面でつなぐ必要はなく、データ(または CLI・API)で作れました。

RegisteredExternalService:

項目 値
DeveloperName / MasterLabel 任意の名前
ExternalServiceProviderId 上の Apex クラスの Id
ExternalServiceProviderType Extension
ExtensionPointName Commerce_Domain_Tax_Service

StoreIntegratedService:

項目 値
StoreId ストアの Id
Integration 上の RegisteredExternalService の Id
ServiceProviderType Extension

つながると、ストアの Settings > Checkout > Taxes タブの「Custom Providers」にある「Tax - Tax Service」の枠に Apex 名が出ます。Settings > Store の Extensions タブには税の札が無い(在庫・販促など6つだけ)ので、探す場所に注意してください。

画面から外すときは、同じ枠の ▼ メニューの Remove です。外すと StoreIntegratedService が 0 件になり、標準の税に戻ります。

確かめた結果(1回目: 米ドルの org)

カートの金額

標準 10% のペン 12.00 と軽減 8% のお茶 21.60 を、同じカートに入れました。

構成 ペンの税 お茶の税 合計
手動の税のまま(お茶に軽減の Tax Policy を付けても) 1.09(10%) 1.96(10%) 3.05
税の拡張 1.09(JCT 10%) 1.60(JCT 8%) 2.69

注文

発注書番号で注文を確定し、注文は Activated になりました。注文の明細の税(OrderItemTaxLineItem)も 10% と 8% に分かれて残りました。

確かめた結果(2回目: 円の org)

カートの金額と注文

ペン10本(1,200円・10%)とお茶10本(2,160円・8%)で、期待値は手計算で出しておきます。

  • 内税で区分ごと・切り捨て: 10% は 1,200 × 10 / 110 = 109.09 → 109、8% は 2,160 × 8 / 108 = 160.00 → 160。合計 269

拡張をつないだカートも 269(109 + 160)になり、注文まで通りました。手動の税のままでは 305(全部 10%)でした。

確かめるときの注意

  • チェックアウト中のカートは、届け先を送り直しても税を計算し直しません。同じ住所・別の住所の Id・番地だけ違う住所の3通りで試しても、CartTax の日時も Apex のログも変わりませんでした。拡張をつないだあとは、新しいカートで確かめてください。
  • 購入者1人に開けるカートは1つまでです(A new cart can be created only when there are no current carts.)。
  • 拡張が例外を投げても、チェックアウトの状態は Completed・税 0.00 と返ることがありました。状態だけを見ずに、CartValidationOutput も確認してください。
  • 商品の税率の API(GET /commerce/webstores/{id}/taxes/products/{商品})は、拡張がつながっていても手動の税の率を返します。拡張は計算だけを上書きしているためです。軽減 8% の商品でも 10% と返りました。商品ページに税率を出す設計だと、この API は使えません。

円の端数の注意(2回目: 円の org)

円のストアでは、端数まわりで3つ引っかかりました。どれも手計算で先に期待値を出し、実際の値と突き合わせています。

1. 丸める単位で合計が変わる

ペン5本(600円)・ノート4冊(1,000円)・お茶10本(2,160円)のカートで、内税・切り捨てのとき、次のとおりです。

丸める単位 計算 合計の税
区分ごと(公式の説明どおり) 10%: 1,600 × 10 / 110 = 145.45 → 145、8%: 160 305
明細ごと ペン 54、ノート 90、お茶 160 304

外税・区分ごと・四捨五入にすると 333(10% が 160、8% が 172.8 → 173)になりました。内税か外税か、丸める単位、切り捨てか四捨五入かは、事業者が決めることです。この拡張では、丸める単位・丸め方・小数の桁をカスタムメタデータに出し、行を反映し直すだけで切り替えられるようにしました。Apex を直さずに切り替わることは、1回目(米ドルの org)で確かめています(同じカートで、区分ごと 2.94 ⇔ 明細ごと 2.93)。

2. 小数の桁を 2 のままにしない

円で拡張の小数の桁を 2 のままにすると、明細の税は 54.54 のように小数になり、カート合計の税は 305 と整数になって、明細の和(305.45)と合いません。円の案件では、拡張の設定の小数の桁を 0 にします。

3. 単価 × 数量 ≠ 合計

  • 小数の単価: 1.5 円の価格は、円の org でもそのまま入りました。3個で 4.5 円ですが、カートの明細の合計は 5 に丸められ、見える単価は ¥2/item でした。裏のデータは SalesPrice 1.5・TotalPrice 5 です。
  • 段階価格(率で割引): ノート 250 円で 10 個以上 7% 引きにすると、単価は 232.5 になります。明細の合計は 2,325・税 211 で期待どおりでしたが、商品ページの段の表と、価格の API の tierUnitPrice は ¥233、カートの UnitAdjustedPrice は 232 でした。見た目の単価 × 数量(233 × 10 = 2,330)と明細(2,325)が合いません。
  • 段階価格を「額」で作ると合う: 20 冊以上を「1個 18 円引き」(AdjustmentAmount)にすると、単価 232・合計 4,640・税 421(4,640 × 10 / 110 = 421.8 の切り捨て)で、表示も明細も期待どおりに揃いました。円の案件では、段階価格は率ではなく額で作ると表示のずれが出ません。
  • NetUnitPrice: カートの NetUnitPrice と OrderItem.UnitPrice は、円でも小数2桁で出ます(例: 109.1、手動の税では 109.09)。手動の税では UnitPrice × 数量 が TotalPrice と合いませんでした。帳票や外部連携で単価を使うなら、整数にするかを、要件で先に決めてください。

要件で先に聞くこと

  • 内税か外税か(ストアの DefaultTaxLocaleType)
  • 丸める単位(区分ごとか明細ごとか)、切り捨てか四捨五入か
  • 税の内訳を画面に出すか(内税のストアでは、カートにも購入手続きにも税の行は出ず、小計・送料・合計だけでした。税そのものはカートの TotalTaxAmount に入っています)
  • 帳票・連携で単価を使うか

まとめ

  • 標準の手動の税では、商品に軽減の Tax Policy を付けても保存できるだけで、計算には使われない。エラーも出ないので気づきにくい
  • 日本の消費税の公式の道は、商品ごとの Tax Policy(税コード)+ 税の拡張。commercestoretax.TaxService で足りる
  • 率は TaxRate から引き、Apex に書かない。TaxRate は購入者の画面で見えるよう without sharing で読む
  • 拡張は RegisteredExternalService と StoreIntegratedService で、データから登録できる
  • 確かめるときは新しいカートで。税の計算 API は setTotalTieredAdjustmentTaxAmount が要る
  • 円では、丸める単位・小数の桁・段階価格の作り方で端数がずれる。先に手計算の期待値を出し、要件で決めておく

筆者は Salesforce の設定の代行・相談を受けています(連絡先は Qiita のプロフィールに)。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?