コンテンツにスキップ

価格計算設計

概要

EC-SPOKEシステムにおける商品価格の計算・表示に関する設計仕様です。税率対応、端数処理、多通貨対応を考慮した価格管理システムの詳細を定義します。

基本方針

価格登録方式

  • 登録方式: 税抜価格で登録(管理画面)
  • 表示方式: 税込価格で表示(フロントエンド)
  • 内部計算: 税抜ベースで統一

データベース保存精度

  • 保存精度: DECIMAL(10, 3)(小数点以下3桁)
  • 理由: 税率50%まで対応、受注時の逆算、フロント表示での整合性を保証

端数処理方式

対応方式

方式 説明 適用場面
四捨五入 0.5以上を切り上げ 一般的な処理
切り上げ 0.1以上を切り上げ 保守的な価格設定
切り捨て 小数点以下を切り捨て 顧客有利な価格設定

端数処理の設定

  • 管理画面で選択可能: システム全体の端数処理方式を設定
  • 商品別設定: 将来的に商品ごとの個別設定も検討

税率対応

対応税率範囲

  • 実用税率: 27%まで対応(世界最高税率:ハンガリー27%)
  • 日本の税率: 10%(標準)、8%(軽減税率)
  • 設計精度: DECIMAL(10, 3)で27%まで十分対応可能

個別税率設定

商品バリエーションレベルでの税率設定

-- 税率マスター
CREATE TABLE tax_rates (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL COMMENT '税率名(消費税10%、軽減税率8%等)',
    rate DECIMAL(5, 2) NOT NULL COMMENT '税率(パーセンテージ)',
    applies_from TIMESTAMP NOT NULL COMMENT '適用開始日時',
    applies_until TIMESTAMP NULL DEFAULT NULL COMMENT '適用終了日時(NULLの場合は無期限)',
    sort_order INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '表示順序',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL DEFAULT NULL COMMENT '削除日時(Soft Delete)'
);

-- 商品バリエーションに税率IDを追加
ALTER TABLE product_vars
ADD COLUMN tax_rate_id BIGINT UNSIGNED NULL COMMENT '税率ID(tax_rates.id、NULLの場合はデフォルト税率)',
ADD CONSTRAINT fk_product_vars_tax_rate
    FOREIGN KEY (tax_rate_id) REFERENCES tax_rates(id)
    ON DELETE SET NULL ON UPDATE RESTRICT;

初期税率データ

INSERT INTO tax_rates (name, rate, applies_from, sort_order) VALUES
('消費税10%', 10.00, '2019-10-01 00:00:00', 1),
('軽減税率8%', 8.00, '2019-10-01 00:00:00', 2);

税率の適用

-- 一般商品(消費税10%)
UPDATE product_vars SET tax_rate_id = 1 WHERE id = 1;

-- 食品(軽減税率8%)
UPDATE product_vars SET tax_rate_id = 2 WHERE id = 10;

-- デフォルト税率を使用
UPDATE product_vars SET tax_rate_id = NULL WHERE id = 100;

価格計算フロー

1. 商品登録時(管理画面)

1. 税抜価格を入力(例:100.000円)
2. 税率を選択(例:10%)
3. 税込価格を計算・表示(参考値)
   - 計算:100.000 × 1.10 = 110.000円
   - 表示:110円(四捨五入)

2. 受注時(フロントエンド)

1. 顧客が税込価格で注文(例:110円)
2. システムが税抜に逆算
   - 計算:110 ÷ 1.10 = 100.000円
3. 内部で税抜価格を保存(100.000円)
4. 小計計算時は税抜ベースで計算

3. フロント表示

1. 税込単価を計算
   - 計算:100.000 × 1.10 = 110.000円
2. 表示用にフォーマット
   - 日本円:110円(小数点以下2桁)
   - 米ドル:$1.1000(小数点以下4桁)

精度要件の根拠

実用税率での検証

