Bestellungen: eingefrorene Positionen, Lagerreservierung, Zahlungs- und Versandstatus, Teillieferungen, Erstattungen
Installieren
genpm add @core/ordersWas du bekommst
- Quellcode in src/lib/orders/, 10 Dateien. (41,3 kB)
- KI-Regeln in src/lib/orders/AGENTS.md, dazu Regeldateien für die IDE.
- Umgebungsvariablen in .env.example ergänzt: ORDER_LINK_SECRET, ORDER_NUMBER_PREFIX, SITE_URL.
- Löst @core/auth, @core/catalog, @core/contracts, @core/db, @core/email, @core/jobs, @core/money für dich auf.
README
Dieses Paket hat keine README.
Genau das liest deine KI, wenn sie in src/lib/orders arbeitet. Sonst wird ihrem Kontext nichts hinzugefügt.
@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.
Der genaue Baum, der nach .genpmignore eingebunden wird. Gepinnt an
// Recurso de panel "Pedidos": lista con filtros, detalle con líneas/envíos/reembolsos/historial y acciones.
import { and, count, desc, ilike, or, type SQL, sql } from 'drizzle-orm';
import { z } from 'zod';
import type { AdminContext, AdminResource } from '../contracts/index.ts';
import { getDb } from '../db/index.ts';
import { addFulfillment, cancelOrder, getOrder, markPaid, type OrderDetail, OrderError, refundOrder } from './orders.ts';
import { orders } from './schema.ts';
async function need(ctx: AdminContext, perm: string) {
if (!(await ctx.can(perm))) throw new OrderError('forbidden', `forbidden: ${perm}`);
}
const reload = async (id: string) => (await getOrder(id))!;
const Fulfill = z.object({ carrier: z.string().max(60).optional(), trackingNumber: z.string().max(100).optional(), trackingUrl: z.url().optional() });
const Refund = z.object({ amount: z.coerce.number().int().positive(), reason: z.string().max(300).optional(), restock: z.coerce.boolean().optional() });
export const ordersAdminResource: AdminResource<OrderDetail> = {
name: 'orders',
label: { singular: 'Order', plural: 'Orders' },
group: 'Store',
fields: [
{ name: 'number', label: 'Order', type: 'text', readOnly: true, list: true },
{ name: 'email', label: 'Customer', type: 'email', readOnly: true, list: true },
{ name: 'status', label: 'Payment', type: 'text', readOnly: true, list: true },
{ name: 'fulfillmentStatus', label: 'Fulfillment', type: 'text', readOnly: true, list: true },
{ name: 'total', label: 'Total', type: 'money', readOnly: true, list: true },
{ name: 'lines', label: 'Items', type: 'json', readOnly: true },
{ name: 'shippingAddress', label: 'Ship to', type: 'json', readOnly: true },
{ name: 'events', label: 'Timeline', type: 'json', readOnly: true },
{ name: 'createdAt', label: 'Date', type: 'datetime', readOnly: true, list: true },
],
input: z.object({}),
title: (o) => `#${o.number}`,
async list(q, ctx) {
await need(ctx, 'orders:read');
const conds: SQL[] = [];
if (q.filters?.status) conds.push(sql`${orders.status} = ${q.filters.status}`);
if (q.filters?.fulfillment) conds.push(sql`${orders.fulfillmentStatus} = ${q.filters.fulfillment}`);
if (q.search) {
const like = `%${q.search.replace(/[%_\\]/g, (m) => `\\${m}`)}%`;
conds.push(or(ilike(orders.number, like), ilike(orders.email, like))!);
}
const where = conds.length ? and(...conds) : undefined;
const [total] = await getDb().select({ n: count() }).from(orders).where(where);
const rows = await getDb().select({ id: orders.id }).from(orders).where(where).orderBy(desc(orders.createdAt)).limit(q.pageSize).offset((Math.max(q.page, 1) - 1) * q.pageSize);
return { rows: await Promise.all(rows.map((r) => reload(r.id))), total: total?.n ?? 0 };
},
async get(id, ctx) {
await need(ctx, 'orders:read');
return getOrder(id);
},
actions: [
{
name: 'mark-paid',
label: 'Mark as paid (manual payment)',
permission: 'orders:update',
confirm: true,
input: z.object({ reference: z.string().min(1).max(100) }),
available: (o) => o.status === 'pending',
async run(id, input, ctx) {
await need(ctx, 'orders:update');
await markPaid(id, { provider: 'manual', ref: z.object({ reference: z.string().min(1).max(100) }).parse(input).reference });
return reload(id);
},
},
{
name: 'fulfill',
label: 'Mark as shipped',
permission: 'orders:fulfill',
input: Fulfill,
available: (o) => (o.status === 'paid' || o.status === 'partially_refunded') && o.fulfillmentStatus !== 'fulfilled' && o.fulfillmentStatus !== 'delivered',
async run(id, input, ctx) {
await need(ctx, 'orders:fulfill');
await addFulfillment(id, { ...Fulfill.parse(input), actor: ctx.user.id });
return reload(id);
},
},
{
name: 'refund',
label: 'Refund',
permission: 'orders:refund',
confirm: true,
input: Refund,
available: (o) => (o.status === 'paid' || o.status === 'partially_refunded' || (o.status === 'cancelled' && !!o.paidAt)) && o.refundedTotal < o.total,
async run(id, input, ctx) {
await need(ctx, 'orders:refund');
await refundOrder(id, { ...Refund.parse(input), actor: ctx.user.id });
return reload(id);
},
},
{
name: 'cancel',
label: 'Cancel order',
permission: 'orders:update',
confirm: true,
available: (o) => o.status !== 'cancelled' && o.fulfillmentStatus === 'unfulfilled',
async run(id, _input, ctx) {
await need(ctx, 'orders:update');
await cancelOrder(id, { actor: ctx.user.id });
return reload(id);
},
},
],
};
Dieses Paket deklariert keine MCP-Server.
| Version | Commit | Veröffentlicht | Prüfung |
|---|---|---|---|
| 1.1.0 | f73e6a4 | vor 2 Stunden | Prüfung bestanden |
- 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
- vorgeschlagen
- GenPM schlägt den npm-Befehl vor und führt ihn nur aus, wenn du zustimmst.
- Prüfung
- Prüfung bestanden · 0 Befunde
- Commit
- v1.1.0 → f73e6a4c90491b16d047c7ba7bc8841b9f29d0de · nach dem Abruf verifiziert
- Skripte
- Keine. GenPM führt niemals Paketcode aus.
- Lizenz
- MIT
- Qualität
- 100/100
- Anerkannte Lizenzerfüllt
- AGENTS.md erklärt den Zweckerfüllt
- AGENTS.md enthält Integrationsschritteerfüllt
- AGENTS.md nennt Konventionen oder Verboteerfüllt
- Enthält Testserfüllt
- Sicherheitsscan bestandenerfüllt
- In den letzten 6 Monaten veröffentlichterfüllt
- Verifizierter Herausgebererfüllt
- Zusammenfassung und Schlagwörtererfüllt
- Meldung
- Stimmt etwas nicht?