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

# 优惠与促销

> 创建优惠券和促销码，让买家在收银台使用

## 可以做什么

* 创建**优惠券**：按百分比或固定金额减免，可限定商品、总使用次数和到期时间，并带有买家在收银台输入的码
* 为同一张优惠券发放更多**促销码**，每个码有自己的买家限制（限定买家邮箱、单买家次数、最低消费）—— 例如每位达人一个码、每个推广渠道一个码，或一批一次性码
* 让买家在收银台输入码、分享已带码的商品链接，或在服务端创建 Checkout Session 时直接应用优惠券、预填促销码

> [!NOTE]
>
> 优惠适用于**一次性商品、新购订阅与更换方案**。在订阅上，优惠券的持续期决定优惠几个计费周期 —— 见[订阅](#subscriptions)。更换方案时，带新码会替换订阅原有的优惠，不带码则原优惠在可延续的情况下延续 —— 见[更换方案](#plan-changes)。

***

## 优惠券与促销码

**优惠券**定义优惠内容，并有一个买家可以输入的码 —— 创建时填写，或由 Pancake 生成。优惠券只限制到期时间与总使用次数，所以在此范围内任何买家都能使用优惠券自己的码。**促销码**是同一张优惠券的其他码，承载买家限制：一张优惠券可以有多个促销码，买家须满足促销码的限制，以及优惠券的到期时间与总使用次数。促销码可以在创建优惠券的同一请求中一并创建，也可以之后补充。从服务端用 `couponId` 应用的优惠券不需要码。

| 规则 | 设置在 | 说明 |
| - | - | - |
| 优惠方式 | 优惠券 | 按百分比减免，或按币种减免固定金额。订单币种不在固定金额中时不能使用，不做汇率换算 |
| 适用商品 | 优惠券 | 仅对部分商品生效，或全店通用 |
| 到期时间 | 优惠券、促销码 | 过了到期时间码不能再使用。促销码的到期时间不能晚于优惠券 |
| 总使用次数 | 优惠券、促销码 | 所有买家合计的使用次数。优惠券的上限合计它自己的码与全部促销码；促销码的上限不能超过优惠券 |
| 限定买家 | 促销码 | 只限这些买家邮箱（不区分大小写） |
| 单买家次数 | 促销码 | 每个买家累计可用该码的次数 |
| 最低消费 | 促销码 | 按币种设定，按优惠前、不含税的金额判断。未设置的币种不设门槛 |

优惠券及其码只属于一个店铺、一个环境：在测试模式创建的码在正式模式中不能使用。码不区分大小写 —— 买家输入 `spring-20` 即等同 `SPRING-20`。

***

## 创建优惠券

### 通过 Dashboard

在 **Dashboard → 优惠券** 中创建优惠券、添加促销码、批量生成码，并查看每个码的使用次数，详见 Dashboard 指南的[优惠券](https://docs.waffo.ai/zh/dashboard/discounts.md)。

### 通过 API 或 SDK

在服务端使用[优惠券](https://docs.waffo.ai/zh/api-reference/endpoints/coupons/overview.md)与[促销码](https://docs.waffo.ai/zh/api-reference/endpoints/promotion-codes/overview.md)端点创建：

```typescript TypeScript theme={"system"}
import { CouponDiscountType, WaffoPancake } from "@waffo/pancake-ts";

const client = new WaffoPancake({
  merchantId: process.env.WAFFO_MERCHANT_ID!,
  privateKey: process.env.WAFFO_PRIVATE_KEY!,
});

const { coupon } = await client.coupons.create(
  {
    storeId: "STO_xxx",
    name: "Spring sale",
    discount: { type: CouponDiscountType.Percentage, percentOff: 20 },
    maxRedemptions: 500,
    code: "SPRING-20", // the coupon's own code; omit it for a random one
    // Promotion codes created together with the coupon (up to 20), each with its own restrictions
    promotionCodes: [{ code: "SPRING-NEW", restrictions: { perCustomerLimit: 1 } }],
  },
  { idempotencyKey: "coupon-spring-2026" },
);

// A code for one influencer: 100 uses, at least $50 before tax
await client.promotionCodes.create({
  couponId: coupon.id,
  code: "KOL-ALICE",
  restrictions: { maxRedemptions: 100, minimumAmount: { USD: "50.00" } },
});
```

```go Go theme={"system"}
res, err := client.Coupons.Create(ctx, pancake.CreateCouponParams{
    StoreID:        "STO_xxx",
    Name:           "Spring sale",
    Discount:       pancake.CouponDiscount{Type: pancake.CouponDiscountTypePercentage, PercentOff: pancake.Ptr(20.0)},
    MaxRedemptions: pancake.Ptr(500),
    Code:           pancake.Ptr("SPRING-20"),
    PromotionCodes: []pancake.CreateCouponPromotionCodeParams{{
        Code:         pancake.Ptr("SPRING-NEW"),
        Restrictions: &pancake.PromotionCodeRestrictions{PerCustomerLimit: pancake.Ptr(1)},
    }},
}, pancake.WithIdempotencyKey("coupon-spring-2026"))
```

## 常见营销场景

商户最常做的几类促销分别该怎么配。每个场景都是一张优惠券，买家规则不同时再加促销码；在 [Dashboard](https://docs.waffo.ai/zh/dashboard/discounts.md) 或用 SDK 配置都可以。

> **新品上市：限时公开码**
>
> 所有人都能用的 8 折码，两周有效，并设上限防止超发。
>
> * **怎么配**：一张优惠券：百分比 20，到期两周后，兑换上限（比如 500），适用全部商品或只限新品。优惠券自带的码就是公开码，比如 `LAUNCH-20`。
> * **买家怎么领**：在收银台输码，优惠券一建好，商品链接就会显示输码框。投放广告和邮件时把码放进链接，买家不用手输：`…/product/{productId}?discount_code=LAUNCH-20`。
> * **注意**：到期在输码时和下单时各校验一次，买家在截止前打开页面、截止后付款会按原价收。优惠券到期也会结束已应用到订阅上的优惠。
>
> ```typescript theme={"system"}
> await client.coupons.create({
>   storeId: "STO_xxx",
>   name: "Launch sale",
>   code: "LAUNCH-20",
>   discount: { type: CouponDiscountType.Percentage, percentOff: 20 },
>   maxRedemptions: 500,
>   redeemBy: "2026-11-15T00:00:00.000Z",
> });
> ```

> **KOL 与渠道：一人一码**
>
> 优惠相同，但要知道每单来自哪个合作方，并分别限制各自的次数。
>
> * **怎么配**：一张优惠券定义优惠，再给它加促销码，一个合作方一个：`KOL-ALICE`、`KOL-BOB`、`PODCAST-10`，各自设总次数，需要的话再加最低订单金额。优惠券自带的码可以不公开。
> * **买家怎么领**：合作方把码或带 `?discount_code=KOL-ALICE` 的商品链接发给粉丝。
> * **怎么看**：优惠券详情里按码显示使用次数，导出 CSV 做合作方结算。合作结束只停用那一个码，其他码不受影响。
>
> ```typescript theme={"system"}
> await client.promotionCodes.create({
>   couponId: "CPN_xxx",
>   code: "KOL-ALICE",
>   restrictions: { maxRedemptions: 200, minimumAmount: { USD: "30.00" } },
> });
> ```

> **邮件订阅或抽奖：一人一个一次性码**
>
> 每位收件人一个只能用一次的码，码不会被转发传播。
>
> * **怎么配**：一张优惠券，在它上面**批量生成**：需要的数量（每批最多 500）、前缀如 `NEWS-`、「每个码最多可用 1 次」。下载 CSV 合并进邮件工具。
> * **买家怎么领**：每封邮件带一个码，买家在收银台输入。
> * **注意**：码在订单付款后用掉；未付款的订单会占用 72 小时，之后码重新可用。批量码是随机的，要知道谁用了哪个码只能拿 CSV 和订单邮箱对照。
>
> ```typescript theme={"system"}
> const { promotionCodes } = await client.promotionCodes.generate({
>   couponId: "CPN_xxx",
>   count: 500,
>   prefix: "NEWS-",
>   restrictions: { maxRedemptions: 1 },
> });
> ```

> **召回或 VIP 名单：只给指定买家**
>
> 只有名单上的买家能用，码泄露也没关系。
>
> * **怎么配**：一个促销码，打开「仅限指定买家使用」粘贴邮箱，再设「每位买家最多可用 1 次」。
> * **买家怎么领**：在收银台输码。码按买家在收银台填的邮箱匹配，所以买家还没填邮箱就应用时，页面会先提示填邮箱；不在名单上的买家会看到该码仅限指定买家。
> * **注意**：按邮箱精确匹配（不区分大小写）。买家用另一个邮箱结账会被拒，告知买家优惠发到了哪个邮箱。
>
> ```typescript theme={"system"}
> await client.promotionCodes.create({
>   couponId: "CPN_xxx",
>   code: "COMEBACK-30",
>   restrictions: { customerEmails: ["jane.doe@example.com"], perCustomerLimit: 1 },
> });
> ```

> **会员等级或合作伙伴价：你来决定，不展示码**
>
> 谁享受优惠由你的系统判断（会员等级、分销商、谈好的报价），买家不该看到或传播码。
>
> * **怎么配**：每个等级一张优惠券（`Gold 15%`、`Partner 25%`），码不用公开。
> * **买家怎么领**：你的服务端用 `couponId` 创建 session，把买家送到它的 `checkoutUrl`。收银台显示优惠和折后合计，不显示码也没有输码框，买家不能移除。
> * **注意**：`couponId` 需要 API Key，资格判断要在服务端做，不能用浏览器传来的值。用 Next.js SDK 时 `createCheckoutAction` 会剥掉 `couponId`，改在服务端调 `@waffo/pancake-ts`。想让买家在此基础上再叠自己的码，改用促销码：`promotionCode` 加 `allowPromotionCodes: true`，优惠券不能和输码并存。
>
> ```typescript theme={"system"}
> // 服务端代码，先判断买家等级
> const session = await client.checkout.createSession({
>   storeId: "STO_xxx",
>   productId: "PROD_xxx",
>   currency: "USD",
>   couponId: tier === "gold" ? "CPN_gold" : undefined,
>   buyerEmail: customer.email,
> });
> redirect(session.checkoutUrl);
> ```

> **订阅：前几个月优惠，或引导转年付**
>
> 月付方案「前 3 个月 75 折」，或者只在年付方案上有效的码，把买家引导到年付。
>
> * **怎么配**：前几个月优惠用持续期「多个计费周期」= 3 的优惠券。引导年付用只限年付商品的优惠券，这样码在月付方案上会被拒，原因 `PRODUCT_NOT_ELIGIBLE`。
> * **买家怎么领**：新订阅在收银台输码。已有的月付买家在买家门户换方案：年付方案有可用的码时，换方案页会显示输码框；也可以由你用 `promotionCode` 创建换方案 session 把链接发给买家。
> * **注意**：免费试用不消耗优惠，优惠从第一个付费周期开始；付费试用会消耗并算作第一个周期。换方案时新码替换当前优惠；不输码则当前优惠带着已用周期延续。见[订阅](#subscriptions)与[更换方案](#plan-changes)。
>
> ```typescript theme={"system"}
> await client.coupons.create({
>   storeId: "STO_xxx",
>   name: "Annual upgrade",
>   code: "GO-ANNUAL",
>   discount: { type: CouponDiscountType.Percentage, percentOff: 25 },
>   duration: CouponDuration.Repeating,
>   durationInPeriods: 3,
>   productIds: ["PROD_annual"],
> });
> ```

> **先测试，再上线**
>
> 优惠券只属于一个环境，测试模式建的券在正式模式看不到。
>
> * **怎么配**：在测试模式建好优惠券和码，用测试卡走一遍收银台。
> * **上线**：打开优惠券，点**复制到正式环境**（商品需先发布到正式环境）。副本从 0 次开始；勾选后生效中且未过期的促销码一起复制。用 API 的话，用正式环境的 API Key 再创建一次。
> * **注意**：正式环境已占用的码会让复制中止，先在正式环境停用它，或不复制码。

***

## 在收银台应用优惠

优惠通过以下四种方式之一进入收银台。买家付款前总能看到优惠后的含税总额。

| 方式 | 做法 | 买家能否更改 |
| - | - | - |
| 输码框 | 买家在收银台输入优惠券的码或促销码 | 能 —— 可以更换或移除 |
| 带码的商品链接 | 在商品链接上加 `discount_code` | 不能 —— 码已应用并锁定 |
| 服务端指定优惠券 | 用商户凭证（API Key 或 JWT）创建 Checkout Session 时传 `couponId` | 不能 —— 优惠已应用并锁定 |
| 服务端预填促销码 | 用商户凭证（API Key 或 JWT）创建 Checkout Session 时传 `promotionCode` | 同时传了 `allowPromotionCodes: true` 才能 —— 此时可以更换或移除 |

### 输码框

商品链接和店铺首页在店铺有可用于该商品的码时显示输码框：一张适用于该商品、未到期、总使用次数未达上限的 `active` 优惠券，且它自身的码或某个促销码为 `active`、未到期、自身使用次数未达上限。限定买家、最低消费等与买家相关的规则只在输码时校验。刚到期或刚达到上限的码，最多 5 分钟内仍可能显示输码框。用 API Key 创建的 Checkout Session 不传 `allowPromotionCodes` 时同样按此规则；传 `true` 始终显示输码框，传 `false` 则隐藏：

```typescript theme={"system"}
const session = await client.checkout.createSession({
  productId: "PROD_xxx",
  currency: "USD",
  allowPromotionCodes: true,
});
```

#### 常见场景

收银台在常见情况下如何判断，以及该怎么传参：

| 场景 | 输码框 | 需要传什么 |
| - | - | - |
| 你在 Dashboard 建了一张适用全部商品的优惠券；买家打开商品链接或店铺首页 | 显示 | 不用传，页面自动检测到优惠券 |
| 优惠券只限商品 A；买家打开商品 B | B 不显示，A 显示 | 不用传 |
| 店里唯一的优惠券已到期、已用完或已停用 | 不显示（最多 5 分钟内生效） | 不用传，想再显示就新建一张优惠券 |
| 优惠券唯一的码限定了买家邮箱 | 显示，检测不考虑买家规则。其他买家输码会被拒，原因 `CUSTOMER_NOT_ELIGIBLE`；买家还没填邮箱就应用时，页面先提示填邮箱 | 不用传 |
| 唯一的优惠券是 USD 固定金额；买家用 EUR 结账 | 显示，检测不考虑币种。输码会被拒，原因 `CURRENCY_MISMATCH` | 新建一张带 EUR 金额的优惠券，或保持现状 |
| 你用 API Key 在服务端创建 session | 不显示，即使店里有可用的码 | `allowPromotionCodes: true` 才显示 |
| 你在服务端创建 session，并希望预填一个码 | 码显示为已应用且锁定 | `promotionCode: "KOL-ALICE"`；再加 `allowPromotionCodes: true` 可让买家更换或移除 |
| 优惠由你自己决定（会员等级、合作伙伴价），不想让买家看到码 | 不显示；优惠已应用并锁定，不展示码 | `couponId: "CPN_xxx"`，不能与 `promotionCode`、`allowPromotionCodes` 同传 |
| 你发的活动链接自带码 | 不显示；码已应用并锁定。码不可用时页面会说明原因，并提供**不使用优惠继续** | 商品链接加 `?discount_code=SPRING-20` |
| 你的自建店面用 Store Slug（非商户凭证）创建 session | 与商品链接相同的默认检测 | 传 `allowPromotionCodes: true` 或 `false` 可覆盖检测结果 |
| 买家在买家门户里换方案 | 新方案的商品有可用的码时显示；不输码时，当前优惠能延续就延续 | 不用传；也可以在你创建的换方案 session 上传 `couponId` / `promotionCode` |
| 你用 Next.js SDK 的 `createCheckoutAction` | 按钮没传 `allowPromotionCodes` 就不显示 | 在 `<CheckoutButton>` 上传 `allowPromotionCodes`；`couponId` 和 `promotionCode` 会被 server action 剥掉，要在服务端用 `@waffo/pancake-ts` 设置 |

### 带码的商品链接

在[商品链接](https://docs.waffo.ai/zh/checkout/checkout-flow.md#product-purchase-links)上加 `discount_code`。页面打开时码即已应用，输码框隐藏：

```
https://pancake.waffo.ai/store/{store-slug}/product/{productId}?currency=USD&discount_code=SPRING-20
```

码不能使用时（例如已到期，或不适用于该商品），收银台会告诉买家原因。

### 服务端指定优惠券

创建 Checkout Session 时传 `couponId`（优惠券的 `CPN_` ID）。买家看到优惠内容（名称、额度、期数），但不显示码，即使优惠券有自己的码；要让买家看到码，改用 `promotionCode` 预填。优惠券会立即校验：属于其他店铺或环境、已停用或已到期、不适用于该商品、固定金额缺少本次币种的优惠券会被拒绝并返回 `409` —— 见[创建 Checkout Session](https://docs.waffo.ai/zh/api-reference/endpoints/orders/create-checkout-session.md#discounts)。

```typescript theme={"system"}
const session = await client.checkout.authenticated.create({
  productId: "PROD_xxx",
  currency: "USD",
  buyerIdentity: "userIdInYourSystem",
  couponId: "CPN_xxx",
});
```

`couponId` 不能与 `allowPromotionCodes` 或 `promotionCode` 同传，同时传返回 `400`。`couponId` 只接受商户凭证（API Key 或 JWT）：Store Slug 请求或 customer 会话令牌会得到 `403`，[Next.js SDK](https://docs.waffo.ai/zh/integrate/nextjs.md#discounts) 的 server action 也会剥离它，买家无法自己挑选优惠券。

### 服务端预填促销码

传 `promotionCode` —— 优惠券的码或促销码本身，大小写不敏感 —— 收银台打开时即已应用该码。它会立即按与 `couponId` 相同的规则校验，不能使用的码返回 `409`。买家能否更改由 `allowPromotionCodes` 决定：

| `allowPromotionCodes` | 收银台上 |
| - | - |
| `true` | 码已应用，买家可以换成别的码或移除。移除后可以再次输码 |
| 不传或 `false` | 码已应用并锁定 |

```typescript theme={"system"}
const session = await client.checkout.createSession({
  productId: "PROD_xxx",
  currency: "USD",
  promotionCode: "KOL-ALICE",
  allowPromotionCodes: true, // let the customer replace or remove it
});
```

与 `couponId` 一样，`promotionCode` 只接受商户凭证（API Key 或 JWT）—— Store Slug 请求或 customer 会话令牌会得到 `403`，Next.js SDK 的 server action 也会剥离它。

***

## 码不能使用时

买家输入码时 Pancake 先检查一遍，最终以提交订单时的判定为准。输码时的检查是预判，以下规则留到下单时判定：

* 访客结账时，输码阶段的总次数只计已支付的订单：已支付次数达到上限才拒绝。未支付的订单与单买家次数都留到下单时判定——剩余名额被其他买家的未支付订单占着时，下单会被拒绝。
* 已登录买家自己用同一个码下的未支付订单，不计入使用次数 —— 仍占着名额的未支付换方案除外：已确认、等待切换日期的换方案，或当前优惠正在延续过去的换方案。
* 买家离开支付页未付款，回来后可以再次输入同一个码；提交新订单时，新订单接替其未支付订单占用的名额。

收银台以买家的语言显示原因：

| 原因 | 含义 |
| - | - |
| `CODE_NOT_FOUND` | 本店本环境没有这个有效的码。已停用的码、属于其他店铺的码也显示这个原因 |
| `CODE_INACTIVE` | 优惠券或促销码已不可用 —— 例如买家应用后被停用 |
| `CODE_EXPIRED` | 码已到期。距到期不足 60 秒即按已到期处理 —— 创建 Checkout Session、输码和提交订单时都是如此 |
| `MAX_REACHED` | 码的总使用次数已用完 |
| `USER_LIMIT_REACHED` | 买家的使用次数已达上限 |
| `CUSTOMER_NOT_ELIGIBLE` | 买家邮箱不在码的限定名单中。限定买家的码只能按邮箱校验：买家还没填邮箱就应用它时，收银台会先提示填写邮箱 |
| `PRODUCT_NOT_ELIGIBLE` | 码不适用于该商品 |
| `CURRENCY_MISMATCH` | 固定金额没有本次结账币种的金额 |
| `MINIMUM_NOT_MET` | 订单未达到码的最低消费（优惠前、不含税），并显示最低金额 |
| `TEMPORARILY_UNAVAILABLE` | 保留值，当前不会返回。买家输码过于频繁时返回 HTTP `429`，稍后即可重试 |

若规则直到提交订单时才不满足（例如最后一个名额刚被别人用掉），订单不会创建、不会扣款，Checkout Session 仍可继续使用。买家接下来能做什么取决于码的来源：

| 码的来源 | 买家可以怎么做 |
| - | - |
| 输码框 | 移除码后按原价付款，或换一个码 |
| 带码的商品链接 | 码已锁定：重新打开不带 `discount_code` 的商品链接。链接中的码在打开页面时即不可用的，该页面会提供「不使用优惠继续」 |
| 服务端设置的优惠券 | 优惠券已锁定：不带 `couponId` 重新创建 Checkout Session |
| 服务端预填的促销码 | 传了 `allowPromotionCodes: true` 时与输码框相同。否则码已锁定：不带 `promotionCode` 重新创建 Checkout Session |

买家用码下单后尚未付款，可以用同一个码再次下单。新订单接替旧订单占用的名额；旧订单仍可支付，但若它也被支付，会进入待核实、不再计一次使用。

未支付的订单最多占用名额 72 小时。超过之后，下一笔使用同一优惠券的订单 —— 无论用的是它的哪个促销码、哪位买家下单 —— 会收回这个名额。未支付的订单不会被取消，状态保持不变；若之后仍被支付，会进入待核实、不再计一次使用。

***

## 订阅

新购订阅与一次性商品一样，可以通过上述方式带上优惠，规则相同。付款前，买家能看到首笔扣款以及之后每期的金额，包括优惠前后。

### 优惠几个周期

| `duration` | 享受优惠的计费周期 |
| - | - |
| `once` | 一个周期 |
| `repeating` | 连续 `durationInPeriods` 个周期 |
| `forever` | 每个周期，直到优惠券的到期时间 |

只有优惠前金额大于零的周期才会消耗优惠。100% 优惠的周期虽然不扣款，也照常消耗一个周期。

### 试用期

| 试用 | 哪些扣款享受优惠 |
| - | - |
| 无试用 | 首笔扣款及之后的周期 |
| 零元试用 | 试用期不享受优惠：试用期不扣款，也不消耗周期。优惠从首个付费周期开始 |
| 付费试用 | 试用期那笔扣款享受优惠，并算作第一个周期 —— `once` 优惠券在试用期用掉，`repeating` 优惠券从试用期开始计数 |

促销码的最低消费按首笔扣款的优惠前、不含税金额判断：付费试用时为试用价，否则为正价。续期不再判断。

例如每月 $10、减 20%、`repeating` 3 期：第 1 至 3 期扣 $8，第 4 期起扣 $10。7 天零元试用加 `once` 优惠券：试用期 $0，首个付费月享受优惠，之后每月按原价。

### 何时计一次使用

* 第一笔享受优惠的扣款成功时计一次使用：即首笔付费扣款，付费试用时为试用期那笔扣款。之后的续期不再计次。
* 在此之前，订阅一直占用这次使用。订阅在这笔扣款之前结束 —— 例如在零元试用期内取消，或首笔支付始终未成功 —— 会归还名额，买家可以再次使用该码。订阅订单超过 72 小时仍未支付时不会被取消，但不再占用名额：下一笔使用同一优惠券（含其任一促销码）的订单会收回这个名额。
* 优惠券在这笔扣款之前已过到期时间、买家按原价扣款时，同样归还名额。
* 同一买家用同一个码再下新单，只会接替从未支付的旧订单的占用。处于零元试用期的有效订阅保留占用：新单不会接替它，这次占用也计入码的次数限制。

### 优惠何时结束

* 持续期用完后，续期按原价扣款。
* **优惠券的到期时间同时结束已挂在订阅上的优惠**：到期后的续期按原价扣款，`forever` 也一样。促销码自己的到期时间只限制新的使用。
* 停用优惠券不会结束已挂在订阅上的优惠，直到订阅下一次更换方案 —— 见[更换方案](#plan-changes)。
* 取消订阅后优惠永久失效。在周期结束前撤销取消，优惠不受影响。
* 更换方案会结束优惠，除非优惠延续到新方案 —— 见[更换方案](#plan-changes)。

### 更换方案

更换方案会把订阅换成一个新方案上的新订阅。换方案 session 允许时，换方案页面显示输码框；你也可以像新购一样在换方案 session 上设置 `couponId` 或 `promotionCode` —— 见[创建 Checkout Session](https://docs.waffo.ai/zh/api-reference/endpoints/orders/create-checkout-session.md#discounts)。商品链接的 `discount_code` 不适用于更换方案。换方案页面不收集邮箱，促销码的限定买家名单按原订阅的买家邮箱匹配 —— 输码和提交换方案时都是如此。

| 换方案时 | 新订阅上的优惠 |
| - | - |
| 带了码 —— 买家在换方案页面输入，或用 `couponId` / `promotionCode` 设置 | 新优惠替换当前优惠，当前优惠剩余的部分作废。这是一次新的兑换：提交换方案时重新校验全部规则，占用并计一次使用，周期从新订阅开始计数。输入订阅已在用的同一个码也算新的兑换 |
| 没带码，且当前优惠可以延续 | 当前优惠延续。已用周期不重置 —— 剩余周期在新方案上继续。延续不是新的兑换：不占用、也不计使用次数 |
| 没带码，且当前优惠不能延续 | 新订阅没有优惠。优惠周期已用完、优惠券已到期或 60 秒内到期、优惠券已停用时，优惠都不能延续。 |

因此，**停用优惠券后，已挂在订阅上的优惠在该订阅下一次更换方案时结束。**

* **第 1 期。** 立即生效的换方案，新订阅第 1 期的金额为新方案价减去当前周期未用部分的折抵 —— 或你用 `changeAmount` / `changeCreditAmount` 指定的金额。该金额大于 0 时享受优惠并消耗一个周期。**优惠按新方案全价计算，再扣除原方案剩余抵扣**：百分比优惠取新方案价的相应比例，固定减额不变，优惠额以第 1 期金额为上限，买家实付为第 1 期金额减去优惠，不低于 0；优惠覆盖全部第 1 期金额时同样消耗一个周期。该金额为 0 时 —— 通常是折抵覆盖新方案价的降级 —— 不享受优惠、不消耗周期，优惠从第 2 期开始。下周期生效的换方案从一个正价周期开始；给了试用的换方案按上文「试用期」的规则处理。
* **折抵按买家实际支付的金额计算。** 当前周期未用部分的折抵按买家这一期的实付计算：这一期享受优惠时取优惠后金额，否则取标价。
* **促销码最低消费**按新方案正价的优惠前、不含税金额判断，不扣除折抵。
* **何时计一次使用。** 新码在新订阅第一笔享受优惠的扣款成功时计一次使用 —— 即第 1 期，第 1 期为 0 时为第 2 期。换方案没有完成 —— 买家没有完成、支付通道报告失败，或下周期生效的换方案被取消 —— 会归还名额。发起后一直没有完成的换方案，72 小时后不再占用名额，由下一笔使用同一优惠券（含其任一促销码）的订单收回 —— 从已结束的订阅延续给它、尚未转过去的那次占用同样如此。当前订阅自己的码还在等待第一笔享受优惠的扣款时，延续的优惠把这次占用一并带到新订阅；带新码或不带码则归还这次占用。

例如每月 $5、每期减 $1、`repeating` 3 期且已用 1 期，买家在周期过半时不带码升级到每月 $10。折抵为 $2 —— 实付 $4 的一半 —— 因此第 1 期优惠前 $8、实付 $7，下一期 $9，之后每期 $10。若换方案时输入一个新的减 20% 的 `once` 码，优惠为 $2 —— 每月 $10 新方案的 20% —— 因此第 1 期优惠前 $8、实付 $6，之后每期 $10：原优惠剩余的一期作废。

### 订阅支付的金额

* 订阅订单保留标价。每笔支付记录 Pancake 对该期的预期金额：原价、优惠额与应付额。[`subscription.payment_succeeded`](https://docs.waffo.ai/zh/api-reference/webhooks) webhook 在 `listPrice` 中携带这些金额（`listTotal`、`discountAmount`、`total`）。
* 扣款与预期优惠不符时 —— 优惠提前结束，或多扣、少扣 —— 由 Pancake 内部对账处理。该笔支付保留预期金额，实际扣款额见 webhook 的 `chargedAmount`。
* 首期收据 —— 付费试用为试用扣款，无试用为首笔扣款 —— 在这笔扣款享受优惠时，依次列出原价、优惠、小计、税额和实付额。零元试用的首期收据金额为 \$0，不显示明细。
* 每次续期收据在该期享受优惠时显示同样的明细。试用结束后的第一笔扣款不发续期收据 —— 零元试用时，这正是享受优惠的那一期。买家在扣款提醒中可以看到这笔扣款的预期金额。
* 邮件中展示的下期扣款额，只在下一期仍享受优惠时才含优惠。
* 更换方案后，新订阅的第一笔支付按第 1 期的金额记录预期（见[更换方案](#plan-changes)），但这笔支付不发收据。第 1 期为 0 时，这笔支付是一笔 \$0 支付；之后的支付按正价期的金额记录预期。买家收到的是一封「方案已变更」邮件，其中显示新方案优惠前的正价、下次扣款日期和下次扣款金额（该期享受优惠时为优惠后），不显示第 1 期金额，也不显示优惠明细。支付记录保留第 1 期的各项金额。之后的续期按上面的规则发收据。

***

## 金额、收据与使用次数

* **优惠作用于含税价格。** 百分比优惠向下取整到币种最小单位；固定金额不会让总额低于零，100% 优惠会产生零元订单。
* **订单、支付记录、收据和 Dashboard 都展示三项金额**：原价、优惠额、实付额。退款上限为实付额。
* **订单价格快照保留标价。** 一次性订单与订阅订单的快照 `total` 都是优惠前的含税价格；每笔扣款的优惠后金额记在支付记录上（`snapshotAmountDetails.total`）。
* **带优惠的收据**依次列出原价、优惠、小计（优惠后、不含税）、税额和实付额。小计与税额两行只在有税时出现。
* **支付成功时才计一次使用** —— 订阅为第一笔享受优惠的扣款（见[订阅](#subscriptions)）。订单取消或过期会归还名额，买家可以再次使用该码。退款不归还使用次数。

***

## 下一步

> **[优惠券端点](https://docs.waffo.ai/zh/api-reference/endpoints/coupons/overview.md)**
>
> 创建、重命名和停用优惠券

> **[创建 Checkout Session](https://docs.waffo.ai/zh/api-reference/endpoints/orders/create-checkout-session.md#discounts)**
>
> 应用优惠券或显示输码框


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.