# 機能設計書：39-Stripeプラグイン

## 1. 機能概要

### 1.1 機能の目的
Stripeプラグイン（@payloadcms/plugin-stripe）は、Payload CMSとStripeの双方向同期を実現するプラグインである。顧客や商品などのデータをPayloadとStripe間で自動同期し、Webhookによるリアルタイム連携をサポートする。

### 1.2 機能の範囲
- PayloadからStripeへのデータ自動同期（作成・更新・削除）
- StripeからPayloadへのWebhook連携
- Stripe APIへのプロキシRESTエンドポイント
- カスタムWebhookハンドラーのサポート
- フィールドマッピングによる柔軟なデータ同期

### 1.3 関連する業務フロー
- 顧客管理システムとStripeの連携
- 商品・価格管理の同期
- サブスクリプション管理
- 決済情報の同期

## 2. 機能詳細

### 2.1 プラグイン初期化

#### 処理フロー
```
stripePlugin(config)
    ↓
プラグイン設定のサニタイズ
    ↓
webhooksエンドポイント追加
    ↓
REST APIエンドポイント追加（オプション）
    ↓
対象コレクションにフック登録
    ↓
追加フィールド注入
```

#### 設定オプション
```typescript
type StripePluginConfig = {
  stripeSecretKey: string                    // Stripe APIシークレットキー
  stripeWebhooksEndpointSecret?: string      // Webhook検証用シークレット
  isTestKey?: boolean                        // テストキー判定フラグ
  logs?: boolean                             // ログ出力設定
  rest?: boolean                             // REST APIプロキシ有効化（デフォルト: false）
  sync?: SyncConfig[]                        // 同期設定配列
  webhooks?: StripeWebhookHandler | StripeWebhookHandlers  // カスタムWebhookハンドラー
}
```

### 2.2 同期設定（SyncConfig）

```typescript
type SyncConfig = {
  collection: CollectionSlug                 // 対象コレクションスラッグ
  stripeResourceType: 'customers' | 'products'  // Stripeリソースタイプ（複数形）
  stripeResourceTypeSingular: 'customer' | 'product'  // Stripeリソースタイプ（単数形）
  fields: FieldSyncConfig[]                  // フィールド同期設定
}

type FieldSyncConfig = {
  fieldPath: string                          // Payloadフィールドパス
  stripeProperty: string                     // Stripeプロパティ名
}
```

### 2.3 追加されるフィールド

| フィールド名 | 型 | 説明 |
|-------------|-----|------|
| stripeID | text | Stripe側のリソースID（readOnly、sidebar） |
| skipSync | checkbox | 同期スキップフラグ（readOnly、sidebar） |
| docUrl | ui | Stripeダッシュボードへのリンク |

### 2.4 フック処理

#### beforeValidate: createNewInStripe
新規作成時にStripeリソースを作成

```
ドキュメント作成/更新
    ↓
skipSync確認
    ↓
syncConfig検索
    ↓
フィールド値をStripeプロパティにマッピング
    ↓
stripe.[resourceType].create()
    ↓
stripeID をドキュメントに設定
```

#### beforeChange: syncExistingWithStripe
更新時にStripeリソースを同期

```
ドキュメント更新
    ↓
skipSync確認
    ↓
stripeID確認
    ↓
フィールド値をStripeプロパティにマッピング
    ↓
stripe.[resourceType].update(stripeID, fields)
```

#### afterDelete: deleteFromStripe
削除時にStripeリソースを削除

```
ドキュメント削除
    ↓
syncConfig検索
    ↓
stripe.[resourceType].retrieve(stripeID)
    ↓
stripe.[resourceType].del(stripeID)
```

### 2.5 エンドポイント

#### POST /api/stripe/webhooks
Stripe Webhookの受信エンドポイント

```
Webhookリクエスト受信
    ↓
stripe-signatureヘッダー検証
    ↓
stripe.webhooks.constructEvent()
    ↓
handleWebhooks（内部ハンドラー）
    ↓
外部webhooksハンドラー呼び出し（設定時）
    ↓
{ received: true } 応答
```

