title: "Scaffold the city module and its resources"
context: |
Build a `city` domain: a shared gazetteer of cities used as reference data
(addresses, venues, facets). Not user-owned — auth is role-based: any
authenticated user reads and lists; only an admin creates/updates/deletes.
Slugs are unique. A city points at a shared `Country` (country module) for the
country name and ISO codes, keeping only name/region/coordinates. If
`modules/city/` already exists, this work is void — do not run.
goal: |
Create the `city` module + needed resources, wired admin-only on mutations.
## Notes
- If `modules/city/` exists, STOP and report. Else `/module:create` city, then
build each resource via its `*:create` skill (`--module=city`), 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 (admin-only guard on create/update/delete, read/list open
to any authenticated user, reuse the permission service); seed if the project
uses seeds (base gazetteer); migration/event/translation only if applicable
(names are translation candidates); storage/queue/workflow skip.
- Depends on `country`: confirm `modules/country/` exists, wire a `Country`
relation, and reject a city whose `country` does not resolve. Do not copy
country fields.
- Enforce slug uniqueness; throw a typed conflict (e.g. `CityAlreadyExistsException`).
- Throw typed exceptions (e.g. `CityNotFoundException`), never return null.
- Locating modules (e.g. `address`) reference this by relation, not copied fields.
### Data Model
- `City.country` ↔ `Country.cities` — ManyToOne / OneToMany
- `City.addresses` ↔ `Address.city` — OneToMany / ManyToOne (inverse on `address`)
dod: |
- [ ] Aborts with a report if `modules/city/` exists
- [ ] `city` module created and registered into the app and `SharedModule`
- [ ] `City` entity with fields: `name`, `slug` (unique), `region`, `country`
(Country relation), `latitude`, `longitude`, `timezone` (IANA), `population`
(nullable), `lang` (`LocaleType`), `createdAt`, `updatedAt`
- [ ] Full CRUD; any authenticated user reads/lists; only admin mutates;
non-admin rejected on create/update/delete
- [ ] Duplicate slug rejected; references a `Country`, not a loose code;
unresolved `country` rejected
- [ ] If seeds are used, a base gazetteer is seeded
- [ ] Unneeded resources skipped and reported with a reason
- [ ] `bun run fmt`, `bun run lint`, `bun run test` pass from the root