feat: user service
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user