#### POST /api/stripe/rest（rest: true時）
Stripe APIプロキシエンドポイント

```
リクエスト受信
    ↓
stripeMethod + stripeArgs 抽出
    ↓
stripeProxy 実行
    ↓
結果返却
```

### 2.6 Webhook処理

#### イベントタイプ別処理
```typescript
// イベントタイプの解析
const resourceType = event.type.split('.')[0]  // 'customer', 'product'
const method = event.type.split('.').pop()      // 'created', 'updated', 'deleted'

switch (method) {
  case 'created':
  case 'updated':
    handleCreatedOrUpdated()
    break
  case 'deleted':
    handleDeleted()
    break
}
```

## 3. データモデル

### 3.1 StripeWebhookHandler型
```typescript
type StripeWebhookHandler<T = any> = (args: {
  config: PayloadConfig
  event: T
  payload: Payload
  pluginConfig?: StripePluginConfig
  req: PayloadRequest
  stripe: Stripe
}) => Promise<void> | void
```

### 3.2 フィールドマッピング例
```typescript
// Payloadフィールド → Stripeプロパティ
const syncedFields = syncConfig.fields.reduce((acc, field) => {
  acc[field.stripeProperty] = data[field.fieldPath]
  return acc
}, {})

// ネストプロパティ対応
syncedFields = deepen(syncedFields)
// { 'metadata.foo': 'bar' } → { metadata: { foo: 'bar' } }
```

### 3.3 ER図
```
+-----------------+         +------------------+
|  Payload Doc    |         |  Stripe Resource |
+-----------------+         +------------------+
| id (PK)         |<------->| id               |
| stripeID (FK)   |-------->|                  |
| name            |<------->| name             |
| email           |<------->| email            |
| skipSync        |         |                  |
+-----------------+         +------------------+
```

## 4. インターフェース仕様

### 4.1 プラグイン使用例
```typescript
import { stripePlugin } from '@payloadcms/plugin-stripe'

export default buildConfig({
  plugins: [
    stripePlugin({
      stripeSecretKey: process.env.STRIPE_SECRET_KEY,
      stripeWebhooksEndpointSecret: process.env.STRIPE_WEBHOOKS_SECRET,
      isTestKey: true,
      logs: true,
      rest: true,
      sync: [
        {
          collection: 'customers',
          stripeResourceType: 'customers',
          stripeResourceTypeSingular: 'customer',
          fields: [
            { fieldPath: 'email', stripeProperty: 'email' },
            { fieldPath: 'name', stripeProperty: 'name' },
          ],
        },
        {
          collection: 'products',
          stripeResourceType: 'products',
          stripeResourceTypeSingular: 'product',
          fields: [
            { fieldPath: 'name', stripeProperty: 'name' },
            { fieldPath: 'description', stripeProperty: 'description' },
          ],
        },
      ],
      webhooks: {
        'invoice.paid': async ({ event, payload }) => {
          // カスタムハンドラー処理
        },
      },
    }),
  ],
})
```

### 4.2 エクスポート
```typescript
// プラグイン本体
export { stripePlugin } from './index.js'

// ユーティリティ
export { stripeProxy } from './utilities/stripeProxy.js'

// クライアントコンポーネント
export { LinkToDoc } from './exports/client.js'

// 型定義
export type {
  StripePluginConfig,
  SyncConfig,
  FieldSyncConfig,
  StripeWebhookHandler,
} from './types.js'
```

### 4.3 REST APIプロキシ
```typescript
// リクエスト
POST /api/stripe/rest
Content-Type: application/json

{
  "stripeMethod": "customers.create",
  "stripeArgs": [{ "email": "test@example.com" }]
}

// レスポンス（成功）
{
  "status": 200,
  "data": { ... }
}

// レスポンス（エラー）
{
  "status": 400,
  "message": "Error message"
}
```

## 5. 画面設計

### 5.1 管理画面

