feat: add TourProgressController and related entities for user tour progress tracking

- Implemented TourProgressController to handle API endpoints for tracking guided tours seen by users.
- Created UserTourProgress entity to store the highest version of tours seen by each user.
- Developed UserTourProgressRepository for database interactions related to user tour progress.
- Introduced TourProgressService to manage business logic for marking tours as seen and retrieving seen maps.
- Added comprehensive tests for API endpoints and entity behavior to ensure functionality and data integrity.
This commit is contained in:
hamed
2026-08-10 09:34:14 +03:30
parent 7716b40f6a
commit 20c8eaaad9
27 changed files with 16088 additions and 477 deletions
+77 -1
View File
@@ -1,6 +1,6 @@
# User Profile API
> **Prefix:** `/api/v1/user-profile`
> **Prefix:** `/api/v1/user-profile` (plus the per-user tour endpoints under `/api/v1/my/tours`)
User medical profiles store health information that can be shared with doctors.
@@ -186,3 +186,79 @@ The stored path is also returned as `avatar` in the profile GET/PATCH responses.
|------|------|-------------|
| `ERR_VALIDATION_001` | 422 | Missing or invalid file |
| `ERR_AUTH_001` | 401 | Missing token |
---
# Guided tours (admin panel)
The admin SPA ships a step-by-step tour per page. A tour runs automatically the first time a
user opens that page, and is available afterwards from the `?` button in the page header.
Which tours a user has already been through is stored server-side, so it follows the account
across browsers and devices.
Each tour carries a `version` in the frontend registry (`assets/admin/lib/tour/tours/*.ts`).
The tour auto-runs again when its version is higher than the stored one, which is how a rewritten
tour reaches users who already saw the old text.
## GET `/api/v1/my/tours`
Tours the current user has already been through.
**Permission:** `IS_AUTHENTICATED_FULLY`
### Response `200`
```json
{ "success": true, "data": { "seen": { "appointments": 1 } } }
```
`seen` is always an object; it is `{}` for a user who has not finished any tour yet.
| Field | Type | Description |
|-------|------|-------------|
| `seen` | object | tour id → highest version the user has seen |
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
---
## POST `/api/v1/my/tours/{tourId}/seen`
Record that the current user has been through a tour. Closing a tour part-way counts as seen —
the client sends this on tour close as well as on completion.
**Permission:** `IS_AUTHENTICATED_FULLY`
| Param | Type | Description |
|-------|------|-------------|
| `tourId` | string | Route-constrained to `[a-z0-9-]{1,64}`; anything else is a 404 |
### Request Body (`application/json`)
```json
{ "version": 1 }
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `version` | integer ≥ 1 | ✅ | Version of the tour definition that was shown |
### Response `200`
```json
{ "success": true, "data": { "tourId": "appointments", "version": 1 } }
```
Repeat calls update the same row. The stored version only ever moves forward, so replaying an
old tour from the `?` button cannot re-trigger a newer one.
### Errors
```json
{ "success": false, "data": null, "errors": [ { "code": "ERR_VALIDATION_001", "message": "نسخهٔ راهنما باید عددی بزرگ‌تر از صفر باشد", "field": "version" } ] }
```
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_001` | 422 | `version` missing, not an integer, or below 1 |
| `ERR_AUTH_001` | 401 | Missing token |
| — | 404 | `tourId` does not match the route constraint |