first commit
This commit is contained in:
126
internal/user/service.go
Normal file
126
internal/user/service.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *Repo
|
||||
reserved []string
|
||||
}
|
||||
|
||||
func NewService(repo *Repo, reserved []string) *Service {
|
||||
return &Service{repo: repo, reserved: reserved}
|
||||
}
|
||||
|
||||
func (s *Service) Repo() *Repo { return s.repo }
|
||||
|
||||
func (s *Service) Reserved() []string { return s.reserved }
|
||||
|
||||
// IsAvailable returns true if no active/in-grace user has the username.
|
||||
func (s *Service) IsAvailable(ctx context.Context, username string) (bool, error) {
|
||||
if err := ValidateUsername(username, s.reserved); err != nil {
|
||||
return false, err
|
||||
}
|
||||
u, err := s.repo.GetByUsername(ctx, NormalizeUsername(username))
|
||||
if errors.Is(err, ErrUserNotFound) {
|
||||
return true, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
_ = u
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// CreateOrActivate inserts a new active user. Caller is responsible for transactional
|
||||
// concerns (e.g. payments worker uses this within a tx).
|
||||
func (s *Service) CreateOrActivate(ctx context.Context, pubkey, username string, sub SubscriptionType, years int, manual bool) (*User, error) {
|
||||
username = NormalizeUsername(username)
|
||||
if err := ValidateUsername(username, s.reserved); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
expiresAt := computeExpiry(sub, years, time.Time{}, now)
|
||||
|
||||
u := &User{
|
||||
Pubkey: pubkey,
|
||||
Username: username,
|
||||
SubscriptionType: sub,
|
||||
ExpiresAt: expiresAt,
|
||||
IsActive: true,
|
||||
ManualUsername: manual,
|
||||
}
|
||||
if err := s.repo.Insert(ctx, u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Renew extends an existing user. Used by payments worker and admin extend.
|
||||
func (s *Service) Renew(ctx context.Context, u *User, sub SubscriptionType, years int) error {
|
||||
now := time.Now().UTC()
|
||||
var current time.Time
|
||||
if u.ExpiresAt != nil {
|
||||
current = *u.ExpiresAt
|
||||
}
|
||||
u.SubscriptionType = sub
|
||||
u.ExpiresAt = computeExpiry(sub, years, current, now)
|
||||
u.IsActive = true
|
||||
u.DeactivatedAt = nil
|
||||
u.ExpiringReminderSentAt = nil
|
||||
return s.repo.Update(ctx, u)
|
||||
}
|
||||
|
||||
func (s *Service) SetUsername(ctx context.Context, pubkey, username string) (*User, error) {
|
||||
username = NormalizeUsername(username)
|
||||
if err := ValidateUsername(username, s.reserved); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u, err := s.repo.GetByPubkey(ctx, pubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing, err := s.repo.GetByUsername(ctx, username); err == nil && existing.Pubkey != pubkey {
|
||||
return nil, ErrUsernameTaken
|
||||
}
|
||||
u.Username = username
|
||||
u.ManualUsername = true
|
||||
if err := s.repo.Update(ctx, u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, pubkey string) error {
|
||||
return s.repo.Delete(ctx, pubkey)
|
||||
}
|
||||
|
||||
// computeExpiry returns *time.Time (nil for lifetime).
|
||||
func computeExpiry(sub SubscriptionType, years int, current time.Time, now time.Time) *time.Time {
|
||||
if sub == SubLifetime {
|
||||
return nil
|
||||
}
|
||||
var cur time.Time
|
||||
if !current.IsZero() {
|
||||
cur = current
|
||||
}
|
||||
return YearlyTargetExpiry(&cur, years, now)
|
||||
}
|
||||
|
||||
// YearlyTargetExpiry computes the new expiry for a yearly subscription using
|
||||
// effective_start = max(now, current_expiry); new_expiry = effective_start + years.
|
||||
// currentExpiry may be nil or zero for first-time purchases.
|
||||
func YearlyTargetExpiry(currentExpiry *time.Time, years int, now time.Time) *time.Time {
|
||||
if years <= 0 {
|
||||
years = 1
|
||||
}
|
||||
base := now
|
||||
if currentExpiry != nil && currentExpiry.After(base) {
|
||||
base = *currentExpiry
|
||||
}
|
||||
t := base.AddDate(years, 0, 0)
|
||||
return &t
|
||||
}
|
||||
Reference in New Issue
Block a user