【Salesforce】B2B コマースで日本の消費税(10%・8%)を1つのカートで分ける方法
はじめに
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 が要る
- 円では、丸める単位・小数の桁・段階価格の作り方で端数がずれる。先に手計算の期待値を出し、要件で決めておく
自社のストアで同じことをしたい方は、ご相談ください(ロジの相談室)。
lancers.jp
参考
- 開発者ガイド「Salesforce Commerce Tax API」 developer.salesforce.com
- ヘルプ「Add a Tax Calculation Service for Custom Checkout」 help.salesforce.com
- ヘルプ「Set Up Taxes in B2B Commerce」 help.salesforce.com
- ヘルプ「Assign a Tax Policy to a Product」 help.salesforce.com
- ヘルプ「Taxing and Promotion Adjustments」 help.salesforce.com
- 開発者ガイド「B2B Commerce Checkout Flow (Aura)」 developer.salesforce.com
- ヘルプ「Create a Store」(Aura と LWR のテンプレート) help.salesforce.com
- 開発者ガイド「Available Extensions」 developer.salesforce.com
- 開発者ガイド「Write Tests for an Extension Provider」 developer.salesforce.com
- 開発者ガイド「Mock the Base Apex Class」 developer.salesforce.com
- 公式の見本: forcedotcom/commerce-extensibility (commerce/domain/tax/)
Mentions
【Salesforce】B2B コマースで日本の消費税(10%・8%)を1つのカートで分ける方法 はじめに 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 が要る - 円では、丸める単位・小数の桁・段階価格の作り方で端数がずれる。先に手計算の期待値を出し、要件で決めておく 自社のストアで同じことをしたい方は、ご相談ください(ロジの相談室)。 lancers.jp 参考 - 開発者ガイド「Salesforce Commerce Tax API」 developer.salesforce.com - ヘルプ「Add a Tax Calculation Service for Custom Checkout」 help.salesforce.com - ヘルプ「Set Up Taxes in B2B Commerce」 help.salesforce.com - ヘルプ「Assign a Tax Policy to a Product」 help.salesforce.com - ヘルプ「Taxing and Promotion Adjustments」 help.salesforce.com - 開発者ガイド「B2B Commerce Checkout Flow (Aura)」 developer.salesforce.com - ヘルプ「Create a Store」(Aura と LWR のテンプレート) help.salesforce.com - 開発者ガイド「Available Extensions」 developer.salesforce.com - 開発者ガイド「Write Tests for an Extension Provider」 developer.salesforce.com - 開発者ガイド「Mock the Base Apex Class」 developer.salesforce.com - 公式の見本: forcedotcom/commerce-extensibility (commerce/domain/tax/)