title: "Scaffold the product module and its resources"
context: |
Build a `product` domain: catalog items a user (vendor) owns and an admin
manages for anyone. Auth is owner-or-admin on every mutation. A product is
classified by an optional `Category` and any number of `Tag`s from the shared
taxonomy modules (not free text). Visibility ties to lifecycle: only a
published product is visible to others — drafts stay private to owner and
admins. If `modules/product/` already exists, this work is void — do not run.
goal: |
Create the `product` module + needed resources, wired owner-or-admin on mutations.
## Notes
- If `modules/product/` exists, STOP and report. Else `/module:create` product,
then build each resource via its `*:create` skill (`--module=product`),
respecting controllers → services → repositories → entities, registering all.
- Judge each resource; create the justified, skip the rest with a reason.
Defaults: entity + repository always; service + controller per use case;
permission always (owner-or-admin on update/delete, reuse the permission
service); storage for product images if uploaded (remove on delete);
migration/seed/event/translation/queue only if applicable (name/description
are translation candidates; route publish/reindex through a queue if used);
workflow only if publishing has steps that must roll back together (e.g.
search index / payment sync), else skip.
- Classify with the shared taxonomy: optional `Category` relation + `Tag`
many-to-many, links owned here. Wire each only if its module exists, else skip
and report.
- Enforce uniqueness on `slug` and `sku`; throw a typed conflict
(`ProductAlreadyExistsException`).
- Visibility by lifecycle: non-owner/non-admin reads/lists only published;
owner and admins also see drafts.
- Reject any update dropping `stock` below zero.
- Throw typed exceptions (e.g. `ProductNotFoundException`), never return null.
### Data Model
- `Product.owner` ↔ `User.products` — ManyToOne / OneToMany
- `Product.category` → `Category` — ManyToOne (owned here, nullable)
- `Product.tags` → `Tag` — ManyToMany (owned here)
dod: |
- [ ] Aborts with a report if `modules/product/` exists
- [ ] `product` module created and registered into the app and `SharedModule`
- [ ] `Product` entity with fields: `name`, `slug` (unique), `sku` (unique),
`description`, `icon`, `color` (`SimpleColorType`), `thumbnail`, `images`,
`price`, `compareAtPrice` (nullable), `currency` (ISO 4217), `stock`,
`weight` (nullable), `status` (`StatusType`, draft → active → archived),
`publishedAt` (nullable), `lang` (`LocaleType`), `category`, `tags`, `owner`,
`createdAt`, `updatedAt`
- [ ] Full CRUD; user mutates only their own, admin any; non-owner/non-admin
rejected on mutation and reads/lists only published
- [ ] Duplicate slug or sku rejected; category/tags reference the shared
taxonomy, not free text
- [ ] Update dropping stock below zero rejected; delete removes stored images
- [ ] If queues are used, publish/reindex work runs through a queue
- [ ] Unneeded resources skipped and reported with a reason
- [ ] `bun run fmt`, `bun run lint`, `bun run test` pass from the root