日本の税率(10%)、個数1,000個

税込価格:110円
税率:10%
個数:1,000個

逆算:110 ÷ 1.10 = 100.000円
保存桁数 税抜単価 税抜小計(×1,000) 税込小計(×1.10、切り捨て) 期待値 一致?
2桁 100.00円 100,000円 110,000円 110,000円
3桁 100.000円 100,000円 110,000円 110,000円

世界最高税率(27%)、個数1,000個

税込価格:127円
税率:27%
個数:1,000個

逆算:127 ÷ 1.27 = 100.000円
保存桁数 税抜単価 税抜小計(×1,000) 税込小計(×1.27、切り捨て) 期待値 一致?
2桁 100.00円 100,000円 127,000円 127,000円
3桁 100.000円 100,000円 127,000円 127,000円

結論: 実用税率(27%まで)では3桁で十分な精度を確保

多通貨対応

通貨別表示精度

通貨タイプ 最小単位 表示桁数
日本円(JPY) 1円 2桁 1,000.00円
米ドル(USD) 1セント 4桁 $10.0000
ユーロ(EUR) 1セント 4桁 €10.0000
韓国ウォン(KRW) 1ウォン 2桁 ₩10,000.00

表示ロジック

<?php

declare(strict_types=1);

namespace App\Services;

final class PriceFormatter
{
    /**
     * 通貨に応じた表示桁数を取得
     */
    private function getDisplayDecimals(string $currency): int
    {
        return match ($currency) {
            'JPY', 'KRW', 'VND' => 2,  // 整数通貨は2桁表示
            'USD', 'EUR', 'GBP', 'CNY' => 4,  // セント通貨は4桁表示
            default => 3,  // その他は3桁
        };
    }

    /**
     * 通貨に応じた小数点以下の桁数で表示用にフォーマット
     */
    public function formatPrice(
        float|string $price,
        string $currency,
        string $locale = 'ja_JP'
    ): string {
        $decimals = $this->getDisplayDecimals($currency);

        return number_format(
            (float) $price,
            $decimals,
            '.',
            ','
        );
    }
}

データベース設計

商品価格テーブル

CREATE TABLE product_vars (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    product_id BIGINT UNSIGNED NOT NULL,

    -- 価格(税抜、DECIMAL(10,3)で統一保存)
    cost_price DECIMAL(10, 3) NULL COMMENT '仕入価格(非公開・税抜)',
    regular_price DECIMAL(10, 3) NOT NULL COMMENT '通常価格(定価・税抜)',
    price DECIMAL(10, 3) NOT NULL COMMENT '販売価格(通常時の実売・税抜)',
    special_price DECIMAL(10, 3) NULL COMMENT 'セール・特別価格(税抜)',
    member_price DECIMAL(10, 3) NULL COMMENT '会員向け価格(税抜)',

    -- 通貨(基軸通貨で保存)
    currency VARCHAR(3) NOT NULL DEFAULT 'JPY',

    -- その他
    stock INT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);

システム設定テーブル

