package ports import ( "context" "fmt" "strings" "time" "galaxy/lobby/internal/domain/application" "galaxy/lobby/internal/domain/common" ) // ApplicationStore stores application records and their secondary indexes. // Adapters are responsible for maintaining the per-game set, per-user set, // and the single-active lookup key together with the record. type ApplicationStore interface { // Save persists a new submitted application record. The adapter must // enforce the single-active constraint — only one non-rejected // application per (applicant_user_id, game_id) pair may exist at a // time — and return application.ErrConflict if the constraint is // violated. Save rejects records whose status is not submitted. Save(ctx context.Context, record application.Application) error // Get returns the record identified by applicationID. It returns // application.ErrNotFound when no record exists. Get(ctx context.Context, applicationID common.ApplicationID) (application.Application, error) // GetByGame returns every application attached to gameID. The order // is adapter-defined; callers may reorder as needed. GetByGame(ctx context.Context, gameID common.GameID) ([]application.Application, error) // GetByUser returns every application submitted by applicantUserID. // The order is adapter-defined; callers may reorder as needed. GetByUser(ctx context.Context, applicantUserID string) ([]application.Application, error) // UpdateStatus applies one status transition in a compare-and-swap // fashion. The adapter must first call application.Transition to // reject invalid pairs without touching the store; on success it must // verify that the current status equals input.ExpectedFrom, update // the primary record, and clear the single-active lookup key when // transitioning to rejected. Adapters set DecidedAt to input.At. UpdateStatus(ctx context.Context, input UpdateApplicationStatusInput) error } // UpdateApplicationStatusInput stores the arguments required to apply one // status transition through an ApplicationStore. type UpdateApplicationStatusInput struct { // ApplicationID identifies the record to mutate. ApplicationID common.ApplicationID // ExpectedFrom stores the status the caller believes the record // currently has. A mismatch results in application.ErrConflict. ExpectedFrom application.Status // To stores the destination status. To application.Status // At stores the wall-clock used for DecidedAt. At time.Time } // Validate reports whether input contains a structurally valid status // transition request. func (input UpdateApplicationStatusInput) Validate() error { if err := input.ApplicationID.Validate(); err != nil { return fmt.Errorf("update application status: application id: %w", err) } if !input.ExpectedFrom.IsKnown() { return fmt.Errorf( "update application status: expected from status %q is unsupported", input.ExpectedFrom, ) } if !input.To.IsKnown() { return fmt.Errorf( "update application status: to status %q is unsupported", input.To, ) } if input.At.IsZero() { return fmt.Errorf("update application status: at must not be zero") } return nil } // NormalizedApplicantUserID trims surrounding whitespace so adapter // keyspace lookups match the form the domain persists. func NormalizedApplicantUserID(userID string) string { return strings.TrimSpace(userID) }