// Package entitlement defines the logical entitlement entities owned by User // Service. package entitlement import ( "fmt" "strings" "time" "galaxy/user/internal/domain/common" ) // PlanCode identifies one supported entitlement plan. type PlanCode string const ( // PlanCodeFree reports the free default entitlement. PlanCodeFree PlanCode = "free" // PlanCodePaidMonthly reports a finite monthly paid entitlement. PlanCodePaidMonthly PlanCode = "paid_monthly" // PlanCodePaidYearly reports a finite yearly paid entitlement. PlanCodePaidYearly PlanCode = "paid_yearly" // PlanCodePaidLifetime reports a non-expiring paid entitlement. PlanCodePaidLifetime PlanCode = "paid_lifetime" ) // IsKnown reports whether PlanCode belongs to the frozen v1 catalog. func (code PlanCode) IsKnown() bool { switch code { case PlanCodeFree, PlanCodePaidMonthly, PlanCodePaidYearly, PlanCodePaidLifetime: return true default: return false } } // IsPaid reports whether PlanCode represents a paid entitlement state. func (code PlanCode) IsPaid() bool { switch code { case PlanCodePaidMonthly, PlanCodePaidYearly, PlanCodePaidLifetime: return true default: return false } } // HasFiniteExpiry reports whether PlanCode requires a bounded `ends_at` // value in the Stage 07 entitlement timeline model. func (code PlanCode) HasFiniteExpiry() bool { switch code { case PlanCodePaidMonthly, PlanCodePaidYearly: return true default: return false } } // EntitlementRecordID identifies one immutable entitlement history record. type EntitlementRecordID string // String returns EntitlementRecordID as its stored identifier string. func (id EntitlementRecordID) String() string { return string(id) } // IsZero reports whether EntitlementRecordID does not contain a usable value. func (id EntitlementRecordID) IsZero() bool { return strings.TrimSpace(string(id)) == "" } // Validate reports whether EntitlementRecordID is non-empty, normalized, and // uses the frozen Stage 02 prefix. func (id EntitlementRecordID) Validate() error { switch { case id.IsZero(): return fmt.Errorf("entitlement record id must not be empty") case strings.TrimSpace(string(id)) != string(id): return fmt.Errorf("entitlement record id must not contain surrounding whitespace") case !strings.HasPrefix(string(id), "entitlement-"): return fmt.Errorf("entitlement record id must start with %q", "entitlement-") case len(string(id)) == len("entitlement-"): return fmt.Errorf("entitlement record id must contain opaque data after %q", "entitlement-") default: return nil } } // PeriodRecord stores one entitlement-period history record. type PeriodRecord struct { // RecordID identifies the immutable history record. RecordID EntitlementRecordID // UserID identifies the account that owns the entitlement record. UserID common.UserID // PlanCode stores the effective plan for the recorded period. PlanCode PlanCode // Source stores the machine-readable mutation source. Source common.Source // Actor stores the audit actor metadata captured for the mutation. Actor common.ActorRef // ReasonCode stores the machine-readable reason for the mutation. ReasonCode common.ReasonCode // StartsAt stores when the period becomes effective. StartsAt time.Time // EndsAt stores the optional planned end of the period. EndsAt *time.Time // CreatedAt stores when the history record was created. CreatedAt time.Time // ClosedAt stores when the period was later closed early by another trusted // mutation. ClosedAt *time.Time // ClosedBy stores optional audit actor metadata for the close mutation. ClosedBy common.ActorRef // ClosedReasonCode stores the reason for closing the period early. ClosedReasonCode common.ReasonCode } // Validate reports whether PeriodRecord satisfies the frozen Stage 02 // structural invariants. func (record PeriodRecord) Validate() error { if err := record.RecordID.Validate(); err != nil { return fmt.Errorf("entitlement period record id: %w", err) } if err := record.UserID.Validate(); err != nil { return fmt.Errorf("entitlement period user id: %w", err) } if !record.PlanCode.IsKnown() { return fmt.Errorf("entitlement period plan code %q is unsupported", record.PlanCode) } if err := record.Source.Validate(); err != nil { return fmt.Errorf("entitlement period source: %w", err) } if err := record.Actor.Validate(); err != nil { return fmt.Errorf("entitlement period actor: %w", err) } if err := record.ReasonCode.Validate(); err != nil { return fmt.Errorf("entitlement period reason code: %w", err) } if err := common.ValidateTimestamp("entitlement period starts at", record.StartsAt); err != nil { return err } if err := validatePlanBounds("entitlement period", record.PlanCode, record.StartsAt, record.EndsAt); err != nil { return err } if err := common.ValidateTimestamp("entitlement period created at", record.CreatedAt); err != nil { return err } if record.ClosedAt == nil { if !record.ClosedBy.IsZero() { return fmt.Errorf("entitlement period closed by must be empty when closed at is absent") } if !record.ClosedReasonCode.IsZero() { return fmt.Errorf("entitlement period closed reason code must be empty when closed at is absent") } return nil } if record.ClosedAt.Before(record.StartsAt) { return fmt.Errorf("entitlement period closed at must not be before starts at") } if record.EndsAt != nil && record.ClosedAt.After(*record.EndsAt) { return fmt.Errorf("entitlement period closed at must not be after ends at") } if record.ClosedAt.Before(record.CreatedAt) { return fmt.Errorf("entitlement period closed at must not be before created at") } if err := record.ClosedBy.Validate(); err != nil { return fmt.Errorf("entitlement period closed by: %w", err) } if err := record.ClosedReasonCode.Validate(); err != nil { return fmt.Errorf("entitlement period closed reason code: %w", err) } return nil } // IsEffectiveAt reports whether PeriodRecord is the currently effective // segment at the supplied timestamp. func (record PeriodRecord) IsEffectiveAt(now time.Time) bool { if record.ClosedAt != nil { return false } if record.StartsAt.After(now) { return false } if record.EndsAt != nil && !record.EndsAt.After(now) { return false } return true } // CurrentSnapshot stores the read-optimized current entitlement state of one // user account. type CurrentSnapshot struct { // UserID identifies the account that owns the current entitlement. UserID common.UserID // PlanCode stores the current effective plan code. PlanCode PlanCode // IsPaid stores the materialized paid/free state used on hot read paths. IsPaid bool // StartsAt stores when the current effective state started. StartsAt time.Time // EndsAt stores the optional end of the current finite entitlement. EndsAt *time.Time // Source stores the machine-readable source of the current state. Source common.Source // Actor stores the actor metadata attached to the last successful mutation. Actor common.ActorRef // ReasonCode stores the machine-readable reason attached to the last // successful mutation. ReasonCode common.ReasonCode // UpdatedAt stores when the snapshot was last recomputed. UpdatedAt time.Time } // Validate reports whether CurrentSnapshot satisfies the frozen Stage 02 // structural invariants. func (record CurrentSnapshot) Validate() error { if err := record.UserID.Validate(); err != nil { return fmt.Errorf("entitlement snapshot user id: %w", err) } if !record.PlanCode.IsKnown() { return fmt.Errorf("entitlement snapshot plan code %q is unsupported", record.PlanCode) } if record.IsPaid != record.PlanCode.IsPaid() { return fmt.Errorf("entitlement snapshot paid flag must match plan code %q", record.PlanCode) } if err := common.ValidateTimestamp("entitlement snapshot starts at", record.StartsAt); err != nil { return err } if err := validatePlanBounds("entitlement snapshot", record.PlanCode, record.StartsAt, record.EndsAt); err != nil { return err } if err := record.Source.Validate(); err != nil { return fmt.Errorf("entitlement snapshot source: %w", err) } if err := record.Actor.Validate(); err != nil { return fmt.Errorf("entitlement snapshot actor: %w", err) } if err := record.ReasonCode.Validate(); err != nil { return fmt.Errorf("entitlement snapshot reason code: %w", err) } if err := common.ValidateTimestamp("entitlement snapshot updated at", record.UpdatedAt); err != nil { return err } return nil } // HasFiniteExpiry reports whether CurrentSnapshot participates in the finite // paid-expiry index. func (record CurrentSnapshot) HasFiniteExpiry() bool { return record.IsPaid && record.EndsAt != nil } // IsExpiredAt reports whether CurrentSnapshot represents a finite paid state // that has already reached its stored expiry. func (record CurrentSnapshot) IsExpiredAt(now time.Time) bool { return record.HasFiniteExpiry() && !record.EndsAt.After(now) } // PaidState identifies the coarse free-versus-paid filter used by admin // listing. type PaidState string const ( // PaidStateFree filters accounts whose current entitlement is free. PaidStateFree PaidState = "free" // PaidStatePaid filters accounts whose current entitlement is paid. PaidStatePaid PaidState = "paid" ) // IsKnown reports whether PaidState belongs to the frozen Stage 02 filter // vocabulary. func (state PaidState) IsKnown() bool { switch state { case "", PaidStateFree, PaidStatePaid: return true default: return false } } func validatePlanBounds( name string, planCode PlanCode, startsAt time.Time, endsAt *time.Time, ) error { switch { case planCode.HasFiniteExpiry(): if endsAt == nil { return fmt.Errorf("%s ends at must be present for plan code %q", name, planCode) } if !endsAt.After(startsAt) { return common.ErrInvertedTimeRange } case endsAt != nil: return fmt.Errorf("%s ends at must be empty for plan code %q", name, planCode) } return nil }