コンテンツにスキップ

同意システムプラグイン (Consent System Plugin)

概要

Ritsubiの医薬部外品・専門取扱商品に対する同意取得要件に対応するVendureカスタムプラグイン。グリコール酸、iMESO等の特定商品購入時の同意記録、法的保持期間管理(7年間)、デジタル署名対応を実現します。

互換性レンジ

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

パッケージ情報

  • パッケージ名: @ritsubi/consent-system-plugin
  • パス: /packages/plugins/consent-system
  • エントリーポイント: src/index.ts

実装機能

1. 商品別同意要件管理

特定の商品に対して、購入前に必要な同意タイプを定義。

同意タイプ

同意タイプ 説明 対象商品例
GLYCOLIC_ACID グリコール酸取扱同意 エクスビアンス グリコール酸配合商品
IMESO iMESO取扱同意 メソセウティカル iMESO商品
SPECIAL_HANDLING 特別取扱注意事項 高濃度美容液等
PRESCRIPTION_REQUIRED 処方箋確認 医療機関専用商品
AGE_VERIFICATION 年齢確認 制限付き商品
PROFESSIONAL_USE_ONLY 専門家使用限定 エステ・クリニック専用商品

2. 同意記録の永続化

すべての同意記録をデータベースに保存し、法的保持期間(7年間)を管理。

記録される情報

  • 同意内容: 同意書のテキストとバージョン
  • 同意日時: 同意した正確な日時
  • 同意方法: オンライン同意、書面アップロード等
  • デジタル署名: 電子署名データ(オプション)
  • アップロード書類: 同意書のPDF等(オプション)

3. 同意の有効性検証

同意の有効期限を管理し、期限切れの場合は再取得を要求。

// 同意の有効性チェック
const isValid = await consentService.isConsentValid(
  customerId,
  productId,
  'GLYCOLIC_ACID',
);

if (!isValid) {
  // 再同意が必要
  redirectToConsentForm();
}

4. GraphQL API提供

フロントエンド(Next.js)からの同意操作のための完全なGraphQL API。

主要クエリ

  • checkProductConsentRequirements: 商品の同意要件を確認
  • consentHistory: 顧客の同意履歴を取得
  • getValidConsents: 有効な同意一覧を取得
  • getConsentTemplate: 同意書テンプレートを取得
  • getConsentFormData: 同意フォーム表示用データを取得

主要ミューテーション

  • recordConsent: 同意を記録
  • denyConsent: 同意を拒否
  • revokeConsent: 同意を撤回

5. 期限切れ同意の自動管理

定期的に期限切れの同意を検出し、顧客に再取得を通知。

技術仕様

プラグイン設定オプション

export interface ConsentSystemPluginOptions {
  /**
   * 商品同意機能を有効にする
   */
  enableProductConsent?: boolean;

  /**
   * 電子署名を有効にする
   */
  enableElectronicSignature?: boolean;

  /**
   * 同意履歴機能を有効にする
   */
  enableConsentHistory?: boolean;

  /**
   * 同意タイプ一覧
   */
  consentTypes?: string[];

  /**
   * 同意記録の保持期間(年)
   */
  retentionPeriodYears?: number;

  /**
   * クリーンアップ間隔(時間)
   */
  cleanupIntervalHours?: number;

  /**
   * デジタル署名必須化
   */
  digitalSignatureRequired?: boolean;

  /**
   * 書類アップロード必須化
   */
  documentUploadRequired?: boolean;
}

プラグイン初期化

import { ConsentSystemPlugin } from '@ritsubi/consent-system-plugin';

export const config: VendureConfig = {
  plugins: [
    ConsentSystemPlugin.init({
      enableProductConsent: true,
      enableElectronicSignature: true,
      enableConsentHistory: true,
      consentTypes: [
        'GLYCOLIC_ACID',
        'IMESO',
        'SPECIAL_HANDLING',
        'PRESCRIPTION_REQUIRED',
        'AGE_VERIFICATION',
        'PROFESSIONAL_USE_ONLY',
      ],
      retentionPeriodYears: 7, // 法的保持期間
      cleanupIntervalHours: 24, // 毎日クリーンアップ
      digitalSignatureRequired: false,
      documentUploadRequired: false,
    }),
  ],
};

