# レシピ：SaaS サブスク

> 暗号資産での月額・年額プラン。期間ごとにリマインダー付きの請求書。アプリは誰にアクセス権があるかを Perly Pay に尋ねます。

暗号資産はウォレットから引き落とせないため、サブスクリプションは請求書で更新します：期間の終わる前に購入者は次の請求書を受け取り、支払われるまでリマインダーが届きます。自動引き落としはありません。アプリは日数を自分で数えず、`entitlements.check` に尋ねます。

> **注:** アシスタントと一緒に：このレシピは、Perly Pay プラグインの `sell-subscription` スキルがあなたのプロジェクトで実行します。動くサンプルは、プラグインのリポジトリの `examples/saas-subscription`（MIT）です。

## 仕組み

1. **プラン** — `model: "subscription"` と `recurring.interval`（`month`、`year`…）を持つ商品と価格。任意でトライアルと猶予日。
2. **サブスクリプション** — ユーザーを顧客にしたその価格の請求書が、サブスクリプションと最初の請求書を作ります。その支払いでプランが有効になります。
3. **機能へのアクセス** — `entitlements.check({ customer_external_id, product })` → `has_access` と `ends_at`。サーバーで、訪問ごとに、または短いキャッシュ付きで。
4. **受け取る** — `invoice.fulfilled` は最初の支払いと毎回の更新。`subscription.past_due`、`subscription.unpaid`、`subscription.ended` で警告し、閉じます。
5. **購入者による管理** — 購入者ポータルへのリンク（`portalSessions.create`）：未払いの更新の支払い、前払い、解約。

**Subscribe, then gate**

```js
const invoice = await pay.invoices.create(
  { line_items: [{ price: PRO_MONTHLY }], customer: customer.id, metadata: { user: userId }, return_url },
  { idempotencyKey: `subscribe-${userId}-${today}` },
);
redirect(invoice.url); // invoice.subscription is the new subscription

const { has_access } = await pay.entitlements.check({ customer_external_id: userId, product: PRO });
```

猶予日、プランの変更、ステータスは [Subscriptions and reminders](https://pay.perly.io/docs/subscriptions.md) に。

## サンプルを動かす

1. **インストール** — `examples/saas-subscription` で `npm install`（Node 20.12+）。
2. **キー** — `.env.example` を `.env` にコピーし、`PERLY_PAY_SECRET_KEY` にテストキー（`sk_test_…`）を書きます。
3. **準備** — `npm run setup` があなたのキーで商品と価格を作成し、`.env` 用にその ID を表示します。
4. **起動** — README のとおり `npm start`（または `npm run dev`）。
5. **ウォレットなしで支払う** — トップページで申し込み、請求書で **Simulate payment** を押し、`/pro?user=you@example.com` を開きます：期間の終わりまで Pro が開きます。

## 落とし穴

- 「ウォレットから自動で引き落とします」と約束すること。何も引き落とされません。「請求書で更新」と伝えてください。
- 顧客のメールがない、または架空のもの：更新の請求書とリマインダーはそこに届きます。
- 1 分遅れただけでアクセスを閉じること。`past_due`（猶予日の範囲内）をどう扱うか決めてください。
- 購入者が支払いから戻ってきた時点でアクセスを開くこと。「支払い済み」を意味するのは、検証済みの Webhook `invoice.fulfilled` か、サーバーからの `entitlements.check` だけです。
