コンテンツにスキップ

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

概要

Ritsubi B2B ECサイトのビジネス要件を実現するためのカスタムVendureプラグイン群を管理します。実装はモノレポの /packages/plugins/@ritsubi/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.2 を使用)
  • Node.js: >=24.7.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-27)。

カテゴリ プラグイン コード配置 主な責務 稼働状況 関連ドキュメント
コンプライアンス 同意システム(Consent System) packages/plugins/src/system-integration/consent/ 医薬部外品・高濃度成分の購入同意取得、署名保管、7年保持 vendure-config.shared.tsで有効(ConsentSystemPlugin 同意システム
顧客管理 顧客管理(Customer Management) packages/plugins/src/customer-management/ 動的顧客ステータス管理・商品アクセス制御 有効(CustomerManagementPlugin.init({}) 顧客管理
顧客管理 顧客可視性(Visibility) packages/plugins/src/rule-engine/visibility/ 顧客・顧客グループ・顧客属性と商品対象条件による表示制御。決済可否は PaymentMethodCollectionAssignment + paymentCollectionMethodEligibilityChecker が正本 有効(VisibilityPlugin.init() 顧客可視性
UX お気に入り(Wishlist) packages/plugins/src/standard-extensions/wishlist/ 商品のお気に入り登録・取得API、Storefront同期 有効(WishlistPlugin.init({}) お気に入り
UX セット商品(Set Products) packages/plugins/src/standard-extensions/set-products/ セット商品定義・構成の提供 有効(SetProductsPlugin.init({}) -
レコメンド 共起レコメンド(CoPurchase Recommendation) packages/plugins/src/standard-extensions/recommendation/ 購入共起データに基づくレコメンドAPI 有効(CoPurchaseRecommendationPlugin.init({}) 共起レコメンド
決済 SB Payment Link packages/plugins/src/payment-integration/sb-payment-link/ SB Paymentリンク型のクレジットカード決済(ブラウザPOST遷移)、ハッシュ生成、コールバック処理 有効(SbPaymentIntegrationPlugin.init({}) SB Paymentリンク型決済
決済 標準決済ハンドラー packages/plugins/src/payment-integration/handlers/ 代引き決済、銀行振込/前入金のハンドラー実装 有効(StandardPaymentsPlugin 決済方法ハンドラー
決済 売掛決済(Credit Sale) packages/plugins/src/b2b-extensions/payments/ 売掛決済ハンドラーの登録と関連拡張 有効(CreditSalePlugin 決済方法ハンドラー
決済 決済可否制御(B2B Eligibility) packages/plugins/src/b2b-extensions/ 顧客ステータス/金額/直送フラグによる決済可否制御 有効(B2BPaymentEligibilityPlugin Payment Eligibility Checkers
プロモーション 商流ルール(Commercial Rules) packages/plugins/src/rule-engine/commercial/ 単価変更、注文調整、特典ギフト、tier 判定を単一ルールで管理 有効(CommercialRulesPlugin.init() 商流ルール / 商流ルール移行メモ
プロモーション ポイント/ギフト券/クーポン(Points System) packages/plugins/src/standard-extensions/points/ ギフト券登録・ポイント残高/履歴・クーポン仮想商品出力 有効(PointsSystemPlugin.init({}) ポイント/ギフト券/クーポン
連携 SMILE連携(Smile Integration) packages/plugins/src/system-integration/smile/ SMILE会計システム向けCSV出力、納品先管理 有効(SmileIntegrationPlugin.init({}) SMILE連携
連携 システム連携(System Integration) packages/plugins/src/system-integration/ Webhook受信など外部システム連携。現行は出荷Webhookの署名検証と production/shadow 比較レーンを含む 有効(SystemIntegrationPlugin.init({}) システム連携
連携 CMS連携(Cms Integration) packages/plugins/src/cms-integration/ WPGraphQL経由のコンテンツ取得 有効(CmsIntegrationPlugin.init(...) CMS連携概要 / CMS連携マッピング / CMS連携認証
ショップAPI Product Variants Resolver packages/plugins/src/cms-integration/shop-api/product-variants.resolver.ts Shop APIにproductVariantsクエリを追加 有効(CmsIntegrationPlugin内で登録) Shop Product Variants
検索 検索戦略(pg_trgm + 標準検索) apps/vendure-server/src/search/pg-trgm-search-strategy.ts DefaultSearchPlugin の searchStrategy を差し替え、商品名/説明/SKU(商品番号)+ nameKana/keywords を ILIKE 検索(かな/カナ揺れはクエリ展開で吸収) 有効(DefaultSearchPlugin + PgTrgmSearchStrategy 検索実装
検索 Search Extension packages/plugins/src/standard-extensions/index.ts SearchSynonym エンティティ登録による検索補助 有効(SearchExtensionPlugin 検索実装
配送 配送計算(Shipping Calculator) packages/plugins/src/rule-engine/shipping/ キャリア×ゾーン別送料、直送+10%サーチャージ、無料閾値 有効(ShippingCalculatorPlugin.init({}) 配送計算
在庫 在庫管理(Inventory Management) packages/plugins/src/standard-extensions/inventory/ Vendure 標準在庫検証・引当、予約受注 split checkout、PurchaseLimitRule による月間上限・最小/最大数量・購入単位制限 有効(InventoryManagementPlugin.init({}) 在庫管理
ダッシュボード Vendure Dashboard拡張 packages/plugins/src/standard-extensions/admin-extensions/ React Dashboard向けカスタム拡張とloadedPluginsクエリ vendure-config.tsで有効(AdminExtensionsPlugin Dashboard拡張 / 受注番号採番
運用 Email Preview Plugin packages/plugins/src/standard-extensions/email-preview/ ダッシュボードでメールテンプレートをプレビューし件名と同期 有効(EmailPreviewPlugin メールテンプレート運用
運用 Notification Plugin packages/plugins/src/standard-extensions/index.ts 通知ハンドラ定義の受け皿。出荷完了通知は EmailPlugin の handler と SystemIntegrationPlugin webhook の組み合わせで実現 有効(NotificationPlugin -
運用 Security Log Plugin packages/plugins/src/system-integration/security/ ログイン試行ログ・通知 有効(SecurityLogPlugin.init({}) -
運用 Maintenance Mode Plugin packages/plugins/src/system-integration/maintenance-mode/ メンテナンスモード切替 有効(MaintenanceModePlugin -
運用 Report Plugin packages/plugins/src/standard-extensions/report/ レポート生成と配信 有効(ReportPlugin レポート機能
運用 CMS API Plugin packages/plugins/src/cms-integration/cms-api/ CMS向けAPI提供 有効(CmsApiPlugin CMS API Plugin
決済制御 Payment Eligibility Checkers packages/plugins/src/b2b-extensions/payments/payment-collection-method-eligibility.ts paymentCollectionMethodEligibilityCheckerPaymentMethodCollectionAssignment(回収方法コード × 許可 PaymentMethod codes)を正本に決済可否を判定 有効(B2BPaymentEligibilityPlugin.configuration() が登録) Payment Eligibility Checkers

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

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

プラグイン 責務 主要機能
顧客管理 顧客ステータスとアクセス制御 動的ステータス管理、商品アクセス制御
商流ルール B2B価格制御と販促統合 単価変更、注文調整、段階特典
配送計算 配送料金計算 キャリア×ゾーン別配送料、直送+10%サーチャージ
商流ルール移行メモ 旧実装からの移行整理 campaign / pricing からの統合差分

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

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

設計原則

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

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

// ✅ 良い例: 明確な責務分離
CustomerManagementPlugin; // 顧客ステータス管理のみ
CommercialRulesPlugin; // 単価変更・注文調整・特典のみ
ShippingCalculatorPlugin; // 配送料計算のみ

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

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

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

```typescript
// CommercialRulesPluginが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
// プラグインの柔軟な設定
CommercialRulesPlugin.init();
```text

### 4. 拡張性(Extensibility)

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

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

5. データの整合性

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

// ✅ 良い例: カスタムフィールドでシンプルなデータを管理
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組み込み拡張

例: 商流ルールプラグイン

commercial/
├── src/
│   ├── index.ts                          # プラグイン定義
│   ├── entities/
│   │   ├── commercial-rule.entity.ts     # Entity Layer
│   ├── services/
│   │   ├── commercial-rule.service.ts    # Service Layer
│   │   └── price-calculation.service.ts
│   ├── api/
│   │   ├── commercial-rule.admin.resolver.ts
│   │   └── commercial-rule.shop.resolver.ts
│   ├── strategies/
│   │   └── ritsubi-price-calculation.strategy.ts  # Strategy Layer
│   └── listeners/
│       └── commercial-order-line.listener.ts

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

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

// Service Interface
export interface ICommercialRuleService {
  simulateCommercialState(...args: unknown[]): Promise<unknown>;
  createCommercialRule(...args: unknown[]): Promise<unknown>;
}

データモデル設計

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

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

例: 将来の独自在庫予約 entity

// 将来、独自在庫予約を実運用へ接続する場合の entity 境界。
// 現行 add-to-cart 経路は Vendure 標準の StockLevel / StockMovement を正本にする。
@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. バッチ処理

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

// 例: 月次バッチ(SMILE連携のCSV出力など)
@Cron('0 2 1 * *')
async runMonthlyBatch() {
  const customers = await this.customerService.findAll();

  for (const customer of customers) {
    await this.jobQueue.add({
      type: 'smile-export',
      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 Commercial Rules", () => {
  it("should apply gifts and order adjustments", async () => {
    const state = await commercialRuleService.simulateCommercialState(ctx, {
      customer,
      lines,
      focusProductVariantId: targetVariantId,
    });

    expect(state.plannedGiftItems).toEqual([{ productCode: "GIFT-001", quantity: 1 }]);
    expect(state.orderAdjustmentTotal).toBe(-3000);
  });
});

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
just vendure

# Vendureサーバー:
#   Portless: http://vendure.localhost:<worktree-proxy-port>
#   localhost: http://localhost:${VENDURE_PORT:-3021}
# React Dashboard: http://localhost:6202/
# Shop API:
#   Portless: http://vendure.localhost:<worktree-proxy-port>/shop-api
#   localhost: http://localhost:${VENDURE_PORT:-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システムを構築します。

関連ドキュメント