546 lines
18 KiB
Go
546 lines
18 KiB
Go
package ports
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"galaxy/user/internal/domain/common"
|
|
"galaxy/user/internal/domain/entitlement"
|
|
"galaxy/user/internal/domain/policy"
|
|
)
|
|
|
|
const (
|
|
// ProfileChangedEventType identifies profile-change events in the shared
|
|
// auxiliary event stream.
|
|
ProfileChangedEventType = "user.profile.changed"
|
|
|
|
// SettingsChangedEventType identifies settings-change events in the shared
|
|
// auxiliary event stream.
|
|
SettingsChangedEventType = "user.settings.changed"
|
|
|
|
// EntitlementChangedEventType identifies entitlement-change events in the
|
|
// shared auxiliary event stream.
|
|
EntitlementChangedEventType = "user.entitlement.changed"
|
|
|
|
// SanctionChangedEventType identifies sanction-change events in the shared
|
|
// auxiliary event stream.
|
|
SanctionChangedEventType = "user.sanction.changed"
|
|
|
|
// LimitChangedEventType identifies limit-change events in the shared
|
|
// auxiliary event stream.
|
|
LimitChangedEventType = "user.limit.changed"
|
|
)
|
|
|
|
// ProfileChangedOperation identifies one profile-change event kind.
|
|
type ProfileChangedOperation string
|
|
|
|
const (
|
|
// ProfileChangedOperationInitialized reports the initial account
|
|
// materialization performed during auth-driven user creation.
|
|
ProfileChangedOperationInitialized ProfileChangedOperation = "initialized"
|
|
|
|
// ProfileChangedOperationUpdated reports a later self-service profile
|
|
// update.
|
|
ProfileChangedOperationUpdated ProfileChangedOperation = "updated"
|
|
)
|
|
|
|
// IsKnown reports whether operation belongs to the frozen profile-change
|
|
// event vocabulary.
|
|
func (operation ProfileChangedOperation) IsKnown() bool {
|
|
switch operation {
|
|
case ProfileChangedOperationInitialized, ProfileChangedOperationUpdated:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// SettingsChangedOperation identifies one settings-change event kind.
|
|
type SettingsChangedOperation string
|
|
|
|
const (
|
|
// SettingsChangedOperationInitialized reports the initial account settings
|
|
// materialization performed during auth-driven user creation.
|
|
SettingsChangedOperationInitialized SettingsChangedOperation = "initialized"
|
|
|
|
// SettingsChangedOperationUpdated reports a later self-service settings
|
|
// update.
|
|
SettingsChangedOperationUpdated SettingsChangedOperation = "updated"
|
|
)
|
|
|
|
// IsKnown reports whether operation belongs to the frozen settings-change
|
|
// event vocabulary.
|
|
func (operation SettingsChangedOperation) IsKnown() bool {
|
|
switch operation {
|
|
case SettingsChangedOperationInitialized, SettingsChangedOperationUpdated:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// EntitlementChangedOperation identifies one entitlement-change event kind.
|
|
type EntitlementChangedOperation string
|
|
|
|
const (
|
|
// EntitlementChangedOperationInitialized reports the initial free snapshot
|
|
// created for a new user.
|
|
EntitlementChangedOperationInitialized EntitlementChangedOperation = "initialized"
|
|
|
|
// EntitlementChangedOperationGranted reports an explicit paid grant.
|
|
EntitlementChangedOperationGranted EntitlementChangedOperation = "granted"
|
|
|
|
// EntitlementChangedOperationExtended reports an explicit paid extension.
|
|
EntitlementChangedOperationExtended EntitlementChangedOperation = "extended"
|
|
|
|
// EntitlementChangedOperationRevoked reports an explicit paid revoke.
|
|
EntitlementChangedOperationRevoked EntitlementChangedOperation = "revoked"
|
|
|
|
// EntitlementChangedOperationExpiredRepaired reports lazy repair of a
|
|
// naturally expired finite paid snapshot.
|
|
EntitlementChangedOperationExpiredRepaired EntitlementChangedOperation = "expired_repaired"
|
|
)
|
|
|
|
// IsKnown reports whether operation belongs to the frozen entitlement-change
|
|
// event vocabulary.
|
|
func (operation EntitlementChangedOperation) IsKnown() bool {
|
|
switch operation {
|
|
case EntitlementChangedOperationInitialized,
|
|
EntitlementChangedOperationGranted,
|
|
EntitlementChangedOperationExtended,
|
|
EntitlementChangedOperationRevoked,
|
|
EntitlementChangedOperationExpiredRepaired:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// SanctionChangedOperation identifies one sanction-change event kind.
|
|
type SanctionChangedOperation string
|
|
|
|
const (
|
|
// SanctionChangedOperationApplied reports a new active sanction.
|
|
SanctionChangedOperationApplied SanctionChangedOperation = "applied"
|
|
|
|
// SanctionChangedOperationRemoved reports explicit removal of an active
|
|
// sanction.
|
|
SanctionChangedOperationRemoved SanctionChangedOperation = "removed"
|
|
)
|
|
|
|
// IsKnown reports whether operation belongs to the frozen sanction-change
|
|
// event vocabulary.
|
|
func (operation SanctionChangedOperation) IsKnown() bool {
|
|
switch operation {
|
|
case SanctionChangedOperationApplied, SanctionChangedOperationRemoved:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// LimitChangedOperation identifies one limit-change event kind.
|
|
type LimitChangedOperation string
|
|
|
|
const (
|
|
// LimitChangedOperationSet reports a new or replacement active limit.
|
|
LimitChangedOperationSet LimitChangedOperation = "set"
|
|
|
|
// LimitChangedOperationRemoved reports explicit removal of an active limit.
|
|
LimitChangedOperationRemoved LimitChangedOperation = "removed"
|
|
)
|
|
|
|
// IsKnown reports whether operation belongs to the frozen limit-change event
|
|
// vocabulary.
|
|
func (operation LimitChangedOperation) IsKnown() bool {
|
|
switch operation {
|
|
case LimitChangedOperationSet, LimitChangedOperationRemoved:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ProfileChangedEvent stores one post-commit auxiliary profile-change event.
|
|
type ProfileChangedEvent struct {
|
|
// UserID identifies the changed user.
|
|
UserID common.UserID
|
|
|
|
// OccurredAt stores the mutation timestamp emitted into the shared event
|
|
// envelope.
|
|
OccurredAt time.Time
|
|
|
|
// Source stores the machine-readable mutation source.
|
|
Source common.Source
|
|
|
|
// TraceID stores the optional OpenTelemetry trace identifier propagated
|
|
// from the current request context.
|
|
TraceID string
|
|
|
|
// Operation stores the profile-change event kind.
|
|
Operation ProfileChangedOperation
|
|
|
|
// UserName stores the immutable handle associated with the account at the
|
|
// moment the event is published.
|
|
UserName common.UserName
|
|
|
|
// DisplayName stores the latest display name after the commit. An empty
|
|
// value is valid and means no display name is set.
|
|
DisplayName common.DisplayName
|
|
}
|
|
|
|
// Validate reports whether event is structurally complete.
|
|
func (event ProfileChangedEvent) Validate() error {
|
|
if err := validateEventEnvelope("profile changed event", event.UserID, event.OccurredAt, event.Source, event.TraceID); err != nil {
|
|
return err
|
|
}
|
|
if !event.Operation.IsKnown() {
|
|
return fmt.Errorf("profile changed event operation %q is unsupported", event.Operation)
|
|
}
|
|
if err := event.UserName.Validate(); err != nil {
|
|
return fmt.Errorf("profile changed event user name: %w", err)
|
|
}
|
|
if err := event.DisplayName.Validate(); err != nil {
|
|
return fmt.Errorf("profile changed event display name: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SettingsChangedEvent stores one post-commit auxiliary settings-change event.
|
|
type SettingsChangedEvent struct {
|
|
// UserID identifies the changed user.
|
|
UserID common.UserID
|
|
|
|
// OccurredAt stores the mutation timestamp emitted into the shared event
|
|
// envelope.
|
|
OccurredAt time.Time
|
|
|
|
// Source stores the machine-readable mutation source.
|
|
Source common.Source
|
|
|
|
// TraceID stores the optional OpenTelemetry trace identifier propagated
|
|
// from the current request context.
|
|
TraceID string
|
|
|
|
// Operation stores the settings-change event kind.
|
|
Operation SettingsChangedOperation
|
|
|
|
// PreferredLanguage stores the latest preferred language after the commit.
|
|
PreferredLanguage common.LanguageTag
|
|
|
|
// TimeZone stores the latest time-zone name after the commit.
|
|
TimeZone common.TimeZoneName
|
|
}
|
|
|
|
// Validate reports whether event is structurally complete.
|
|
func (event SettingsChangedEvent) Validate() error {
|
|
if err := validateEventEnvelope("settings changed event", event.UserID, event.OccurredAt, event.Source, event.TraceID); err != nil {
|
|
return err
|
|
}
|
|
if !event.Operation.IsKnown() {
|
|
return fmt.Errorf("settings changed event operation %q is unsupported", event.Operation)
|
|
}
|
|
if err := event.PreferredLanguage.Validate(); err != nil {
|
|
return fmt.Errorf("settings changed event preferred language: %w", err)
|
|
}
|
|
if err := event.TimeZone.Validate(); err != nil {
|
|
return fmt.Errorf("settings changed event time zone: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EntitlementChangedEvent stores one post-commit auxiliary entitlement-change
|
|
// event.
|
|
type EntitlementChangedEvent struct {
|
|
// UserID identifies the changed user.
|
|
UserID common.UserID
|
|
|
|
// OccurredAt stores the mutation timestamp emitted into the shared event
|
|
// envelope.
|
|
OccurredAt time.Time
|
|
|
|
// Source stores the machine-readable mutation source.
|
|
Source common.Source
|
|
|
|
// TraceID stores the optional OpenTelemetry trace identifier propagated
|
|
// from the current request context.
|
|
TraceID string
|
|
|
|
// Operation stores the entitlement-change event kind.
|
|
Operation EntitlementChangedOperation
|
|
|
|
// PlanCode stores the effective plan after the commit.
|
|
PlanCode entitlement.PlanCode
|
|
|
|
// IsPaid stores the effective paid/free flag after the commit.
|
|
IsPaid bool
|
|
|
|
// StartsAt stores when the effective entitlement state started.
|
|
StartsAt time.Time
|
|
|
|
// EndsAt stores the optional finite paid expiry.
|
|
EndsAt *time.Time
|
|
|
|
// ReasonCode stores the mutation reason.
|
|
ReasonCode common.ReasonCode
|
|
|
|
// Actor stores the audit actor metadata attached to the mutation.
|
|
Actor common.ActorRef
|
|
|
|
// UpdatedAt stores when the current entitlement snapshot was recomputed.
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// Validate reports whether event is structurally complete.
|
|
func (event EntitlementChangedEvent) Validate() error {
|
|
if err := validateEventEnvelope("entitlement changed event", event.UserID, event.OccurredAt, event.Source, event.TraceID); err != nil {
|
|
return err
|
|
}
|
|
if !event.Operation.IsKnown() {
|
|
return fmt.Errorf("entitlement changed event operation %q is unsupported", event.Operation)
|
|
}
|
|
if !event.PlanCode.IsKnown() {
|
|
return fmt.Errorf("entitlement changed event plan code %q is unsupported", event.PlanCode)
|
|
}
|
|
if event.IsPaid != event.PlanCode.IsPaid() {
|
|
return fmt.Errorf("entitlement changed event paid flag must match plan code %q", event.PlanCode)
|
|
}
|
|
if err := common.ValidateTimestamp("entitlement changed event starts at", event.StartsAt); err != nil {
|
|
return err
|
|
}
|
|
if event.PlanCode.HasFiniteExpiry() {
|
|
if event.EndsAt == nil {
|
|
return fmt.Errorf("entitlement changed event ends at must be present for plan code %q", event.PlanCode)
|
|
}
|
|
if !event.EndsAt.After(event.StartsAt) {
|
|
return common.ErrInvertedTimeRange
|
|
}
|
|
} else if event.EndsAt != nil {
|
|
return fmt.Errorf("entitlement changed event ends at must be empty for plan code %q", event.PlanCode)
|
|
}
|
|
if err := event.ReasonCode.Validate(); err != nil {
|
|
return fmt.Errorf("entitlement changed event reason code: %w", err)
|
|
}
|
|
if err := event.Actor.Validate(); err != nil {
|
|
return fmt.Errorf("entitlement changed event actor: %w", err)
|
|
}
|
|
if err := common.ValidateTimestamp("entitlement changed event updated at", event.UpdatedAt); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SanctionChangedEvent stores one post-commit auxiliary sanction-change event.
|
|
type SanctionChangedEvent struct {
|
|
// UserID identifies the changed user.
|
|
UserID common.UserID
|
|
|
|
// OccurredAt stores the mutation timestamp emitted into the shared event
|
|
// envelope.
|
|
OccurredAt time.Time
|
|
|
|
// Source stores the machine-readable mutation source.
|
|
Source common.Source
|
|
|
|
// TraceID stores the optional OpenTelemetry trace identifier propagated
|
|
// from the current request context.
|
|
TraceID string
|
|
|
|
// Operation stores the sanction-change event kind.
|
|
Operation SanctionChangedOperation
|
|
|
|
// SanctionCode stores the affected sanction code.
|
|
SanctionCode policy.SanctionCode
|
|
|
|
// Scope stores the machine-readable sanction scope.
|
|
Scope common.Scope
|
|
|
|
// ReasonCode stores the mutation reason.
|
|
ReasonCode common.ReasonCode
|
|
|
|
// Actor stores the audit actor metadata attached to the mutation.
|
|
Actor common.ActorRef
|
|
|
|
// AppliedAt stores when the sanction became effective.
|
|
AppliedAt time.Time
|
|
|
|
// ExpiresAt stores the optional planned sanction expiry.
|
|
ExpiresAt *time.Time
|
|
|
|
// RemovedAt stores the optional sanction removal timestamp.
|
|
RemovedAt *time.Time
|
|
}
|
|
|
|
// Validate reports whether event is structurally complete.
|
|
func (event SanctionChangedEvent) Validate() error {
|
|
if err := validateEventEnvelope("sanction changed event", event.UserID, event.OccurredAt, event.Source, event.TraceID); err != nil {
|
|
return err
|
|
}
|
|
if !event.Operation.IsKnown() {
|
|
return fmt.Errorf("sanction changed event operation %q is unsupported", event.Operation)
|
|
}
|
|
if !event.SanctionCode.IsKnown() {
|
|
return fmt.Errorf("sanction changed event sanction code %q is unsupported", event.SanctionCode)
|
|
}
|
|
if err := event.Scope.Validate(); err != nil {
|
|
return fmt.Errorf("sanction changed event scope: %w", err)
|
|
}
|
|
if err := event.ReasonCode.Validate(); err != nil {
|
|
return fmt.Errorf("sanction changed event reason code: %w", err)
|
|
}
|
|
if err := event.Actor.Validate(); err != nil {
|
|
return fmt.Errorf("sanction changed event actor: %w", err)
|
|
}
|
|
if err := common.ValidateTimestamp("sanction changed event applied at", event.AppliedAt); err != nil {
|
|
return err
|
|
}
|
|
if event.ExpiresAt != nil && !event.ExpiresAt.After(event.AppliedAt) {
|
|
return common.ErrInvertedTimeRange
|
|
}
|
|
if event.RemovedAt != nil && event.RemovedAt.Before(event.AppliedAt) {
|
|
return fmt.Errorf("sanction changed event removed at must not be before applied at")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LimitChangedEvent stores one post-commit auxiliary limit-change event.
|
|
type LimitChangedEvent struct {
|
|
// UserID identifies the changed user.
|
|
UserID common.UserID
|
|
|
|
// OccurredAt stores the mutation timestamp emitted into the shared event
|
|
// envelope.
|
|
OccurredAt time.Time
|
|
|
|
// Source stores the machine-readable mutation source.
|
|
Source common.Source
|
|
|
|
// TraceID stores the optional OpenTelemetry trace identifier propagated
|
|
// from the current request context.
|
|
TraceID string
|
|
|
|
// Operation stores the limit-change event kind.
|
|
Operation LimitChangedOperation
|
|
|
|
// LimitCode stores the affected limit code.
|
|
LimitCode policy.LimitCode
|
|
|
|
// Value stores the active limit value when the operation is `set`.
|
|
Value *int
|
|
|
|
// ReasonCode stores the mutation reason.
|
|
ReasonCode common.ReasonCode
|
|
|
|
// Actor stores the audit actor metadata attached to the mutation.
|
|
Actor common.ActorRef
|
|
|
|
// AppliedAt stores when the limit became effective.
|
|
AppliedAt time.Time
|
|
|
|
// ExpiresAt stores the optional planned limit expiry.
|
|
ExpiresAt *time.Time
|
|
|
|
// RemovedAt stores the optional explicit limit removal timestamp.
|
|
RemovedAt *time.Time
|
|
}
|
|
|
|
// Validate reports whether event is structurally complete.
|
|
func (event LimitChangedEvent) Validate() error {
|
|
if err := validateEventEnvelope("limit changed event", event.UserID, event.OccurredAt, event.Source, event.TraceID); err != nil {
|
|
return err
|
|
}
|
|
if !event.Operation.IsKnown() {
|
|
return fmt.Errorf("limit changed event operation %q is unsupported", event.Operation)
|
|
}
|
|
if !event.LimitCode.IsSupported() {
|
|
return fmt.Errorf("limit changed event limit code %q is unsupported", event.LimitCode)
|
|
}
|
|
switch event.Operation {
|
|
case LimitChangedOperationSet:
|
|
if event.Value == nil {
|
|
return fmt.Errorf("limit changed event value must be present for operation %q", event.Operation)
|
|
}
|
|
if *event.Value < 0 {
|
|
return fmt.Errorf("limit changed event value must not be negative")
|
|
}
|
|
case LimitChangedOperationRemoved:
|
|
if event.Value != nil && *event.Value < 0 {
|
|
return fmt.Errorf("limit changed event value must not be negative")
|
|
}
|
|
}
|
|
if err := event.ReasonCode.Validate(); err != nil {
|
|
return fmt.Errorf("limit changed event reason code: %w", err)
|
|
}
|
|
if err := event.Actor.Validate(); err != nil {
|
|
return fmt.Errorf("limit changed event actor: %w", err)
|
|
}
|
|
if err := common.ValidateTimestamp("limit changed event applied at", event.AppliedAt); err != nil {
|
|
return err
|
|
}
|
|
if event.ExpiresAt != nil && !event.ExpiresAt.After(event.AppliedAt) {
|
|
return common.ErrInvertedTimeRange
|
|
}
|
|
if event.RemovedAt != nil && event.RemovedAt.Before(event.AppliedAt) {
|
|
return fmt.Errorf("limit changed event removed at must not be before applied at")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ProfileChangedPublisher publishes auxiliary profile-change notifications.
|
|
type ProfileChangedPublisher interface {
|
|
// PublishProfileChanged propagates one committed profile-change event.
|
|
PublishProfileChanged(ctx context.Context, event ProfileChangedEvent) error
|
|
}
|
|
|
|
// SettingsChangedPublisher publishes auxiliary settings-change notifications.
|
|
type SettingsChangedPublisher interface {
|
|
// PublishSettingsChanged propagates one committed settings-change event.
|
|
PublishSettingsChanged(ctx context.Context, event SettingsChangedEvent) error
|
|
}
|
|
|
|
// EntitlementChangedPublisher publishes auxiliary entitlement-change
|
|
// notifications.
|
|
type EntitlementChangedPublisher interface {
|
|
// PublishEntitlementChanged propagates one committed entitlement-change
|
|
// event.
|
|
PublishEntitlementChanged(ctx context.Context, event EntitlementChangedEvent) error
|
|
}
|
|
|
|
// SanctionChangedPublisher publishes auxiliary sanction-change notifications.
|
|
type SanctionChangedPublisher interface {
|
|
// PublishSanctionChanged propagates one committed sanction-change event.
|
|
PublishSanctionChanged(ctx context.Context, event SanctionChangedEvent) error
|
|
}
|
|
|
|
// LimitChangedPublisher publishes auxiliary limit-change notifications.
|
|
type LimitChangedPublisher interface {
|
|
// PublishLimitChanged propagates one committed limit-change event.
|
|
PublishLimitChanged(ctx context.Context, event LimitChangedEvent) error
|
|
}
|
|
|
|
func validateEventEnvelope(name string, userID common.UserID, occurredAt time.Time, source common.Source, traceID string) error {
|
|
if err := userID.Validate(); err != nil {
|
|
return fmt.Errorf("%s user id: %w", name, err)
|
|
}
|
|
if err := common.ValidateTimestamp(name+" occurred at", occurredAt); err != nil {
|
|
return err
|
|
}
|
|
if err := source.Validate(); err != nil {
|
|
return fmt.Errorf("%s source: %w", name, err)
|
|
}
|
|
if traceID != "" {
|
|
if strings.TrimSpace(traceID) != traceID {
|
|
return fmt.Errorf("%s trace id must not contain surrounding whitespace", name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|