# Chargebee (/docs/plugins/chargebee)

用于 Better Auth 的 Chargebee 插件，用于管理订阅和支付。



Chargebee 插件将 [Chargebee 的](https://www.chargebee.com) 订阅管理和计费功能与 Better Auth 集成。由于支付和认证通常紧密耦合，此插件简化了 Chargebee 集成到应用程序的流程，处理客户创建、订阅管理和 Webhook 处理。

<Callout>
  此插件由 Chargebee 团队维护。关于错误、问题或功能请求，
  请访问 [Chargebee GitHub 仓库](https://github.com/chargebee/js-framework-adapters)。
</Callout>

<Card href="https://discord.com/invite/gpsNqnhDm2" title="在 Chargebee Discord 上获取支持">
  有问题吗？我们的团队随时在 Discord 上为您提供帮助。
</Card>

## 功能 [#功能]

* 用户注册时自动创建 Chargebee 客户
* 管理订阅计划和定价（基于项目：计划、附加组件、费用）
* 处理订阅生命周期事件（创建、更新、取消）
* 使用 Basic Auth 验证安全处理 Chargebee Webhook
* 将订阅数据暴露给应用程序
* 支持试用期和多项目订阅
* 自动试用滥用预防——每个账户在所有计划中只能获得一次试用
* 灵活的引用系统，用于将订阅与用户或组织关联
* 团队订阅支持及席位管理
* 通过 Chargebee 托管页面进行托管结账和门户
* 自助计费门户，用于管理支付方式、发票和订阅

## 安装 [#安装]

<Steps>
  <Step>
    ### 安装插件 [#安装插件]

    首先，安装插件：

    <CodeBlockTabs defaultValue="npm" groupId="persist-install">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm install @chargebee/better-auth
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add @chargebee/better-auth
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add @chargebee/better-auth
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add @chargebee/better-auth
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    <Callout>
      如果您使用单独的客户端和服务器设置，请确保在项目的两个部分中都安装该插件。
    </Callout>
  </Step>

  <Step>
    ### 安装 Chargebee SDK [#安装-chargebee-sdk]

    接下来，在服务器上安装 Chargebee SDK：

    <CodeBlockTabs defaultValue="npm" groupId="persist-install">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm install chargebee
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add chargebee
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add chargebee
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add chargebee
        ```
      </CodeBlockTab>
    </CodeBlockTabs>
  </Step>

  <Step>
    ### 将插件添加到认证配置 [#将插件添加到认证配置]

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth"
    import { chargebee } from "@chargebee/better-auth"
    import Chargebee from "chargebee"

    const chargebeeClient = new Chargebee({
        apiKey: process.env.CHARGEBEE_API_KEY!,
        site: process.env.CHARGEBEE_SITE!,
    })

    export const auth = betterAuth({
        // ... 您的现有配置
        plugins: [
            chargebee({
                chargebeeClient,
                createCustomerOnSignUp: true,
                webhookUsername: process.env.CHARGEBEE_WEBHOOK_USERNAME,
                webhookPassword: process.env.CHARGEBEE_WEBHOOK_PASSWORD,
            })
        ]
    })
    ```
  </Step>

  <Step>
    ### 添加客户端插件 [#添加客户端插件]

    ```ts title="auth-client.ts"
    import { createAuthClient } from "better-auth/client"
    import { chargebeeClient } from "@chargebee/better-auth/client"

    export const authClient = createAuthClient({
        // ... 您的现有配置
        plugins: [
            chargebeeClient({
                subscription: true // 如果要启用订阅管理
            })
        ]
    })
    ```
  </Step>

  <Step>
    ### 迁移数据库 [#迁移数据库]

    运行迁移或生成架构以向数据库添加必要的表。

    <Tabs items="[&#x22;migrate&#x22;, &#x22;generate&#x22;]">
      <Tab value="migrate">
        ```bash
        npx auth migrate
        ```
      </Tab>

      <Tab value="generate">
        ```bash
        npx auth generate
        ```
      </Tab>
    </Tabs>

    请参阅 [架构](#schema) 部分以手动添加表。
  </Step>

  <Step>
    ### 设置 Chargebee Webhook [#设置-chargebee-webhook]

    在您的 Chargebee 仪表板中创建一个指向以下地址的 Webhook 端点：

    ```
    https://your-domain.com/api/auth/chargebee/webhook
    ```

    `/api/auth` 是认证服务器的默认路径。

    确保至少选择以下事件：

    * `subscription_created`
    * `subscription_activated`
    * `subscription_changed`
    * `subscription_renewed`
    * `subscription_started`
    * `subscription_cancelled`
    * `subscription_cancellation_scheduled`
    * `customer_deleted`

    如果设置了 `webhookUsername` 和 `webhookPassword`，请在 Chargebee Webhook 设置中配置相同的 Basic Authentication 凭据。
  </Step>
</Steps>

## 用法 [#用法]

### 客户管理 [#客户管理]

您可以将此插件仅用于客户管理，而无需启用订阅。这在您只想将 Chargebee 客户与用户关联时非常有用。

当设置 `createCustomerOnSignUp: true` 时，注册时会自动创建一个 Chargebee 客户，并将其关联到数据库中的用户。
您可以自定义客户创建流程：

```ts title="auth.ts"
chargebee({
    // ... 其他选项
    createCustomerOnSignUp: true,
    onCustomerCreate: async ({ chargebeeCustomer, user }) => {
        // 对新创建的客户执行一些操作
        console.log(`Customer ${chargebeeCustomer.id} created for user ${user.id}`);
    },
})
```

#### 传递额外的客户参数 [#传递额外的客户参数]

Better Auth 将名称存储在单个 `user.name` 字段中。如果您希望在创建时传递 `first_name`、`last_name` 或任何其他 Chargebee 客户字段，请使用 `getCustomerCreateParams`：

```ts title="auth.ts"
chargebee({
    // ... 其他选项
    createCustomerOnSignUp: true,
    getCustomerCreateParams: (user) => {
        const [firstName, ...rest] = (user.name ?? "").split(" ");
        return {
            first_name: firstName || undefined,
            last_name: rest.join(" ") || undefined,
            // phone: user.phoneNumber,
            // 任何其他 Chargebee Customer.CreateInputParam 字段
        };
    },
})
```

此回调接收 `user` 对象和一个可选的 `ctx`（请求上下文——仅在按需创建客户时可用，而不是在注册期间）。

<Callout type="info">
  `getCustomerCreateParams` 适用于 `createCustomerOnSignUp` 和按需客户创建（例如，当用户首次订阅且没有先前注册时）。对于组织，请使用 `organization.getCustomerCreateParams`。
</Callout>

### 订阅管理 [#订阅管理]

#### 定义计划 [#定义计划]

Chargebee 使用基于项目的计费模型。您可以静态定义订阅计划，也可以从数据库中动态定义：

**静态计划：**

```ts title="auth.ts"
subscription: {
    enabled: true,
    plans: [
        {
            name: "starter", // 存储到数据库时计划名称会自动转为小写
            itemPriceId: "starter-USD-Monthly", // 来自 Chargebee 的 item price ID
            type: "plan",
            limits: {
                projects: 5,
                storage: 10
            }
        },
        {
            name: "pro",
            itemPriceId: "pro-USD-Monthly",
            type: "plan",
            limits: {
                projects: 20,
                storage: 50
            },
            freeTrial: {
                days: 14,
            }
        }
    ]
}
```

**从数据库动态获取计划（推荐）：**

推荐从您自己的数据库中获取计划。这样您可以完全控制计划数据，使用自定义元数据（限制、功能、展示信息）丰富计划，并避免在认证配置中硬编码 Chargebee 配置：

```ts title="auth.ts"
subscription: {
    enabled: true,
    plans: async () => {
        const plans = await db.query("SELECT * FROM plans");
        return plans.map(plan => ({
            name: plan.name,
            itemPriceId: plan.chargebee_item_price_id,
            type: "plan" as const,
            limits: JSON.parse(plan.limits)
        }));
    }
}
```

更多内容请参见 [计划配置](#plan-configuration)。

#### 创建订阅 [#创建订阅]

要创建新订阅，请使用 `subscription.create` 方法：

**Endpoint:** `POST /subscription/create`

### Client Side

```ts
const { data, error } = await authClient.subscription.create({
    itemPriceId: "pro-USD-Monthly", // required, 来自 Chargebee 的 item price ID(s)。单个字符串或数组用于多项目订阅。
    referenceId: "123", // 订阅的引用 ID。默认基于 customerType。
    metadata, // 要与订阅一起存储的附加元数据。
    customerType, // 计费客户的类型。（默认："user"）
    seats: 1, // 席位数量（如果适用）。
    successUrl, // required, 用户完成付款或设置后重定向的 URL。
    cancelUrl, // required, 如果设置，用户取消后将跳转至此。
    disableRedirect: false, // required, 禁用订阅成功后的重定向。
    trialEnd, // 试用结束时间（Unix 时间戳）。
});
```

### Server Side

```ts
const data = await auth.api.createSubscription({
    body: {
        itemPriceId: "pro-USD-Monthly", // required, 来自 Chargebee 的 item price ID(s)。单个字符串或数组用于多项目订阅。
        referenceId: "123", // 订阅的引用 ID。默认基于 customerType。
        metadata, // 要与订阅一起存储的附加元数据。
        customerType, // 计费客户的类型。（默认："user"）
        seats: 1, // 席位数量（如果适用）。
        successUrl, // required, 用户完成付款或设置后重定向的 URL。
        cancelUrl, // required, 如果设置，用户取消后将跳转至此。
        disableRedirect: false, // required, 禁用订阅成功后的重定向。
        trialEnd, // 试用结束时间（Unix 时间戳）。
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type createSubscription = {
    /**
     * 来自 Chargebee 的 item price ID(s)。单个字符串或数组用于多项目订阅。
     */
    itemPriceId: string | string[] = "pro-USD-Monthly"
    /**
     * 订阅的引用 ID。默认基于 customerType。
     */
    referenceId?: string = "123"
    /**
     * 要与订阅一起存储的附加元数据。
     */
    metadata?: Record<string, any>
    /**
     * 计费客户的类型。（默认："user"）
     */
    customerType?: "user" | "organization"
    /**
     * 席位数量（如果适用）。
     */
    seats?: number = 1
    /**
     * 用户完成付款或设置后重定向的 URL。
     */
    successUrl: string
    /**
     * 如果设置，用户取消后将跳转至此。
     */
    cancelUrl: string
    /**
     * 禁用订阅成功后的重定向。
     */
    disableRedirect: boolean = false
    /**
     * 试用结束时间（Unix 时间戳）。
     */
    trialEnd?: number
}
```

**简单示例：**

这将创建一个 Chargebee Hosted Page，并将用户重定向到 Chargebee 结账页面。

```ts
await authClient.subscription.create({
    itemPriceId: "pro-USD-Monthly",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
});
```

<Callout type="info">
  **结账重定向的工作原理**

  插件**不会**直接重定向到您的 `successUrl`。相反，它将 Chargebee 的 `redirect_url` 设置为一个内部注册的端点：

  ```
  GET {baseURL}/subscription/success?callbackURL=<your-successUrl>&subscriptionId=<id>
  ```

  Chargebee 在成功结账后访问该端点，然后插件会立即将用户转发到您原始的 `successUrl`。这为插件提供了一个钩子点，位于 Chargebee 的托管式页面重定向和您的应用程序之间，这样可以在不更改调用站点代码的情况下插入未来的中间件（例如会话刷新、订阅同步）。
</Callout>

#### 切换计划 [#切换计划]

要将现有订阅切换到不同计划，请使用 `subscription.update` 方法。这确保用户只为新计划付费，而不是同时为两个计划付费：

**Endpoint:** `POST /subscription/update`

### Client Side

```ts
const { data, error } = await authClient.subscription.update({
    itemPriceId: "pro-USD-Monthly", // required, 来自 Chargebee 的 item price ID(s)。单个字符串或数组用于多项目订阅。
    referenceId: "123", // 订阅的引用 ID。默认基于 customerType。
    subscriptionId: "sub_123", // 要更新的订阅 ID。
    metadata, // 要与订阅一起存储的附加元数据。
    customerType, // 计费客户的类型。（默认："user"）
    seats: 1, // 要更新到的席位数量（如果适用）。
    successUrl, // required, 用户完成付款或设置后重定向的 URL。
    cancelUrl, // required, 如果设置，用户取消后将跳转至此。
    returnUrl, // 返回的 URL。
    disableRedirect: false, // required, 禁用重定向。
});
```

### Server Side

```ts
const data = await auth.api.updateSubscription({
    body: {
        itemPriceId: "pro-USD-Monthly", // required, 来自 Chargebee 的 item price ID(s)。单个字符串或数组用于多项目订阅。
        referenceId: "123", // 订阅的引用 ID。默认基于 customerType。
        subscriptionId: "sub_123", // 要更新的订阅 ID。
        metadata, // 要与订阅一起存储的附加元数据。
        customerType, // 计费客户的类型。（默认："user"）
        seats: 1, // 要更新到的席位数量（如果适用）。
        successUrl, // required, 用户完成付款或设置后重定向的 URL。
        cancelUrl, // required, 如果设置，用户取消后将跳转至此。
        returnUrl, // 返回的 URL。
        disableRedirect: false, // required, 禁用重定向。
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type updateSubscription = {
    /**
     * 来自 Chargebee 的 item price ID(s)。单个字符串或数组用于多项目订阅。
     */
    itemPriceId: string | string[] = "pro-USD-Monthly"
    /**
     * 订阅的引用 ID。默认基于 customerType。
     */
    referenceId?: string = "123"
    /**
     * 要更新的订阅 ID。
     */
    subscriptionId?: string = "sub_123"
    /**
     * 要与订阅一起存储的附加元数据。
     */
    metadata?: Record<string, any>
    /**
     * 计费客户的类型。（默认："user"）
     */
    customerType?: "user" | "organization"
    /**
     * 要更新到的席位数量（如果适用）。
     */
    seats?: number = 1
    /**
     * 用户完成付款或设置后重定向的 URL。
     */
    successUrl: string
    /**
     * 如果设置，用户取消后将跳转至此。
     */
    cancelUrl: string
    /**
     * 返回的 URL。
     */
    returnUrl?: string
    /**
     * 禁用重定向。
     */
    disableRedirect: boolean = false
}
```

```ts title="client.ts"
await authClient.subscription.update({
    itemPriceId: "enterprise-USD-Monthly", // 新的 item price id
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
});
```

<Callout type="info">
  该插件仅支持每个引用 ID（用户或组织）同时只有一个活跃或试用中的订阅。使用 `subscription.update` 当用户已有活跃订阅并希望切换计划时。当用户没有活跃订阅时，请使用 `subscription.create`。

  如果用户已有活跃订阅，则**必须**使用 `subscription.update` 并提供 `subscriptionId` 参数。否则，尝试使用 `subscription.create` 创建新订阅将因 `ALREADY_SUBSCRIBED` 错误而失败。
</Callout>

#### 列出活跃订阅 [#列出活跃订阅]

要获取当前用户或组织的活跃订阅，请使用 `subscription.list` 方法：

**Endpoint:** `GET /subscription/list`

### Client Side

```ts
const { data, error } = await authClient.subscription.list({
    query: {
        referenceId, // 订阅的引用 ID。默认基于 customerType。
        customerType, // 计费客户的类型。（默认："user"）
    },
});
```

### Server Side

```ts
const data = await auth.api.listActiveSubscriptions({
    query: {
        referenceId, // 订阅的引用 ID。默认基于 customerType。
        customerType, // 计费客户的类型。（默认："user"）
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type listActiveSubscriptions = {
    /**
     * 订阅的引用 ID。默认基于 customerType。
     */
    referenceId?: string
    /**
     * 计费客户的类型。（默认："user"）
     */
    customerType?: "user" | "organization"
}
```

```ts title="client.ts"
const { data } = await authClient.subscription.list();
// data → 经过计划限制和 itemPriceId 增强的活跃/试用中订阅数组

// 对于组织：
const { data: orgSubscriptions } = await authClient.subscription.list({
    query: {
        referenceId: "org_123",
        customerType: "organization"
    }
});
```

#### 取消订阅 [#取消订阅]

要取消订阅，请使用 `subscription.cancel` 方法。这会将用户重定向到 Chargebee Portal，用户可以在那里取消订阅。当订阅在当前计费周期结束时取消时，Chargebee 会将其标记为 `non_renewing`。只有在周期结束时，订阅状态才会变为 `cancelled`。

**Endpoint:** `POST /subscription/cancel`

### Client Side

```ts
const { data, error } = await authClient.subscription.cancel({
    referenceId: 'org_123', // 要取消的订阅的引用 ID。默认基于 customerType。
    customerType, // 计费客户的类型。（默认："user"）
    subscriptionId: 'sub_123', // 要取消的订阅的 ID。
    returnUrl: '/account', // required, 用户点击返回您的网站链接时要跳转的 URL。
});
```

### Server Side

```ts
const data = await auth.api.cancelSubscription({
    body: {
        referenceId: 'org_123', // 要取消的订阅的引用 ID。默认基于 customerType。
        customerType, // 计费客户的类型。（默认："user"）
        subscriptionId: 'sub_123', // 要取消的订阅的 ID。
        returnUrl: '/account', // required, 用户点击返回您的网站链接时要跳转的 URL。
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type cancelSubscription = {
    /**
     * 要取消的订阅的引用 ID。默认基于 customerType。
     */
    referenceId?: string = 'org_123'
    /**
     * 计费客户的类型。（默认："user"）
     */
    customerType?: "user" | "organization"
    /**
     * 要取消的订阅的 ID。
     */
    subscriptionId?: string = 'sub_123'
    /**
     * 用户点击返回您的网站链接时要跳转的 URL。
     */
    returnUrl: string = '/account'
}
```

<Callout type="info">
  **理解取消状态**

  Chargebee 支持不同的取消行为：

  | 字段           | 描述                      |
  | ------------ | ----------------------- |
  | `canceledAt` | 订阅被取消的时间。               |
  | `status`     | 当订阅结束时状态变为 "cancelled"。 |
</Callout>

#### 计费门户会话 [#计费门户会话]

为了获得完整的自助计费体验，您可以打开 Chargebee 客户门户，让用户管理其计费的所有方面：

**Endpoint:** `POST /subscription/portal`

### Client Side

```ts
const { data, error } = await authClient.subscription.portal({
    referenceId: 'org_123', // 订阅的引用 ID。默认基于 customerType。
    customerType, // 计费客户的类型。（默认："user"）
    returnUrl: '/account', // required, 用户完成门户会话后重定向的 URL。
    disableRedirect: false, // 禁用重定向。
});
```

### Server Side

```ts
const data = await auth.api.createPortalSession({
    body: {
        referenceId: 'org_123', // 订阅的引用 ID。默认基于 customerType。
        customerType, // 计费客户的类型。（默认："user"）
        returnUrl: '/account', // required, 用户完成门户会话后重定向的 URL。
        disableRedirect: false, // 禁用重定向。
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type createPortalSession = {
    /**
     * 订阅的引用 ID。默认基于 customerType。
     */
    referenceId?: string = 'org_123'
    /**
     * 计费客户的类型。（默认："user"）
     */
    customerType?: "user" | "organization"
    /**
     * 用户完成门户会话后重定向的 URL。
     */
    returnUrl: string = '/account'
    /**
     * 禁用重定向。
     */
    disableRedirect?: boolean = false
}
```

**示例：**

```ts title="client.ts"
await authClient.subscription.portal({
    returnUrl: "/account/billing",
    fetchOptions: {
        onSuccess: (ctx) => {
            // 重定向到 Chargebee 门户
            window.location.href = ctx.data.url;
        }
    }
});
```

**用于组织计费：**

```ts title="client.ts"
await authClient.subscription.portal({
    referenceId: "org_123456",
    customerType: "organization",
    returnUrl: "/org/billing"
});
```

门户允许用户：

* 更新支付方式（信用卡、银行账户）
* 查看和下载发票
* 管理订阅（升级、降级、取消）
* 更新账单地址和联系人信息
* 查看订阅历史记录
* 应用促销优惠码

<Callout type="info">
  门户会话提供完整的自助服务体验，并推荐在您希望让用户完全控制其计费时，优于单独的操作（如取消）。
</Callout>

### 引用系统 [#引用系统]

默认情况下，订阅会关联到用户 ID。不过，您可以使用自定义引用 ID 将订阅关联到其他实体，例如组织：

```ts title="client.ts"
// 为组织创建订阅
await authClient.subscription.create({
    itemPriceId: "team-USD-Monthly",
    referenceId: "org_123456",
    customerType: "organization",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    seats: 10 // 团队计划的座位数
});

// 列出组织的订阅
const { data: subscriptions } = await authClient.subscription.list({
    query: {
        referenceId: "org_123456",
        customerType: "organization"
    }
});
```

#### 带席位数的团队订阅 [#带席位数的团队订阅]

对于团队或组织计划，您可以指定席位数量：

```ts
await authClient.subscription.create({
    itemPriceId: "team-USD-Monthly",
    referenceId: "org_123456",
    customerType: "organization",
    seats: 10, // 10 名团队成员
    successUrl: "/org/billing/success",
    cancelUrl: "/org/billing"
});
```

`seats` 参数将作为订阅项的数量传递给 Chargebee。您可以在应用逻辑中使用该值来限制团队或组织中的成员数量。

要授权引用 ID，请实现 `authorizeReference` 函数：

```ts title="auth.ts"
subscription: {
    // ... 其他选项
    authorizeReference: async ({ user, session, referenceId, action }) => {
        // 检查用户是否有权限管理此引用的订阅
        if (action === "create-subscription" || action === "update-subscription" || action === "cancel-subscription" || action === "billing-portal") {
            const org = await db.member.findFirst({
                where: {
                    organizationId: referenceId,
                    userId: user.id
                }   
            });
            return org?.role === "owner"
        }
        return true;
    }
}
```

### Webhook 处理 [#webhook-处理]

此插件会自动处理来自 Chargebee 的常见 Webhook 事件：

* **`subscription_created`** – 在 Chargebee 中创建订阅时创建订阅。
* **`subscription_activated`** – 当订阅在 Chargebee 中变为活跃时更新订阅。
* **`subscription_changed`** – 当 Chargebee 中发生更改时更新订阅。
* **`subscription_renewed`** – 在续订时更新订阅。
* **`subscription_started`** – 当试用结束并且订阅开始时更新订阅。
* **`subscription_cancelled`** – 将订阅标记为已取消。
* **`subscription_cancellation_scheduled`** – 使用计划取消的详细信息更新订阅。
* **`customer_deleted`** – 移除客户及其关联的任何订阅。

您也可以使用 `webhookHandler` 处理自定义事件，这为您提供了对类型化处理程序的直接访问：

```ts title="auth.ts"
import { WebhookEventType, WebhookHandler } from "chargebee"

chargebee({
    chargebeeClient,
    createCustomerOnSignUp: true,
    webhookHandler: (handler: WebhookHandler) => {
        handler.on(WebhookEventType.PaymentFailed, async ({ event }) => {
            // 处理支付失败
        });
        handler.on(WebhookEventType.InvoiceGenerated, async ({ event }) => {
            // 处理已生成的发票
        });
    }
})
```

### 订阅生命周期钩子 [#订阅生命周期钩子]

您可以在各种订阅生命周期事件中挂钩：

```ts title="auth.ts"
subscription: {
    // ... 其他选项
    onSubscriptionComplete: async ({ subscription, chargebeeSubscription, plan }) => {
        // 当通过托管页面成功创建订阅时调用
        await sendWelcomeEmail(subscription.referenceId, plan.name);
    },
    onSubscriptionCreated: async ({ subscription, chargebeeSubscription, plan }) => {
        // 当订阅被创建时调用
        await sendSubscriptionCreatedEmail(subscription.referenceId, plan.name);
    },
    onSubscriptionUpdate: async ({ subscription }) => {
        // 当订阅被更新时调用
        console.log(`Subscription ${subscription.id} updated`);
    },
    onSubscriptionDeleted: async ({ subscription, chargebeeSubscription }) => {
        // 当订阅被删除时调用
        await sendCancellationEmail(subscription.referenceId);
    },
    onTrialStart: async ({ subscription }) => {
        // 当试用开始时调用
        await sendTrialStartEmail(subscription.referenceId);
    },
    onTrialEnd: async ({ subscription }) => {
        // 当试用结束时调用
        await sendTrialEndEmail(subscription.referenceId);
    }
}
```

### 试用期 [#试用期]

您可以为计划配置试用期：

```ts title="auth.ts"
{
    name: "pro",
    itemPriceId: "pro-USD-Monthly",
    type: "plan",
    freeTrial: {
        days: 14,  // 自动应用 14 天试用期
    },
    limits: {
        projects: 100,
        storage: 500,
    }
}
```

当用户订阅此计划时，**试用期会自动应用**——无需手动传递 `trialEnd`：

```ts
// 将根据计划配置自动计算并应用试用期
await authClient.subscription.create({
    itemPriceId: "pro-USD-Monthly",  // 带 14 天试用期的计划
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
});
// ✅ 用户会自动获得 14 天试用期！
```

插件会将试用结束日期计算为：**当前日期 + 试用天数**。

#### 防止重复试用 [#防止重复试用]

为了防止用户获得多次试用，请启用 `preventDuplicateTrials`：

```ts
subscription: {
    enabled: true,
    plans,
    preventDuplicateTrials: true,  // 用户只能获得一次试用
}
```

#### 覆盖试用结束日期（可选） [#覆盖试用结束日期可选]

要设置自定义试用结束日期，请传入 `trialEnd`（Unix 时间戳）：

```ts
await authClient.subscription.create({
    itemPriceId: "pro-USD-Monthly",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    trialEnd: 1735689600,  // 自定义试用结束：2025 年 1 月 1 日
});
```

<Callout type="info">
  试用期仅适用于**新订阅**。对现有订阅的更新不能包含试用期（Chargebee 限制）。
</Callout>

## 架构 [#架构]

Chargebee 插件会向你的数据库添加以下表：

### 用户 [#用户]

表名：`user`



<DatabaseTable name="user" fields="chargebeeUserTableFields" />

### 组织 [#组织]

表名：`organization` <small className="text-xs">(仅当 `organization.enabled` 为 `true` 时)</small>



<DatabaseTable name="organization" fields="chargebeeOrganizationTableFields" />

### 订阅 [#订阅]

表名：`subscription`



<DatabaseTable name="subscription" fields="chargebeeSubscriptionTableFields" />

### 订阅项 [#订阅项]

表名：`subscriptionItem`



<DatabaseTable name="subscriptionItem" fields="chargebeeSubscriptionItemTableFields" />

### 自定义架构 [#自定义架构]

如果支持，你可以向 Chargebee 插件传入 `schema` 选项来更改架构表名或字段：

```ts title="auth.ts"
chargebee({
    // ... 其他选项
    schema: {
        subscription: {
            modelName: "chargebeeSubscriptions", // 将 subscription 表映射到 chargebeeSubscriptions
            fields: {
                referenceId: "userId" // 将 referenceId 字段映射到 userId
            }
        }
    }
})
```

## 选项 [#选项]

| 选项                        | 类型          | 描述                                                                                         |
| ------------------------- | ----------- | ------------------------------------------------------------------------------------------ |
| `chargebeeClient`         | `Chargebee` | Chargebee 客户端实例。&#x2A;*必需。**                                                               |
| `webhookUsername`         | `string`    | Webhook 端点的基本认证用户名。建议在生产环境中使用。                                                             |
| `webhookPassword`         | `string`    | Webhook 端点的基本认证密码。建议在生产环境中使用。                                                              |
| `createCustomerOnSignUp`  | `boolean`   | 用户注册时是否自动创建 Chargebee 客户。默认值：`false`。                                                      |
| `getCustomerCreateParams` | `function`  | 返回传递给 `cb.customer.create` 的额外参数（用于用户客户），例如 `first_name`、`last_name`。接收 `user` 和可选的 `ctx`。 |
| `onCustomerCreate`        | `function`  | 客户创建后的回调函数。接收 `{ chargebeeCustomer, user }`。                                               |
| `webhookHandler`          | `function`  | 接收 webhook 处理程序实例的回调函数。调用 `handler.on(EventType, fn)` 来注册类型化的事件监听器。                        |
| `subscription`            | `object`    | 订阅配置。参见下面的 [订阅选项](#订阅选项)。                                                                  |
| `organization`            | `object`    | 启用组织客户支持。参见下面的 [组织选项](#组织选项)。                                                              |

### 订阅选项 [#订阅选项]

| 选项                         | 类型                             | 描述                                                                              |
| -------------------------- | ------------------------------ | ------------------------------------------------------------------------------- |
| `enabled`                  | `boolean`                      | 是否启用订阅功能。&#x2A;*必需。**                                                           |
| `plans`                    | `ChargebeePlan[]` 或 `function` | 订阅计划数组或返回计划的异步函数。如果启用，则**必需**。                                                  |
| `requireEmailVerification` | `boolean`                      | 是否要求在允许创建订阅之前进行电子邮件验证。默认值：`false`。                                              |
| `preventDuplicateTrials`   | `boolean`                      | 防止用户获得多个试用。默认值：`false`。                                                         |
| `authorizeReference`       | `function`                     | 授权参考 ID。接收 `{ user, session, referenceId, action }` 和上下文。                       |
| `getHostedPageParams`      | `function`                     | 自定义 Chargebee Hosted Page 参数。接收 `{ user, session, plan, subscription }`、请求和上下文。 |
| `onSubscriptionComplete`   | `function`                     | 通过托管页面创建订阅后调用。接收 `{ subscription, chargebeeSubscription, plan }`。               |
| `onSubscriptionCreated`    | `function`                     | 订阅创建后调用。接收 `{ subscription, chargebeeSubscription, plan }`。                     |
| `onSubscriptionUpdate`     | `function`                     | 订阅更新时调用。接收 `{ subscription }`。                                                  |
| `onSubscriptionDeleted`    | `function`                     | 订阅删除时调用。接收 `{ subscription, chargebeeSubscription }`。                           |
| `onTrialStart`             | `function`                     | 试用开始时调用。接收 `{ subscription }`。                                                  |
| `onTrialEnd`               | `function`                     | 试用结束时调用。接收 `{ subscription }`。                                                  |

#### 计划配置 [#计划配置]

| 选项                | 类型       | 描述                                              |
| ----------------- | -------- | ----------------------------------------------- |
| `name`            | `string` | 计划名称。&#x2A;*必需。**                               |
| `itemPriceId`     | `string` | Chargebee 项目价格 ID。&#x2A;*必需。**                  |
| `itemId`          | `string` | Chargebee 项目 ID。可选。                             |
| `itemFamilyId`    | `string` | Chargebee 项目系列 ID。可选。                           |
| `type`            | `string` | 类型：`"plan"`、`"addon"` 或 `"charge"`。&#x2A;*必需。** |
| `limits`          | `object` | 计划限制（例如 `{ projects: 10, storage: 5 }`）。        |
| `freeTrial`       | `object` | 试用配置。参见下面的 [免费试用配置](#免费试用配置)。                   |
| `trialPeriod`     | `number` | 试用期长度。可选。                                       |
| `trialPeriodUnit` | `string` | `"day"` 或 `"month"`。可选。                         |
| `billingCycles`   | `number` | 计费周期数。可选。                                       |

#### 免费试用配置 [#免费试用配置]

| 选项     | 类型       | 描述                |
| ------ | -------- | ----------------- |
| `days` | `number` | 试用天数。&#x2A;*必需。** |

### 组织选项 [#组织选项]

| 选项                        | 类型         | 描述                                                          |
| ------------------------- | ---------- | ----------------------------------------------------------- |
| `enabled`                 | `boolean`  | 启用组织客户支持。&#x2A;*必需。**                                       |
| `getCustomerCreateParams` | `function` | 为组织自定义 Chargebee 客户创建参数。接收 `organization` 和上下文。             |
| `onCustomerCreate`        | `function` | 组织客户创建后的回调函数。接收 `{ chargebeeCustomer, organization }` 和上下文。 |

## 高级用法 [#高级用法]

### 与组织一起使用 [#与组织一起使用]

Chargebee 插件与 [organization 插件](/docs/plugins/organization) 集成，可将组织作为 Chargebee 客户使用。订阅的计费主体不再是单个用户，而是组织。对于 B2B 服务，这很有用，因为计费绑定到组织而不是个人用户。

<Callout type="info">
  **启用组织客户时：**

  * 组织首次订阅时，会自动创建 Chargebee 客户
  * 组织名称更改会同步到 Chargebee 客户
  * 拥有活动订阅的组织无法被删除
</Callout>

#### 启用组织客户 [#启用组织客户]

要启用组织客户，请将 `organization.enabled` 设置为 `true`，并确保已安装 organization 插件：

```ts title="auth.ts"
plugins: [
    organization(),
    chargebee({
        // ... 其他选项
        subscription: {
            enabled: true,
            plans: [...],
        },
        organization: { // [!code highlight]
            enabled: true // [!code highlight]
        } // [!code highlight]
    })
]
```

<Callout type="info">
  当 `organization.enabled: true` 时，插件会自动从 `user` 表中排除 `chargebeeCustomerId` 并禁用用户级计费钩子。使用仅限组织范围的计费时，无需将该列添加到数据库架构中。
</Callout>

#### 创建组织订阅 [#创建组织订阅]

即使启用了组织客户，用户订阅仍然可用，并且是默认选项。若要使用组织作为计费主体，请传入 `customerType: "organization"`：

```ts title="client.ts"
await authClient.subscription.create({
    itemPriceId: "team-USD-Monthly",
    referenceId: activeOrg.id,
    customerType: "organization", // [!code highlight]
    seats: 10,
    successUrl: "/org/billing/success",
    cancelUrl: "/org/billing"
});
```

#### 授权 [#授权]

请务必实现 `authorizeReference` 函数，以验证用户是否有权限管理该组织的订阅：

```ts title="auth.ts"
subscription: {
    // ... 其他订阅选项
    authorizeReference: async ({ user, referenceId, action }) => {
        const member = await db.members.findFirst({
            where: {
                userId: user.id,
                organizationId: referenceId
            }
        });

        return member?.role === "owner" || member?.role === "admin";
    }
}
```

#### 组织计费邮箱 [#组织计费邮箱]

与用户不同，组织的计费邮箱不会自动同步，因为组织本身没有唯一的邮箱。组织通常会使用一个与用户账户分开的专用计费邮箱。
要在结账后更改计费邮箱，请通过 Chargebee Dashboard 更新，或使用 `chargebeeClient` 实现自定义逻辑：

```ts
await chargebeeClient.customer.update(organization.chargebeeCustomerId, {
    email: "billing@company.com"
});
```

### 自定义 Hosted Page 参数 [#自定义-hosted-page-参数]

你可以为 Chargebee Hosted Page 自定义附加参数：

```ts title="auth.ts"
getHostedPageParams: async ({ user, session, plan, subscription }, request, ctx) => {
    return {
        embed: false,
        layout: "in_app",
        pass_thru_content: JSON.stringify({
            userId: user.id,
            planType: "business"
        }),
        redirect_url: "https://yourdomain.com/success",
        cancel_url: "https://yourdomain.com/cancel"
    };
}
```

### 试用期管理 [#试用期管理]

Chargebee 插件会自动防止用户获得多个免费试用期。一旦用户使用过一次试用期（无论哪个计划），他们就不再有资格在任何计划上获得额外试用。

**工作原理：**

* 系统会跟踪每个用户在所有计划中的试用使用情况
* 当用户订阅带有试用的计划时，系统会检查其订阅历史记录
* 如果用户曾经有过试用（通过 `trialStart`/`trialEnd` 字段或 `in_trial` 状态标识），则不会提供新的试用
* 这可以防止用户取消订阅后重新注册以获得多个免费试用

**示例场景：**

1. 用户订阅带有 7 天试用期的 "Starter" 计划
2. 用户在试用期结束后取消订阅
3. 用户尝试订阅 "Premium" 计划——不会提供试用
4. 用户将立即被收取 Premium 计划费用

启用 `preventDuplicateTrials` 后，这种行为会自动生效，无需额外配置。试用资格会在创建订阅时确定，且不能通过配置覆盖。

## 故障排查 [#故障排查]

### 列/字段命名错误 [#列字段命名错误]

如果你看到类似 `no such column: "chargebee_customer_id"` 或 `no such column: "chargebeeCustomerId"` 的错误：

**原因：** 数据库列名与适配器的 schema 定义不匹配。

**解决方案：**

1. 运行 `npx auth generate`，使用 Chargebee 插件字段重新生成 schema。
2. 运行 `npx auth migrate` 将迁移应用到数据库，或使用 ORM 的迁移工具。
3. 如果是从另一个适配器手动迁移，请确保列名符合数据库适配器的命名约定。
4. 查阅[数据库文档](/docs/concepts/database)，了解与你的适配器（Prisma、Drizzle、Kysely 等）对应的字段名映射。

### Webhook 问题 [#webhook-问题]

如果 webhook 没有被正确处理：

1. 检查 webhook URL 是否已在 Chargebee dashboard 中正确配置
2. 验证 Basic Auth 凭据（`webhookUsername` 和 `webhookPassword`）是否正确
3. 确保已在 Chargebee dashboard 中选择所有必要事件
4. 检查服务器日志中 webhook 处理期间是否有任何错误

### 订阅状态问题 [#订阅状态问题]

如果订阅状态没有正确更新：

1. 确保 webhook 事件已被接收并处理
2. 检查 `chargebeeCustomerId` 和 `chargebeeSubscriptionId` 字段是否已正确填充
3. 验证应用和 Chargebee 之间的 reference ID 是否匹配

### 在本地测试 Webhook [#在本地测试-webhook]

对于本地开发，你可以使用隧道（例如 ngrok）将 webhook 转发到本地环境：

```bash
ngrok http 3000
```

然后将 Chargebee webhook 配置为指向：

```
https://your-ngrok-url/api/auth/chargebee/webhook
```

请确保在 Chargebee 和本地环境变量中使用相同的 Basic Auth 凭据。

## 资源 [#资源]

* [Chargebee 文档](https://www.chargebee.com/docs/)
* [Better Auth 文档](https://www.better-auth.com/)