エンティティ

ConsentRecordEntity

同意記録を管理するエンティティ。

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

  @Column({ nullable: true })
  productId: string;

  @Column({ nullable: true })
  orderId: string;

  @Column()
  consentType: string; // GLYCOLIC_ACID, IMESO等

  @Column('enum', {
    enum: ['PENDING', 'CONSENTED', 'DENIED', 'REVOKED', 'EXPIRED'],
  })
  consentStatus: ConsentStatus;

  @Column({ nullable: true })
  consentedAt: Date;

  @Column({ nullable: true })
  expiresAt: Date;

  @Column()
  consentMethod: string; // ONLINE, PAPER, PHONE等

  @Column('jsonb')
  consentDetails: Record<string, unknown>;

  @Column('text', { nullable: true })
  digitalSignature: string; // Base64エンコードされた署名データ

  @Column('jsonb', { nullable: true })
  uploadedDocuments: Array<{
    filename: string;
    url: string;
    uploadedAt: Date;
  }>;

  @Column({ nullable: true })
  consentVersion: string; // 同意書のバージョン
}

ConsentTemplateEntity

同意書テンプレートを管理するエンティティ。

@Entity()
export class ConsentTemplateEntity extends VendureEntity {
  @Column()
  name: string;

  @Column()
  consentType: string;

  @Column()
  version: string; // v1.0, v2.0等

  @Column('text')
  consentText: string; // 同意書本文

  @Column('jsonb')
  formConfiguration: Record<string, unknown>; // フォーム設定(JSON)

  @Column()
  validFrom: Date;

  @Column({ nullable: true })
  validTo: Date;

  @Column({ default: true })
  isActive: boolean;

  @Column({ default: false })
  isDefault: boolean;
}

サービス

ConsentService

同意管理のコアロジックを担当するサービス。

主要メソッド:

  • checkProductConsentRequirements(productId: ID, customerId?: ID): Promise<ConsentRequirementResult>
  • 商品の同意要件をチェック
  • recordConsent(input: RecordConsentInput): Promise<ConsentRecordEntity>
  • 同意を記録
  • getConsentHistory(customerId: ID, productId?: ID): Promise<ConsentRecordEntity[]>
  • 同意履歴を取得
  • isConsentValid(customerId: ID, productId: ID, consentType: string): Promise<boolean>
  • 同意の有効性を検証
  • revokeConsent(consentId: ID, reason: string): Promise<void>
  • 同意を撤回
  • getConsentTemplate(consentType: string): Promise<ConsentTemplateEntity>
  • 同意書テンプレートを取得

カスタムフィールド

Product

  • consentRequired (boolean): 同意が必要
  • consentTypes (string[]): 必要な同意タイプ
  • consentInstructions (string): 同意取得時の注意事項

Customer

  • professionalLicense (text): 専門資格情報(JSON形式)
  • consentPreferences (text): 同意設定(JSON形式)

OrderLine

  • consentConfirmed (boolean): 同意確認済み
  • consentRecordIds (text): 関連する同意記録のID(JSON配列形式)

Order

  • hasConsentRequiredItems (boolean): 同意必要商品含む
  • consentStatus (string): 注文全体の同意ステータス

GraphQL スキーマ

主要クエリ

type Query {
  checkProductConsentRequirements(
    productId: ID!
    customerId: ID
  ): ProductConsentCheckResult!

  consentHistory(customerId: ID!, productId: ID): [ConsentRecord!]!

  getValidConsents(
    customerId: ID!
    productId: ID
    consentTypes: [String!]
  ): [ConsentRecord!]!

  getConsentTemplate(consentType: String!): ConsentTemplate

  getConsentFormData(consentType: String!, productId: ID): ConsentFormPayload!
}

主要ミューテーション

type Mutation {
  recordConsent(
    customerId: ID!
    productId: ID
    orderId: ID
    consentType: String!
    consentMethod: String!
    consentDetails: JSON!
    digitalSignature: String
    uploadedDocuments: JSON
  ): ConsentRecord!

  denyConsent(
    customerId: ID!
    productId: ID!
    consentType: String!
    reason: String!
  ): ConsentRecord!

  revokeConsent(consentId: ID!, revocationReason: String!): ConsentRecord!
}

