|
| 1 | +# Product Rating |
| 2 | + |
| 3 | +## Meta |
| 4 | + |
| 5 | +| Field | Value | |
| 6 | +| -------------- | --------------------------- | |
| 7 | +| Status | `approved` | |
| 8 | +| Author | bartstc | |
| 9 | +| Created | 2026-04-17 | |
| 10 | +| Last updated | 2026-04-17 | |
| 11 | +| Spec directory | `specs/004-product-rating/` | |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## 1. Goal & Context |
| 16 | + |
| 17 | +The `StarRating` component currently renders read-only stars derived from the marketing product's aggregate rating. Users have no way to contribute their own rating. This feature makes the stars interactive: clicking a star submits a 1–5 integer rating to `PATCH /api/marketing/products/:id/rate`, updates the displayed rating optimistically, and disables further rating in that session. Unauthenticated users see a warning toast instead. |
| 18 | + |
| 19 | +## 2. Requirements |
| 20 | + |
| 21 | +- **R1**: WHEN a logged-in user clicks a star (1–5), THE SYSTEM SHALL immediately update the displayed rating optimistically and call `PATCH /api/marketing/products/:id/rate` with the selected integer. |
| 22 | +- **R2**: WHEN the API call fails, THE SYSTEM SHALL revert the displayed rating to its pre-click value and show an error toast. |
| 23 | +- **R3**: WHEN an unauthenticated user clicks a star, THE SYSTEM SHALL show a warning toast "You have to log in to rate the product" and make no API call. |
| 24 | +- **R4**: WHEN a user has submitted a rating in the current session, THE SYSTEM SHALL disable the star controls so no further rating is possible. |
| 25 | +- **R5**: WHEN the rating mutation is pending, THE SYSTEM SHALL visually disable the star controls to prevent double submission. |
| 26 | + |
| 27 | +## 3. Non-Goals / Out of Scope |
| 28 | + |
| 29 | +- Persisting "already rated" state across sessions (no localStorage / server-side deduplication). |
| 30 | +- Half-star or decimal rating input. |
| 31 | +- Showing the user's own previously submitted rating vs. the aggregate. |
| 32 | +- Any changes to the "see reviews" button behaviour. |
| 33 | +- Rate-limiting or throttling beyond disabling after one submission. |
| 34 | + |
| 35 | +## 4. Building Blocks Diff |
| 36 | + |
| 37 | +### Added |
| 38 | + |
| 39 | +- `useRateProductMutation` (mutation-hook) — `src/lib/api/marketing/{product-id}/use-rate-product-mutation.ts` — calls `PATCH /api/marketing/products/:id/rate`, applies optimistic update to cached `MarketingProductDto`, rolls back on error |
| 40 | +- `useRateProductMutation` (mutation-hook, providers re-export) — `src/features/products/providers/use-rate-product-mutation.ts` |
| 41 | +- `useRateProductNotifications` (notification-hook) — `src/features/products/application/use-rate-product-notifications.ts` — warning toast for unauthenticated, error toast on failure |
| 42 | +- `useRateProduct` (use-case-hook) — `src/features/products/application/use-rate-product.ts` — checks auth, calls mutation, dispatches notifications; returns `{ rate, isPending, hasRated }` |
| 43 | + |
| 44 | +### Modified |
| 45 | + |
| 46 | +- `StarRating` (component) — `src/features/products/components/StarRating.tsx` — add `productId` prop, wire `useRateProduct`, make stars clickable, disable after rating or while pending |
| 47 | +- `ProductDetails` (component) — `src/features/products/components/ProductDetails.tsx` — pass `product.id` as `productId` to `StarRating` |
| 48 | +- `public/locales/en/translation.json` — add toast keys under `features.products.rating` |
| 49 | + |
| 50 | +## 5. Design Decisions |
| 51 | + |
| 52 | +- **Optimistic update in the mutation, not local state**: The cache already holds `MarketingProductDto`; mutating it directly keeps one source of truth. A separate `useState` would duplicate state and diverge on rollback. |
| 53 | +- **Auth check in `useRateProduct`**: Consistent with `useAddToCart` — auth guard lives in the use-case hook, not in the component or mutation. Component stays presentational. |
| 54 | +- **`hasRated` as local state in `useRateProduct`**: Session-only disable with no persistence, per requirements. Clean `useState<boolean>` inside the hook keeps the mutation layer unaware. |
| 55 | + |
| 56 | +## 6. Boundaries |
| 57 | + |
| 58 | +### ✅ Always |
| 59 | + |
| 60 | +- Create/modify files under `src/features/products/` |
| 61 | +- Create files under `src/lib/api/marketing/` |
| 62 | +- Add translation keys to `public/locales/en/translation.json` |
| 63 | + |
| 64 | +### ⚠️ Ask First |
| 65 | + |
| 66 | +- Any change to `MarketingProductDto` in `src/lib/api/marketing/{product-id}/marketing-product-dto.ts` |
| 67 | + |
| 68 | +### 🚫 Never |
| 69 | + |
| 70 | +- Modify `server/` source files |
| 71 | +- Remove or skip existing tests |
| 72 | +- Change the `rateMarketingProductHandler` API contract |
| 73 | + |
| 74 | +## 7. Task Breakdown |
| 75 | + |
| 76 | +1. [S] Add `useRateProductMutation` with optimistic update and rollback — `src/lib/api/marketing/{product-id}/use-rate-product-mutation.ts` (R1, R2) |
| 77 | +2. [S] Re-export `useRateProductMutation` from providers — `src/features/products/providers/use-rate-product-mutation.ts` (R1, R2) |
| 78 | +3. [P] Add `useRateProductNotifications` — `src/features/products/application/use-rate-product-notifications.ts` (R2, R3) |
| 79 | +4. [P] Add translation keys for rating toasts — `public/locales/en/translation.json` (R2, R3) |
| 80 | +5. [S] Add `useRateProduct` use-case hook — `src/features/products/application/use-rate-product.ts` (R1, R2, R3, R4, R5) |
| 81 | +6. [S] Modify `StarRating` — add `productId` prop, wire `useRateProduct`, clickable + disabled states — `src/features/products/components/StarRating.tsx` (R1, R4, R5) |
| 82 | +7. [S] Pass `productId` from `ProductDetails` to `StarRating` — `src/features/products/components/ProductDetails.tsx` (R1) |
| 83 | + |
| 84 | +## 8. Error & Edge Cases |
| 85 | + |
| 86 | +- GIVEN the API returns a non-2xx response, WHEN the mutation settles, THEN revert the optimistic rating to the pre-click cached value and show an error toast. |
| 87 | +- GIVEN the user is unauthenticated, WHEN a star is clicked, THEN show the warning toast and do not call the mutation; stars remain enabled so the user can log in and retry. |
| 88 | +- GIVEN the mutation is in-flight, WHEN the user attempts to click another star, THEN controls are disabled and no second request is made. |
| 89 | +- GIVEN the user has successfully rated, WHEN the component re-renders due to a query refetch, THEN `hasRated` persists in hook state and stars remain disabled. |
| 90 | +- GIVEN the cached `MarketingProductDto` is absent, WHEN a star is clicked, THEN the optimistic update is skipped gracefully and the mutation still fires; rollback is a no-op. |
| 91 | + |
| 92 | +## 10. Open Questions |
| 93 | + |
| 94 | +_(none)_ |
| 95 | + |
| 96 | +## 11. References |
| 97 | + |
| 98 | +- `server/src/modules/marketing/marketing.handlers.ts` — `rateMarketingProductHandler` implementation |
| 99 | +- `server/src/modules/marketing/marketing.routes.ts` — `PATCH /api/marketing/products/:id/rate` route |
| 100 | +- `src/features/carts/application/use-add-to-cart.ts` — auth-check pattern to follow |
0 commit comments