title: "Scaffold the event module and its resources"
context: |
Build an `event` domain: a calendar event a user organizes and others attend.
The organizer owns it; an admin manages any. (This is a domain entity — a
scheduled happening with start/end — not the framework's pub/sub event resource
type, which is one of the resources below.) Auth is owner-or-admin on every
mutation; an attendee link never grants ownership. An event has a window
(`startsAt` → `endsAt`) and attendees; its location is either an online
`meetingUrl` or an in-person `Address` drawn from the organizer's own addresses
(address module). If `modules/event/` already exists, this work is void — do not run.
goal: |
Create the `event` module + needed resources, wired owner-or-admin on mutations.
## Notes
- If `modules/event/` exists, STOP and report. Else `/module:create` event, then
build each resource via its `*:create` skill (`--module=event`), 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); event (pub/sub) a strong fit — emit on create/update/cancel to
notify attendees, only if something subscribes; queue if the project uses
queues (route attendee notifications/reminders — fan-out off the request
thread); migration/seed/translation only if applicable; storage only if the
cover is an uploaded file; workflow skip (status moves freely).
- Depends on `address`: confirm `modules/address/` exists, wire an `Address`
relation; do not redefine address fields.
- Validate location by mode: online (`isOnline` true) → `meetingUrl`, no
address; in-person → `address`, no `meetingUrl`. Reject mixed/missing.
- The referenced `address` must belong to the event's owner; reject another
user's address.
- Reject `endsAt` not strictly after `startsAt`. When `capacity` is set, reject
attendees beyond it (unset = unlimited). Attendees get no update/delete rights.
- Throw typed exceptions (e.g. `EventNotFoundException`), never return null.
### Data Model
- `Event.owner` ↔ `User.events` — ManyToOne / OneToMany
- `Event.address` ↔ `Address.events` — ManyToOne / OneToMany (nullable for online)
- `Event.attendees` ↔ `User.attendingEvents` — ManyToMany
dod: |
- [ ] Aborts with a report if `modules/event/` exists
- [ ] `event` module created and registered into the app and `SharedModule`
- [ ] `Event` entity with fields: `title`, `slug` (unique), `description`,
`icon`, `color` (`SimpleColorType`), `cover`, `status` (`StatusType`,
scheduled → active → completed), `isOnline`, `meetingUrl` (nullable),
`address` (nullable), `startsAt`, `endsAt`, `timezone`, `capacity`
(nullable), `lang` (`LocaleType`), `owner`, `attendees`, `createdAt`, `updatedAt`
- [ ] Full CRUD; user mutates only their own, admin any; non-owner/non-admin
rejected; attending grants no update/delete
- [ ] In-person references an `Address` (not free text); online has
`meetingUrl`/no address, in-person has address/no `meetingUrl`
- [ ] Address not owned by the event owner rejected; `endsAt` ≤ `startsAt`
rejected; attendee beyond `capacity` rejected
- [ ] If queues are used, attendee notifications run through a queue
- [ ] Unneeded resources skipped and reported with a reason
- [ ] `bun run fmt`, `bun run lint`, `bun run test` pass from the root