> ## Documentation Index
> Fetch the complete documentation index at: https://docs.clinkbill.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 优惠与促销码

> Coupon 和 Promotion Code 怎么建，以及在各种接入方式里怎么应用。

Clink 的优惠分两层。搞清楚这两层的关系，后面的配置就都好理解了。

## 两层模型

**Coupon 是折扣规则** —— 减多少、适用哪些商品、持续几个账期、总共能兑换多少次。

**Promotion Code 是客户输入的那串码** —— 或者由你在后台预先绑定。

<Warning>
  建了 Coupon 不等于客户有码可用。**必须在这个 Coupon 下至少建一个 Promotion Code**，客户才有东西可输入。
</Warning>

所有接入示例里的 `promotionCode` 传的都是**客户可见的代码字符串**，比如 `WELCOME20`，不是 `couponId`，也不是 `promotionCodeId`。

## 创建

一次调用可以把 Coupon 和它下面的码一起建出来：

```json theme={null}
{
  "couponName": "Welcome offer",
  "discountType": "percentage",
  "percentage": 20,
  "applyType": "product",
  "applicableProducts": ["prd_xxxxx"],
  "durationType": "repeating",
  "durationMonths": 3,
  "promotionCodes": [
    {
      "code": "WELCOME20",
      "firstOrderOnly": true,
      "maxRedemptionLimit": 100
    }
  ]
}
```

字段规则：

* `discountType` 是 `percentage` 或 `fixed_amount`。百分比要大于 0 且不超过 100
* 固定金额用 `fixedAmounts` 按币种配置，金额仍然是**主货币单位**（19.99 就写 `19.99`）
* `applyType` 是 `none`、`product` 或 `price`。选后两个时必须给对应的 ID
* `durationType` 是 `once`、`repeating` 或 `forever`
* Promotion Code 只允许 1 到 32 位英文字母或数字。省略 `code` 时平台可以生成随机码
* 有效期用 13 位 Unix 毫秒时间戳。促销码的结束时间不会超过所属 Coupon 的结束时间

<Warning>
  **`durationMonths` 这个字段名有误导性。** 它按**计费期**算，不是自然月。月付订阅传 `3` 是三个月，周付订阅传 `3` 是三周。
</Warning>

单独给已有 Coupon 加码用 [`POST /promotion-code/{couponId}`](/cn/api-reference/endpoint/create-promotion-code)。完整字段见 [创建优惠券](/cn/api-reference/endpoint/create-coupon)。

## 在各种接入方式里怎么用

| 场景                       | 配置或调用                                                                                                   | 客户体验                                                      |
| ------------------------ | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| Hosted Checkout，允许客户自己输入 | 创建 Session 时传 `allowPromotionCodes: true`                                                               | 默认展示输入框，客户可以应用或移除                                         |
| 预先绑定，不让客户改               | `allowPromotionCodes: true` + `showPromotionCode: false` + `promotionCode: "WELCOME20"`                 | 创建 Session 时就校验；结账页展示优惠结果，但没有可编辑的输入框                      |
| Elements                 | Session 开启 `allowPromotionCodes`；前端调 `promoCodeChange({ type: "apply", code })` 或 `({ type: "clear" })` | 输入框你自己实现，用 `amount-change` 更新金额，用 `promo-code-error` 展示错误 |
| 直接支付                     | `POST /payment` 传 `promotionCode`                                                                       | 后端算完折扣再发起支付                                               |
| 直接创建订阅                   | `POST /subscription` 传 `promotionCode`                                                                  | 优惠进入首次账单，后续账期按 Coupon 的持续时间决定                             |
| 订阅升降级                    | preview 和 confirm **都传同一个** `promotionCode`                                                             | 预览返回折扣和补差价，确认后才真正应用                                       |

Elements 的完整前端事件代码在 [Elements 嵌入式收银台](/cn/elements)，这里不重复一份。

## 校验规则

这几条是接入时最容易踩的：

**临时商品只能配不限商品的 Coupon。** `priceDataList` 建的临时商品没有 `productId` 和 `priceId`，所以只能用 `applyType: none` 的 Coupon。限定了商品或价格的 Coupon 会校验失败。

**固定金额 Coupon 必须覆盖订单币种。** 订单原始币种在 `fixedAmounts` 里没配金额，这个 Coupon 就不适用。折扣大于订单金额时最多减到 0，不会产生负数应付金额。

**百分比折扣按币种最大小数位向下取整。** 前端不要自己重算 —— 展示 Session、预览接口或 Elements `amount-change` 返回的金额就行。

**`firstOrderOnly` 按这个 Customer 有没有成功过 Order 判断。** 不是按浏览器、不是按邮箱文本、也不是按你自己页面的访问次数。

**`minimumSpend` 按订单原始金额和原始币种校验。** 对应币种没配置就不满足条件。

**错误出现的时机不一样。** 隐藏优惠码模式在**创建 Session 时**就失败并返回错误；可见输入模式在**客户点应用时**才展示错误。

**兑换次数不用你自己核销。** 系统在订单创建时预占，支付成功后确认，支付失败后撤销。

## 订阅优惠持续多久

| `durationType` | 实际行为                                        |
| -------------- | ------------------------------------------- |
| `once`         | 只覆盖**一个实际付费账期**。订阅有免费试用时，试用不消耗这一次，首个付费账期才应用 |
| `repeating`    | 连续覆盖 `durationMonths` 个**计费期**。单位是期，不是自然月   |
| `forever`      | 后续符合商品、价格和币种条件的续费账单持续应用                     |

## 优惠不产生自己的 Webhook

优惠**不会**产生独立的「付款成功」事件。判断收款仍然看原来那套：

* 一次性付款看 Order / Session 结果
* 订阅看 Subscription / Invoice 结果

你可以在本地记录里存 `couponId`、`promotionCode`、原价、折扣金额和实付金额，用于展示和对账。但**不要在 Webhook 到达后自己重新算一遍折扣** —— 以 Clink 返回的金额为准。

## 两处待确认

<Warning>
  下面两点在实现和文档之间存在差异，接入时先别依赖：

  **`restrictedCustomerIds` 不要传空数组。** 接口描述说空数组表示不限制，但当前实现对非 null 的空数组会执行包含校验，结果可能是谁都用不了。不限制时**直接省略这个字段**。

  **`perCustomerRedemptionLimit` 的实际生效情况未确认。** 字段能存能返回，但在当前促销码校验链路里没找到按客户统计并拦截的逻辑。要靠它做每人限领时，先自己验证一遍。
</Warning>

## 接下来

<CardGroup cols={2}>
  <Card title="订阅支付" icon="repeat" href="/cn/subscriptions">
    周期价格、订阅状态和续费。
  </Card>

  <Card title="优惠券概念" icon="ticket" href="/cn/guides/resources/coupon">
    资源定义和后台管理。
  </Card>
</CardGroup>
