# @krizaka/coupons 中文文档

规范地址：https://npmcn.com/package/%40krizaka/coupons/

版本：0\.1\.4
npm 元数据更新时间：2026-11-01T14:49:02.327Z
中文译文更新时间：2026-11-01T17:06:41.851Z
软件包许可：Apache-2\.0

本文由上游 README 机器翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40krizaka/coupons
原文对照：https://npmcn.com/package/%40krizaka/coupons/?lang=original
项目主页：https://github.com/krizaka/krizaka-ts/tree/main/packages/coupons
源码仓库：https://github.com/krizaka/krizaka-ts

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：351 kB，18 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=22
- 运行依赖：3 个
- peer 依赖：zod \>=3\.23，vitest \>=4
- 首次发布：2026-10-27T00:17:41.072Z
- 最新版本发布：2026-11-01T14:49:01.989Z
- 已发布版本数：6（近 12 个月 6 个）

## 中文简介

管理员为每个Krizaka产品管理折扣和信用券：一个带有窗口和限制的代码，在结账时按行锁定保留，在付款事件中确认一次，未付款时释放，退款；归因伙伴。数据库i

## 安装

```sh
npm install @krizaka/coupons
```

也可以使用：`pnpm add @krizaka/coupons`、`yarn add @krizaka/coupons`、`bun add @krizaka/coupons`

## README

# @krizaka /优惠券

**折扣券和信用券由行政管理**，对于每一个Krizaka产品(决策： [adr - 002](https://npmcn.com/package/docs/adr/ADR-002-coupons.md)设计: [coupons.md](https://npmcn.com/package/docs/design/coupons.md)）. 产品的数据库是真理的源泉；支付提供商从该模块提供的不可变条款中应用折扣；信用券授予有限额的信用 `@krizaka/credits`． 它发展现有的东西：它不启动第二个计费系统。

> **免费提供的总是有上限的信用券，而不是100%的折扣。** 折扣止于99.99%和管理员 `coupons.maxPercentOff`;固定的金额不会永远持续下去，也不会超过价格的那一部分；信用券上总是有积分、有效期以及用完后会发生什么。

两种，一个代码（字母和数字，不带大小写，空格或连字符）：«summer-2026»是 `SUMMER2026`):

-   **折扣**：一个百分比（基点）或与其货币挂钩的固定金额，一个期限(`once`， `repeating` N个月, `forever`)、计划范围和最低限度。在结帐时应用。
-   **信贷**： `credits` 当然,可选 `capPerDay` / `capPerMonth`， `creditsValidDays`， `onExhausted` （`stop` 或者 `fallback_paid`）. 在产品兑换积分的地方兑换。

两个: `startsAt` / `endsAt`， `maxRedemptions` 整体和 `maxPerAccount`， `newCustomersOnly`目的；目的；`partner`， `promo`， `demo`， `support`)，对于合作伙伴，则是与它一起销售的合作伙伴。

