注文:明細の固定、在庫引当、支払いと配送の状態、分割出荷、返金、メールとイベント
インストール
genpm add @core/orders含まれるもの
- src/lib/orders/ にソースコード(10 ファイル)。 (41.3 KB)
- src/lib/orders/AGENTS.md に AI ルール、加えて IDE 用のルールファイル。
- .env.example に追加される環境変数: ORDER_LINK_SECRET, ORDER_NUMBER_PREFIX, SITE_URL。
- @core/auth, @core/catalog, @core/contracts, @core/db, @core/email, @core/jobs, @core/money を自動で解決します。
README
このパッケージには README がありません。
これは AI が src/lib/orders で作業するときに読む内容そのものです。それ以外はコンテキストに追加されません。
@core/orders — rules for AI agents
Purpose
Orders after payment: lines frozen at purchase time (name, price, discount), stock reserved in the same transaction
(@core/catalog), sequential order numbers, two independent states — payment (pending → paid → partially_refunded/refunded,
or cancelled) and fulfillment (unfulfilled → partial → fulfilled → delivered) — partial shipments with tracking,
refunds through the registered PaymentProvider, a timeline, customer access by owner session or signed link, and
background events (onOrderEvent) with built-in emails (confirmation, shipped, refund). Payment itself is @core/checkout.
Map
index.ts— public API:createOrder,markPaid,addFulfillment,markDelivered,cancelOrder,refundOrder,getOrder,getOrderForCustomer,onOrderEvent,onOrderPaidTx,registerPaymentProvider,ordersAdminResource,setOrderMessages.orders.ts— logic and theorders.eventjob.emails.ts— default emails.admin.ts— admin.schema.ts— tables.adapters/hono.ts—orderRoutes({ currentUserId }).adapters/next.ts—orderRoute,publicOrder.
Integration
- Env:
ORDER_LINK_SECRET(≥ 32 chars),SITE_URL,EMAIL_FROM, optionalORDER_NUMBER_PREFIX. Migrations as insrc/lib/db/AGENTS.md; the @core/jobs cron must run. - Orders are created by @core/checkout from the paid cart; for manual flows call
createOrder({ … }). - Customer page
/orders/[number]:getOrderForCustomer(number, { userId, token })(404 when null) rendered withpublicOrder. - React to events in a module imported at startup:
onOrderEvent('paid', 'my-crm', async (order) => …). Translate emails withsetOrderMessages((locale) => ({ … })). For bookkeeping that must be atomic with the payment (e.g. counting coupon uses) useonOrderPaidTx(name, async (order, tx) => …): it runs inside the order transaction, must be fast and must not throw for business rules (a throw aborts the order). - Add
ordersAdminResourcetosrc/genpm/admin.ts(permissionsorders:read|update|fulfill|refund). - Verify: create a paid order, ship it from the admin with a tracking URL, the customer gets both emails.
Conventions
- Change states only with these functions; they lock the order row and validate transitions.
- Cancel only unfulfilled orders; after shipping, refund instead (optionally restocking returned lines).
- A paid order that was cancelled is refunded with
refundOrder(up tototal - refundedTotal, norestock: the stock was returned when cancelling); it stayscancelled. - Event handlers must be idempotent; each runs once per event and retries if it fails.
Don't
- Don't delete orders or edit their lines/totals after creation (legal and accounting record).
- Don't expose orders by id or number without the owner check or the signed token.
- Don't mark orders paid from the browser's success redirect; only from the payment webhook or the admin.
# @core/orders — rules for AI agents
## Purpose
Orders after payment: lines frozen at purchase time (name, price, discount), stock reserved in the same transaction
(@core/catalog), sequential order numbers, two independent states — payment (`pending → paid → partially_refunded/refunded`,
or `cancelled`) and fulfillment (`unfulfilled → partial → fulfilled → delivered`) — partial shipments with tracking,
refunds through the registered `PaymentProvider`, a timeline, customer access by owner session or signed link, and
background events (`onOrderEvent`) with built-in emails (confirmation, shipped, refund). Payment itself is @core/checkout.
## Map
- `index.ts` — public API: `createOrder`, `markPaid`, `addFulfillment`, `markDelivered`, `cancelOrder`, `refundOrder`, `getOrder`, `getOrderForCustomer`, `onOrderEvent`, `onOrderPaidTx`, `registerPaymentProvider`, `ordersAdminResource`, `setOrderMessages`.
- `orders.ts` — logic and the `orders.event` job. `emails.ts` — default emails. `admin.ts` — admin. `schema.ts` — tables.
- `adapters/hono.ts` — `orderRoutes({ currentUserId })`. `adapters/next.ts` — `orderRoute`, `publicOrder`.
## Integration
1. Env: `ORDER_LINK_SECRET` (≥ 32 chars), `SITE_URL`, `EMAIL_FROM`, optional `ORDER_NUMBER_PREFIX`. Migrations as in `src/lib/db/AGENTS.md`; the @core/jobs cron must run.
2. Orders are created by @core/checkout from the paid cart; for manual flows call `createOrder({ … })`.
3. Customer page `/orders/[number]`: `getOrderForCustomer(number, { userId, token })` (404 when null) rendered with `publicOrder`.
4. React to events in a module imported at startup: `onOrderEvent('paid', 'my-crm', async (order) => …)`.
Translate emails with `setOrderMessages((locale) => ({ … }))`. For bookkeeping that must be atomic with the payment
(e.g. counting coupon uses) use `onOrderPaidTx(name, async (order, tx) => …)`: it runs inside the order transaction,
must be fast and must not throw for business rules (a throw aborts the order).
5. Add `ordersAdminResource` to `src/genpm/admin.ts` (permissions `orders:read|update|fulfill|refund`).
6. Verify: create a paid order, ship it from the admin with a tracking URL, the customer gets both emails.
## Conventions
- Change states only with these functions; they lock the order row and validate transitions.
- Cancel only unfulfilled orders; after shipping, refund instead (optionally restocking returned lines).
- A paid order that was cancelled is refunded with `refundOrder` (up to `total - refundedTotal`, no `restock`: the
stock was returned when cancelling); it stays `cancelled`.
- Event handlers must be idempotent; each runs once per event and retries if it fails.
## Don't
- Don't delete orders or edit their lines/totals after creation (legal and accounting record).
- Don't expose orders by id or number without the owner check or the signed token.
- Don't mark orders paid from the browser's success redirect; only from the payment webhook or the admin.
.genpmignore 適用後に組み込まれる正確なツリーです。固定先:
// Tablas de @core/orders. Las recoge drizzle-kit vía src/lib/db/drizzle.config.ts.
import { boolean, index, integer, jsonb, pgSequence, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
import { users } from '../auth/index.ts';
import { primaryId, timestamps } from '../db/index.ts';
const ts = (name: string) => timestamp(name, { withTimezone: true, mode: 'date' });
export type Address = {
name: string;
line1: string;
line2?: string;
city: string;
postalCode: string;
region?: string;
/** ISO 3166-1 alpha-2. */
country: string;
phone?: string;
company?: string;
taxId?: string;
};
export type OrderAdjustment = { source: string; label: string; amount: number; code?: string };
/** Número de pedido legible y secuencial (no revela el id interno). */
export const orderNumberSeq = pgSequence('order_number_seq', { startWith: 1001 });
export const orders = pgTable(
'orders',
{
id: primaryId('ord'),
number: text('number').notNull().unique(),
/** Ciclo del pago: pending → paid → (partially_)refunded; o cancelled. */
status: text('status', { enum: ['pending', 'paid', 'cancelled', 'refunded', 'partially_refunded'] }).notNull().default('pending'),
/** Ciclo del envío, independiente del pago. */
fulfillmentStatus: text('fulfillment_status', { enum: ['unfulfilled', 'partial', 'fulfilled', 'delivered'] }).notNull().default('unfulfilled'),
email: text('email').notNull(),
userId: text('user_id').references(() => users.id, { onDelete: 'set null' }),
locale: text('locale'),
currency: text('currency').notNull(),
subtotal: integer('subtotal').notNull(),
discountTotal: integer('discount_total').notNull().default(0),
shippingTotal: integer('shipping_total').notNull().default(0),
taxTotal: integer('tax_total').notNull().default(0),
total: integer('total').notNull(),
refundedTotal: integer('refunded_total').notNull().default(0),
discounts: jsonb('discounts').$type<OrderAdjustment[]>().notNull().default([]),
shippingMethod: jsonb('shipping_method').$type<{ label: string; rateId?: string } | null>(),
shippingAddress: jsonb('shipping_address').$type<Address | null>(),
billingAddress: jsonb('billing_address').$type<Address | null>(),
customerNote: text('customer_note'),
paymentProvider: text('payment_provider'),
paymentRef: text('payment_ref'),
/** Carrito de origen: hace idempotente la creación desde un webhook repetido. */
cartId: text('cart_id').unique(),
paidAt: ts('paid_at'),
cancelledAt: ts('cancelled_at'),
...timestamps,
},
(t) => [index('orders_user_idx').on(t.userId), index('orders_status_idx').on(t.status, t.createdAt), index('orders_email_idx').on(t.email)],
);
export const orderLines = pgTable(
'order_lines',
{
id: primaryId('oli'),
orderId: text('order_id')
.notNull()
.references(() => orders.id, { onDelete: 'cascade' }),
variantId: text('variant_id'),
productId: text('product_id'),
/** Copia de lo comprado: el pedido no cambia si luego cambia el producto. */
sku: text('sku'),
name: text('name').notNull(),
title: text('title').notNull().default(''),
kind: text('kind', { enum: ['physical', 'digital'] }).notNull().default('physical'),
quantity: integer('quantity').notNull(),
unitPrice: integer('unit_price').notNull(),
subtotal: integer('subtotal').notNull(),
discount: integer('discount').notNull().default(0),
total: integer('total').notNull(),
fulfilledQuantity: integer('fulfilled_quantity').notNull().default(0),
refundedQuantity: integer('refunded_quantity').notNull().default(0),
/** Orden de las líneas tal como estaban en el carrito. */
position: integer('position').notNull().default(0),
},
(t) => [index('order_lines_order_idx').on(t.orderId)],
);
export const fulfillments = pgTable(
'order_fulfillments',
{
id: primaryId('ful'),
orderId: text('order_id')
.notNull()
.references(() => orders.id, { onDelete: 'cascade' }),
lines: jsonb('lines').$type<Array<{ lineId: string; quantity: number }>>().notNull(),
status: text('status', { enum: ['shipped', 'delivered', 'failed'] }).notNull().default('shipped'),
carrier: text('carrier'),
trackingNumber: text('tracking_number'),
trackingUrl: text('tracking_url'),
/** Quién lo envía: `manual`, o el proveedor de dropshipping. */
source: text('source').notNull().default('manual'),
deliveredAt: ts('delivered_at'),
...timestamps,
},
(t) => [index('order_fulfillments_order_idx').on(t.orderId)],
);
export const refunds = pgTable('order_refunds', {
id: primaryId('rfd'),
orderId: text('order_id')
.notNull()
.references(() => orders.id, { onDelete: 'cascade' }),
amount: integer('amount').notNull(),
reason: text('reason'),
lines: jsonb('lines').$type<Array<{ lineId: string; quantity: number }>>().notNull().default([]),
restocked: boolean('restocked').notNull().default(false),
providerRef: text('provider_ref'),
...timestamps,
});
/** Historial del pedido (línea de tiempo del panel) y registro de avisos ya enviados. */
export const orderEvents = pgTable(
'order_events',
{
id: primaryId('oev'),
orderId: text('order_id')
.notNull()
.references(() => orders.id, { onDelete: 'cascade' }),
type: text('type').notNull(),
data: jsonb('data').$type<Record<string, unknown>>().notNull().default({}),
actor: text('actor'),
...timestamps,
},
(t) => [index('order_events_order_idx').on(t.orderId, t.createdAt)],
);
export type Order = typeof orders.$inferSelect;
export type OrderLine = typeof orderLines.$inferSelect;
export type Fulfillment = typeof fulfillments.$inferSelect;
export type Refund = typeof refunds.$inferSelect;
このパッケージは MCP サーバーを宣言していません。
| バージョン | コミット | 公開日 | スキャン |
|---|---|---|---|
| 1.1.0 | f73e6a4 | 4 時間前 | スキャン合格 |
- genpm
- @core/auth ^1.1.0@core/catalog ^1.0.0@core/contracts ^1.0.0@core/db ^1.0.0@core/email ^1.0.1@core/jobs ^1.0.0@core/money ^1.0.0
- npm
- zod ^4.0.0
- 提案
- GenPM は npm コマンドを提案し、あなたが承認した場合にのみ実行します。
- スキャン
- スキャン合格 · 指摘 0 件
- コミット
- v1.1.0 → f73e6a4c90491b16d047c7ba7bc8841b9f29d0de · 取得後に検証済み
- スクリプト
- なし。GenPM はパッケージのコードを実行しません。
- ライセンス
- MIT
- 品質
- 100/100
- 認識されたライセンス達成
- AGENTS.md に目的の説明がある達成
- AGENTS.md に統合手順がある達成
- AGENTS.md に規約や禁止事項がある達成
- テストを含む達成
- セキュリティスキャンに合格達成
- 過去 6 か月以内に公開達成
- 認証済みの公開者達成
- 概要とキーワード達成
- 報告
- 問題を見つけましたか?