同意システムプラグイン (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. 注文を確定
└─ 同意ステータスを「完了」に更新
セキュリティ考慮事項¶
- 同意記録の改ざん防止: すべての同意記録は追記のみ可能(削除不可)
- デジタル署名の検証: 署名データの整合性を検証
- アクセス制御: 同意記録は顧客本人と管理者のみアクセス可能
- 暗号化: デジタル署名データは暗号化して保存
- 監査ログ: すべての同意操作を監査ログに記録
パフォーマンス最適化¶
- 同意要件のキャッシング: 商品の同意要件をRedisにキャッシュ
- 有効性チェックの最適化: 有効な同意をメモリキャッシュ
- バッチクリーンアップ: 期限切れ同意の定期的な一括処理
トラブルシューティング¶
よくある問題¶
問題: 同意が記録されない
- 原因: 必須フィールドが不足
- 解決:
consentMethod,consentDetailsを確認
問題: 同意が期限切れとして表示される
- 原因:
expiresAtの設定ミス - 解決: 同意書テンプレートの
expirationMonthsを確認
関連ドキュメント¶
- 顧客管理プラグイン - 顧客資格情報との連携
- SMILE連携プラグイン - 同意記録のCSV出力
今後の拡張予定¶
- 多言語対応(英語同意書)
- 複数証人署名機能
- ビデオ同意記録機能
- AI不正同意検出機能