割引コードと自動割引:率・定額・送料無料、商品/カテゴリ指定、利用回数と初回限定の制限
インストール
genpm add @core/discounts含まれるもの
- src/lib/discounts/ にソースコード(6 ファイル)。 (21.5 KB)
- src/lib/discounts/AGENTS.md に AI ルール、加えて IDE 用のルールファイル。
- @core/cart, @core/catalog, @core/db, @core/money, @core/orders を自動で解決します。
README
このパッケージには README がありません。
これは AI が src/lib/discounts で作業するときに読む内容そのものです。それ以外はコンテキストに追加されません。
@core/discounts — rules for AI agents
Purpose
Discount codes and automatic discounts for @core/cart: percent, fixed amount or free shipping; optional scope by
product or category slugs, minimum order, start/end dates, total and per-customer usage limits, first-order-only and
combinability (non-combinable ones compete: the biggest wins). Registers the discounts (100) and free-shipping
(250) totals steps and records redemptions inside the transaction that creates/pays the order (onOrderPaidTx of
@core/orders), with the discount row locked. Limits count recorded redemptions plus open payments carrying the code
(pendingCodeUses of @core/cart, fed by @core/checkout). If a limit was exceeded anyway (the customer already paid),
the order is kept, the redemption recorded and a discount_over_limit event is added to the order timeline for review.
Tables: discounts, discount_redemptions.
Map
index.ts— public API:upsertDiscount,DiscountInput,discountsStep,freeShippingStep,recordRedemptions,discountsAdminResource.discounts.ts— validation, application and the redemption handler (registered on import).admin.ts— admin resource.
Integration
- Migrations as in
src/lib/db/AGENTS.md. Import this module at startup (it registers its steps and handler). - Cart page: a code field calling
PUT /api/cart/codes {codes: [code]}(@core/cart); showtotals.discountsand anyinvalid_codeissues. - Pass the customer email to
computeTotals(cart, { email })once known so per-customer and first-order limits apply. - Add
discountsAdminResourcetosrc/genpm/admin.ts(permissionsdiscounts:read|update). - Verify: create
TEN(10 %), apply it, the total drops and each line shows its share.
Conventions
- Codes are stored uppercase (
A–Z 0–9 _ -); values: percent 1–100, fixed in minor units. - Deleting a discount deactivates it (redemption history stays).
- Free-shipping adjustments carry their
code(amount 0), so they count towards usage limits. - Check the order timeline (
discount_over_limit) when a usage-limited campaign ends. - Prefer automatic discounts for store-wide sales; codes for campaigns.
Don't
- Don't show a "was" price or a percentage off that isn't real: in the EU a price reduction must refer to the lowest price of the previous 30 days (Omnibus Directive). Use @core/pricing's price history for compare-at prices.
- Don't apply discounts on the client or trust discount amounts sent by the browser.
- Don't create fake urgency (countdowns that reset, "only today" when it isn't).
# @core/discounts — rules for AI agents
## Purpose
Discount codes and automatic discounts for @core/cart: percent, fixed amount or free shipping; optional scope by
product or category slugs, minimum order, start/end dates, total and per-customer usage limits, first-order-only and
combinability (non-combinable ones compete: the biggest wins). Registers the `discounts` (100) and `free-shipping`
(250) totals steps and records redemptions inside the transaction that creates/pays the order (`onOrderPaidTx` of
@core/orders), with the discount row locked. Limits count recorded redemptions plus open payments carrying the code
(`pendingCodeUses` of @core/cart, fed by @core/checkout). If a limit was exceeded anyway (the customer already paid),
the order is kept, the redemption recorded and a `discount_over_limit` event is added to the order timeline for review.
Tables: `discounts`, `discount_redemptions`.
## Map
- `index.ts` — public API: `upsertDiscount`, `DiscountInput`, `discountsStep`, `freeShippingStep`, `recordRedemptions`, `discountsAdminResource`.
- `discounts.ts` — validation, application and the redemption handler (registered on import). `admin.ts` — admin resource.
## Integration
1. Migrations as in `src/lib/db/AGENTS.md`. Import this module at startup (it registers its steps and handler).
2. Cart page: a code field calling `PUT /api/cart/codes {codes: [code]}` (@core/cart); show `totals.discounts` and any `invalid_code` issues.
3. Pass the customer email to `computeTotals(cart, { email })` once known so per-customer and first-order limits apply.
4. Add `discountsAdminResource` to `src/genpm/admin.ts` (permissions `discounts:read|update`).
5. Verify: create `TEN` (10 %), apply it, the total drops and each line shows its share.
## Conventions
- Codes are stored uppercase (`A–Z 0–9 _ -`); values: percent 1–100, fixed in minor units.
- Deleting a discount deactivates it (redemption history stays).
- Free-shipping adjustments carry their `code` (amount 0), so they count towards usage limits.
- Check the order timeline (`discount_over_limit`) when a usage-limited campaign ends.
- Prefer automatic discounts for store-wide sales; codes for campaigns.
## Don't
- Don't show a "was" price or a percentage off that isn't real: in the EU a price reduction must refer to the lowest
price of the previous 30 days (Omnibus Directive). Use @core/pricing's price history for compare-at prices.
- Don't apply discounts on the client or trust discount amounts sent by the browser.
- Don't create fake urgency (countdowns that reset, "only today" when it isn't).
.genpmignore 適用後に組み込まれる正確なツリーです。固定先:
// Panel: descuentos con su número de usos.
import { count, desc, eq, ilike, or } from 'drizzle-orm';
import type { AdminContext, AdminResource } from '../contracts/index.ts';
import { getDb } from '../db/index.ts';
import { DiscountInput, upsertDiscount } from './discounts.ts';
import { type Discount, discountRedemptions, discounts } from './schema.ts';
type Row = Discount & { uses: number };
class DiscountsForbidden extends Error {
readonly code = 'forbidden';
}
async function need(ctx: AdminContext, action: 'read' | 'update') {
if (!(await ctx.can(`discounts:${action}`))) throw new DiscountsForbidden(`forbidden: discounts:${action}`);
}
async function withUses(d: Discount): Promise<Row> {
const [r] = await getDb().select({ n: count() }).from(discountRedemptions).where(eq(discountRedemptions.discountId, d.id));
return { ...d, uses: r?.n ?? 0 };
}
export const discountsAdminResource: AdminResource<Row> = {
name: 'discounts',
label: { singular: 'Discount', plural: 'Discounts' },
group: 'Store',
fields: [
{ name: 'name', label: 'Name', type: 'text', required: true, list: true },
{ name: 'code', label: 'Code (empty = automatic)', type: 'text', list: true },
{ name: 'type', label: 'Type', type: 'select', required: true, list: true, options: [{ value: 'percent', label: '% off' }, { value: 'fixed', label: 'Amount off' }, { value: 'free_shipping', label: 'Free shipping' }] },
{ name: 'value', label: 'Value (% or cents)', type: 'number', list: true },
{ name: 'minSubtotal', label: 'Minimum order', type: 'money' },
{ name: 'productSlugs', label: 'Only products (slugs)', type: 'json' },
{ name: 'categorySlugs', label: 'Only categories (slugs)', type: 'json' },
{ name: 'firstOrderOnly', label: 'First order only', type: 'boolean' },
{ name: 'usageLimit', label: 'Total uses', type: 'number' },
{ name: 'perCustomerLimit', label: 'Uses per customer', type: 'number' },
{ name: 'combinable', label: 'Combines with other discounts', type: 'boolean' },
{ name: 'startsAt', label: 'Starts', type: 'datetime' },
{ name: 'endsAt', label: 'Ends', type: 'datetime' },
{ name: 'active', label: 'Active', type: 'boolean', list: true },
{ name: 'uses', label: 'Uses', type: 'number', readOnly: true, list: true },
],
input: DiscountInput,
title: (d) => d.code ?? d.name,
async list(q, ctx) {
await need(ctx, 'read');
const where = q.search ? or(ilike(discounts.name, `%${q.search.replace(/[%_\\]/g, '')}%`), ilike(discounts.code, `%${q.search.replace(/[%_\\]/g, '')}%`)) : undefined;
const [total] = await getDb().select({ n: count() }).from(discounts).where(where);
const rows = await getDb().select().from(discounts).where(where).orderBy(desc(discounts.createdAt)).limit(q.pageSize).offset((Math.max(q.page, 1) - 1) * q.pageSize);
return { rows: await Promise.all(rows.map(withUses)), total: total?.n ?? 0 };
},
async get(id, ctx) {
await need(ctx, 'read');
const [d] = await getDb().select().from(discounts).where(eq(discounts.id, id));
return d ? withUses(d) : null;
},
async create(input, ctx) {
await need(ctx, 'update');
return withUses(await upsertDiscount(input as never));
},
async update(id, input, ctx) {
await need(ctx, 'update');
return withUses(await upsertDiscount(input as never, id));
},
async delete(id, ctx) {
await need(ctx, 'update');
await getDb().update(discounts).set({ active: false }).where(eq(discounts.id, id));
},
};
このパッケージは MCP サーバーを宣言していません。
| バージョン | コミット | 公開日 | スキャン |
|---|---|---|---|
| 1.0.1 | 6b23d34 | 4 時間前 | スキャン合格 |
- npm
- zod ^4.0.0
- 提案
- GenPM は npm コマンドを提案し、あなたが承認した場合にのみ実行します。
- 利用元(1)
- @core/kit-store ^1.0.0
- スキャン
- スキャン合格 · 指摘 0 件
- コミット
- v1.0.1 → 6b23d346ddd5be7a9154a7106859687008386baa · 取得後に検証済み
- スクリプト
- なし。GenPM はパッケージのコードを実行しません。
- ライセンス
- MIT
- 品質
- 100/100
- 認識されたライセンス達成
- AGENTS.md に目的の説明がある達成
- AGENTS.md に統合手順がある達成
- AGENTS.md に規約や禁止事項がある達成
- テストを含む達成
- セキュリティスキャンに合格達成
- 過去 6 か月以内に公開達成
- 認証済みの公開者達成
- 概要とキーワード達成
- 報告
- 問題を見つけましたか?