コンテンツにスキップ

配送計算プラグイン(ShippingCalculatorPlugin

概要

Ritsubi の都道府県別配送料金と直送出荷サーチャージを計算する Vendure カスタムプラグイン。ProvinceDeliveryProfileShippingMethod × 都道府県)を SSOT として送料と地域差分の配送日数を解決します。

  • 実装パス: packages/plugins/src/rule-engine/shipping/
  • 有効化箇所: apps/vendure-server/src/config/plugins.ts
  • 実装名: ShippingCalculatorPlugin

2026-05 撤去: 旧 ZoneShippingRule(キャリア × 配送ゾーン 9 区分)は都道府県別 profile に一本化したため廃止しました。Dashboard の 配送料ゾーン設定 画面 / zoneShippingRules GraphQL / ritsubi_zone_shipping_rule テーブルは削除済みです。 配送日数は配送方法ごとの deliveryDateLeadDays に、同じ配送方法へ紐づく都道府県別 leadDaysOffset を加算します。

出荷方式と配送方法

この機能では、ShippingModeShippingMethod を別概念として扱います。

  • ShippingMode: 注文全体にかかる業務モード。NORMAL / DIRECT の 2 値を持ち、価格計算、商品可否、決済制約、受注番号、外部連携の分岐に使います。
  • ShippingMethod: Vendure 標準の配送方法。チェックアウトで選ぶ配送ラインの候補であり、配送見積もりや shippingLines の確定に使います。

直送は「配送方法の一種」ではなく「注文全体の業務モード」であるため、ShippingMethod に全面的に寄せず、Order.customFields.shippingMode を正本として扱います。

出荷方式 (ShippingMode)

出荷方式は 通常出荷直送出荷 の固定2種類です。顧客が直送出荷を選択した場合、注文内容に対して一定のサーチャージを加算します。管理画面から方式自体を増減・編集することはありません。

1. 固定定義

  • standard: 通常出荷(リツビから発送)
  • direct: 直送出荷(メーカーからエンドユーザーへ直接配送、+10%加算)

固定定義の参照元は packages/plugins/src/shared/shipping.ts です。shippingModes / shippingMode(id) Query はこの固定定義を read-only で返し、注文への反映は setOrderShippingMode(mode) で行います。

2. 計算ロジック

正本は 価格・送料計算マトリクス (SSOT) の U4 ステージ。本節は概要のみ。

直送加算は 配送料 ではなく 商品単価 に乗る。式は finalUnitPrice = max(finalUnitPrice, originalUnitPrice × (1 + 加算率))。実装は commercial-rule-simulation.ts:739-755

送料は U4 と独立にこのファイルが扱う「都道府県別配送料 + 送料無料閾値」のみで決まる。shipping-calculator.service.tsgetOrderShippingMode() でモードを取得し、metadata.isDirectShipping フラグに反映するが、送料額の分岐には使わない。

3. ストアフロントでの表示

直送出荷が有効な場合、以下の箇所に注意文(DirectShippingNotice コンポーネント)が表示されます。

  • カート画面: カート項目の上部。
  • チェックアウト画面: 各ステップ(配送先、配送・支払、確認)のメインエリア上部。
  • 注文完了画面: 注文サマリーの上部。

技術仕様

金額単位

送料ルールや送料無料しきい値などの業務設定は円単位で保持します。一方、Vendure の shipping calculator が返す値と Shop API の Money field は Vendure Money 値です。JPY でも 900円 = 90_000 として返す必要があります。

  • 円単位 → Vendure Money 値: toVendureMoneyFromYen()
  • Vendure Money 値 → 円単位: toYenFromVendureMoney()
  • Storefront 表示: formatVendurePriceValue()

金額文脈で * 100 / / 100 を直書きせず、詳細は Vendure Money 単位運用 を参照してください。

注文と配送方法の責務分担

  • Order.customFields.shippingMode: 注文全体の出荷モードを保持する SSOT
  • Customer.customFields.allowedShippingModes: 顧客ごとに選択可能な出荷モード
  • Customer.customFields.directShippingBlocked: true の場合は直送出荷を禁止(opt-out フラグ)

ShippingMethodShippingMode を置き換えるものではなく、選択済みの ShippingMode に応じて表示候補を絞り込む下位概念として利用します。 ShippingMethod.customFieldsdeliveryCarrierCode / deliveryDateLeadDays 等の配送日時系フィールドで構成され、shippingMode カスタムフィールドは持ちません。

料金参照ロジック

  • 計算対象の ShippingMethod と配送先 provinceProvinceDeliveryProfile を解決します(ProvinceDeliveryProfileService.resolveShippingFeeYen)。
  • 配送方法別 profile が無い場合は 0 円 に丸めず例外にします。住所未入力時だけは、まだ料金を確定できない正常状態として 0 円 を返します。
  • ProvinceDeliveryProfile.provinceALL_PREFECTURES の日本語表記に限定し、誤表記は保存時に fail-fast させます。

