# 配方：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` 用你的密钥创建商品和价格，并打印它们的 ID 供 `.env` 使用。
4. **启动** — 按其 README 运行 `npm start`（或 `npm run dev`）。
5. **无需钱包即可付款** — 在首页订阅，在账单上点 **Simulate payment**，然后打开 `/pro?user=you@example.com`：Pro 已开通至周期结束。

## 陷阱

- 承诺“我们会自动从你的钱包扣款”。什么都不会被扣；要说“按账单续订”。
- 客户邮箱缺失或是编的：续订账单和提醒都发到那里。
- 一逾期就立刻关闭访问。先决定 `past_due` 意味着什么（在宽限天数内）。
- 买家从付款页返回时就开通访问。“已付款”只意味着经过校验的 `invoice.fulfilled` Webhook，或你的服务器调用的 `entitlements.check`。
