# 레시피: 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`(유예 기간 안)가 무엇을 뜻할지 정하세요.
- 구매자가 결제에서 돌아왔을 때 이용권을 여는 것. '결제됨'은 검증된 `invoice.fulfilled` 웹훅이나 내 서버의 `entitlements.check`만 뜻합니다.
