148 lines
4.3 KiB
Go
148 lines
4.3 KiB
Go
// Package application defines the application record domain model, status
|
|
// machine, and sentinel errors owned by Game Lobby Service for public-game
|
|
// enrollment requests.
|
|
package application
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"galaxy/lobby/internal/domain/common"
|
|
)
|
|
|
|
// Application stores one durable application record owned by Game Lobby
|
|
// Service. Applications are used exclusively by public games; private
|
|
// games use the invite flow instead.
|
|
type Application struct {
|
|
// ApplicationID identifies the record.
|
|
ApplicationID common.ApplicationID
|
|
|
|
// GameID identifies the game this application belongs to.
|
|
GameID common.GameID
|
|
|
|
// ApplicantUserID stores the platform user id of the applicant.
|
|
ApplicantUserID string
|
|
|
|
// RaceName stores the desired in-game name submitted with the
|
|
// application.
|
|
RaceName string
|
|
|
|
// Status stores the current lifecycle state.
|
|
Status Status
|
|
|
|
// CreatedAt stores when the record was created.
|
|
CreatedAt time.Time
|
|
|
|
// DecidedAt stores when the record transitioned out of submitted. It
|
|
// is nil while the application is still submitted.
|
|
DecidedAt *time.Time
|
|
}
|
|
|
|
// NewApplicationInput groups all fields required to create a submitted
|
|
// application record.
|
|
type NewApplicationInput struct {
|
|
// ApplicationID identifies the new record.
|
|
ApplicationID common.ApplicationID
|
|
|
|
// GameID identifies the game the applicant is applying to.
|
|
GameID common.GameID
|
|
|
|
// ApplicantUserID stores the platform user id of the applicant.
|
|
ApplicantUserID string
|
|
|
|
// RaceName stores the desired in-game name submitted by the
|
|
// applicant.
|
|
RaceName string
|
|
|
|
// Now stores the creation wall-clock used for CreatedAt.
|
|
Now time.Time
|
|
}
|
|
|
|
// New validates input and returns a submitted Application record.
|
|
// Validation errors are returned verbatim so callers can surface them as
|
|
// invalid_request.
|
|
func New(input NewApplicationInput) (Application, error) {
|
|
if err := input.Validate(); err != nil {
|
|
return Application{}, err
|
|
}
|
|
|
|
record := Application{
|
|
ApplicationID: input.ApplicationID,
|
|
GameID: input.GameID,
|
|
ApplicantUserID: strings.TrimSpace(input.ApplicantUserID),
|
|
RaceName: strings.TrimSpace(input.RaceName),
|
|
Status: StatusSubmitted,
|
|
CreatedAt: input.Now.UTC(),
|
|
}
|
|
|
|
if err := record.Validate(); err != nil {
|
|
return Application{}, err
|
|
}
|
|
|
|
return record, nil
|
|
}
|
|
|
|
// Validate reports whether input satisfies the frozen application-record
|
|
// invariants required to construct a submitted record.
|
|
func (input NewApplicationInput) Validate() error {
|
|
if err := input.ApplicationID.Validate(); err != nil {
|
|
return fmt.Errorf("application id: %w", err)
|
|
}
|
|
if err := input.GameID.Validate(); err != nil {
|
|
return fmt.Errorf("game id: %w", err)
|
|
}
|
|
if strings.TrimSpace(input.ApplicantUserID) == "" {
|
|
return fmt.Errorf("applicant user id must not be empty")
|
|
}
|
|
if strings.TrimSpace(input.RaceName) == "" {
|
|
return fmt.Errorf("race name must not be empty")
|
|
}
|
|
if input.Now.IsZero() {
|
|
return fmt.Errorf("now must not be zero")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Validate reports whether record satisfies the full invariants.
|
|
// Every marshal and unmarshal round-trip calls Validate to guarantee that
|
|
// the Redis store never exposes malformed records.
|
|
func (record Application) Validate() error {
|
|
if err := record.ApplicationID.Validate(); err != nil {
|
|
return fmt.Errorf("application id: %w", err)
|
|
}
|
|
if err := record.GameID.Validate(); err != nil {
|
|
return fmt.Errorf("game id: %w", err)
|
|
}
|
|
if strings.TrimSpace(record.ApplicantUserID) == "" {
|
|
return fmt.Errorf("applicant user id must not be empty")
|
|
}
|
|
if strings.TrimSpace(record.RaceName) == "" {
|
|
return fmt.Errorf("race name must not be empty")
|
|
}
|
|
if !record.Status.IsKnown() {
|
|
return fmt.Errorf("status %q is unsupported", record.Status)
|
|
}
|
|
if record.CreatedAt.IsZero() {
|
|
return fmt.Errorf("created at must not be zero")
|
|
}
|
|
if record.Status == StatusSubmitted {
|
|
if record.DecidedAt != nil {
|
|
return fmt.Errorf("decided at must be nil for submitted applications")
|
|
}
|
|
} else {
|
|
if record.DecidedAt == nil {
|
|
return fmt.Errorf("decided at must not be nil for %q applications", record.Status)
|
|
}
|
|
if record.DecidedAt.IsZero() {
|
|
return fmt.Errorf("decided at must not be zero when present")
|
|
}
|
|
if record.DecidedAt.Before(record.CreatedAt) {
|
|
return fmt.Errorf("decided at must not be before created at")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|