title: "Scaffold the coupon module and its resources"
context: |
Build a `coupon` domain: discount codes an admin creates and customers redeem
at checkout. Auth splits: only an admin may create/update/delete/LIST coupons
(listing leaks every code), while any authenticated user may validate and
redeem by code. A coupon carries a discount (percentage or fixed amount), an
optional validity window, and usage caps (overall and per user). If
`modules/coupon/` already exists, this work is void — do not run.
goal: |
Create the `coupon` module + needed resources, admin management + user redemption.
## Notes
- If `modules/coupon/` exists, STOP and report. Else `/module:create` coupon,
then build each resource via its `*:create` skill (`--module=coupon`),
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
(including validate-by-code and redeem endpoints); permission always
(admin-only on create/update/delete/list; validate/redeem open to any
authenticated user; reuse the permission service); seed if the project uses
seeds; migration/event/translation only if applicable; storage/queue/workflow
skip (redemption is transactional service logic).
- Optional product scope: wire a `Product` many-to-many (link owned here) only
if `modules/product/` exists, else skip and report. Unscoped = any purchase.
- Enforce code uniqueness; throw a typed conflict (`CouponAlreadyExistsException`).
- Validate discount shape: percentage 0–100; fixed amount positive with a `currency`.
- On redeem, enforce all rules atomically with typed exceptions: active and
within `startsAt`/`expiresAt` (`CouponExpiredException`); under
`maxRedemptions` and `maxRedemptionsPerUser` (`CouponExhaustedException`);
meets `minPurchase` (`CouponInvalidException`). Increment `redemptionCount`
atomically so concurrent redeems never overshoot a cap.
- Throw typed exceptions (e.g. `CouponNotFoundException`), never return null.
### Data Model
- `Coupon.redeemedBy` ↔ `User.redeemedCoupons` — ManyToMany
- `Coupon.products` → `Product` — optional ManyToMany scope (owned here)
- For repeat redemptions per user, use a join carrying `redeemedAt` instead.
dod: |
- [ ] Aborts with a report if `modules/coupon/` exists
- [ ] `coupon` module created and registered into the app and `SharedModule`
- [ ] `Coupon` entity with fields: `code` (unique), `description`, `type`
(percentage/fixed), `value`, `currency` (required for fixed, else nullable),
`minPurchase` (nullable), `maxRedemptions` (nullable), `maxRedemptionsPerUser`
(nullable), `redemptionCount`, `startsAt`/`expiresAt` (nullable), `isActive`,
`status` (`StatusType`), `lang` (`LocaleType`), `products`, `redeemedBy`,
`createdAt`, `updatedAt`
- [ ] CRUD + redeem; only admin creates/updates/deletes/lists (non-admin
rejected); any authenticated user validates/redeems by code
- [ ] Duplicate code rejected; bad discount shape (percent outside 0–100, fixed
without currency) rejected
- [ ] Expired/inactive, over-cap, or below-`minPurchase` redemptions rejected
- [ ] Concurrent redemptions never push `redemptionCount` past a cap
- [ ] Product scope, when used, references the `product` module, not free text
- [ ] Unneeded resources skipped and reported with a reason
- [ ] `bun run fmt`, `bun run lint`, `bun run test` pass from the root