対応する要件

要件定義書との対応

  • グリコール酸取扱同意: エクスビアンス商品購入時の同意取得
  • iMESO取扱同意: メソセウティカル商品購入時の同意取得
  • 法的保持期間: 同意記録の7年間保管義務
  • 電子署名対応: デジタル署名による法的有効性確保

使用例

フロントエンド(Next.js)での使用

// 商品の同意要件を確認
const { data } = await apolloClient.query({
  query: CHECK_CONSENT_REQUIREMENTS,
  variables: {
    productId: 'glycolic-acid-product',
    customerId: currentUser.id,
  },
});

if (data.requiresConsent) {
  console.log(`必要な同意: ${data.consentTypes.join(', ')}`);

  // 既存の同意をチェック
  if (data.existingConsents.length === 0) {
    // 同意フォームを表示
    showConsentForm(data.consentTypes);
  }
}

同意の記録

// 同意を記録
await apolloClient.mutate({
  mutation: RECORD_CONSENT,
  variables: {
    customerId: currentUser.id,
    productId: 'glycolic-acid-product',
    consentType: 'GLYCOLIC_ACID',
    consentMethod: 'ONLINE',
    consentDetails: {
      ipAddress: '192.168.1.1',
      userAgent: navigator.userAgent,
      timestamp: new Date().toISOString(),
    },
    digitalSignature: signatureDataUrl, // Canvas署名データ
  },
});

同意履歴の確認

// 顧客の同意履歴を取得
const { data } = await apolloClient.query({
  query: GET_CONSENT_HISTORY,
  variables: { customerId: currentUser.id },
});

data.consentHistory.forEach(consent => {
  console.log(`${consent.consentType}: ${consent.consentStatus}`);
  console.log(`同意日: ${consent.consentedAt}`);
  console.log(`有効期限: ${consent.expiresAt}`);
});

管理画面での使用

// 同意書テンプレートを作成
await consentService.createTemplate({
  name: 'グリコール酸取扱同意書 v2.0',
  consentType: 'GLYCOLIC_ACID',
  version: 'v2.0',
  consentText: '...',
  formConfiguration: {
    requiresSignature: true,
    requiresWitnessSignature: false,
    expirationMonths: 12,
  },
  validFrom: new Date(),
});

// 期限切れ同意の確認
const expiring = await consentService.getExpiringSoonConsents(30); // 30日以内
console.log(`${expiring.length}件の同意が期限切れ間近です`);

同意取得フロー

商品購入時の同意フロー

1. カートに商品追加
   └─ 同意要件をチェック

2. 同意が必要な場合
   ├─ 既存の有効な同意を確認
   ├─ 有効な同意がない場合
   │  └─ 同意フォームを表示
   └─ 顧客が同意

3. 同意を記録
   ├─ デジタル署名を保存
   ├─ 同意詳細を記録
   └─ 注文に同意IDを関連付け

4. 注文を確定
   └─ 同意ステータスを「完了」に更新

セキュリティ考慮事項

  1. 同意記録の改ざん防止: すべての同意記録は追記のみ可能(削除不可)
  2. デジタル署名の検証: 署名データの整合性を検証
  3. アクセス制御: 同意記録は顧客本人と管理者のみアクセス可能
  4. 暗号化: デジタル署名データは暗号化して保存
  5. 監査ログ: すべての同意操作を監査ログに記録

パフォーマンス最適化

  • 同意要件のキャッシング: 商品の同意要件をRedisにキャッシュ
  • 有効性チェックの最適化: 有効な同意をメモリキャッシュ
  • バッチクリーンアップ: 期限切れ同意の定期的な一括処理

トラブルシューティング

よくある問題

問題: 同意が記録されない

  • 原因: 必須フィールドが不足
  • 解決: consentMethod, consentDetails を確認

問題: 同意が期限切れとして表示される

  • 原因: expiresAt の設定ミス
  • 解決: 同意書テンプレートの expirationMonths を確認

関連ドキュメント

今後の拡張予定

  • 多言語対応(英語同意書)
  • 複数証人署名機能
  • ビデオ同意記録機能
  • AI不正同意検出機能