```ts
import { createCoupons } from "@krizaka/coupons";
import { readCouponSettings } from "@krizaka/coupons/settings";

const coupons = createCoupons({ clock, settings: () => readCouponSettings((key) => settings.get(key)), audit, rateLimit: limiter, credits, outbox });

// 1. the person types the code in « Code promo » (read-only; counts one attempt on the limiter, before any lookup)
const v = await coupons.validate(query, typed, { mode: "checkout", accountId, plan: "pro", amountMinor: 3900, currency: "cad", hasPaidBefore });
//    v = { ok: true, effect: { discountMinor, payableMinor, … } }  or  { ok: false, code: "COUPON_EXPIRED" }

// 2. « Pay »: three steps, NO network call inside a transaction or under the coupon's row lock
const applied = await transaction(connect, (tx) => coupons.apply(tx, { code: typed, accountId, plan: "pro", amountMinor: 3900, currency: "cad", idempotencyKey: attemptId })); // reserves a use
const session = await payments.checkout({ …, discount: applied.discount, idempotencyKey: applied.redemptionId, metadata: { redemption_id: applied.redemptionId }, expiresAt }); // @krizaka/payments
await transaction(connect, (tx) => coupons.attachCheckout(tx, applied.redemptionId, session.checkoutRef, session.expiresAt));
//    if the session fails: coupons.release(tx, { redemptionId })

// 3. the payment webhook, in the event's transaction (applyOnceInTransaction of @krizaka/payments)
payment_paid:     (e, tx) => coupons.redeem(tx, { redemptionId: e.metadata.redemption_id, checkoutRef: e.checkoutRef, paymentRef: e.paymentRef ?? e.invoiceRef, amountPaidMinor: e.amountTotal, amountDiscountMinor: e.amountDiscount, currency: e.currency })
checkout_expired: (e, tx) => coupons.release(tx, { checkoutRef: e.checkoutRef })
payment_refunded: (e, tx) => coupons.refund(tx, { paymentRef: e.paymentRef, invoiceRef: e.invoiceRef, amountRefunded: e.amountRefunded })

// a credit code: the same rules, then a capped grant in the same transaction
await transaction(connect, (tx) => coupons.redeemCredit(tx, { code: typed, accountId, idempotencyKey: attemptId }));
```

## 规则

-   **一次使用按一条规则计算**赎回是一种使用，当它是 `confirmed` 或者 `revoked` 而不是 `use_restored`，或者 `reserved` 而且没有超过最后期限。一个 `released` 一个永远不重要。总体限制和每个帐户限制都适用它。“过期”、“耗尽”和“未启动”将被计算，因此不需要cron。
-   **预定，确认，释放。** `apply` 锁定优惠券行(`SELECT … FOR UPDATE`)，重新检查所有内容，插入一个 `reserved` 有期限的赎回(`coupons.reservationMinutes`)和提交：最后一次使用的两个请求给一个（在PostgreSQL上测试，50轮）。 `redeem` 在支付事件的事务中运行；它的谨慎过渡(`UPDATE … WHERE status IN ('reserved', 'released')`)和独一无二的 `payment_ref` 意味着一次使用被确认，不管交付的次数是多少。webhook通过 `metadata.redemption_id` 即使 `attachCheckout` 从来没有跑。
-   **逾期付款受到尊重。** 对于已释放或过期的预订的付款无论如何都会得到确认并标记 `over_limit`顾客是善意支付折扣价的。它被审计并显示在管理员中。
-   **退款** 加起来（金额是累积的），永远不要改变状态。部分退款保留使用；一个满的人只有在 `coupons.refundRestoresUse` 开启（默认关闭），因此退款不能重置一次性代码。
-   **一旦任何人使用了优惠券，条款就不会改变** （`COUPON_TERMS_LOCKED`)：提供者的对象是由它们组成的 `termsVersion`． 日期，限制，计划，状态和合作伙伴保持可编辑。当一个计划的价格发生变化时， `admin.checkPrices(prices)` 暂停现在将超过上限的固定金额息票。
-   **税是供应商的。** 条款是 `@krizaka/payments`，其适配器将折扣一起发送 `automatic_tax`：分条税按折扣金额计算GST和QST。这些数字 `effect` 是信息。这里没有写税。
-   **滥用。** `validate`， `apply` 和 `redeemCredit` 每人在限位器上尝试一次(`COUPON_ATTEMPT_ACTION`,限制了 `coupons.attemptsPerAccountPerHour` / `attemptsPerIpPerHour`)进行查找；超过极限，他们回答 `COUPON_RATE_LIMITED` 与 `retryAfterSeconds`.
-   **拒绝** 键入(`COUPON_CODE`): `UNKNOWN`， `NOT_STARTED`， `EXPIRED`， `PAUSED`， `EXHAUSTED`， `ACCOUNT_LIMIT`， `NEW_CUSTOMERS_ONLY`， `NOT_FOR_PLAN`， `MINIMUM`， `CURRENCY`， `TOO_LARGE`， `KIND`， `PROVIDER`， `RATE_LIMITED`， `DISABLED`. `CouponError` 包含代码和HTTP `status` （404、422、429……）。

