forcedotcom

Activity

Stars and mentions · Last 30 days, Oct 5 – Nov 3

GitHub stars0
Mentions3
01234Oct 5Oct 19Nov 3
UTC days · repositories forcedotcom owns0 repositories

Top repositories

forcedotcom’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside forcedotcom’s account where forcedotcom opened or reviewed pull requests in the past 365 days.

Who noticed forcedotcom

Everyone who starred or forked forcedotcom’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

3posts
Nov 40 posts
  1. 【Salesforce】日本の B2B コマースでカード払いをつなぐ — Salesforce Payments が使えないので、決済会社のアダプタを Apex で作る はじめに B2B コマース(拡張 LWR のストア)でカード払いを付けたい。公式の手順を読むと、まず出てくるのは Salesforce Payments(Stripe・Adyen を Salesforce 経由で使う仕組み)です。 ところがヘルプの「Salesforce Payments」には、次の注記があります。 > ブラジル・インド・日本で Salesforce と契約しているお客様は、Salesforce Payments を使えない(要旨) 日本の会社が日本の Salesforce と契約しているなら、この道は使えません。残る道は 「支払いのゲートウェイのアダプタ」を Apex で作り、決済会社の API に自分でつなぐ ことです。 この記事では、Stripe のテスト環境を相手に、アダプタを作ってストアにつなぎ、与信が通れば注文になり、断られれば注文が止まる ところまでを通した手順と、途中のつまずき2つをまとめます。 対象読者 - 日本で B2B コマースのストアにカード払いを付けたい管理者・開発者 - Salesforce Payments が使えないと分かって、次の手を探している方 対象外 - 売上の確定(キャプチャ)・返金・3D セキュア。この記事のアダプタは 与信だけ です - B2C コマース(SFCC) - 日本の決済代行会社ごとの API の違い(Stripe でしか確かめていません) 検証の環境 Developer Edition から作ったスクラッチ org(通貨は円)に、B2B コマースのストア(拡張 LWR)と購入者のユーザーを用意しました。決済の相手は Stripe のテスト環境(サンドボックス。お金は動きません)です。設定は Salesforce CLI で反映し、Claude Code を道具に使って「通るはず」「止まるはず」を先に書いてから結果と突き合わせました。カード番号と注文のボタンは人が画面で入れています。 全体の形 部品は5つです。上から順に作ります。 | # | 部品 | 役目 | 作り方 | |---|---|---|---| | 1 | Apex のクラス(commercepayments.PaymentGatewayAdapter) | チェックアウトから呼ばれ、決済会社の API を呼ぶ | メタデータ | | 2 | 外部資格情報 + 名前付き資格情報 | 決済会社の URL とキーを、コードの外に置く | メタデータ + キーは API で | | 3 | 権限セット | 資格情報を使える人を決める | メタデータ | | 4 | PaymentGatewayProvider | 「このアダプタを使う決済の提供元」の登録 | メタデータ | | 5 | PaymentGateway と StoreIntegratedService | 提供元 + 資格情報をまとめ、ストアにつなぐ | レコードの作成 | チェックアウトで呼ばれるのは Tokenize(カードをトークンに)→ Authorize(与信) の2回です。注文は与信が通ったあとに作られます。 1. アダプタ(Apex) 要るところだけ抜き出します(全体で約110行。テストクラスは別)。 global class SampleStripeAdapter implements commercepayments.PaymentGatewayAdapter { global commercepayments.GatewayResponse processRequest(commercepayments.PaymentGatewayContext ctx) { commercepayments.RequestType t = ctx.getPaymentRequestType(); try { if (t == commercepayments.RequestType.Tokenize) { return tokenize((commercepayments.PaymentMethodTokenizationRequest) ctx.getPaymentRequest()); } if (t == commercepayments.RequestType.Authorize) { return authorize((commercepayments.AuthorizationRequest) ctx.getPaymentRequest()); } } catch (Exception e) { // 例外の中身は購入者に出さない。管理者が読む記録に、種類と行だけ残す return new commercepayments.GatewayErrorResponse('500', e.getTypeName() + ' line ' + e.getLineNumber()); } return new commercepayments.GatewayErrorResponse('400', 'not supported ' + t); } commercepayments.GatewayResponse authorize(commercepayments.AuthorizationRequest req) { // Tokenize で返したトークンは、カードの支払い方法(CardPaymentMethod)の GatewayToken に入っている String pm = null; if (req.paymentMethod != null && req.paymentMethod.id != null) { for (CardPaymentMethod m : [SELECT GatewayToken FROM CardPaymentMethod WHERE Id = :req.paymentMethod.id]) { pm = m.GatewayToken; } } String cur = req.currencyIsoCode == null ? 'jpy' : req.currencyIsoCode.toLowerCase(); String body = 'amount=' + toMinor(Decimal.valueOf(req.amount), cur) + '&currency=' + cur + '&payment_method=' + pm + '&confirm=true&capture_method=manual' + '&automatic_payment_methods[enabled]=true&automatic_payment_methods[allow_redirects]=never'; Map<String, Object> r = send('/v1/payment_intents', body); commercepayments.AuthorizationResponse res = new commercepayments.AuthorizationResponse(); res.setGatewayDate(System.now()); res.setAmount(req.amount); Map<String, Object> err = (Map<String, Object>) r.get('error'); if (err == null && r.get('status') == 'requires_capture') { res.setGatewayReferenceNumber((String) r.get('id')); res.setGatewayResultCode('requires_capture'); res.setSalesforceResultCodeInfo(new commercepayments.SalesforceResultCodeInfo( commercepayments.SalesforceResultCode.Success)); } else { // 断られた・追加の認証(3D セキュア)が要る、はどちらも断りとして返す res.setGatewayResultCode(err == null ? String.valueOf(r.get('status')) : String.valueOf(err.get('code'))); res.setSalesforceResultCodeInfo(new commercepayments.SalesforceResultCodeInfo( commercepayments.SalesforceResultCode.Decline)); } return res; } // Stripe の amount は最小の単位。円は小数なし(5690 = ¥5,690)、ドルは 100 倍 public static Long toMinor(Decimal a, String cur) { Set<String> zeroDecimal = new Set<String>{ 'jpy', 'krw', 'vnd' }; return (zeroDecimal.contains(cur) ? a : a * 100).setScale(0, RoundingMode.HALF_UP).longValue(); } // 宛先は '/v1/...' の後半だけを渡す(下の「つまずき1」) static Map<String, Object> send(String path, String body) { HttpRequest h = new HttpRequest(); h.setEndpoint(path); h.setMethod('POST'); h.setHeader('Content-Type', 'application/x-www-form-urlencoded'); h.setBody(body); HttpResponse resp = new commercepayments.PaymentsHttp().send(h); return (Map<String, Object>) JSON.deserializeUntyped(resp.getBody()); } } ポイントは3つです。 - 与信だけ: capture_method=manual で、Stripe の状態は requires_capture(枠を押さえただけ)になります。Salesforce に Success を返すのはこのときだけにしました - 円は小数なし: Stripe の amount は最小の単位です。円は 5690 が ¥5,690、ドルは 100 倍。ここを取り違えると 100 倍の与信になります。テストクラスで「3,360円 → 3360」「33.6ドル → 3360」を先に確かめました - 呼び出しは commercepayments.PaymentsHttp: 普通の Http ではなく、支払い用のこのクラスで送ります。どの名前付き資格情報を使うかは、あとで作る PaymentGateway の設定で決まります Tokenize について この検証では、Tokenize で Stripe の テスト用の払い方(pm_card_visa。断りを試すときは pm_card_visa_chargeDeclined)を返しました。画面で入れたカード番号は Stripe に送っていません。理由は「確かめていないこと」の節に書きます。 2. 外部資格情報・名前付き資格情報 キーはコードにもメタデータにも書きません。外部資格情報(Custom)で「Authorization の見出しに Bearer + キー」を作り、名前付き資格情報から参照します。 <!-- externalCredentials/SampleStripeExt.externalCredential-meta.xml --> <ExternalCredential xmlns="http://soap.sforce.com/2006/04/metadata"> <authenticationProtocol>Custom</authenticationProtocol> <externalCredentialParameters> <parameterName>SamplePrincipal</parameterName> <parameterType>NamedPrincipal</parameterType> <sequenceNumber>1</sequenceNumber> </externalCredentialParameters> <externalCredentialParameters> <parameterName>Authorization</parameterName> <parameterType>AuthHeader</parameterType> <parameterValue>{!&apos;Bearer &apos; &amp; $Credential.SampleStripeExt.SecretKey}</parameterValue> <sequenceNumber>1</sequenceNumber> </externalCredentialParameters> <label>Sample Stripe (test key)</label> </ExternalCredential> <!-- namedCredentials/SampleStripe.namedCredential-meta.xml --> <NamedCredential xmlns="http://soap.sforce.com/2006/04/metadata"> <allowMergeFieldsInBody>true</allowMergeFieldsInBody> <allowMergeFieldsInHeader>true</allowMergeFieldsInHeader> <calloutStatus>Enabled</calloutStatus> <generateAuthorizationHeader>false</generateAuthorizationHeader> <label>Sample Stripe</label> <namedCredentialParameters> <parameterName>Url</parameterName> <parameterType>Url</parameterType> <parameterValue>https://api.stripe.com</parameterValue> </namedCredentialParameters> <namedCredentialParameters> <externalCredential>SampleStripeExt</externalCredential> <parameterName>ExternalCredential</parameterName> <parameterType>Authentication</parameterType> </namedCredentialParameters> <namedCredentialType>SecuredEndpoint</namedCredentialType> </NamedCredential> キーの値(SecretKey)は、反映のあとに Connect API で入れました。値がコマンドの履歴に残らないよう、標準入力で渡しています。 printf '{"externalCredential":"SampleStripeExt","principalName":"SamplePrincipal","principalType":"NamedPrincipal","credentials":{"SecretKey":{"value":"%s","encrypted":true}}}' "$KEY" \ | sf api request rest /services/data/v65.0/named-credentials/credential --method POST --body - -o <org> Stripe のキーは、テスト環境の 制限付きキー(rk_test_) を使いました。新しい Stripe のダッシュボードでは、シークレットキーは最初から用意されておらず、「シークレットキーを作成」から作ります。 3. 権限セット 外部資格情報は、プリンシパルを権限セットで付けた人しか使えません。 <!-- permissionsets/SampleStripeAccess.permissionset-meta.xml --> <PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata"> <externalCredentialPrincipalAccesses> <enabled>true</enabled> <externalCredentialPrincipal>SampleStripeExt-SamplePrincipal</externalCredentialPrincipal> </externalCredentialPrincipalAccesses> <hasActivationRequired>false</hasActivationRequired> <label>Sample Stripe Access</label> </PermissionSet> デバッグログで見ると、アダプタは購入者のユーザーとして動いていました。この検証では、管理者と購入者の両方にこの権限セットを付けてから試しています。 4. 提供元・ゲートウェイ・ストアへのつなぎ 提供元(PaymentGatewayProvider)はメタデータで作ります。 <!-- paymentGatewayProviders/SampleStripeProvider.paymentGatewayProvider-meta.xml --> <PaymentGatewayProvider xmlns="http://soap.sforce.com/2006/04/metadata"> <apexAdapter>SampleStripeAdapter</apexAdapter> <comments>Stripe test mode (authorize only)</comments> <idempotencySupported>Yes</idempotencySupported> <masterLabel>Sample Stripe Provider</masterLabel> </PaymentGatewayProvider> Tooling API でレコードとして作ろうとすると「You must provide a valid Metadata field」で止まりました。メタデータで反映すれば通ります。 残りの2つはレコードです。 # 提供元 + 名前付き資格情報をまとめたゲートウェイ sf data create record -s PaymentGateway -o <org> \ -v "PaymentGatewayName='Sample Stripe' MerchantCredentialId=<名前付き資格情報の Id> PaymentGatewayProviderId=<提供元の Id> Status=Active" # ストアにつなぐ sf data create record -s StoreIntegratedService -o <org> \ -v "StoreId=<ストアの Id> Integration=<ゲートウェイの Id> ServiceProviderType=Payment" つなぐと、ストアの Settings > Checkout > Payments に提供元の名前が「Active」で出て、購入者のチェックアウトの画面に「Credit Card」の入力が出ます。 結果 | # | 試したこと | 先に書いた期待 | 結果 | |---|---|---|---| | 1 | テストクラス(作り物の応答 HttpCalloutMock で5本) | 5/5 | 5/5・カバー率 93% | | 2 | Salesforce から Stripe の残高を読むだけの呼び出し | 200 | 200・livemode=false(テスト環境) | | 3 | 購入者の画面でカートの ¥5,690 を注文 | Stripe に 5690 jpy・requires_capture、注文ができる | 注文が Activated、PaymentAuthorization が Processed・5,690。Stripe のダッシュボードにも同じ参照番号で 5690 jpy・requires_capture | | 4 | 断られる払い方で ¥120 を注文 | 断り・注文は増えない・カートはそのまま | 画面は「Unable to Process Payment」。Stripe は card_declined / generic_decline、注文は増えず、カートは払い直せる状態 | 3 はつまずき2つを直して、3回目に通りました。 つまずき1: callout: を付けると「Named credential namespace part must be alphabetic」 普通の Apex の外向きの呼び出しと同じように h.setEndpoint('callout:SampleStripe/v1/payment_intents') と書くと、次の例外で止まりました。 Named credential namespace part must be alphabetic commercepayments.PaymentsHttp は、PaymentGateway に登録した名前付き資格情報を 自分で付けて 送ります。渡すのは /v1/payment_intents のような後半だけ でした。 なお、購入者の画面に出るのは「Unable to Process Payment」という決まりの文だけで、原因は分かりません。この例外は、アダプタの catch で種類と行をゲートウェイの記録に残すようにして見つけました。困ったら PaymentGatewayLog を見るのが公式の案内です。 つまずき2: Stripe が payment_method_types で 400 Stripe の古い見本にある payment_method_types[]=card を付けると、Stripe が 400 を返しました(このパラメーターは使えなくなった、という趣旨のエラー)。代わりに次の2つを付けて通りました。 automatic_payment_methods[enabled]=true automatic_payment_methods[allow_redirects]=never allow_redirects=never は、別の画面に飛ぶ払い方を外すためです。この記事のアダプタは、飛んだ先から戻ってくる処理を持っていません。 落とし穴: 画面のカード番号は、Apex には見えない 作り物のアダプタで「この番号なら断る」と分けようとしたところ、断るはずの番号でも通って 注文ができてしまいました。 デバッグログで見ると、画面で入れたカード番号は Apex に数字では届かず、{!$CREDENTIAL.…} という 差し込みの印 で届いていました。本物の番号は、名前付き資格情報を通した外向きの呼び出しの本文に入るときにだけ差し込まれます(名前付き資格情報の allowMergeFieldsInBody を true にしているのはこのためです)。 そのため、アダプタの中で番号を見て分けることはできません。テストでは、カードの名義(Decline Test なら断る)で分けました。 確かめていないこと 本番に持っていく前に、次は確かめが要ります。 - 【未確認】画面の番号を Stripe に送る形: 上の差し込みで POST /v1/payment_methods に送る作りは用意しましたが、試していません。Stripe はカード番号をそのまま受ける API を、サポートに申し込んで有効にしてもらう決まりです(テストは書類なし、本番は PCI DSS の書類が毎年要る。Stripe のサポートの記事) - 【推測】日本の決済代行会社は、カード番号(かトークン)を受ける API が前提のところが多く、上の差し込みの形がそのまま使える見込みです。会社ごとの API は確かめていません - 【未確認】購入者に権限セットを付けないと止まるか(付けた状態でしか試していません) - 【未確認】売上の確定・返金・3D セキュア。この記事のアダプタは、追加の認証が要る応答を断りとして返します - 【推測】Stripe の最小額(円は 50円)に届かない注文は、Stripe に断られる見込みです。少額の注文があるストアでは、要件で先に聞いておく点です まとめ - 日本で Salesforce と契約しているなら、Salesforce Payments は使えない(公式のヘルプの注記)。カード払いは 自作のアダプタ でつなぐ - 部品は5つ: Apex のアダプタ → 外部資格情報 + 名前付き資格情報 → 権限セット → PaymentGatewayProvider(メタデータ)→ PaymentGateway + StoreIntegratedService(レコード) - チェックアウトでするのは 与信だけ。Success を返せば注文になり、Decline を返せば注文は作られずカートが残る - PaymentsHttp の宛先は 後半だけ(callout: を付けない) - Stripe は payment_method_types ではなく automatic_payment_methods - 画面のカード番号は Apex に見えない(差し込みの印で届く)。テストで番号によって分けることはできない - 円は小数なし。amount の桁を通貨ごとにテストで押さえる 筆者は Salesforce の設定の代行・相談を受けています(連絡先は Qiita のプロフィールに)。 参考 - ヘルプ「Salesforce Payments」(日本・ブラジル・インドの注記) help.salesforce.com - Stripe「Testing」(テスト用の払い方) docs.stripe.com - Stripe「Currencies」(円は小数なし・最小額) docs.stripe.com - Stripe サポート「Enabling access to raw card data APIs」 support.stripe.com - 公式の参考実装(Stripe は無く、Payeezy などの見本) forcedotcom/Core-Payments-Reference-Gateway-Integration-Adapters

  2. 【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/)

  3. Reddit · · r/salesforce · u/thedeathmachine · forcedotcom/sf-skills

    Sales Cloud for Claude - but in MS Copilot Hi everyone - Many people at my company went to Dreamforce and saw Sales Cloud for Claude and how you can utilize Claude's interface to do things like create reports, draft emails, view at-risk accounts that need attention, etc. They want to same thing in Microsoft Teams Copilot as our enterprise does not use Claude.ai. I am trying to work with our AI team to make this happen as I have zero access to anything other than Salesforce. I tried building my own Agent in Copilot but was blocked from actually testing it. I know Salesforce has out of box MCP's we can utilize that an Copilot agent can access - however what I am also looking for are the specific Sales Cloud skills Sales Cloud for Claude is utilizing. Im browsing forcedotcom/sf-skills but all I am seeing are development related skills, nothing around Sales Cloud skills. Basically our AI team is saying before we proceed with anything we need a full list of skills the agent can access. Other than giving them a high level of what we want the agent to do, I'm not finding the skills themselves. Looking for any guidance here - I'm pretty new to all this but unfortunately I have no ability to actually get my hands on anything unless I sign up for personal accounts for Teams (which I am currently doing just to learn and experiment). Thanks

    6 points · 12 comments

Achievements

Awards earned by the repositories forcedotcom owns. Select an award to embed its badge.

Gold
11st place finishes
Silver
02nd place finishes
Bronze
03rd place finishes
Awarded repositories
1Owned by forcedotcom now

Repositories

Ordered by gold, then silver, then bronze.

  1. sfdx-travisciBest: 1st by forks, 117 forks100

Every award

Top-three finishes of these repositories in completed trending days and weeks, newest first.

  1. sfdx-travisci

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.