CREATE TABLE system_settings (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    setting_key VARCHAR(100) UNIQUE NOT NULL,
    setting_value TEXT NOT NULL,
    description VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 初期データ
INSERT INTO system_settings (setting_key, setting_value, description) VALUES
('price_registration_type', '0', '価格登録方式(0:税抜, 1:税込)'),
('tax_rounding_mode', 'round', '端数処理方法(round:四捨五入, ceil:切り上げ, floor:切り捨て)'),
('default_tax_rate', '10.00', 'デフォルト税率(%)');

実装例

税込価格計算サービス

<?php

declare(strict_types=1);

namespace App\Services;

use InvalidArgumentException;

final class TaxCalculator
{
    /**
     * 端数処理モード
     */
    private const ROUNDING_ROUND = 'round'; // 四捨五入
    private const ROUNDING_CEIL = 'ceil';   // 切り上げ
    private const ROUNDING_FLOOR = 'floor'; // 切り捨て

    public function __construct(
        private readonly string $roundingMode = self::ROUNDING_ROUND,
        private readonly int $precision = 3, // 内部計算精度
    ) {
        if (!in_array($roundingMode, [self::ROUNDING_ROUND, self::ROUNDING_CEIL, self::ROUNDING_FLOOR], true)) {
            throw new InvalidArgumentException("Invalid rounding mode: {$roundingMode}");
        }
    }

    /**
     * 税込価格を計算(税抜価格から)
     */
    public function calculateIncludingTax(float|string $priceExcludingTax, float $taxRate): float
    {
        $price = (float) $priceExcludingTax;
        $rate = 1 + ($taxRate / 100);
        $result = $price * $rate;

        return $this->applyRounding($result);
    }

    /**
     * 税抜価格を計算(税込価格から)
     */
    public function calculateExcludingTax(float|string $priceIncludingTax, float $taxRate): float
    {
        $price = (float) $priceIncludingTax;
        $rate = 1 + ($taxRate / 100);
        $result = $price / $rate;

        return $this->applyRounding($result);
    }

    /**
     * 端数処理を適用
     */
    private function applyRounding(float $value): float
    {
        return match ($this->roundingMode) {
            self::ROUNDING_CEIL => ceil($value),           // 切り上げ
            self::ROUNDING_FLOOR => floor($value),         // 切り捨て
            default => round($value, $this->precision),    // 四捨五入
        };
    }
}

価格表示サービス

<?php

declare(strict_types=1);

namespace App\Services;

final class PriceDisplayService
{
    public function __construct(
        private readonly TaxCalculator $taxCalculator,
        private readonly PriceFormatter $priceFormatter,
    ) {}

    /**
     * 商品価格を表示用にフォーマット
     */
    public function formatProductPrice(
        float $priceExcludingTax,
        float $taxRate,
        string $currency = 'JPY'
    ): array {
        // 税込価格を計算
        $priceIncludingTax = $this->taxCalculator->calculateIncludingTax(
            $priceExcludingTax,
            $taxRate
        );

        return [
            'excluding_tax' => $this->priceFormatter->formatPrice($priceExcludingTax, $currency),
            'including_tax' => $this->priceFormatter->formatPrice($priceIncludingTax, $currency),
            'raw_excluding_tax' => $priceExcludingTax,
            'raw_including_tax' => $priceIncludingTax,
        ];
    }
}

注意事項

1. 表示と内部計算の分離

  • 表示価格: 通貨に応じた桁数でフォーマット
  • 内部計算: 常に3桁精度で計算
  • 整合性: 表示価格と内部計算結果の整合性を保つ

2. 受注時の逆算

  • 税込価格が正: 顧客が入力した税込価格を基準とする
  • 逆算精度: 3桁精度で逆算し、再度計算時に整合性を保つ
  • 端数処理: 逆算後の再計算でも同じ端数処理を適用

3. パフォーマンス考慮

  • 計算キャッシュ: 頻繁にアクセスされる価格はキャッシュ化
  • バッチ処理: 大量の価格計算はバッチ処理で実行
  • インデックス: 価格範囲検索用のインデックスを適切に設定

将来の拡張

1. 動的税率

  • 商品別税率: 商品ごとに異なる税率を設定
  • 期間限定税率: 特定期間のみ適用される税率
  • 地域別税率: 配送先に応じた税率

2. 複雑な価格体系

  • 段階価格: 数量に応じた価格設定
  • 会員価格: 会員ランク別の価格設定
  • 時間帯価格: 時間帯に応じた価格設定

3. 為替レート対応

  • リアルタイム為替: 外部APIから為替レートを取得
  • 為替レート履歴: 過去の為替レートを保存
  • 為替レート予測: 機械学習による為替レート予測

更新日: 2024-01-XX バージョン: 1.0 作成者: EC-SPOKE開発チーム