## 合伙人（第25号决议）

合作伙伴是为我们进行销售的人：一个显示名称和一个可选的电子邮件，用于一个目的（归因于销售），仅对管理员可见。 `admin.partners.report` 给，每张优惠券和货币，兑换，折扣，收入（支付减去退款）和信用授予，从来没有客户的身份；每次读取都被审计。 `export` 返回记录及其聚合； `erase` 清空名称和E-mail并保留汇总（赎回是会计记录）。 **佣金超出了范围**：钩子是 `evt.coupon.redeemed.v1`，它携带 `partnerId`，金额和积分，供后面的模块从发件箱中消费。

## 你提供什么，它就会给予什么

-   **依赖关系** 都是结构端口，所以这个包不导入它们： `audit` （`createAudit` 的 `@krizaka/audit`)， `rateLimit` （`createLimiter` 的 `@krizaka/rate-limit`)， `credits` （`createCredits` 的 `@krizaka/credits`)， `outbox` （`createOutbox` 的 `@krizaka/outbox`）。
-   **事件** 通过发件箱，在呼叫者的交易中，id衍生出以下事实： `evt.coupon.redeemed.v1`， `evt.coupon.released.v1`， `evt.coupon.refunded.v1`， `evt.coupon.exhausted.v1`，每个都有一个JSON模式(`COUPON_EVENT_SCHEMAS`）。
-   **设置** （`@krizaka/coupons/settings`（由设置服务审核的每个更改）： `coupons.enabled` (掉), `reservationMinutes` （60,40 - 1440：提供商的会话窗口）， `attemptsPerAccountPerHour` (10), `attemptsPerIpPerHour` (30), `minCodeLength` (8), `generatedCodeLength` (10), `maxPercentOff` (50, 1 - 99: **默认是Oussama的呼叫**krizaka-ts # 61) `refundRestoresUse` (掉)。 `credits.timeZone` 是 `@krizaka/credits`”。环境中没有任何新内容：提供程序密钥保留了下来 `PAYMENTS_*`.
-   **表** （`couponsSchema`、Postgres模式 `coupons`): `partner`， `coupon`， `redemption`，使用重复规则的支票（不能存储免费折扣）。被删除的帐户会留下其赎回金额(`ON DELETE SET NULL`）。
-   `couponsContract` （`@krizaka/coupons/testkit`)是每个商店的合同； `memoryCouponStore()` 是用于单元测试的（它不锁定任何东西：并发性在PostgreSQL上得到了证明）。

## 去生活

代码只能在提供程序的测试模式下运行。在实时模式下，一个产品接一个产品地打开优惠券是所有者的决定（krizaka-ts#61），保留兑换记录和免费积分的税收状况也是如此（krizaka-ts#62，对会计来说）：这里没有任何假设。

## 常见问题

### @krizaka/coupons 是什么？

管理员为每个Krizaka产品管理折扣和信用券：一个带有窗口和限制的代码，在结账时按行锁定保留，在付款事件中确认一次，未付款时释放，退款；归因伙伴。数据库i

### 如何安装 @krizaka/coupons？

在项目目录运行 npm install @krizaka/coupons，或使用 pnpm add / yarn add / bun add。

### @krizaka/coupons 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @krizaka/coupons 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @krizaka/coupons 需要什么版本的 Node\.js？

package\.json 的 engines 字段要求 Node\.js \>=22。版本不符时 npm 默认只给出警告，开启 engine-strict 后会拒绝安装。

### @krizaka/coupons 可以商用吗？

@krizaka/coupons 使用 Apache-2\.0 许可。宽松许可：允许商业使用、修改和再分发，并附带明确的专利授权；分发时需保留许可与 NOTICE 文件，修改过的文件要注明改动。以上为概括，不构成法律意见，请以许可原文为准。

### @krizaka/coupons 还在维护吗？

最新版本 0\.1\.4 发布于 2026/11/01（1 天前）；至今共发布 6 个版本，其中过去 12 个月发布了 6 个；首次发布于 2026/10/27。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

