Ctrl K
ComunidadGuías…

internal/domain/collectible_phone.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package domain

import (
	"errors"
	"strings"
	"time"
)

const (
	MinCollectiblePhoneLength = 7
	MaxCollectiblePhoneLength = 15
)

var (
	ErrCollectiblePhoneInvalid    = errors.New("collectible phone invalid")
	ErrCollectiblePhoneNotFound   = errors.New("collectible phone not found")
	ErrCollectiblePhoneNotOwned   = errors.New("collectible phone not owned")
	ErrCollectiblePhoneBurned     = errors.New("collectible phone burned")
	ErrCollectiblePhoneOwnerLimit = errors.New("user already owns a collectible phone")
)

// CollectiblePhoneTier controls visibility. Standard numbers follow the
// account phone privacy rule; exclusive numbers are a server extension that
// remain visible on every projection of their owner.
type CollectiblePhoneTier string

const (
	CollectiblePhoneTierStandard  CollectiblePhoneTier = "standard"
	CollectiblePhoneTierExclusive CollectiblePhoneTier = "exclusive"
)

func (t CollectiblePhoneTier) Valid() bool {
	return t == CollectiblePhoneTierStandard || t == CollectiblePhoneTierExclusive
}

type CollectiblePhone struct {
	ID                  int64
	Phone               string
	Tier                CollectiblePhoneTier
	Status              CollectibleUsernameStatus
	OwnerUserID         int64
	PurchaseDate        time.Time
	Currency            string
	Amount              int64
	CryptoCurrency      string
	CryptoAmount        int64
	URL                 string
	OriginalOwnerUserID int64
	TransferCount       int
	Version             int64
	CreatedAt           time.Time
	UpdatedAt           time.Time
}

func (c CollectiblePhone) Owned() bool {
	return c.Status == CollectibleUsernameStatusOwned && c.OwnerUserID > 0
}

func (c CollectiblePhone) AlwaysVisible() bool {
	return c.Owned() && c.Tier == CollectiblePhoneTierExclusive
}

func (c CollectiblePhone) Info() CollectibleInfo {
	date := 0
	if !c.PurchaseDate.IsZero() {
		date = int(c.PurchaseDate.Unix())
	}
	return CollectibleInfo{PurchaseDate: date, Currency: c.Currency, Amount: c.Amount,
		CryptoCurrency: c.CryptoCurrency, CryptoAmount: c.CryptoAmount, URL: c.URL}
}

func (c CollectiblePhone) Validate() error {
	if !ValidCollectiblePhone(c.Phone) || !c.Tier.Valid() || !c.Status.Valid() {
		return ErrCollectiblePhoneInvalid
	}
	if (c.Status == CollectibleUsernameStatusOwned) != (c.OwnerUserID > 0) {
		return ErrCollectiblePhoneInvalid
	}
	if err := ValidateCollectiblePhonePrice(c.Currency, c.Amount, c.CryptoCurrency, c.CryptoAmount); err != nil {
		return err
	}
	if len(c.URL) > MaxCollectibleUsernameURLLength || c.OriginalOwnerUserID < 0 || c.TransferCount < 0 {
		return ErrCollectiblePhoneInvalid
	}
	return nil
}

// ValidateCollectiblePhonePrice mirrors the shape Telegram Desktop expects in
// fragment.collectibleInfo: TON (nanotons) is drawn first and the USD purchase
// amount (cents) is drawn in parentheses. Leaving the crypto leg empty makes the
// client render the literal fallback "{:0}".
func ValidateCollectiblePhonePrice(currency string, amount int64, cryptoCurrency string, cryptoAmount int64) error {
	if currency != CollectibleCurrencyUSD || amount <= 0 || cryptoCurrency != CollectibleCryptoCurrencyTON || cryptoAmount <= 0 {
		return ErrCollectibleCurrencyInvalid
	}
	return nil
}

// NormalizeCollectiblePhone accepts the formatting clients commonly send and
// returns the canonical digits shared by virtual users.phone login identities
// and inputCollectiblePhone assets. Login identity and asset ownership remain
// independent facts.
func NormalizeCollectiblePhone(phone string) string {
	phone = strings.TrimSpace(phone)
	var b strings.Builder
	for i, r := range phone {
		switch {
		case r >= '0' && r <= '9':
			b.WriteRune(r)
		case r == '+' && i == 0:
		case r == ' ' || r == '-' || r == '(' || r == ')' || r == '\t':
		default:
			return ""
		}
	}
	return b.String()
}

