Catálogo de produtos: variantes com opções, preços em centavos, categorias, coleções, traduções e reserva de estoque segura
Instalar
genpm add @core/catalogO que você recebe
- Código em src/lib/catalog/, 8 arquivos. (38,6 kB)
- Regras de IA em src/lib/catalog/AGENTS.md, mais arquivos de regras da IDE.
- Variáveis de ambiente adicionadas ao .env.example: STORE_CURRENCY.
- Resolve @core/contracts, @core/db, @core/money, @core/rich-text para você.
README
Este pacote não tem README.
Isto é exatamente o que sua IA lê quando trabalha em src/lib/catalog. Nada mais é adicionado ao contexto dela.
@core/catalog — rules for AI agents
Purpose
Products for a store: up to 3 options (Size, Color…) and their variants with SKU, price/compare-at/cost in minor
units, stock with backorder rules, weight and image; categories (tree), manual ordered collections, tags, attributes,
per-locale translations, media references (@core/media ids), sitemap/search sources and admin resources.
reserveStock locks the variant row so two buyers can't get the last unit. No cart, prices rules or multi-warehouse.
Map
index.ts— public API:createProduct,updateProduct,getProduct,listProducts,reserveStock,releaseStock,upsertCategory,setCollection,categoryTree,lowStockVariants,catalogAdminResources.products.ts— products, variants, listing, stock.taxonomy.ts— categories and collections.sources.ts—productSitemapSource,productSearchSource.admin.ts— admin resources.schema.ts— tables.
Integration
- Env:
STORE_CURRENCY(ISO code, defaultEUR). Migrations as insrc/lib/db/AGENTS.md. - Product page:
const p = await getProduct(slug, { locale })(404 when null); showp.variantswithformat(v.price, locale)(@core/money) andv.availability; renderp.descriptionwith @core/rich-text. - Listing:
listProducts({ category, tag, collection, sort, page, minPrice, maxPrice, inStock }). - Register
productSitemapSource()(seo),productSearchSource()(search) and...catalogAdminResources()(admin). - JSON-LD with
jsonLd.product(@core/seo) usingavailabilityand the real price. - Verify: create a product with two variants in the admin and see it at
/products/<slug>.
Conventions
- Prices are integers in
STORE_CURRENCY; the server price is the only valid price (carts recompute it). - Reserve stock only inside the order transaction (
reserveStock(id, qty, tx)); release it on cancel/expiry. - Archive products instead of deleting them: orders keep references to their variants.
- Permissions:
products:read|create|update|delete.
Don't
- Don't show a compare-at price that isn't real (the EU requires the lowest price of the last 30 days for discounts).
- Don't decrement
stockwith plain updates outsidereserveStock/releaseStock. - Don't expose
costAmountin public pages or APIs.
# @core/catalog — rules for AI agents
## Purpose
Products for a store: up to 3 options (Size, Color…) and their variants with SKU, price/compare-at/cost in minor
units, stock with backorder rules, weight and image; categories (tree), manual ordered collections, tags, attributes,
per-locale translations, media references (@core/media ids), sitemap/search sources and admin resources.
`reserveStock` locks the variant row so two buyers can't get the last unit. No cart, prices rules or multi-warehouse.
## Map
- `index.ts` — public API: `createProduct`, `updateProduct`, `getProduct`, `listProducts`, `reserveStock`, `releaseStock`, `upsertCategory`, `setCollection`, `categoryTree`, `lowStockVariants`, `catalogAdminResources`.
- `products.ts` — products, variants, listing, stock. `taxonomy.ts` — categories and collections.
- `sources.ts` — `productSitemapSource`, `productSearchSource`. `admin.ts` — admin resources. `schema.ts` — tables.
## Integration
1. Env: `STORE_CURRENCY` (ISO code, default `EUR`). Migrations as in `src/lib/db/AGENTS.md`.
2. Product page: `const p = await getProduct(slug, { locale })` (404 when null); show `p.variants` with
`format(v.price, locale)` (@core/money) and `v.availability`; render `p.description` with @core/rich-text.
3. Listing: `listProducts({ category, tag, collection, sort, page, minPrice, maxPrice, inStock })`.
4. Register `productSitemapSource()` (seo), `productSearchSource()` (search) and `...catalogAdminResources()` (admin).
5. JSON-LD with `jsonLd.product` (@core/seo) using `availability` and the real price.
6. Verify: create a product with two variants in the admin and see it at `/products/<slug>`.
## Conventions
- Prices are integers in `STORE_CURRENCY`; the server price is the only valid price (carts recompute it).
- Reserve stock only inside the order transaction (`reserveStock(id, qty, tx)`); release it on cancel/expiry.
- Archive products instead of deleting them: orders keep references to their variants.
- Permissions: `products:read|create|update|delete`.
## Don't
- Don't show a compare-at price that isn't real (the EU requires the lowest price of the last 30 days for discounts).
- Don't decrement `stock` with plain updates outside `reserveStock`/`releaseStock`.
- Don't expose `costAmount` in public pages or APIs.
A árvore exata que será injetada, após o .genpmignore. Fixada em
// Tablas de @core/catalog. Las recoge drizzle-kit vía src/lib/db/drizzle.config.ts.
import { boolean, index, integer, jsonb, pgTable, primaryKey, text, timestamp } from 'drizzle-orm/pg-core';
import { primaryId, timestamps } from '../db/index.ts';
import type { Doc } from '../rich-text/index.ts';
export type ProductOption = { name: string; values: string[] };
export type ProductTranslation = { name?: string; description?: Doc; seoTitle?: string; seoDescription?: string };
export const products = pgTable(
'products',
{
id: primaryId('prd'),
slug: text('slug').notNull().unique(),
name: text('name').notNull(),
description: jsonb('description').$type<Doc>(),
status: text('status', { enum: ['draft', 'active', 'archived'] }).notNull().default('draft'),
kind: text('kind', { enum: ['physical', 'digital'] }).notNull().default('physical'),
brand: text('brand'),
/** Opciones de variante en orden: `[{ name: 'Size', values: ['S','M'] }]`. */
options: jsonb('options').$type<ProductOption[]>().notNull().default([]),
tags: jsonb('tags').$type<string[]>().notNull().default([]),
/** Atributos libres para fichas y filtros (`material: ceramic`). */
attributes: jsonb('attributes').$type<Record<string, string>>().notNull().default({}),
translations: jsonb('translations').$type<Record<string, ProductTranslation>>().notNull().default({}),
seoTitle: text('seo_title'),
seoDescription: text('seo_description'),
publishedAt: timestamp('published_at', { withTimezone: true, mode: 'date' }),
...timestamps,
},
(t) => [index('products_status_idx').on(t.status, t.publishedAt)],
);
export const productVariants = pgTable(
'product_variants',
{
id: primaryId('var'),
productId: text('product_id')
.notNull()
.references(() => products.id, { onDelete: 'cascade' }),
sku: text('sku').unique(),
/** `M / Red` (o vacío si el producto no tiene opciones). */
title: text('title').notNull().default(''),
options: jsonb('options').$type<Record<string, string>>().notNull().default({}),
priceAmount: integer('price_amount').notNull(),
/** Precio "antes" (tachado). Solo si es real: ver @core/discounts y @core/pricing (regla de 30 días UE). */
compareAtAmount: integer('compare_at_amount'),
/** Coste (para márgenes; nunca se muestra). */
costAmount: integer('cost_amount'),
currency: text('currency').notNull(),
stock: integer('stock').notNull().default(0),
trackStock: boolean('track_stock').notNull().default(true),
/** Vender sin stock (bajo pedido). */
allowBackorder: boolean('allow_backorder').notNull().default(false),
lowStockThreshold: integer('low_stock_threshold').notNull().default(3),
weightGrams: integer('weight_grams'),
position: integer('position').notNull().default(0),
active: boolean('active').notNull().default(true),
/** Media de la variante (p. ej. foto del color). */
mediaId: text('media_id'),
...timestamps,
},
(t) => [index('product_variants_product_idx').on(t.productId, t.position)],
);
export const productMedia = pgTable(
'product_media',
{
productId: text('product_id')
.notNull()
.references(() => products.id, { onDelete: 'cascade' }),
mediaId: text('media_id').notNull(),
position: integer('position').notNull().default(0),
},
(t) => [primaryKey({ columns: [t.productId, t.mediaId] })],
);
export const categories = pgTable('product_categories', {
id: primaryId('cat'),
slug: text('slug').notNull().unique(),
name: text('name').notNull(),
description: text('description'),
parentId: text('parent_id'),
position: integer('position').notNull().default(0),
translations: jsonb('translations').$type<Record<string, { name?: string; description?: string }>>().notNull().default({}),
...timestamps,
});
export const productCategories = pgTable(
'product_category_links',
{
productId: text('product_id')
.notNull()
.references(() => products.id, { onDelete: 'cascade' }),
categoryId: text('category_id')
.notNull()
.references(() => categories.id, { onDelete: 'cascade' }),
},
(t) => [primaryKey({ columns: [t.productId, t.categoryId] }), index('product_category_links_cat_idx').on(t.categoryId)],
);
/** Colecciones manuales ordenadas (portada, "novedades", "más vendidos"…). */
export const collections = pgTable('product_collections', {
id: primaryId('col'),
slug: text('slug').notNull().unique(),
name: text('name').notNull(),
description: text('description'),
...timestamps,
});
export const collectionProducts = pgTable(
'product_collection_items',
{
collectionId: text('collection_id')
.notNull()
.references(() => collections.id, { onDelete: 'cascade' }),
productId: text('product_id')
.notNull()
.references(() => products.id, { onDelete: 'cascade' }),
position: integer('position').notNull().default(0),
},
(t) => [primaryKey({ columns: [t.collectionId, t.productId] })],
);
export type Product = typeof products.$inferSelect;
export type Variant = typeof productVariants.$inferSelect;
export type Category = typeof categories.$inferSelect;
export type Collection = typeof collections.$inferSelect;
Este pacote não declara servidores MCP.
| Versão | Commit | Publicado | Análise |
|---|---|---|---|
| 1.0.0 | 79d3468 | há 7 horas | análise aprovada |
- npm
- zod ^4.0.0
- proposto
- O GenPM propõe o comando npm e só o executa se você disser sim.
- análise
- análise aprovada · 0 achados
- commit
- v1.0.0 → 79d3468925acbde57d17a18a32e9d4bdefc28b5e · verificado após o download
- scripts
- Nenhum. O GenPM nunca executa código de pacotes.
- licença
- MIT
- Qualidade
- 100/100
- Licença reconhecidacumprido
- AGENTS.md explica o propósitocumprido
- AGENTS.md tem passos de integraçãocumprido
- AGENTS.md lista convenções ou proibiçõescumprido
- Inclui testescumprido
- Escaneamento de segurança aprovadocumprido
- Publicado nos últimos 6 mesescumprido
- Publicador verificadocumprido
- Resumo e palavras-chavecumprido
- denúncia
- Viu algo errado?