@krizaka/coupons

中文文档
0.1.4发布于 2026/11/01Apache-2.0TypeScriptES Module构建来源已验证

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

中文使用文档
文档目录4 节
  1. 规则
  2. 合伙人(第25号决议)
  3. 你提供什么,它就会给予什么
  4. 去生活

@krizaka /优惠券

折扣券和信用券由行政管理,对于每一个Krizaka产品(决策: adr - 002设计: 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),对于合作伙伴,则是与它一起销售的合作伙伴。

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 共发布 6 个版本,首次发布于 2026/10/27;过去 12 个月发布了 6 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.1.4latest2026/11/011 天前
0.1.32026/10/304 天前
0.1.22026/10/295 天前
0.1.12026/10/277 天前
0.1.02026/10/277 天前
0.0.0-stage2026/10/277 天前

关于 @krizaka/coupons 的常见问题

@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 和提交记录判断。