func ValidCollectiblePhone(phone string) bool {
	if len(phone) < MinCollectiblePhoneLength || len(phone) > MaxCollectiblePhoneLength || !strings.HasPrefix(phone, "888") {
		return false
	}
	for _, r := range phone {
		if r < '0' || r > '9' {
			return false
		}
	}
	return true
}

type MintCollectiblePhoneRequest struct {
	Phone          string
	Tier           CollectiblePhoneTier
	OwnerUserID    int64
	PurchaseDate   time.Time
	Currency       string
	Amount         int64
	CryptoCurrency string
	CryptoAmount   int64
	URL            string
	Actor          string
	Reason         string
	CommandKey     string
}

func (r MintCollectiblePhoneRequest) Validate() error {
	if !ValidCollectiblePhone(r.Phone) || !r.Tier.Valid() || r.OwnerUserID < 0 {
		return ErrCollectiblePhoneInvalid
	}
	if err := ValidateCollectiblePhonePrice(r.Currency, r.Amount, r.CryptoCurrency, r.CryptoAmount); err != nil {
		return err
	}
	if len(r.URL) > MaxCollectibleUsernameURLLength || len(r.Actor) > MaxCollectibleUsernameActorLength ||
		len(r.Reason) > MaxCollectibleUsernameReasonLength || len(r.CommandKey) > MaxCollectibleUsernameCommandKeyLength {
		return ErrCollectiblePhoneInvalid
	}
	return nil
}

type UpdateCollectiblePhonePriceRequest struct {
	Phone          string
	Currency       string
	Amount         int64
	CryptoCurrency string
	CryptoAmount   int64
	Actor          string
	Reason         string
}

func (r UpdateCollectiblePhonePriceRequest) Validate() error {
	if !ValidCollectiblePhone(r.Phone) || len(r.Actor) > MaxCollectibleUsernameActorLength || len(r.Reason) > MaxCollectibleUsernameReasonLength {
		return ErrCollectiblePhoneInvalid
	}
	return ValidateCollectiblePhonePrice(r.Currency, r.Amount, r.CryptoCurrency, r.CryptoAmount)
}

type TransferCollectiblePhoneRequest struct {
	Phone                     string
	ToUserID                  int64
	Actor, Reason, CommandKey string
}

func (r TransferCollectiblePhoneRequest) Validate() error {
	if !ValidCollectiblePhone(r.Phone) || r.ToUserID <= 0 || len(r.Actor) > MaxCollectibleUsernameActorLength || len(r.Reason) > MaxCollectibleUsernameReasonLength || len(r.CommandKey) > MaxCollectibleUsernameCommandKeyLength {
		return ErrCollectiblePhoneInvalid
	}
	return nil
}

type RevokeCollectiblePhoneRequest struct {
	Phone                     string
	Burn                      bool
	Actor, Reason, CommandKey string
}

func (r RevokeCollectiblePhoneRequest) Validate() error {
	if !ValidCollectiblePhone(r.Phone) || len(r.Actor) > MaxCollectibleUsernameActorLength || len(r.Reason) > MaxCollectibleUsernameReasonLength || len(r.CommandKey) > MaxCollectibleUsernameCommandKeyLength {
		return ErrCollectiblePhoneInvalid
	}
	return nil
}

type DeleteCollectiblePhoneRequest struct {
	Phone                     string
	Actor, Reason, CommandKey string
}

func (r DeleteCollectiblePhoneRequest) Validate() error {
	if !ValidCollectiblePhone(r.Phone) || len(r.Actor) > MaxCollectibleUsernameActorLength || len(r.Reason) > MaxCollectibleUsernameReasonLength || len(r.CommandKey) > MaxCollectibleUsernameCommandKeyLength {
		return ErrCollectiblePhoneInvalid
	}
	return nil
}

type CollectiblePhoneTransfer struct {
	ID, CollectibleID         int64
	Kind                      CollectibleUsernameTransferKind
	FromUserID, ToUserID      int64
	Currency                  string
	Amount                    int64
	Actor, Reason, CommandKey string
	CreatedAt                 time.Time
}

type CollectiblePhoneFilter struct {
	Status      CollectibleUsernameStatus
	Tier        CollectiblePhoneTier
	OwnerUserID int64
	Query       string
	BeforeID    int64
	Limit       int
}