価格計算設計
概要
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開発チーム