コンテンツにスキップ

Vendure カスタムプラグイン - 概要と設計方針

概要

Ritsubi B2B ECサイトのビジネス要件を実現するためのカスタムVendureプラグイン群を管理します。実装はモノレポの /packages/plugins/@ritsubi/plugins として解決)と apps/vendure-server/src/plugins/ に配置され、apps/vendure-server/src/vendure-config.shared.ts および apps/vendure-server/src/vendure-config.ts で有効化状況を定義しています。

プラグイン開発の基礎: プラグインの実装手順はプラグイン開発の基礎を参照してください。VendureのアーキテクチャやコアコンセプトはVendure開発ハンドブックで確認できます。

互換性レンジ(全プラグイン共通)

  • Vendure: 3.5.x(apps/vendure-server は ^3.5.1 を使用)
  • Node.js: >=22.11.0(apps/vendure-server/package.json の engines に準拠)

プラグイン一覧

一元管理テーブル(Single View)

稼働状況は apps/vendure-server/src/vendure-config.shared.tsapps/vendure-server/src/vendure-config.ts に基づく(最終確認: 2026-01-09)。

カテゴリ プラグイン コード配置 主な責務 稼働状況 関連ドキュメント
コンプライアンス 同意システム(Consent System) packages/plugins/consent-system 医薬部外品・高濃度成分の購入同意取得、署名保管、7年保持 vendure-config.shared.tsで有効(ConsentSystemPlugin 同意システム
顧客管理 顧客管理(Customer Management) packages/plugins/src/customer-management.ts 15種類の顧客ステータス・33カテゴリアクセス制御 有効(CustomerManagementPlugin.init({}) 顧客管理
顧客管理 顧客可視性(Customer Visibility) packages/plugins/src/customer-visibility.ts 顧客グループコードによる表示/決済可否制御(paymentGroupEligibilityCheckerを提供) 有効(CustomerVisibilityPlugin.init() 顧客可視性
アカウント運用 Customer Password Admin packages/plugins/src/customer-password-admin.ts 管理者によるsetCustomerPasswordByAdminミューテーション、mustChangeフラグ、発行履歴/メール通知 有効(CustomerPasswordAdminPlugin Customer Password Admin
UX お気に入り(Wishlist) packages/plugins/wishlist 商品のお気に入り登録・取得API、Storefront同期 有効(WishlistPlugin.init({}) お気に入り
レコメンド 共起レコメンド(CoPurchase Recommendation) packages/plugins/co-purchase-recommendation/src 購入共起データに基づくレコメンドAPI 有効(CoPurchaseRecommendationPlugin.init({}) 共起レコメンド
決済 SB Payment Link packages/plugins/src/sb-payment-link.ts SB Paymentリンク型決済、XML生成、コールバック処理 有効(SbPaymentLinkPlugin.init({}) SB Paymentリンク型決済
決済 決済方法ハンドラー packages/plugins/src/payment-handlers/ 売掛決済、代引き決済、銀行振込/前入金のハンドラー実装 実装ありだが未登録(configに未組み込み) 決済方法ハンドラー
プロモーション キャンペーンエンジン(Campaign Engine) packages/plugins/src/campaign-engine.ts 6種類の複合キャンペーン判定 有効(CampaignEnginePlugin.init({}) キャンペーンエンジン
プロモーション ポイント/ギフト券/クーポン(Points System) packages/plugins/points-system ギフト券登録・ポイント残高/履歴・クーポン仮想商品出力 有効(PointsSystemPlugin.init({}) ポイント/ギフト券/クーポン
連携 SMILE連携(Smile Integration) packages/plugins/src/smile-integration.ts SMILE会計システム向けCSV出力、納品先管理 有効(SmileIntegrationPlugin.init({}) SMILE連携
連携 WordPress連携(WordPress Plugin) packages/plugins/src/wordpress.ts WPGraphQL経由のコンテンツ取得 有効(WordPressPlugin.init(...) CMS連携概要 / CMS連携マッピング / CMS連携認証
ショップAPI 顧客拡張(Customer Extensions) apps/vendure-server/src/plugins/customer-extensions.plugin.ts Shop APIに顧客階層を返すcustomerHierarchyクエリを追加 有効(CustomerExtensionsPlugin 顧客拡張
ショップAPI Shop Product Variants Plugin packages/plugins/src/shop-product-variants.plugin.ts Shop APIにproductVariantsクエリを追加 未登録(vendure-config.*.ts に未追加) Shop Product Variants
検索 検索戦略(pg_trgm + 標準検索) apps/vendure-server/src/search/pg-trgm-search-strategy.ts DefaultSearchPlugin の searchStrategy を差し替え、商品名/説明/SKU(商品コード)+ nameKana/keywords を ILIKE 検索(かな/カナ揺れはクエリ展開で吸収) 有効(DefaultSearchPlugin + PgTrgmSearchStrategy 検索実装
配送 配送計算(Shipping Calculator) packages/plugins/src/shipping-calculator.ts ブランド別送料、直送+10%サーチャージ、無料閾値 実装ありだが未登録 配送計算
価格 価格システム(Pricing System) packages/plugins/src/pricing-system.ts 掛率・割戻金・段階割引ロジック 実装ありだが未登録 価格システム
在庫 在庫管理(Inventory Management) packages/plugins/src/inventory-management.ts 在庫予約、バックオーダー、購入単位制御 実装ありだが未登録 在庫管理
ダッシュボード Vendure Dashboard拡張 apps/vendure-server/src/plugins/ritsubi-admin-extensions React Dashboard向けカスタム拡張とloadedPluginsクエリ vendure-config.tsで有効(RitsubiAdminExtensionsPlugin Dashboard拡張
運用 Email Preview Plugin apps/vendure-server/src/plugins/email-preview/email-preview.plugin.ts ダッシュボードでメールテンプレートをプレビューし件名と同期 有効(EmailPreviewPlugin メールテンプレート運用
決済制御 Payment Eligibility Checkers apps/vendure-server/src/plugins/payment-eligibility/checkers.ts 顧客ステータス・金額レンジ・直送フラグによる決済可否制御 有効(paymentOptions.paymentMethodEligibilityCheckersに登録) Payment Eligibility Checkers

更新ルール: apps/vendure-server/src/vendure-config.shared.ts / vendure-config.ts のプラグイン・チェッカー登録を変更したら、この表と関連ドキュメントを必ず同期させる。

コアビジネスロジック(4プラグイン)

プラグイン 責務 主要機能
顧客管理 顧客ステータスとアクセス制御 15種類のステータス管理、33カテゴリアクセス制御
価格システム B2B価格制御 顧客別掛率、月次割戻金計算、段階的割引
配送計算 配送料金計算 ブランド別配送料、直送+10%サーチャージ
キャンペーンエンジン プロモーション管理 6種類のキャンペーンタイプ

サポート機能(4プラグイン)

プラグイン 責務 主要機能
在庫管理 在庫制御 在庫予約、バックオーダー、購入数量制御
同意システム 法的同意管理 グリコール酸等の同意取得、7年間保存
SMILE連携 会計システム連携 CSV出力、納品先管理、バッチ処理
お気に入り ユーザー体験向上 商品お気に入り機能
ポイント/ギフト券/クーポン 特典運用 ギフト券登録、ポイント残高/履歴、仮想商品出力

設計原則

1. 関心の分離(Separation of Concerns)

各プラグインは明確に定義された単一の責務を持ち、他のプラグインと疎結合で連携します。

// ✅ 良い例: 明確な責務分離
CustomerManagementPlugin; // 顧客ステータス管理のみ
PricingSystemPlugin; // 価格計算のみ
ShippingCalculatorPlugin; // 配送料計算のみ

// ❌ 悪い例: 複数の責務を持つ
CustomerAndPricingPlugin; // 顧客管理 + 価格計算(密結合)
```ts

### 2. プラグイン間の連携

プラグイン間の連携は、Vendureの標準的な依存性注入(DI)とGraphQL
APIを通じて行います。

```typescript
// PricingSystemPluginがCustomerManagementPluginのサービスを利用
@Injectable()
export class PriceCalculationService {
  constructor(
    private customerStatusService: CustomerStatusService, // DI
  ) {}

  async calculatePrice(customerId: ID, productId: ID): Promise<number> {
    // 顧客ステータスを取得
    const status =
      await this.customerStatusService.getCustomerStatus(customerId);

    // ステータスに基づいて価格を計算
    return this.applyDiscount(productId, status.baseDiscountRate);
  }
}
```text

### 3. 設定可能性(Configurability)

すべてのプラグインは、初期化時にオプションで動作をカスタマイズ可能。

```typescript
// プラグインの柔軟な設定
PricingSystemPlugin.init({
  enableCustomerPricing: true,
  enableMonthlyRebate: true,
  rebateCalculationDay: 'last',
  lowPriceCustomerThreshold: 0.7,
});
```text

### 4. 拡張性(Extensibility)

将来の機能追加に対応できるよう、拡張ポイントを設計。

```typescript
// Phase 2での拡張を考慮
export interface InventoryManagementPluginOptions {
  enableBackorder: boolean;
  enableReservation: boolean;
  enableComplexQuantityControl?: boolean; // Phase 2で実装予定
}

5. データの整合性

エンティティとカスタムフィールドを適切に使い分け、データの整合性を保証。

// ✅ 良い例: 専用エンティティで複雑なデータを管理
@Entity()
export class MonthlyRebateEntity {
  @Column()
  customerId: string;

  @Column('decimal')
  rebateAmount: number;

  @Column()
  calculationMonth: string;
}

// ✅ 良い例: カスタムフィールドでシンプルなデータを管理
Customer.customFields.favoriteProductIds: string[]

アーキテクチャパターン

レイヤー構造

各プラグインは以下のレイヤー構造を持ちます:

プラグイン
├── API Layer (GraphQL)
│   ├── Resolvers
│   └── Schema
├── Service Layer
│   ├── Business Logic
│   └── Data Access
├── Entity Layer
│   ├── Database Entities
│   └── Custom Fields
└── Strategy/Calculator Layer (オプション)
    └── Vendure組み込み拡張

例: 価格システムプラグイン

pricing-system/
├── src/
│   ├── index.ts                          # プラグイン定義
│   ├── entities/
│   │   ├── customer-pricing.entity.ts    # Entity Layer
│   │   └── monthly-rebate.entity.ts
│   ├── services/
│   │   ├── customer-pricing.service.ts   # Service Layer
│   │   └── price-calculation.service.ts
│   ├── resolvers/
│   │   ├── customer-pricing.resolver.ts  # API Layer
│   │   └── monthly-rebate.resolver.ts
│   ├── strategies/
│   │   └── ritsubi-price-calculation.strategy.ts  # Strategy Layer
│   └── jobs/
│       └── monthly-rebate-calculation.job.ts

サービス指向アーキテクチャ(SOA)

各プラグインのサービスは、明確なインターフェースを持つ独立したコンポーネント。

// Service Interface
export interface IPriceCalculationService {
  calculatePrice(productId: ID, customerId: ID, quantity: number): Promise<number>;
  calculateTieredDiscount(customerId: ID, items: OrderItem[]): Promise<number>;
}

// Implementation
@Injectable()
export class PriceCalculationService implements IPriceCalculationService {
  async calculatePrice(...): Promise<number> {
    // 実装
  }
}

データモデル設計

エンティティ vs カスタムフィールド

使用ケース 推奨 理由
複雑なリレーション Entity 外部キー、結合クエリが必要
履歴管理が必要 Entity タイムスタンプ、ステータス管理
大量のデータ Entity インデックス、パフォーマンス最適化
シンプルな値 CustomField 軽量、高速アクセス
既存モデルの拡張 CustomField 既存のVendureモデルを拡張

例: 在庫予約

// ✅ Entity: 複雑なリレーションと履歴管理
@Entity()
export class InventoryReservationEntity {
  @Column()
  customerId: string;

  @Column()
  productVariantId: string;

  @Column()
  quantity: number;

  @Column()
  reservedAt: Date;

  @Column()
  expiresAt: Date;

  @Column('enum')
  status: ReservationStatus;
}

例: お気に入り商品

// ✅ CustomField: シンプルなID配列
Customer.customFields.favoriteProductIds: string[]

// 理由:
// - リレーション不要
// - 履歴管理不要
// - データ量が少ない(最大100件)

統合パターン

Vendure標準機能との統合

1. ShippingCalculator統合

const RitsubiShippingCalculator = new ShippingCalculator({
  code: 'ritsubi-shipping-calculator',
  calculate: (ctx, order) => {
    return calculatorService.calculate(ctx, order);
  },
});

// プラグイン設定で自動登録
config.shippingOptions.shippingCalculators.push(RitsubiShippingCalculator);

2. PromotionAction統合(キャンペーン)

const campaignPromotionAction = new PromotionItemAction({
  code: 'ritsubi-campaign-discount',
  execute: (ctx, orderLine, args) => {
    return campaignEngineService.applyDiscount(ctx, orderLine);
  },
});

3. CustomFields統合

// プラグイン初期化時にカスタムフィールドを追加
@VendurePlugin({
  configuration: config => {
    config.customFields.Customer.push({
      name: 'customerStatus',
      type: 'string',
      label: [{ languageCode: LanguageCode.ja, value: '顧客ステータス' }],
    });
    return config;
  },
})

パフォーマンス最適化戦略

1. キャッシング

Redis を使用して頻繁にアクセスされるデータをキャッシュ。

// 顧客ステータスのキャッシング(5分TTL)
const cacheKey = `customer-status:${customerId}`;
let status = await redis.get(cacheKey);

if (!status) {
  status = await this.getCustomerStatusFromDB(customerId);
  await redis.set(cacheKey, JSON.stringify(status), 'EX', 300);
}

2. バッチ処理

重い処理はバックグラウンドで実行。

// 月次割戻金計算(毎月1日午前2時実行)
@Cron('0 2 1 * *')
async calculateMonthlyRebates() {
  const customers = await this.customerService.findAll();

  for (const customer of customers) {
    await this.jobQueue.add({
      type: 'calculate-rebate',
      data: { customerId: customer.id },
    });
  }
}

3. インデックス最適化

頻繁に検索されるカラムにインデックスを作成。

@Entity()
export class ConsentRecordEntity {
  @Index()
  @Column()
  customerId: string;

  @Index()
  @Column()
  productId: string;

  @Index(['customerId', 'consentType'])
  @Column()
  consentType: string;
}

エラーハンドリング戦略

1. カスタムエラークラス

export class InsufficientInventoryError extends Error {
  constructor(productId: string, requested: number, available: number) {
    super(
      `在庫不足: 商品${productId} (要求: ${requested}, 利用可能: ${available})`,
    );
    this.name = 'InsufficientInventoryError';
  }
}

2. グレースフルデグラデーション

async calculateShipping(order: Order): Promise<number> {
  try {
    // 外部API呼び出し
    return await this.externalShippingApi.getRate(order);
  } catch (error) {
    // フォールバック: デフォルト料金
    this.logger.warn(`外部API失敗、デフォルト料金を使用: ${error.message}`);
    return this.getDefaultShippingRate(order);
  }
}

3. リトライメカニズム

@Retry({ maxAttempts: 3, backoff: 1000 })
async exportToSmile(order: Order): Promise<void> {
  await this.smileCsvExportService.exportOrder(order);
}

セキュリティベストプラクティス

1. 認証・認可

@Mutation()
@Allow(Permission.UpdateCustomer) // 管理者のみ
async updateCustomerStatus(
  @Ctx() ctx: RequestContext,
  @Args() args: UpdateCustomerStatusInput
): Promise<Customer> {
  // 処理
}

2. データ検証

async recordConsent(input: RecordConsentInput): Promise<ConsentRecord> {
  // 入力検証
  if (!input.consentType || !input.customerId) {
    throw new UserInputError('必須フィールドが不足しています');
  }

  // ビジネスルール検証
  const product = await this.productService.findOne(input.productId);
  if (!product.customFields.consentRequired) {
    throw new ForbiddenError('この商品に同意は不要です');
  }

  // 処理
}

3. 監査ログ

async updateCustomerStatus(customerId: ID, newStatus: string): Promise<void> {
  // ステータス更新
  await this.customerService.update({ id: customerId, status: newStatus });

  // 監査ログ記録
  await this.auditLogService.log({
    action: 'UPDATE_CUSTOMER_STATUS',
    entityType: 'Customer',
    entityId: customerId,
    oldValue: oldStatus,
    newValue: newStatus,
    userId: ctx.activeUserId,
    timestamp: new Date(),
  });
}

テスト戦略

1. ユニットテスト

サービスとビジネスロジックのテスト。

describe('PriceCalculationService', () => {
  it('should apply customer discount rate', async () => {
    const price = await priceCalculationService.calculatePrice(
      'product-123',
      'customer-456',
      10,
    );

    expect(price).toBe(7000); // 定価10,000円 × 掛率0.7
  });
});

2. 統合テスト

プラグイン間の連携をテスト。

describe('Order with Campaign', () => {
  it('should apply campaign discount and calculate final price', async () => {
    // キャンペーン作成
    const campaign = await campaignService.createCampaign({...});

    // 注文作成
    const order = await orderService.create({...});

    // キャンペーン適用
    await campaignEngineService.applyCampaign(order, campaign);

    // 最終価格を検証
    expect(order.totalWithTax).toBe(expectedTotal);
  });
});

3. E2Eテスト

GraphQL APIのエンドツーエンドテスト。

describe('Wishlist GraphQL', () => {
  it('should add product to favorites', async () => {
    const result = await shopClient.query(gql`
      mutation {
        addToFavorites(productId: "product-123")
      }
    `);

    expect(result.addToFavorites).toBe(true);
  });
});

デプロイメント戦略

開発環境

# ローカル開発
pnpm install
pnpm run dev

# Vendureサーバー: http://localhost:3021
# Dashboard: http://localhost:3021/dashboard
# Shop API: http://localhost:3021/shop-api

ステージング環境

# Docker Compose
docker-compose -f docker-compose.staging.yml up -d

# プラグインのホットリロード有効

本番環境

# ビルド
pnpm run build

# デプロイ(例: Railway)
railway up

# 環境変数設定
ENABLE_CUSTOMER_PRICING=true
ENABLE_MONTHLY_REBATE=true
REDIS_URL=redis://...

モニタリングとロギング

1. 構造化ログ

this.logger.log({
  level: 'info',
  message: 'Customer status updated',
  customerId: customer.id,
  oldStatus: oldStatus,
  newStatus: newStatus,
  timestamp: new Date().toISOString(),
});

2. パフォーマンスメトリクス

@Timed('price-calculation')
async calculatePrice(productId: ID, customerId: ID): Promise<number> {
  // 処理時間を自動記録
}

3. エラー追跡

try {
  await this.exportToSmile(order);
} catch (error) {
  // Sentry等のエラートラッキングサービスに送信
  Sentry.captureException(error, {
    tags: { plugin: 'smile-integration' },
    extra: { orderId: order.id },
  });
  throw error;
}

今後の拡張計画

Phase 2 機能

  • 在庫管理: 複雑な数量制御(日次・月次制限)
  • 価格システム: AI予測による最適掛率提案
  • キャンペーン: A/Bテスト機能
  • 同意システム: 多言語対応

Phase 3 機能

  • リアルタイムSMILE連携: API連携(CSV→API)
  • 高度な分析機能: BI連携、ダッシュボード
  • マルチテナント対応: 複数ブランドの統合管理

まとめ

Ritsubiのカスタムプラグインは、以下の原則に基づいて設計されています:

  1. モジュール化: 各プラグインは独立した責務を持つ
  2. 拡張性: 将来の機能追加に柔軟に対応
  3. パフォーマンス: キャッシング、バッチ処理による最適化
  4. セキュリティ: 認証、認可、監査ログの徹底
  5. 保守性: テスト、ログ、ドキュメントの充実

これらのプラグインにより、複雑なB2Bビジネス要件を効率的に実現し、高品質なECシステムを構築します。

関連ドキュメント