> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Coupon Resource

> A ready-to-run spec that scaffolds the `coupon` module as admin-managed discount codes with redemption rules, usage caps, and code-level redemption open to any authenticated user.

## The prompt

```yaml theme={null}
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.
  - Dependencies — required `user`; optional `product`. Resolve before the
    `/module:create` step above: create the missing required `user` module FIRST
    from https://docs.ooneex.com/ai/prompts/resources/user-resource. Create
    `product` from https://docs.ooneex.com/ai/prompts/resources/product-resource
    only when you wire its scope; otherwise skip and report.
  - 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 (or was created from its docs prompt above),
    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
  - [ ] Required dependency (`user`) created first from its docs prompt; `product` created from its docs prompt when scoped
  - [ ] `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
```
