コンテンツにスキップ

価格システムプラグイン (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 - 割戻金対象フラグ

セキュリティ考慮事項

  1. 価格情報の秘匿: 顧客別価格は他の顧客には非公開
  2. 掛率変更権限: 掛率変更は管理者のみ可能
  3. 割戻金承認フロー: 自動計算後、管理者承認が必要
  4. 価格履歴の保存: すべての価格変更履歴を記録

パフォーマンス最適化

  • 価格キャッシング: 計算済み価格をRedisにキャッシュ(5分TTL)
  • バッチ計算: 大量の価格計算はバックグラウンドで処理
  • インデックス最適化: 顧客IDと商品IDの複合インデックス作成

トラブルシューティング

よくある問題

問題: 価格が正しく計算されない

  • 原因: 掛率設定が有効期間外
  • 解決: effectiveFromeffectiveTo を確認

問題: 割戻金が計算されない

  • 原因: エクスビアンス商品のタグ付けが不足
  • 解決: 商品に exuviance タグを追加

関連ドキュメント

今後の拡張予定

  • AI予測による最適掛率提案
  • リアルタイム割戻金シミュレーション
  • 動的な割戻金率調整機能
  • 複数ティアの自動昇格システム