feat: user service

This commit is contained in:
Ilia Denisov
2026-04-10 19:05:02 +02:00
committed by GitHub
parent 710bad712e
commit 23ffcb7535
140 changed files with 33418 additions and 952 deletions
+136
View File
@@ -0,0 +1,136 @@
// Package account defines the logical user-account entities owned directly by
// User Service.
package account
import (
"fmt"
"strings"
"time"
"galaxy/user/internal/domain/common"
)
// RaceNameCanonicalKey stores the policy-produced reservation key used to
// enforce replaceable race-name uniqueness.
type RaceNameCanonicalKey string
// String returns RaceNameCanonicalKey as its stored canonical string.
func (key RaceNameCanonicalKey) String() string {
return string(key)
}
// IsZero reports whether RaceNameCanonicalKey does not contain a usable value.
func (key RaceNameCanonicalKey) IsZero() bool {
return strings.TrimSpace(string(key)) == ""
}
// Validate reports whether RaceNameCanonicalKey is non-empty and trimmed.
func (key RaceNameCanonicalKey) Validate() error {
switch {
case key.IsZero():
return fmt.Errorf("race name canonical key must not be empty")
case strings.TrimSpace(string(key)) != string(key):
return fmt.Errorf("race name canonical key must not contain surrounding whitespace")
default:
return nil
}
}
// UserAccount stores the current editable account state of one regular user.
type UserAccount struct {
// UserID identifies the durable regular-user account.
UserID common.UserID
// Email stores the normalized login/contact address of the account.
Email common.Email
// RaceName stores the original-casing user-facing race name.
RaceName common.RaceName
// PreferredLanguage stores the current declared language tag.
PreferredLanguage common.LanguageTag
// TimeZone stores the current declared time-zone name.
TimeZone common.TimeZoneName
// DeclaredCountry stores the latest effective declared-country value. The
// zero value means the geo workflow has not synchronized any country yet.
DeclaredCountry common.CountryCode
// CreatedAt stores the account creation timestamp.
CreatedAt time.Time
// UpdatedAt stores the last account mutation timestamp.
UpdatedAt time.Time
}
// Validate reports whether UserAccount satisfies the frozen Stage 02
// structural invariants.
func (record UserAccount) Validate() error {
if err := record.UserID.Validate(); err != nil {
return fmt.Errorf("user account user id: %w", err)
}
if err := record.Email.Validate(); err != nil {
return fmt.Errorf("user account email: %w", err)
}
if err := record.RaceName.Validate(); err != nil {
return fmt.Errorf("user account race name: %w", err)
}
if err := record.PreferredLanguage.Validate(); err != nil {
return fmt.Errorf("user account preferred language: %w", err)
}
if err := record.TimeZone.Validate(); err != nil {
return fmt.Errorf("user account time zone: %w", err)
}
if !record.DeclaredCountry.IsZero() {
if err := record.DeclaredCountry.Validate(); err != nil {
return fmt.Errorf("user account declared country: %w", err)
}
}
if err := common.ValidateTimestamp("user account created at", record.CreatedAt); err != nil {
return err
}
if err := common.ValidateTimestamp("user account updated at", record.UpdatedAt); err != nil {
return err
}
if record.UpdatedAt.Before(record.CreatedAt) {
return fmt.Errorf("user account updated at must not be before created at")
}
return nil
}
// RaceNameReservation stores the current uniqueness reservation for one
// canonicalized race-name key.
type RaceNameReservation struct {
// CanonicalKey stores the policy-produced uniqueness key.
CanonicalKey RaceNameCanonicalKey
// UserID identifies the account that owns the reservation.
UserID common.UserID
// RaceName stores the original-casing name linked to the reservation.
RaceName common.RaceName
// ReservedAt stores when the reservation was acquired.
ReservedAt time.Time
}
// Validate reports whether RaceNameReservation satisfies the frozen Stage 02
// structural invariants.
func (record RaceNameReservation) Validate() error {
if err := record.CanonicalKey.Validate(); err != nil {
return fmt.Errorf("race name reservation canonical key: %w", err)
}
if err := record.UserID.Validate(); err != nil {
return fmt.Errorf("race name reservation user id: %w", err)
}
if err := record.RaceName.Validate(); err != nil {
return fmt.Errorf("race name reservation race name: %w", err)
}
if err := common.ValidateTimestamp("race name reservation reserved at", record.ReservedAt); err != nil {
return err
}
return nil
}
+119
View File
@@ -0,0 +1,119 @@
package account
import (
"testing"
"time"
"galaxy/user/internal/domain/common"
"github.com/stretchr/testify/require"
)
func TestUserAccountValidate(t *testing.T) {
t.Parallel()
createdAt := time.Unix(1_775_240_000, 0).UTC()
updatedAt := createdAt.Add(2 * time.Hour)
tests := []struct {
name string
record UserAccount
wantErr bool
}{
{
name: "valid without declared country",
record: UserAccount{
UserID: common.UserID("user-123"),
Email: common.Email("pilot@example.com"),
RaceName: common.RaceName("Pilot Nova"),
PreferredLanguage: common.LanguageTag("en"),
TimeZone: common.TimeZoneName("Europe/Berlin"),
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
},
{
name: "valid with declared country",
record: UserAccount{
UserID: common.UserID("user-123"),
Email: common.Email("pilot@example.com"),
RaceName: common.RaceName("Pilot Nova"),
PreferredLanguage: common.LanguageTag("en"),
TimeZone: common.TimeZoneName("Europe/Berlin"),
DeclaredCountry: common.CountryCode("DE"),
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
},
{
name: "updated before created",
record: UserAccount{
UserID: common.UserID("user-123"),
Email: common.Email("pilot@example.com"),
RaceName: common.RaceName("Pilot Nova"),
PreferredLanguage: common.LanguageTag("en"),
TimeZone: common.TimeZoneName("Europe/Berlin"),
CreatedAt: createdAt,
UpdatedAt: createdAt.Add(-time.Second),
},
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.record.Validate()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func TestRaceNameReservationValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
record RaceNameReservation
wantErr bool
}{
{
name: "valid",
record: RaceNameReservation{
CanonicalKey: RaceNameCanonicalKey("pilot-nova"),
UserID: common.UserID("user-123"),
RaceName: common.RaceName("Pilot Nova"),
ReservedAt: time.Unix(1_775_240_100, 0).UTC(),
},
},
{
name: "empty canonical key",
record: RaceNameReservation{
UserID: common.UserID("user-123"),
RaceName: common.RaceName("Pilot Nova"),
ReservedAt: time.Unix(1_775_240_100, 0).UTC(),
},
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.record.Validate()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}