#### 同期対象コレクション編集画面
- サイドバー:
  - Stripe ID（readOnly）
  - Skip Sync（readOnly）
  - Stripeダッシュボードリンク（LinkToDoc）

### 5.2 UIコンポーネント

#### LinkToDoc
- Stripeダッシュボードへの直接リンク
- テストモード/本番モードを自動判定
- stripeResourceTypeに応じたURL生成

## 6. ビジネスロジック

### 6.1 同期スキップ制御
```typescript
// Webhookからの更新時は同期スキップ
if (data?.skipSync) {
  payload.logger.info('Bypassing collection-level hooks.')
  return
}

// 初期化
dataRef.skipSync = false

// Stripe作成成功後
dataRef.skipSync = true  // afterChangeでの重複同期防止

// beforeChange最後
data.skipSync = false    // 次回の同期を有効化
```

### 6.2 deepenユーティリティ
```typescript
// フラットなオブジェクトをネスト構造に変換
const flat = { 'metadata.foo': 'bar', 'metadata.baz': 'qux' }
const nested = deepen(flat)
// → { metadata: { foo: 'bar', baz: 'qux' } }
```

### 6.3 テスト環境対応
```typescript
if (process.env.NODE_ENV === 'test') {
  dataRef.stripeID = 'test'
  return dataRef  // Stripe API呼び出しをスキップ
}
```

## 7. エラー処理

### 7.1 Stripe作成エラー
```typescript
try {
  const stripeResource = await stripe[resourceType].create(syncedFields)
} catch (error) {
  const msg = error instanceof Error ? error.message : error
  throw new APIError(
    `Failed to create new '${resourceType}' resource in Stripe: ${msg}`
  )
}
```

### 7.2 Webhook署名検証エラー
```typescript
try {
  event = stripe.webhooks.constructEvent(body, stripeSignature, secret)
} catch (err) {
  req.payload.logger.error(`Error constructing Stripe event: ${msg}`)
  returnStatus = 400
}
```

### 7.3 削除時のリソース確認
```typescript
const found = await stripe[resourceType].retrieve(doc.stripeID)
if (found) {
  await stripe[resourceType].del(doc.stripeID)
} else {
  payload.logger.info(`Stripe document with ID: '${doc.stripeID}' not found, skipping...`)
}
```

## 8. セキュリティ考慮事項

### 8.1 APIキー管理
- stripeSecretKeyは環境変数で管理推奨
- クライアントサイドに漏洩しないよう注意

### 8.2 Webhook署名検証
- stripeWebhooksEndpointSecretによる署名検証必須
- 未検証イベントは処理しない

### 8.3 REST APIプロキシ
- rest: falseがデフォルト（明示的に有効化が必要）
- 認証・認可は別途実装が必要

## 9. パフォーマンス考慮事項

### 9.1 同期タイミング
- beforeValidate: 新規作成時のStripeリソース作成
- beforeChange: 更新時のStripe同期
- afterDelete: 削除時のStripeリソース削除

### 9.2 非同期Webhook処理
```typescript
// voidで非同期処理（レスポンスを待たない）
void handleWebhooks({ ... })
void webhooks({ ... })
```

## 10. 非機能要件対応

### 10.1 ログ出力
```typescript
if (logs) {
  payload.logger.info(`A new '${collectionSlug}' document was created in Payload...`)
  payload.logger.info(`✅ Successfully created new '${resourceType}' resource in Stripe...`)
  payload.logger.error(`- Error creating Stripe document: ${msg}`)
}
```

### 10.2 Stripe APIバージョン
```typescript
const stripe = new Stripe(stripeSecretKey, {
  apiVersion: '2022-08-01',
  appInfo: {
    name: 'Stripe Payload Plugin',
    url: 'https://payloadcms.com',
  },
})
```

## 11. コードリーディングガイド

### 11.1 推奨読解順序

1. **型定義の理解**
   - `packages/plugin-stripe/src/types.ts` (1-53行目)
   - StripePluginConfig、SyncConfig、FieldSyncConfig型

