キャンペーンエンジンプラグイン (Campaign Engine Plugin)¶
概要¶
Ritsubiの6種類の複雑なキャンペーンタイプに対応するVendureカスタムプラグイン。購入金額・数量ベースのギフト・割引、複雑な条件判定、ハイブリッドキャンペーンなどを実現します。
互換性レンジ¶
- Vendure: 3.5.x(apps/vendure-server は ^3.5.1 を使用)
- Node.js: >=22.11.0(apps/vendure-server/package.json の engines に準拠)
パッケージ情報¶
- パッケージ名:
@ritsubi/campaign-engine-plugin - パス:
/packages/plugins/campaign-engine - エントリーポイント:
src/index.ts
実装機能¶
6種類のキャンペーンタイプ¶
1. AMOUNT_BASED_GIFT - 購入金額に基づくギフト¶
購入金額が一定額を超えた場合、ギフト商品を自動的にカートに追加。
例:
- 30,000円以上購入で、サンプル商品Aをプレゼント
- 50,000円以上購入で、サンプル商品B + Cをプレゼント
{
type: 'AMOUNT_BASED_GIFT',
threshold: 30000,
gifts: [
{ productId: 'sample-a', quantity: 1 }
]
}
2. QUANTITY_BASED_DISCOUNT - 購入数量に基づく割引¶
特定商品を一定数量以上購入した場合、割引を適用。
例:
- エクスビアンス商品10個以上で5%割引
- エクスビアンス商品20個以上で10%割引
{
type: 'QUANTITY_BASED_DISCOUNT',
targetProducts: ['exuviance-*'],
tiers: [
{ minQuantity: 10, discountRate: 0.05 },
{ minQuantity: 20, discountRate: 0.10 }
]
}
3. QUANTITY_BASED_GIFT - 購入数量に基づくギフト¶
特定商品を一定数量以上購入した場合、ギフト商品を提供。
例:
- メソセウティカル商品5個以上で、ギフトAを1個プレゼント
- メソセウティカル商品10個以上で、ギフトBを2個プレゼント
{
type: 'QUANTITY_BASED_GIFT',
targetProducts: ['mesoceutical-*'],
tiers: [
{ minQuantity: 5, gifts: [{ productId: 'gift-a', quantity: 1 }] },
{ minQuantity: 10, gifts: [{ productId: 'gift-b', quantity: 2 }] }
]
}
4. COMPLEX_CAMPAIGN - 複雑な条件のキャンペーン¶
複数の条件を組み合わせた高度なキャンペーン。
例:
- エクスビアンス10個 + メソセウティカル5個 + 合計金額50,000円以上で、特別ギフトをプレゼント
{
type: 'COMPLEX_CAMPAIGN',
conditions: [
{ type: 'PRODUCT_QUANTITY', productPattern: 'exuviance-*', minQuantity: 10 },
{ type: 'PRODUCT_QUANTITY', productPattern: 'mesoceutical-*', minQuantity: 5 },
{ type: 'AMOUNT_THRESHOLD', minAmount: 50000 }
],
action: {
type: 'ADD_GIFTS',
gifts: [{ productId: 'special-gift', quantity: 1 }]
}
}
5. MULTI_CATEGORY_SELECTION - 複数カテゴリ選択型¶
複数のカテゴリから一定数量を選択購入した場合に特典を提供。
例:
- カテゴリA、B、Cから合計15個以上購入で、特別割引10%
{
type: 'MULTI_CATEGORY_SELECTION',
categories: ['category-a', 'category-b', 'category-c'],
minTotalQuantity: 15,
action: {
type: 'DISCOUNT',
discountRate: 0.10
}
}
6. HYBRID_CAMPAIGN - ハイブリッドキャンペーン¶
購入金額と数量の両方の条件を組み合わせたキャンペーン。
例:
- エクスビアンス商品10個以上 かつ 合計30,000円以上で、ギフト + 5%割引
{
type: 'HYBRID_CAMPAIGN',
quantityCondition: {
targetProducts: ['exuviance-*'],
minQuantity: 10
},
amountCondition: {
minAmount: 30000
},
actions: [
{ type: 'ADD_GIFTS', gifts: [{ productId: 'gift-x', quantity: 1 }] },
{ type: 'DISCOUNT', discountRate: 0.05 }
]
}
追加機能¶
セミナーURL連動キャンペーン¶
特定のセミナーURLから流入したユーザー限定のキャンペーン。
{
enableSeminarUrls: true,
seminarUrlPattern: 'https://example.com/seminar/{campaignCode}'
}
隠しキャンペーンコード¶
管理画面でのみ表示される特別なキャンペーンコード。
{
enableHiddenCodes: true,
hiddenCodes: ['SPECIAL2025', 'VIP-ONLY']
}
技術仕様¶
プラグイン設定オプション¶
export interface CampaignEnginePluginOptions {
enableAmountBasedGift?: boolean;
enableQuantityBasedDiscount?: boolean;
enableQuantityBasedGift?: boolean;
enableComplexCampaign?: boolean;
enableMultiCategorySelection?: boolean;
enableHybridCampaign?: boolean;
enableSeminarUrls?: boolean;
enableHiddenCodes?: boolean;
}
プラグイン初期化¶
import { CampaignEnginePlugin } from '@ritsubi/campaign-engine-plugin';
export const config: VendureConfig = {
plugins: [
CampaignEnginePlugin.init({
enableAmountBasedGift: true,
enableQuantityBasedDiscount: true,
enableQuantityBasedGift: true,
enableComplexCampaign: true,
enableMultiCategorySelection: true,
enableHybridCampaign: true,
enableSeminarUrls: true,
enableHiddenCodes: true,
}),
],
};
エンティティ¶
CampaignEntity¶
キャンペーンの定義を管理するエンティティ。
@Entity()
export class CampaignEntity extends VendureEntity {
@Column()
name: string;
@Column()
campaignCode: string;
@Column('enum', {
enum: [
'AMOUNT_BASED_GIFT',
'QUANTITY_BASED_DISCOUNT',
'QUANTITY_BASED_GIFT',
'COMPLEX_CAMPAIGN',
'MULTI_CATEGORY_SELECTION',
'HYBRID_CAMPAIGN',
],
})
campaignType: CampaignType;
@Column('jsonb')
conditions: CampaignCondition[];
@Column('jsonb')
actions: CampaignAction[];
@Column()
startDate: Date;
@Column()
endDate: Date;
@Column()
isActive: boolean;
@Column({ default: false })
isHidden: boolean; // 隠しキャンペーン
@Column('int', { default: 0 })
priority: number; // 複数キャンペーン適用時の優先度
}
CampaignUsageEntity¶
キャンペーン使用履歴を記録するエンティティ。
@Entity()
export class CampaignUsageEntity extends VendureEntity {
@Column()
campaignId: string;
@Column()
orderId: string;
@Column()
customerId: string;
@Column('jsonb')
appliedActions: Record<string, unknown>;
@Column()
appliedAt: Date;
@Column('decimal', { precision: 12, scale: 2 })
discountAmount: number;
@Column('jsonb', { nullable: true })
giftsAdded: Array<{ productId: string; quantity: number }>;
}
サービス¶
CampaignService¶
キャンペーンの管理を行うサービス。
主要メソッド:
createCampaign(input: CreateCampaignInput): Promise<CampaignEntity>- 新しいキャンペーンを作成
getCampaign(campaignCode: string): Promise<CampaignEntity>- キャンペーンを取得
getActiveCampaigns(): Promise<CampaignEntity[]>- 有効なキャンペーン一覧を取得
validateCampaignCode(code: string): Promise<boolean>- キャンペーンコードの有効性を検証
CampaignEngineService¶
キャンペーンの適用ロジックを担当するサービス。
主要メソッド:
evaluateCampaigns(order: Order): Promise<ApplicableCampaign[]>- 注文に適用可能なキャンペーンを評価
applyCampaign(order: Order, campaign: CampaignEntity): Promise<void>- キャンペーンを注文に適用
calculateDiscount(order: Order, campaign: CampaignEntity): Promise<number>- キャンペーン割引額を計算
addGiftItems(order: Order, gifts: GiftItem[]): Promise<void>- ギフト商品をカートに追加
カスタムフィールド¶
Order¶
appliedCampaigns(text): 適用されたキャンペーン情報(JSON形式)
OrderLine¶
campaignGift(boolean): キャンペーンによるギフト商品かどうかoriginalPrice(int): キャンペーン適用前の価格
対応する要件¶
要件定義書との対応¶
- キャンペーン6種:
/docs/specifications/キャンペーン.xlsxの全6種類のキャンペーンタイプに対応 - セミナーURL連動: 特定セミナー参加者向けキャンペーン
- 隠しキャンペーンコード: 管理画面限定表示
使用例¶
フロントエンド(Next.js)での使用¶
// キャンペーンコード入力
await apolloClient.mutate({
mutation: APPLY_CAMPAIGN_CODE,
variables: {
orderId: currentOrder.id,
campaignCode: 'SUMMER2025',
},
});
// 適用可能なキャンペーン一覧を取得
const { data } = await apolloClient.query({
query: GET_APPLICABLE_CAMPAIGNS,
variables: { orderId: currentOrder.id },
});
data.campaigns.forEach(campaign => {
console.log(`${campaign.name}: ${campaign.description}`);
});
キャンペーンギフトの表示¶
// カート内のギフト商品を識別
const giftItems = order.lines.filter(line => line.customFields.campaignGift);
giftItems.forEach(item => {
console.log(`ギフト: ${item.productVariant.name}`);
});
管理画面での使用¶
// 新しいキャンペーンを作成
await campaignService.createCampaign({
name: '夏の大感謝祭',
campaignCode: 'SUMMER2025',
campaignType: 'HYBRID_CAMPAIGN',
conditions: [
{
type: 'PRODUCT_QUANTITY',
productPattern: 'exuviance-*',
minQuantity: 10,
},
{ type: 'AMOUNT_THRESHOLD', minAmount: 30000 },
],
actions: [
{ type: 'ADD_GIFTS', gifts: [{ productId: 'gift-sample', quantity: 1 }] },
{ type: 'DISCOUNT', discountRate: 0.05 },
],
startDate: new Date('2025-07-01'),
endDate: new Date('2025-08-31'),
});
// キャンペーン使用状況を確認
const usage = await campaignService.getCampaignUsageStats('SUMMER2025');
console.log(`使用回数: ${usage.totalUsages}`);
console.log(`総割引額: ${usage.totalDiscountAmount}円`);
キャンペーン評価フロー¶
複数キャンペーンの同時適用¶
1. アクティブなキャンペーンを取得
├─ 期間内のキャンペーンをフィルタ
├─ 顧客ステータスで絞り込み
└─ 優先度順にソート
2. 各キャンペーンの条件を評価
├─ 金額条件をチェック
├─ 数量条件をチェック
├─ カテゴリ条件をチェック
└─ カスタム条件をチェック
3. 適用可能なキャンペーンを選択
├─ 優先度の高い順に適用
├─ 重複適用のルールを確認
└─ 最大適用数を考慮
4. キャンペーンアクションを実行
├─ 割引を適用
├─ ギフト商品を追加
└─ 使用履歴を記録
セキュリティ考慮事項¶
- キャンペーンコード検証: フロントエンドからのコードをバックエンドで検証
- 重複適用防止: 同一キャンペーンの重複適用をブロック
- 隠しコードの保護: 隠しキャンペーンコードは管理者のみアクセス可能
- 使用履歴の記録: すべてのキャンペーン適用を記録
パフォーマンス最適化¶
- キャンペーンキャッシング: アクティブなキャンペーンをRedisにキャッシュ
- 条件評価の最適化: 早期リターンで不要な評価をスキップ
- バッチ処理: 大量のキャンペーン適用はバックグラウンドで処理
トラブルシューティング¶
よくある問題¶
問題: キャンペーンが適用されない
- 原因: キャンペーン期間外または条件未達
- 解決:
startDate,endDate,conditionsを確認
問題: ギフト商品が重複して追加される
- 原因: キャンペーン重複適用の制御が不足
- 解決:
CampaignUsageEntityで使用履歴を確認し、重複をブロック
関連ドキュメント¶
- 価格システムプラグイン - 割引計算との連携
- 顧客管理プラグイン - ステータス別キャンペーン適用
今後の拡張予定¶
- AIによるキャンペーン効果予測
- A/Bテスト機能
- 自動キャンペーン生成機能
- リアルタイムキャンペーン効果分析ダッシュボード