価格システムプラグイン (Pricing System Plugin)¶
概要¶
Ritsubi B2B ECサイトの複雑な価格制御要件に対応するVendureカスタムプラグイン。顧客別掛率(割引率)管理、月次割戻金計算、段階的特別掛率適用など、B2B特有の価格システムを実現します。
互換性レンジ¶
- Vendure: 3.5.x(apps/vendure-server は ^3.5.1 を使用)
- Node.js: >=22.11.0(apps/vendure-server/package.json の engines に準拠)
パッケージ情報¶
- パッケージ名:
@ritsubi/pricing-system-plugin - パス:
/packages/plugins/pricing-system - エントリーポイント:
src/index.ts
実装機能¶
1. 顧客別掛率(割引率)管理¶
各顧客に個別の掛率(割引率)を設定し、商品価格を動的に計算。
掛率の種類¶
- 基本掛率: 顧客ステータスに基づく標準割引率(例: 0.70 = 30%割引)
- 商品別掛率: 特定商品カテゴリに対する個別掛率
- 期間限定掛率: キャンペーン期間中の特別掛率
- 数量別掛率: 購入数量に応じた段階的掛率
価格計算式¶
最終価格 = 定価 × 基本掛率 × (1 - 追加割引率)
2. 月次割戻金計算(エクスビアンス商品)¶
エクスビアンス商品の月次購入実績に基づく割戻金を自動計算。
割戻金計算ロジック¶
月次購入金額 = 当月のエクスビアンス商品購入合計
割戻金率 = 段階的に決定(購入金額に応じて変動)
割戻金額 = 月次購入金額 × 割戻金率
割戻金の段階設定例¶
| 月次購入金額 | 割戻金率 |
|---|---|
| 〜50万円 | 0% |
| 50〜100万円 | 2% |
| 100〜200万円 | 3% |
| 200万円〜 | 5% |
3. 段階的特別掛率適用¶
複数商品群の数量に応じて、段階的に特別掛率を適用。
適用条件¶
- 商品群A: 10個以上購入で追加5%割引
- 商品群B: 20個以上購入で追加10%割引
- 組み合わせ: 複数商品群の合計数量でも計算可能
4. 「特に安価な顧客」判定ロジック¶
すでに低価格で購入している顧客(掛率70%以下)は、追加割引の対象外とする制御。
if (顧客の基本掛率 <= 0.7) {
// 追加割引キャンペーン適用不可
return false;
}
5. 年次割戻金サマリー¶
月次割戻金の年間合計を集計し、年次報告書として出力。
技術仕様¶
プラグイン設定オプション¶
export interface PricingSystemPluginOptions {
/**
* 顧客別価格制御を有効にする
*/
enableCustomerPricing?: boolean;
/**
* 月次割戻金計算を有効にする
*/
enableMonthlyRebate?: boolean;
/**
* 段階的割引制御を有効にする
*/
enableTieredDiscount?: boolean;
/**
* 割戻金計算日
* 'last' = 月末, 数値 = 特定日
*/
rebateCalculationDay?: 'last' | number;
/**
* 割戻金支払日
*/
rebatePaymentDay?: number;
/**
* 「特に安価な顧客」判定閾値
* この掛率以下の顧客は追加割引対象外
*/
lowPriceCustomerThreshold?: number;
}
プラグイン初期化¶
import { PricingSystemPlugin } from '@ritsubi/pricing-system-plugin';
export const config: VendureConfig = {
plugins: [
PricingSystemPlugin.init({
enableCustomerPricing: true,
enableMonthlyRebate: true,
enableTieredDiscount: true,
rebateCalculationDay: 'last', // 月末計算
rebatePaymentDay: 15, // 翌月15日支払い
lowPriceCustomerThreshold: 0.7, // 70%以下は「特に安価な顧客」
}),
],
};
エンティティ¶
CustomerPricingEntity¶
顧客別価格設定を管理するエンティティ。
@Entity()
export class CustomerPricingEntity extends VendureEntity {
@Column()
customerId: string;
@Column('decimal', { precision: 5, scale: 2 })
baseDiscountRate: number; // 基本掛率
@Column('jsonb', { nullable: true })
categorySpecificRates: Record<string, number>; // カテゴリ別掛率
@Column('jsonb', { nullable: true })
tieredDiscounts: TieredDiscount[]; // 段階的割引設定
@Column()
effectiveFrom: Date;
@Column({ nullable: true })
effectiveTo: Date;
}
MonthlyRebateEntity¶
月次割戻金の計算結果を記録するエンティティ。
@Entity()
export class MonthlyRebateEntity extends VendureEntity {
@Column()
customerId: string;
@Column()
calculationMonth: string; // YYYY-MM形式
@Column('decimal', { precision: 12, scale: 2 })
totalPurchaseAmount: number; // 月次購入金額
@Column('decimal', { precision: 5, scale: 2 })
rebateRate: number; // 適用された割戻金率
@Column('decimal', { precision: 12, scale: 2 })
rebateAmount: number; // 割戻金額
@Column('enum', { enum: ['CALCULATED', 'APPROVED', 'PAID'] })
status: string;
@Column()
calculatedAt: Date;
@Column({ nullable: true })
paidAt: Date;
}
サービス¶
CustomerPricingService¶
顧客別価格設定の管理サービス。
主要メソッド:
getCustomerPricing(customerId: ID): Promise<CustomerPricingEntity>setBaseDiscountRate(customerId: ID, rate: number): Promise<void>setCategorySpecificRate(customerId: ID, categoryId: ID, rate: number): Promise<void>getEffectiveDiscountRate(customerId: ID, productId: ID, quantity: number): Promise<number>
PriceCalculationService¶
価格計算ロジックを担当するサービス。
主要メソッド:
calculatePrice(productId: ID, customerId: ID, quantity: number): Promise<number>calculateTieredDiscount(customerId: ID, items: OrderItem[]): Promise<number>applyLowPriceCustomerRule(customerId: ID, additionalDiscount: number): Promise<number>
MonthlyRebateService¶
月次割戻金の計算と管理を行うサービス。
主要メソッド:
calculateMonthlyRebate(customerId: ID, month: string): Promise<MonthlyRebateEntity>approveRebate(rebateId: ID): Promise<void>markAsPaid(rebateId: ID): Promise<void>getAnnualRebateSummary(customerId: ID, year: number): Promise<RebateSummary>
対応する要件¶
要件定義書との対応¶
- 3.4.1 顧客別価格制御・月次割戻金システム: 掛率管理と割戻金計算
- 複数商品群の数量ベース割引制御: 段階的特別掛率適用
- 年次割戻金サマリー: 年間集計機能
使用例¶
フロントエンド(Next.js)での使用¶
// 顧客別価格を取得
const { data } = await apolloClient.query({
query: GET_PRODUCT_PRICE,
variables: {
productId: 'exuviance-moisturizer',
customerId: currentUser.id,
quantity: 10,
},
});
console.log(`定価: ${data.listPrice}`);
console.log(`掛率適用後: ${data.customerPrice}`);
console.log(`段階的割引: ${data.tieredDiscount}`);
console.log(`最終価格: ${data.finalPrice}`);
月次割戻金の確認¶
// 顧客の月次割戻金履歴を取得
const { data } = await apolloClient.query({
query: GET_MONTHLY_REBATES,
variables: {
customerId: currentUser.id,
year: 2025,
},
});
data.monthlyRebates.forEach(rebate => {
console.log(`${rebate.month}: ${rebate.rebateAmount}円`);
});
管理画面での使用¶
// 顧客の掛率を設定
await customerPricingService.setBaseDiscountRate('customer123', 0.75);
// 月次割戻金バッチ計算
await monthlyRebateService.calculateAllMonthlyRebates('2025-10');
// 割戻金承認
await monthlyRebateService.approveRebate('rebate456');
バッチ処理¶
月次割戻金計算ジョブ¶
毎月末に自動実行されるバッチ処理。
@Cron('0 2 1 * *') // 毎月1日 午前2時実行
async calculateMonthlyRebates() {
const lastMonth = moment().subtract(1, 'month').format('YYYY-MM');
const customers = await this.customerService.findAll();
for (const customer of customers) {
await this.monthlyRebateService.calculateMonthlyRebate(
customer.id,
lastMonth
);
}
}
データモデル¶
カスタムフィールド¶
Customer.baseDiscountRate- 基本掛率Customer.specialPricingTier- 特別価格ティアProductVariant.listPrice- 定価OrderLine.appliedDiscountRate- 適用された掛率OrderLine.rebateEligible- 割戻金対象フラグ
セキュリティ考慮事項¶
- 価格情報の秘匿: 顧客別価格は他の顧客には非公開
- 掛率変更権限: 掛率変更は管理者のみ可能
- 割戻金承認フロー: 自動計算後、管理者承認が必要
- 価格履歴の保存: すべての価格変更履歴を記録
パフォーマンス最適化¶
- 価格キャッシング: 計算済み価格をRedisにキャッシュ(5分TTL)
- バッチ計算: 大量の価格計算はバックグラウンドで処理
- インデックス最適化: 顧客IDと商品IDの複合インデックス作成
トラブルシューティング¶
よくある問題¶
問題: 価格が正しく計算されない
- 原因: 掛率設定が有効期間外
- 解決:
effectiveFromとeffectiveToを確認
問題: 割戻金が計算されない
- 原因: エクスビアンス商品のタグ付けが不足
- 解決: 商品に
exuvianceタグを追加
関連ドキュメント¶
- 顧客管理プラグイン - 顧客ステータスと連携
- キャンペーンエンジンプラグイン - 追加割引キャンペーン
- 価格計算アルゴリズム
今後の拡張予定¶
- AI予測による最適掛率提案
- リアルタイム割戻金シミュレーション
- 動的な割戻金率調整機能
- 複数ティアの自動昇格システム