都道府県別配送プロファイル

送料と配送日数差分は同じ都道府県別プロファイルを参照します。通常運用の正本は React Dashboard の 設定 / 都道府県別配送設定 です。この画面では先に配送方法を選び、 選択中の配送方法に対する 47 都道府県分の shippingFeeYen / leadDaysOffset を編集します。

  • shippingFeeYen: 配送方法 × 都道府県ごとの送料(円単位)。
  • leadDaysOffset: ShippingMethod.customFields.deliveryDateLeadDays に加算する地域差分。
  • Dashboard の保存先は ritsubi_province_delivery_profile です。複合ユニーク制約は (shippingMethodId, province) です。
  • migration は既存の都道府県別 profile を既存の全 ShippingMethod へ複製し、現行料金を維持します。

fail-closed 方針

  • 配送ルール取得や集計中に例外が発生した場合、計算結果を 0 円 に丸めず例外を再送出します。
  • これにより、設定不備や DB 障害がそのまま「送料無料」へ倒れる fail-open を防ぎます。
  • shippingAddress 未設定のような正常系の未入力状態だけは、従来どおり 0 円 を返します。

Admin API / 認可

  • provinceDeliveryProfiles(shippingMethodId) / provinceDeliveryProfile(shippingMethodId, province)ReadSettings 権限で一覧 / 単体取得。
  • upsertProvinceDeliveryProfile(input) / deleteProvinceDeliveryProfile(id)UpdateSettings 権限で作成 / 更新 / 削除。
  • 複合ユニーク制約は (shippingMethodId, province)

API エクステンション

Query / Mutation

  • shippingModes: 固定の出荷方式一覧を取得
  • shippingMode(id): 固定の出荷方式定義を取得
  • setOrderShippingMode(mode): アクティブな注文の出荷方式を更新

送料無料進捗サービス(FreeShippingProgressService

packages/plugins/src/rule-engine/shipping/services/free-shipping-progress.service.ts で定義。Shop API の freeShippingProgress Query を経由して Storefront の送料無料バナーに残額を返す。

返却値 (FreeShippingProgress)

フィールド 説明
qualified boolean 送料無料閾値を超えているか
thresholdExTax number 送料無料閾値(Vendure Money、税抜き)
subtotalExTax number 現在の注文小計(Vendure Money、税抜き)
remainingExTax number 残額(Vendure Money、税抜き)
remainingDisplayWithTax number 表示用残額(Vendure Money、税込み 10% 加算)
shippingMode string 注文の出荷方式(NORMAL / DIRECT

税込み残額の算出方法(重要)

remainingDisplayWithTax固定 10% 乗算 で算出する。

// 整数算術で float 乗算の精度誤差を防ぐ
const applyStandardTaxMultiplier = (exTaxMoney: number): number =>
  Math.round((exTaxMoney * 11) / 10);

1.1 の float 乗算を使わない理由(dogfood ISSUE-016):

1_600_000 * 1.1 は JavaScript の IEEE 754 浮動小数点演算で 1_760_000.0000000002 を返す。
これを Math.ceil で円単位に丸めると +1 円 ぶれ、Storefront 上でカートとチェックアウトの
残額が異なって見える問題が発生した。

また order.subTotal / subTotalWithTax の比から動的に税率を導出する方式も、
line-item の丸め累積と promotion adjustment で render ごとに数円ぶれる原因になるため廃止。
残額表示の精度より一貫性を優先し、閾値設定と同じ固定 10% を正本とする。

閾値の決定

顧客の customerStatus と請求先 (billingParent) の customerStatus を参照し、
@ritsubi/domainFREE_SHIPPING_THRESHOLDS から閾値を解決する。
匿名・未設定の場合は FREE_SHIPPING_THRESHOLDS.DEFAULT を使用する。

開発とテスト

単体テスト

送料計算プラグインの主要ロジックは以下のコマンドで検証できます。

pnpm -C packages/plugins exec vitest run src/rule-engine/shipping

E2Eテスト (検証用)

VITE_PUBLIC_STOREFRONT_API_MOCKING=enabled pnpm exec nx run ritsubi-storefront:test:e2e -- tests/e2e/direct-shipping-notice.spec.ts

運用上の注意

出荷方式はマスタ編集ではなく、顧客別の allowedShippingModes / directShippingBlocked と注文の shippingMode custom field で運用します。新しいモードを追加する場合は、UI だけでなく価格計算・商品可否判定・配送方法の絞り込み・決済制約・SMILE連携まで含めて仕様変更してください。