Files
galaxy-game/user/PLAN.md
T
2026-04-10 19:05:02 +02:00

716 lines
22 KiB
Markdown

# User Service Implementation Plan
This plan has been already implemented and stays here for historical reasons.
It should NOT be threated as source of truth for service functionality.
## Planning Principles
This plan is aligned with the current repository architecture and is written
for an experienced middle-level Go developer implementing an internal trusted
microservice.
Execution priorities:
- preserve the frozen auth and geo ownership boundaries
- keep user state authoritative in one service
- prefer explicit command behavior over generic patch behavior
- keep synchronous read paths simple for auth and lobby
- separate current effective state from append-only or historical records where
fast reads matter
- keep the first version storage-agnostic at the domain boundary even if Redis
is the initial backend
## ~~Stage 01~~ — Freeze Vocabulary, Contracts, and Cross-Service Ownership
Status: implemented.
### Goal
Remove naming ambiguity and freeze the service boundary before implementation.
### Tasks
- Freeze the regular-user-only scope of `User Service`.
- Freeze that system-admin identity is out of scope and belongs to later
`Admin Service`.
- Freeze the self-service vocabulary:
- `GetMyAccount`
- `UpdateMyProfile`
- `UpdateMySettings`
- Freeze that `race_name` replaces `display_name`.
- Freeze the current ownership split for `declared_country`:
- current value in `User Service`
- workflow and history in `Geo Profile Service`
- Freeze the auth-facing internal REST endpoints already reserved by
`Auth / Session Service`.
- Freeze the exact create-only registration context shape on
`EnsureUserByEmail`:
- `preferred_language`
- `time_zone`
### Deliverables
- service README with stable terminology
- short internal ADR or equivalent note for `declared_country` ownership split
- short internal ADR or equivalent note for regular-user versus admin identity
split
### Exit Criteria
- no unresolved naming conflict remains around `race_name`,
`declared_country`, entitlement, sanction, or limit semantics
- no service boundary question remains open for auth, lobby, or geo
### Targeted Tests
- none yet beyond documentation review
## ~~Stage 02~~ — Define Domain Entities and Redis-Backed Logical State
Status: implemented.
### Goal
Describe the persistent state clearly enough that storage adapters can be built
without revisiting core semantics.
### Tasks
- Define logical entities for:
- user account
- race-name reservation
- blocked e-mail subject
- entitlement period record
- current entitlement snapshot
- sanction record
- limit record
- Freeze required fields, timestamps, and identifiers for each entity.
- Decide Redis logical key layout and lookup indexes without leaking them into
the domain layer.
- Freeze deterministic pagination keys for admin listing.
- Define how active/effective evaluation works for sanctions and limits.
### Deliverables
- domain entity definitions
- storage design notes for Redis keys and secondary indexes
- active/effective evaluation rules
### Exit Criteria
- every required read and mutation can map to a clear logical entity set
- Redis adapters can be implemented directly from the frozen logical model
### Targeted Tests
- domain validation tests for required fields
- tests for effective-state evaluation of active versus expired records
## ~~Stage 03~~ — Implement Auth-Facing Resolution, Ensure, Existence, and E-Mail Blocking
Status: implemented.
### Goal
Provide the minimum trusted API needed by `Auth / Session Service`.
### Tasks
- Implement:
- resolve by e-mail
- ensure by e-mail
- exists by user id
- block by user id
- block by e-mail
- Preserve exact route shapes already reserved by the auth REST client.
- Implement the separate blocked-email-subject model.
- Make `BlockByEmail` idempotent for both existing-user and no-user cases.
- Ensure `ResolveByEmail` and `EnsureUserByEmail` both respect blocked-email
subjects.
### Deliverables
- trusted internal REST handlers for auth-facing endpoints
- domain services for resolution and block behavior
- Redis-backed storage for user existence and blocked-email subjects
- runnable `cmd/userservice` process using `Gin` and `go-redis/v9`
- durable create path that already materializes:
- opaque `user_id`
- generated `player-<shortid>` race name
- stored `preferred_language` and `time_zone`
- initial free entitlement snapshot
### Exit Criteria
- auth can distinguish `existing`, `creatable`, and `blocked`
- blocked e-mail subjects prevent user creation before a user exists
- `BlockByUserID` and `BlockByEmail` are idempotent
### Targeted Tests
- resolve existing/creatable/blocked by e-mail
- ensure existing/created/blocked outcomes
- blocked e-mail subject prevents creation before user record exists
- block by user id on unknown user returns not found
- repeated block calls stay idempotent
## ~~Stage 04~~ — Implement New-User Creation Context from Auth
Status: implemented.
### Goal
Tighten the already-implemented first-login create path with stricter semantic
validation.
### Tasks
- Preserve the already-frozen create-only `EnsureUserByEmail`
registration context with:
- `preferred_language`
- `time_zone`
- Tighten `preferred_language` validation to BCP 47 semantics.
- Tighten `time_zone` validation to IANA TZ semantics.
- Preserve generated initial `race_name` in `player-<shortid>` form during
creation.
- Preserve the newly created user initialization with:
- free entitlement
- no active sanctions
- no custom limits
- Ignore registration context for existing users.
- Document required follow-up changes in `gateway` and `authsession`.
### Deliverables
- create-user domain service using the frozen ensure-by-email request model
- generated-race-name helper
- create-path validation for `preferred_language` and `time_zone`
### Exit Criteria
- first successful ensure-create path can fully initialize a new user
- existing-user ensure does not overwrite language or time zone
### Targeted Tests
- new user created with generated `race_name`, derived `preferred_language`,
and required client `time_zone`
- existing user ensure ignores create-only registration context
- invalid BCP 47 or IANA inputs are rejected on create path
## ~~Stage 05~~ — Implement Self-Service Account Read and Split Profile/Settings Mutations
Status: implemented.
### Goal
Expose the minimal authenticated account surface routed by `Edge Gateway`.
### Tasks
- Implement `GetMyAccount`.
- Implement `UpdateMyProfile` for `race_name` only.
- Implement `UpdateMySettings` for:
- `preferred_language`
- `time_zone`
- Ensure `GetMyAccount` returns:
- account identity fields
- current entitlement snapshot
- active sanctions
- active effective limits
- read-only `declared_country`
- Reject attempts to mutate `email` or `declared_country` through self-service
flows.
- Enforce `profile_update_block` sanction on both self-service mutations.
### Deliverables
- authenticated application services for account read and updates
- gateway-facing handler or adapter contracts for future routing
- DTOs for account aggregate and mutation requests
### Exit Criteria
- authenticated users can read current account state in one aggregate
- profile and settings changes are clearly separated
- self-service updates cannot mutate forbidden fields
### Targeted Tests
- `GetMyAccount` returns current entitlement, active sanctions, active limits,
and read-only `declared_country`
- `UpdateMyProfile` cannot change email or `declared_country`
- `UpdateMySettings` validates BCP 47 and IANA values
- active `profile_update_block` denies both update flows
## ~~Stage 06~~ — Implement race_name Uniqueness Policy Behind a Dedicated Interface
Status: implemented.
### Goal
Keep `race_name` uniqueness strict and replaceable.
### Tasks
- Introduce a dedicated race-name policy interface.
- Implement canonicalization for uniqueness checks:
- case-insensitive folding
- confusable anti-fraud normalization
- Add Redis-backed reservation storage for canonicalized keys.
- Preserve original casing for stored and returned `race_name`.
- Ensure rename flow handles reservation swap safely.
- Keep the interface narrow so a future shared name-catalog service can replace
the local implementation.
### Deliverables
- race-name policy interface
- local normalization implementation
- reservation adapter and conflict handling
### Exit Criteria
- no two users can hold conflicting `race_name` values under the frozen policy
- self-service rename is atomic with respect to uniqueness reservation
### Targeted Tests
- uniqueness rejects case-insensitive collisions
- uniqueness rejects common anti-fraud-confusable collisions
- rename releases the old reservation only after the new one is secured
- failed reservation backend causes mutation to fail closed
## ~~Stage 07~~ — Implement Entitlement History Plus Materialized Current Snapshot
Status: implemented.
### Goal
Support both auditability and fast synchronous entitlement reads.
### Tasks
- Implement period-based entitlement history records.
- Implement a materialized current entitlement snapshot.
- Define the v1 plan catalog:
- `free`
- `paid_monthly`
- `paid_yearly`
- `paid_lifetime`
- Implement explicit trusted entitlement commands:
- grant paid access
- extend paid access
- revoke paid access
- Update current snapshot transactionally with each successful entitlement
mutation.
- Ensure the default new-user path creates the correct free snapshot.
### Deliverables
- entitlement domain model
- history store
- current snapshot store
- trusted entitlement command handlers
### Exit Criteria
- current effective entitlement is always readable without replaying history
- history and snapshot stay consistent across supported mutation paths
### Targeted Tests
- entitlement period mutations update the materialized current snapshot
correctly
- free default is created for new users
- extending or revoking access preserves deterministic current-state behavior
## ~~Stage 08~~ — Implement Sanctions and Limit Records with Active/Effective Evaluation
Status: implemented.
### Goal
Support negative policy and quota overrides without scattering policy logic into
consumers.
### Tasks
- Implement sanction records with optional expiry.
- Implement limit records with numeric values and optional expiry.
- Freeze v1 sanction catalog:
- `login_block`
- `private_game_create_block`
- `private_game_manage_block`
- `game_join_block`
- `profile_update_block`
- Freeze v1 limit catalog:
- `max_owned_private_games`
- `max_pending_public_applications`
- `max_active_game_memberships`
- Freeze supported v1 limit semantics:
- paid effective defaults:
- `max_owned_private_games=3`
- `max_pending_public_applications=10`
- `max_active_game_memberships=10`
- free effective defaults:
- `max_owned_private_games` is omitted
- `max_pending_public_applications=3`
- `max_active_game_memberships=3`
- `max_active_game_memberships` applies only to public games
- `max_pending_public_applications` is the total public-games budget and is
interpreted by `Game Lobby` together with current active public
memberships
- Keep legacy retired limit codes backward-compatible on reads, but reject
them for new trusted limit commands.
- Implement active/effective evaluation with current time.
- Implement trusted explicit commands to apply/remove sanctions and set/remove
limits.
### Deliverables
- sanction model and store
- limit model and store
- effective-state evaluator
- trusted mutation handlers
### Exit Criteria
- active sanctions and active limits can be read consistently from one user
account view
- expired or removed records are not treated as active
### Targeted Tests
- active sanctions appear in account reads
- expired sanctions and limits stop affecting effective state
- retired legacy limit records are ignored during reads and effective
evaluation
- retired legacy limit codes are rejected by trusted limit commands
- applying and removing sanctions/limits is idempotent where appropriate
## ~~Stage 09~~ — Implement Lobby Eligibility Snapshot API
Status: implemented.
### Goal
Give `Game Lobby` one synchronous read that contains everything it needs for
user-level access decisions.
### Tasks
- Design and implement one trusted query by `user_id`.
- Return:
- existence
- current entitlement snapshot
- active lobby-relevant sanctions
- effective lobby-relevant limits
- derived booleans for lobby decisions
- Freeze the lobby-facing effective limit catalog:
- paid users receive `max_owned_private_games=3`,
`max_pending_public_applications=10`, and
`max_active_game_memberships=10`
- free users omit `max_owned_private_games` and receive
`max_pending_public_applications=3` and
`max_active_game_memberships=3`
- `max_pending_public_applications` remains the total public-games budget
consumed together with current active public memberships inside
`Game Lobby`
- Keep the response read-optimized so lobby does not need multiple dependent
calls back into `User Service`.
- Define deterministic not-found behavior.
### Deliverables
- lobby eligibility query endpoint
- response DTO
- mapping from entitlement/sanction/limit state to derived eligibility fields
### Exit Criteria
- `Game Lobby` can decide create/join/manage eligibility from one read
- no extra fan-out to other user sub-queries is required
### Targeted Tests
- lobby eligibility snapshot reflects paid status, sanctions, and limits
- unknown user returns stable not-found behavior
- derived booleans remain consistent with raw effective state
- free and paid snapshots materialize the reduced three-code effective limit
catalog correctly
## ~~Stage 10~~ — Implement Geo declared_country Sync Command
Status: implemented.
### Goal
Support the current-country denormalization path owned by `Geo Profile Service`.
### Tasks
- Implement one explicit trusted command to sync current `declared_country`.
- Validate ISO alpha-2 input.
- Ensure the command updates only the current value on the user account.
- Do not add country history behavior to `User Service`.
- Preserve explicit not-found behavior for unknown `user_id`.
- Emit the corresponding auxiliary declared-country change event after a
successful commit.
### Deliverables
- geo-facing sync endpoint
- application service for country sync
- event publication on successful mutation
### Exit Criteria
- geo can synchronize current `declared_country` without introducing hidden
history in `User Service`
- unknown users are rejected deterministically
### Targeted Tests
- geo country sync changes only current `declared_country`
- invalid country codes are rejected
- country sync emits the correct auxiliary event after commit
## ~~Stage 11~~ — Implement Admin Lookup, Filtered Listing, and Explicit Trusted Mutations
Status: implemented.
### Goal
Provide the operational surface required by future `Admin Service` and manual
operations.
### Tasks
- Implement exact reads by:
- `user_id`
- normalized `email`
- exact `race_name`
- Implement paginated listing with richer filters:
- paid/free state
- paid expiry
- current `declared_country`
- sanction code
- limit code
- eligibility markers
- Freeze deterministic ordering for the listing.
- Implement the explicit trusted command surface for:
- entitlement grant/extend/revoke
- sanction apply/remove
- limit set/remove
- declared-country sync
- Preserve audit metadata on every trusted mutation.
### Deliverables
- admin/internal read endpoints
- filtered listing endpoint
- explicit trusted mutation endpoints
### Exit Criteria
- future `Admin Service` can operate fully through this trusted API without
needing direct storage access
- list filtering and pagination are deterministic
### Targeted Tests
- admin listing filters behave deterministically
- exact lookups by `user_id`, email, and `race_name` resolve the correct user
- every trusted mutation preserves actor and reason metadata
## ~~Stage 12~~ — Add Per-Domain-Area Async Events and Observability
Status: implemented.
### Goal
Make production behavior observable without treating events as the source of
truth.
### Tasks
- Publish per-domain-area events for:
- profile changes
- settings changes
- entitlement changes
- sanction changes
- limit changes
- declared-country changes
- Add structured logs for trusted mutations and critical failures.
- Add metrics for:
- auth-facing resolution outcomes
- user creation outcomes
- race-name reservation conflicts
- entitlement mutation outcomes
- sanction and limit mutation outcomes
- event publication failures
- Add tracing spans on synchronous internal request paths where useful.
### Deliverables
- event publisher integration
- structured logging hooks
- metrics and tracing instrumentation
### Exit Criteria
- mutation flows are observable in production without ad hoc logging
- event publication failure does not compromise source-of-truth persistence
### Targeted Tests
- async event publication failure does not lose source-of-truth state
- event payloads include minimum required metadata
- observability hooks do not change business behavior
## ~~Stage 13~~ — Add Contract Tests Against Auth, Lobby, and Geo Expectations
Status: implemented.
### Goal
Verify the service not only in isolation, but against the internal contracts it
must satisfy for other services.
### Tasks
- Add compatibility tests against the frozen auth-facing REST contract.
- Add compatibility tests for the future ensure-by-email registration context.
- Add lobby eligibility snapshot contract tests.
- Add geo country-sync contract tests.
- Add account aggregate tests matching gateway-routed user expectations.
- Add tests for deterministic admin listing filters and ordering.
### Deliverables
- cross-service contract test suite
- test fixtures for auth/lobby/geo integration expectations
### Exit Criteria
- no ambiguity remains about service behavior expected by auth, lobby, or geo
- regressions in reserved internal contract shapes are caught automatically
### Targeted Tests
- new user created on first successful confirm with generated `race_name`,
derived `preferred_language`, and required client `time_zone`
- existing user confirm ignores create-only registration context
- blocked e-mail subject prevents user creation before a user record exists
- `GetMyAccount` returns current entitlement, active sanctions, active limits,
and read-only `declared_country`
- lobby eligibility snapshot reflects paid status, sanctions, and limits
- geo country sync changes only current `declared_country`
## ~~Stage 14~~ — Add Rollout Notes for Gateway/Auth/OpenAPI Updates and Shared geoip
Status: implemented.
### Goal
Prepare the surrounding platform changes required for the service to work in
its intended end-to-end form.
### Tasks
- Document the required `gateway` public `confirm-email-code` dependency on
`time_zone`.
- Document the required `authsession` public OpenAPI preservation of the same
`time_zone` requirement.
- Document that the frozen `authsession -> user` ensure contract requires
create-only `registration_context` with `preferred_language` and
`time_zone`.
- Document the required shared `pkg/geoip` package for gateway and geo.
- Document README follow-up updates needed in `gateway` and `geoprofile`.
- Define rollout order so the cross-service contract changes do not land in an
unsafe sequence.
### Deliverables
- rollout checklist
- dependency order notes
- cross-repo or cross-module follow-up ticket list
### Exit Criteria
- the implementation can be integrated into surrounding services without
rediscovering hidden dependencies
- no required upstream or downstream change is left implicit
### Targeted Tests
- documentation review only
## Recommended First Working Slice
The smallest useful end-to-end slice is:
1. Stage 01
2. Stage 02
3. Stage 03
4. Stage 04
This slice makes it possible to support auth-driven user creation and blocking
before the rest of the service surface exists.
## Recommended Second Slice
The next highest-value slice is:
1. Stage 05
2. Stage 06
3. Stage 07
4. Stage 08
5. Stage 09
This slice gives the platform usable account reads, self-service profile and
settings updates, and the lobby eligibility integration.
## Final Acceptance Criteria
The first production-capable v1 of `User Service` should satisfy all of the
following:
- new users can be created through auth with generated `race_name`, derived
`preferred_language`, and required client `time_zone`
- existing-user auth confirm ignores create-only registration context
- blocked e-mail subjects prevent new-user creation before a user record exists
- `race_name` uniqueness rejects case-insensitive and anti-fraud-confusable
collisions
- `GetMyAccount` returns current entitlement, active sanctions, active limits,
and read-only `declared_country`
- `UpdateMyProfile` cannot change email or `declared_country`
- `UpdateMySettings` validates BCP 47 and IANA values
- entitlement period mutations update the materialized current snapshot
correctly
- lobby eligibility snapshot reflects paid status, sanctions, and limits
- geo `declared_country` sync changes only current account state
- admin listing filters and ordering are deterministic
- async event publication failure does not lose source-of-truth state
## Implementation Order Summary
Recommended implementation order:
1. freeze vocabulary and ownership
2. define domain entities and logical storage
3. build auth-facing resolution and blocking
4. add new-user creation context
5. build self-service account read and updates
6. add race-name uniqueness policy
7. build entitlement history and current snapshot
8. build sanctions and limits
9. add lobby eligibility snapshot
10. add geo country sync
11. add admin reads, listing, and mutations
12. add events and observability
13. add cross-service contract tests
14. document and sequence rollout dependencies