2. **プラグイン本体**
   - `packages/plugin-stripe/src/index.ts` (14-113行目)
   - エンドポイント登録とフック設定

3. **フィールド追加**
   - `packages/plugin-stripe/src/fields/getFields.ts` (13-54行目)
   - stripeID、skipSync、docUrlフィールド

4. **同期フック**
   - `packages/plugin-stripe/src/hooks/createNewInStripe.ts` (21-143行目)
   - `packages/plugin-stripe/src/hooks/syncExistingWithStripe.ts` (21-93行目)
   - `packages/plugin-stripe/src/hooks/deleteFromStripe.ts` (19-67行目)

5. **Webhook処理**
   - `packages/plugin-stripe/src/routes/webhooks.ts` (9-88行目)
   - `packages/plugin-stripe/src/webhooks/index.ts` (6-56行目)

### 11.2 プログラム呼び出し階層図
```
stripePlugin(config)
├── endpoints
│   ├── /stripe/webhooks
│   │   └── stripeWebhooks
│   │       ├── stripe.webhooks.constructEvent
│   │       ├── handleWebhooks
│   │       │   ├── handleCreatedOrUpdated
│   │       │   └── handleDeleted
│   │       └── 外部webhooksハンドラー
│   └── /stripe/rest (rest: true時)
│       └── stripeREST
│           └── stripeProxy
└── コレクションフック
    ├── beforeValidate: createNewInStripe
    │   └── stripe[resourceType].create
    ├── beforeChange: syncExistingWithStripe
    │   └── stripe[resourceType].update
    └── afterDelete: deleteFromStripe
        ├── stripe[resourceType].retrieve
        └── stripe[resourceType].del
```

### 11.3 データフロー図
```
[Payload → Stripe]
+------------------+
| beforeValidate   |
| createNewInStripe|
+------------------+
        ↓
+------------------+
| フィールドマッピング|
| deepen()         |
+------------------+
        ↓
+------------------+
| stripe.create()  |
+------------------+
        ↓
+------------------+
| stripeID保存     |
| skipSync=true    |
+------------------+

[Stripe → Payload (Webhook)]
+------------------+
| Webhook受信      |
| 署名検証         |
+------------------+
        ↓
+------------------+
| イベント解析      |
| resourceType     |
| method           |
+------------------+
        ↓
+------------------+
| handleCreatedOrUpdated |
| handleDeleted    |
+------------------+
        ↓
+------------------+
| Payload更新      |
| skipSync=true    |
+------------------+
```

### 11.4 関連ファイル一覧
| ファイルパス | 役割 |
|-------------|------|
| packages/plugin-stripe/src/index.ts | プラグインメイン実装 |
| packages/plugin-stripe/src/types.ts | 型定義 |
| packages/plugin-stripe/src/fields/getFields.ts | フィールド追加 |
| packages/plugin-stripe/src/hooks/createNewInStripe.ts | 新規作成同期 |
| packages/plugin-stripe/src/hooks/syncExistingWithStripe.ts | 更新同期 |
| packages/plugin-stripe/src/hooks/deleteFromStripe.ts | 削除同期 |
| packages/plugin-stripe/src/routes/webhooks.ts | Webhookエンドポイント |
| packages/plugin-stripe/src/routes/rest.ts | REST APIプロキシ |
| packages/plugin-stripe/src/webhooks/index.ts | Webhookハンドラー振り分け |
| packages/plugin-stripe/src/webhooks/handleCreatedOrUpdated.ts | 作成・更新Webhook処理 |
| packages/plugin-stripe/src/webhooks/handleDeleted.ts | 削除Webhook処理 |
| packages/plugin-stripe/src/utilities/stripeProxy.ts | Stripe APIプロキシ |
| packages/plugin-stripe/src/utilities/deepen.ts | オブジェクトネスト化 |
| packages/plugin-stripe/package.json | パッケージ設定 |

## 12. 変更履歴

| 日付 | バージョン | 変更内容 |
|------|-----------|----------|
| 2026-03-01 | 1.0 | 初版作成 |
