Public Access
Captures the Core slices of the cross-service product roadmap: per-game tracking schema (status, rating, notes, completion date), picker endpoint, HLTB integration, achievement aggregation, goals + digest data, wishlist + sale alerts, library enrichment, and public profile data. Cross-links to the umbrella roadmap in ToBePlayed-Deployment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
169 lines
11 KiB
Markdown
169 lines
11 KiB
Markdown
# To Be Played (TBP) - Core Service
|
||
|
||
This is the Core Service for the "To Be Played" project. It acts as the central modulith, handling all business logic in vertical micro-service ready slices.
|
||
|
||
## 🚀 Tech Stack
|
||
|
||
* **Language:** Java 25
|
||
* **Framework:** Spring Boot 4.1.x
|
||
* **Security:** Spring Security & JSON Web Tokens (JWT)
|
||
* **Database:** PostgreSQL 17
|
||
* **Migrations:** Flyway
|
||
* **Observability:** Spring Boot Actuator & Micrometer (Prometheus)
|
||
* **Build Tool:** Maven Wrapper (`mvnw`)
|
||
* **CI/CD:** Gitea Actions (Forgejo)
|
||
|
||
## 🏗️ Architecture (Clean Architecture)
|
||
|
||
The project strictly follows a Domain-Driven / Clean Architecture (Ports & Adapters) approach to ensure high maintainability and testability. The codebase is divided into three main layers to prevent framework lock-in and keep business logic isolated.
|
||
|
||
### Layer Breakdown
|
||
|
||
1. **API / Web Layer (`api`)**
|
||
* Contains REST endpoints, DTOs (`Request`/`Response` records), and Global Exception Handlers.
|
||
* Uses `ApiMapper` to map between DTOs and Domain Models.
|
||
* **Rule:** This layer delegates to the Service layer and never accesses persistence directly.
|
||
|
||
2. **Business / Core Layer (`model`, `service`, `exception`)**
|
||
* The heart of the application. Contains pure Java classes (Models) and Business Logic (Services).
|
||
* Throws specific business exceptions (e.g., `InvalidCredentialsException`).
|
||
* **Rule:** This layer has **no dependencies** on the API or Persistence layer. It defines interfaces that the persistence layer must implement.
|
||
|
||
3. **Persistence Layer (`persistence`)**
|
||
* Handles database operations. Contains JPA Entities (`UserEntity`), Spring Data Repositories, and the `EntityMapper`.
|
||
* Implements the interfaces defined in the Domain layer.
|
||
* **Rule:** Entities never leak into the API layer.
|
||
|
||
Architectural boundaries are enforced via ArchUnit (`src/test/java/core_service/architecture/ArchitectureTest.java`), which runs as part of the standard `./mvnw test` quality gate. Abstract test bases live under `core_service.shared.test` (`AbstractUnitTest`, `AbstractIntegrationTest`, `AbstractPersistenceTest`, `AbstractWebMvcTest`, `AbstractArchitectureTest`).
|
||
|
||
> **Core-specific structure note.** Unlike `auth-service`, Core keeps JPA `@Entity` classes in `..domain..` (rather than a separate `..persistence.entities..` package) and repository interfaces in `..repo..`. The ArchUnit rules are adapted to this layout. See "Roadmap / Open Topics" below for the planned tightening.
|
||
|
||
## ⚙️ Local Development
|
||
|
||
Running the application locally is fully automated via Spring Boot's Docker Compose Support.
|
||
|
||
### Prerequisites
|
||
|
||
* Java 25 installed
|
||
* Docker & Docker Compose running
|
||
|
||
### How to start
|
||
|
||
1. Simply run the application from your IDE (e.g., IntelliJ) via the `CoreServiceApplication.java` main class.
|
||
2. Spring Boot will automatically detect the `compose.yaml` and spin up a local PostgreSQL container (`tbp_db_local`) on port `5432`.
|
||
3. **Flyway** will automatically apply all database migrations located in `src/main/resources/db/migration`.
|
||
4. The application will be available at `http://localhost:8090`.
|
||
|
||
To stop the environment and clean the database volume (if needed):
|
||
|
||
```bash
|
||
docker compose down -v
|
||
```
|
||
|
||
## 🔐 API Versioning
|
||
|
||
All API endpoints utilize Query Parameter Versioning to keep URLs clean and manageable.
|
||
Example: GET /api/users/me?v=1
|
||
|
||
## 🐳 CI/CD Pipeline
|
||
|
||
The project uses Gitea Actions for Continuous Integration and Deployment.
|
||
|
||
Quality Gate: Runs ./mvnw clean verify to ensure tests and builds pass.
|
||
|
||
Docker Build: Packages the application using a multi-stage Dockerfile.
|
||
|
||
Registry: Pushes the lightweight Alpine-based image to the private Gitea container registry (git.andreas-dahm.eu/lobo_corp/tbp-core-service:latest).
|
||
|
||
## 📈 Observability
|
||
|
||
Metrics and health checks are exposed out-of-the-box via Spring Boot Actuator:
|
||
|
||
Health: http://localhost:8090/actuator/health
|
||
|
||
Prometheus Metrics: http://localhost:8090/actuator/prometheus
|
||
|
||
## 🗺️ Roadmap / Open Topics
|
||
|
||
Tracking known work that is not yet implemented. Each item is independently actionable.
|
||
|
||
### Architecture / ArchUnit follow-ups
|
||
|
||
Currently active rules cover: cycles, controller→repository direction, repository naming, controller/service annotations, shared/configuration isolation, and a relaxed slice rule. The following rules are intentionally **not** yet enforced because they would fail today; addressing them is a separate refactor.
|
||
|
||
- **Strict module independence.** `library` currently imports from `steam_adapter` (e.g. `SteamApiService`, response DTOs). The ArchUnit rule exempts that edge via `ignoreDependency(library.., steam_adapter..)`. Long-term: either move `steam_adapter` out of `modules` (into `core_service.adapters.steam` or `core_service.shared.steam`) to make its role as a shared outbound port explicit, then enable strict `notDependOnEachOther()` without exemptions.
|
||
- **"DTOs only in `..api..`" rule.** Steam adapter response records currently live in `..api.dto..` and are consumed by `library` services. Move them to `core_service.modules.steam_adapter.model` (or `.port`) so they read as domain types, not API DTOs, then re-enable the strict rule.
|
||
- **"Entities only in `..persistence..`" rule.** Skipped because Core mixes entities with the domain package. Decide whether to (a) accept that pattern and keep the rule off, or (b) split entities into `..repo.entity..` and re-enable the rule for consistency with `auth-service`.
|
||
|
||
### Security
|
||
|
||
- **Drop the shared `JWT_SECRET`.** Today both Auth and Core hold the same HMAC secret, which means a Core compromise lets an attacker forge tokens for Auth. Once Auth ships EdDSA signing (see Auth README roadmap), Core should hold only the **public key** (`JWT_PUBLIC_KEY`) and verify with `Jwts.parser().verifyWith(publicKey).build()`. Touches `JwtConfig` (rename `secret` → `publicKey`), `JwtService` (replace `Keys.hmacShaKeyFor` with `KeyFactory.getInstance("Ed25519")`), and the deployment env wiring.
|
||
- **Consume a JWKS endpoint** once Auth exposes one (`GET https://auth.tbp.andreas-dahm.eu/.well-known/jwks.json`). Cache keys at startup, refresh on `kid` miss. Avoids redeploying Core when keys rotate.
|
||
- **Add a `JwtServiceTest`.** No verification tests exist for Core's JWT parser today; adding one is also a natural place to add the EdDSA key fixture.
|
||
|
||
### Testing
|
||
|
||
- **First Steam adapter integration test.** `SteamApiServiceTest` is a unit test; an integration test with WireMock (or Spring's `MockRestServiceServer`) would lock in the contract with the external Steam API and the Resilience4j retry/rate-limit configuration.
|
||
- **Persistence-layer tests.** `library/repo/*Repository` has no `AbstractPersistenceTest`-based test today. Good first targets: `UserGameRepository`, `UserSteamSyncRepository`.
|
||
- **Controller WebMvc tests.** `LibraryController`, `PublicStatsController`, `SteamController` have no `AbstractWebMvcTest`-based tests yet. The Auth project's `*ControllerTest` classes are the pattern to follow.
|
||
|
||
### CI
|
||
|
||
- **Wire the ArchUnit + scaffolding into the existing Forgejo Actions quality gate.** `./mvnw verify` already runs them; confirm CI fails (not just warns) on architecture violations so drift is caught before merge.
|
||
|
||
## 🎯 Product Roadmap (Core-owned slices)
|
||
|
||
The full cross-service product roadmap lives in `ToBePlayed-Deployment/README.md`. This section lists only the slices that land in Core, in suggested execution order.
|
||
|
||
### 1. Per-game tracking — the namesake feature
|
||
|
||
Today `user_game` mirrors Steam ownership and playtime, nothing else. Closing this gap turns the library from a Steam mirror into an actual backlog tracker.
|
||
|
||
- Add a `status` column on `user_game` (`BACKLOG / PLAYING / COMPLETED / DROPPED / ON_HOLD / WISHLIST`) with a Flyway migration. Default at import: playtime > 0 → `PLAYING`, otherwise `BACKLOG`.
|
||
- Add `rating` (smallint 1–10, nullable), `completed_at` (timestamp, nullable), `notes` (text, markdown, server-side rendered or sanitised on read).
|
||
- Endpoint: `PATCH /api/library/games/{appid}` for partial updates of these fields. Stick with query-parameter versioning (`?v=1`).
|
||
- Endpoint: filter `GET /api/library/games?status=BACKLOG` (extend the existing pagination contract).
|
||
- New module candidate or extend `library` — keep it in `library` until tracking grows its own queries / reports, then split.
|
||
|
||
### 2. Picker / "what to play next"
|
||
|
||
- Endpoint: `GET /api/library/picker?budget=<minutes>&mood=<tag>&count=3`. Pure read, uses status + (eventually) HLTB hours + tags as filters. Randomised within filter set.
|
||
- No new persistence yet; build it on top of (1).
|
||
|
||
### 3. HowLongToBeat integration
|
||
|
||
- New module `core_service.modules.hltb` (or `library/hltb` if kept internal). External adapter pattern mirrors `steam_adapter`.
|
||
- Schema: `game_completion_time(appid, main_hours, plus_sides_hours, completionist_hours, fetched_at)`.
|
||
- Scheduled job to backfill + refresh (low cadence: monthly).
|
||
- Risk: HLTB has no official API; community libs scrape. Cache aggressively, degrade gracefully when missing.
|
||
|
||
### 4. Achievement aggregation (data already in place)
|
||
|
||
- `UserGameAchievement` + `GameAchievementSchema` are already synced. Add aggregate endpoints:
|
||
- `GET /api/library/games/{appid}/achievements/summary` → `{ unlocked, total, percent }`.
|
||
- `GET /api/library/me/achievements/summary` → library-wide unlocked / total / percent.
|
||
- `GET /api/library/me/achievements/closest-to-completion?limit=10`.
|
||
- Pure SQL views or projection DTOs; no new external calls.
|
||
|
||
### 5. Goals and digests
|
||
|
||
- Schema: `user_goal(user_id, year, target_games_completed, ...)`.
|
||
- Endpoint: `GET/PUT /api/library/me/goal?v=1`.
|
||
- Scheduled weekly digest job: aggregate per-user "played hours, backlog delta, status changes" for last 7 days. Push to Auth (which owns email) or to a notification queue.
|
||
|
||
### 6. Wishlist + sale alerts
|
||
|
||
- Extend `steam_adapter` with a wishlist endpoint.
|
||
- New module / adapter for IsThereAnyDeal price polling.
|
||
- Scheduled job; threshold per-user; trigger an email via Auth.
|
||
|
||
### 7. Library enrichment
|
||
|
||
- Genre / tag filters: Steam store tags exist in some Steam API responses — verify which calls return them and add to the cache.
|
||
- DLC awareness: Steam's `GetAppDetails` returns DLC; persist a `game_dlc(appid_base, appid_dlc)` table and a `GET .../games/{appid}/dlc` endpoint.
|
||
- Series / franchise grouping: needs IGDB integration (separate adapter, requires a free IGDB OAuth token).
|
||
- **Multi-platform support**: bigger schema change. Decouple `Game` (platform-agnostic) from `SteamGame` (platform-specific). New `source` enum (`STEAM`, `GOG`, `EPIC`, `MANUAL`, ...). Defer until at least one non-Steam adapter is on the immediate horizon.
|
||
|
||
### 8. Public profile data
|
||
|
||
- Endpoint: `GET /api/public/users/{username}/profile` returning currently-playing + recently-completed + total hours, gated by a `profile_public` flag (owned by Auth, mirrored or fetched on demand). |