Ctrl K
ComunidadGuías…

internal/domain/premium.go

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

Archivo original ↗Preguntar a KiBot
package domain

import (
	"errors"
	"fmt"
	"strings"
	"unicode/utf16"
	"unicode/utf8"
)

// PremiumPromoCatalog is the immutable, domain-only media catalog returned by
// help.getPremiumPromo. VideoSections[i] describes Videos[i]; callers must
// preserve the one-to-one ordering because official clients use positional
// lookup.
type PremiumPromoCatalog struct {
	VideoSections []string
	Videos        []Document
}

const (
	PremiumCurrencyStars          = "XTR"
	PremiumDefaultFiatCurrency    = "USD"
	PremiumPaymentFormTTLSeconds  = 10 * 60
	MaxPremiumGiftMessageRunes    = 128
	MaxPremiumGiftMessageEntities = 100
	MaxPremiumPlanMonths          = 120
	MaxPremiumPlanDurationDays    = 36500
	MaxPremiumPlanAmountStars     = int64(1_000_000_000_000_000)
	MaxPremiumUnixTimestamp       = 1<<31 - 1
)

type PremiumPurchaseKind string

const (
	PremiumPurchaseSelf PremiumPurchaseKind = "self"
	PremiumPurchaseGift PremiumPurchaseKind = "gift"
)

type PremiumEntitlementSource string

const (
	PremiumEntitlementPurchase PremiumEntitlementSource = "purchase"
	PremiumEntitlementGift     PremiumEntitlementSource = "gift"
	PremiumEntitlementGiftCode PremiumEntitlementSource = "gift_code"
	PremiumEntitlementAdmin    PremiumEntitlementSource = "admin"
	PremiumEntitlementPromo    PremiumEntitlementSource = "promo"
)

type PremiumEntitlementStatus string

const (
	PremiumEntitlementActive   PremiumEntitlementStatus = "active"
	PremiumEntitlementExpired  PremiumEntitlementStatus = "expired"
	PremiumEntitlementRevoked  PremiumEntitlementStatus = "revoked"
	PremiumEntitlementRefunded PremiumEntitlementStatus = "refunded"
)

type PremiumPaymentStatus string

const (
	PremiumPaymentPending    PremiumPaymentStatus = "pending"
	PremiumPaymentProcessing PremiumPaymentStatus = "processing"
	PremiumPaymentPaid       PremiumPaymentStatus = "paid"
	PremiumPaymentFailed     PremiumPaymentStatus = "failed"
	PremiumPaymentRefunded   PremiumPaymentStatus = "refunded"
	PremiumPaymentExpired    PremiumPaymentStatus = "expired"
)

// PremiumPlan is the authoritative server-side price and duration snapshot.
// Version is copied into every payment intent, so a catalog edit can never
// change the amount or duration of an already issued form.
type PremiumPlan struct {
	Months        int
	DurationDays  int
	AmountStars   int64
	FiatCurrency  string
	FiatAmount    int64
	StoreProduct  string
	StoreQuantity int
	Enabled       bool
	SortOrder     int
	Label         string
	ManagedBy     string
	Version       int64
	UpdatedAt     int
}

func (p PremiumPlan) Valid() bool {
	currency := p.EffectiveFiatCurrency()
	fiatAmount := p.EffectiveFiatAmount()
	return p.Months > 0 && p.Months <= MaxPremiumPlanMonths &&
		p.DurationDays > 0 && p.DurationDays <= MaxPremiumPlanDurationDays &&
		p.AmountStars > 0 && p.AmountStars <= MaxPremiumPlanAmountStars &&
		p.Version > 0 && p.Label != "" && validPremiumCurrency(currency) &&
		fiatAmount > 0 && fiatAmount <= MaxPremiumPlanAmountStars &&
		len(p.StoreProduct) <= 256 && p.StoreQuantity >= 0 &&
		((p.StoreProduct == "" && p.StoreQuantity == 0) ||
			(p.StoreProduct != "" && p.StoreQuantity > 0))
}

func (p PremiumPlan) EffectiveFiatCurrency() string {
	currency := strings.ToUpper(strings.TrimSpace(p.FiatCurrency))
	if currency == "" {
		return PremiumDefaultFiatCurrency
	}
	return currency
}

func (p PremiumPlan) EffectiveFiatAmount() int64 {
	if p.FiatAmount <= 0 {
		return p.AmountStars
	}
	return p.FiatAmount
}

func validPremiumCurrency(currency string) bool {
	if len(currency) != 3 {
		return false
	}
	for _, r := range currency {
		if r < 'A' || r > 'Z' {
			return false
		}
	}
	return true
}

const (
	PremiumPlanManagedByConfig = "config"
	PremiumPlanManagedByAdmin  = "admin"
)

// PremiumPlanUpsertRequest is the operator-facing optimistic write model.
// ExpectedVersion=0 creates a new month option; a positive value updates
// exactly that revision. Plans are disabled instead of deleted because issued
// payment forms retain their catalog snapshot and version.
type PremiumPlanUpsertRequest struct {
	Months          int
	DurationDays    int
	AmountStars     int64
	FiatCurrency    string
	FiatAmount      int64
	StoreProduct    string
	StoreQuantity   int
	Enabled         bool
	SortOrder       int
	Label           string
	ExpectedVersion int64
	ActorUserID     int64
	Date            int
	Reason          string
	CommandKey      string
}

func (r PremiumPlanUpsertRequest) Valid() bool {
	return PremiumPlan{
		Months:        r.Months,
		DurationDays:  r.DurationDays,
		AmountStars:   r.AmountStars,
		FiatCurrency:  r.FiatCurrency,
		FiatAmount:    r.FiatAmount,
		StoreProduct:  r.StoreProduct,
		StoreQuantity: r.StoreQuantity,
		Enabled:       r.Enabled,
		SortOrder:     r.SortOrder,
		Label:         r.Label,
		Version:       1,
	}.Valid() &&
		r.ExpectedVersion >= 0 && r.ActorUserID > 0 && r.Date > 0 &&
		r.CommandKey != "" && len(r.CommandKey) <= 256 && len(r.Reason) <= 1024
}

type PremiumGiftMessage struct {
	Text     string          `json:"text"`
	Entities []MessageEntity `json:"entities,omitempty"`
}

func (m PremiumGiftMessage) Valid() bool {
	if !utf8.ValidString(m.Text) ||
		len([]rune(m.Text)) > MaxPremiumGiftMessageRunes ||
		len(m.Entities) > MaxPremiumGiftMessageEntities {
		return false
	}
	encoded := utf16.Encode([]rune(m.Text))
	units := len(encoded)
	boundaries := make([]bool, units+1)
	boundaries[0] = true
	position := 0
	for _, r := range m.Text {
		position += len(utf16.Encode([]rune{r}))
		boundaries[position] = true
	}
	for _, entity := range m.Entities {
		if !PremiumGiftEntityTypeAllowed(entity.Type) {
			return false
		}
		if entity.Offset < 0 || entity.Length <= 0 ||
			entity.Offset > units || entity.Length > units-entity.Offset ||
			!boundaries[entity.Offset] || !boundaries[entity.Offset+entity.Length] {
			return false
		}
		if entity.Type == MessageEntityCustomEmoji && entity.DocumentID <= 0 {
			return false
		}
	}
	return true
}

// PremiumGiftEntityTypeAllowed mirrors the formatted-text subset accepted by
// Telegram's giftPremiumSubscription surface. Keeping it in the domain model
// makes MTProto, Bot API and persisted invoice validation identical.
func PremiumGiftEntityTypeAllowed(entityType MessageEntityType) bool {
	switch entityType {
	case MessageEntityBold, MessageEntityItalic, MessageEntityUnderline,
		MessageEntityStrike, MessageEntitySpoiler, MessageEntityCustomEmoji:
		return true
	default:
		return false
	}
}

// PremiumInvoice is persisted as message media for invoices sent by
// @premiumbot. It contains no client-controlled price.
type PremiumInvoice struct {
	Kind            PremiumPurchaseKind `json:"kind"`
	RecipientUserID int64               `json:"recipient_user_id,omitempty"`
	Months          int                 `json:"months"`
	DurationDays    int                 `json:"duration_days"`
	AmountStars     int64               `json:"amount_stars"`
	PaymentCurrency string              `json:"payment_currency,omitempty"`
	PaymentAmount   int64               `json:"payment_amount,omitempty"`
	DebitStars      bool                `json:"debit_stars,omitempty"`
	PlanVersion     int64               `json:"plan_version"`
	Title           string              `json:"title"`
	Description     string              `json:"description"`
	StartParam      string              `json:"start_param,omitempty"`
	Message         PremiumGiftMessage  `json:"message,omitempty"`
}

func (i PremiumInvoice) Valid() bool {
	if i.Kind != PremiumPurchaseSelf && i.Kind != PremiumPurchaseGift {
		return false
	}
	if i.Kind == PremiumPurchaseSelf && i.RecipientUserID != 0 {
		return false
	}
	if i.Kind == PremiumPurchaseGift && i.RecipientUserID <= 0 {
		return false
	}
	return i.Months > 0 && i.Months <= MaxPremiumPlanMonths &&
		i.DurationDays > 0 && i.DurationDays <= MaxPremiumPlanDurationDays &&
		i.AmountStars > 0 && i.AmountStars <= MaxPremiumPlanAmountStars &&
		i.PlanVersion > 0 && i.Title != "" && i.Description != "" &&
		validPremiumCurrency(i.EffectivePaymentCurrency()) &&
		i.EffectivePaymentAmount() > 0 &&
		i.EffectivePaymentAmount() <= MaxPremiumPlanAmountStars &&
		i.Message.Valid()
}

func (i PremiumInvoice) EffectivePaymentCurrency() string {
	if currency := strings.ToUpper(strings.TrimSpace(i.PaymentCurrency)); currency != "" {
		return currency
	}
	return PremiumCurrencyStars
}

func (i PremiumInvoice) EffectivePaymentAmount() int64 {
	if i.PaymentAmount > 0 {
		return i.PaymentAmount
	}
	return i.AmountStars
}

func (i PremiumInvoice) EffectiveDebitStars() bool {
	return i.PaymentCurrency == "" || i.EffectivePaymentCurrency() == PremiumCurrencyStars || i.DebitStars
}

type PremiumPaymentForm struct {
	ID              int64
	IdempotencyKey  string
	BuyerUserID     int64
	Kind            PremiumPurchaseKind
	RecipientUserID int64
	Months          int
	DurationDays    int
	AmountStars     int64
	PaymentCurrency string
	PaymentAmount   int64
	DebitStars      bool
	PlanVersion     int64
	Message         PremiumGiftMessage
	IssuedAt        int
	ExpiresAt       int
}

func (f PremiumPaymentForm) Valid() bool {
	if f.ID != 0 || f.BuyerUserID <= 0 ||
		len(f.IdempotencyKey) > 256 ||
		f.IssuedAt > MaxPremiumUnixTimestamp-PremiumPaymentFormTTLSeconds ||
		f.IssuedAt <= 0 || f.ExpiresAt != f.IssuedAt+PremiumPaymentFormTTLSeconds ||
		f.Months <= 0 || f.Months > MaxPremiumPlanMonths ||
		f.DurationDays <= 0 || f.DurationDays > MaxPremiumPlanDurationDays ||
		f.AmountStars <= 0 || f.AmountStars > MaxPremiumPlanAmountStars ||
		f.PlanVersion <= 0 || !validPremiumCurrency(f.EffectivePaymentCurrency()) ||
		f.EffectivePaymentAmount() <= 0 ||
		f.EffectivePaymentAmount() > MaxPremiumPlanAmountStars || !f.Message.Valid() {
		return false
	}
	switch f.Kind {
	case PremiumPurchaseSelf:
		return f.RecipientUserID == f.BuyerUserID
	case PremiumPurchaseGift:
		return f.RecipientUserID > 0 && f.RecipientUserID != f.BuyerUserID
	default:
		return false
	}
}

func (f PremiumPaymentForm) EffectivePaymentCurrency() string {
	if currency := strings.ToUpper(strings.TrimSpace(f.PaymentCurrency)); currency != "" {
		return currency
	}
	return PremiumCurrencyStars
}

func (f PremiumPaymentForm) EffectivePaymentAmount() int64 {
	if f.PaymentAmount > 0 {
		return f.PaymentAmount
	}
	return f.AmountStars
}

func (f PremiumPaymentForm) EffectiveDebitStars() bool {
	return f.PaymentCurrency == "" || f.EffectivePaymentCurrency() == PremiumCurrencyStars || f.DebitStars
}

type PremiumPurchaseRequest struct {
	BuyerUserID      int64
	FormID           int64
	Kind             PremiumPurchaseKind
	RecipientUserID  int64
	Months           int
	PlanVersion      int64
	Message          PremiumGiftMessage
	Date             int
	CommandKey       string
	OriginAuthKeyID  [8]byte
	OriginSessionID  int64
	RecipientBlocked bool
}

type PremiumEntitlement struct {
	ID              int64                    `json:"id"`
	UserID          int64                    `json:"user_id"`
	Source          PremiumEntitlementSource `json:"source"`
	SourceUserID    int64                    `json:"source_user_id"`
	PaymentIntentID int64                    `json:"payment_intent_id,omitempty"`
	TransactionID   int64                    `json:"transaction_id,omitempty"`
	Months          int                      `json:"months"`
	DurationDays    int                      `json:"duration_days"`
	StartsAt        int                      `json:"starts_at"`
	ExpiresAt       int                      `json:"expires_at"`
	Status          PremiumEntitlementStatus `json:"status"`
	CommandKey      string                   `json:"command_key,omitempty"`
	CreatedAt       int                      `json:"created_at"`
}

// PremiumPaymentIntent is the immutable checkout snapshot plus lifecycle
// pointers used by the protected operator read surface.
type PremiumPaymentIntent struct {
	ID                 int64                `json:"id"`
	FormID             int64                `json:"form_id"`
	IdempotencyKey     string               `json:"idempotency_key"`
	BuyerUserID        int64                `json:"buyer_user_id"`
	Kind               PremiumPurchaseKind  `json:"kind"`
	RecipientUserID    int64                `json:"recipient_user_id"`
	Months             int                  `json:"months"`
	DurationDays       int                  `json:"duration_days"`
	AmountStars        int64                `json:"amount_stars"`
	Currency           string               `json:"currency"`
	PaymentAmount      int64                `json:"payment_amount"`
	DebitStars         bool                 `json:"debit_stars"`
	PlanVersion        int64                `json:"plan_version"`
	Message            PremiumGiftMessage   `json:"message"`
	Status             PremiumPaymentStatus `json:"status"`
	IssuedAt           int                  `json:"issued_at"`
	ExpiresAt          int                  `json:"expires_at"`
	PaidAt             int                  `json:"paid_at,omitempty"`
	RefundedAt         int                  `json:"refunded_at,omitempty"`
	StarsTransactionID int64                `json:"stars_transaction_id,omitempty"`
	SenderMessageID    int                  `json:"sender_message_id,omitempty"`
	RecipientMessageID int                  `json:"recipient_message_id,omitempty"`
	CreatedAt          int                  `json:"created_at"`
	UpdatedAt          int                  `json:"updated_at"`
}

type PremiumPaymentDetails struct {
	Intent      PremiumPaymentIntent `json:"payment_intent"`
	Entitlement PremiumEntitlement   `json:"entitlement"`
	Transaction StarsTransaction     `json:"ledger_transaction"`
}

type PremiumPurchaseResult struct {
	Form        PremiumPaymentForm
	Entitlement PremiumEntitlement
	User        User
	Balance     StarsBalance
	Send        SendPrivateTextResult
	Duplicate   bool
}

type PremiumAdminGrantRequest struct {
	UserID       int64
	ActorUserID  int64
	Months       int
	DurationDays int
	Date         int
	Reason       string
	CommandKey   string
}

type PremiumAdminRevokeRequest struct {
	UserID        int64
	ActorUserID   int64
	EntitlementID int64
	Date          int
	Reason        string
	CommandKey    string
}

type PremiumRefundRequest struct {
	PaymentIntentID int64
	ActorUserID     int64
	Date            int
	Reason          string
	CommandKey      string
}

// PremiumSubscriptionActiveError prevents the single-recipient Premium gift
// flow from silently stacking another entitlement on an already active one.
// Until is the recipient's current expiry as a Unix timestamp.
type PremiumSubscriptionActiveError struct {
	Until int
}

func (e PremiumSubscriptionActiveError) Error() string {
	return fmt.Sprintf("premium: subscription active until %d", e.Until)
}

var (
	ErrPremiumPlanInvalid         = errors.New("premium: invalid plan")
	ErrPremiumPlanUnavailable     = errors.New("premium: plan unavailable")
	ErrPremiumPlanConflict        = errors.New("premium: plan version conflict")
	ErrPremiumLastPlan            = errors.New("premium: at least one enabled plan is required")
	ErrPremiumRecipientInvalid    = errors.New("premium: invalid recipient")
	ErrPremiumRecipientRestricted = errors.New("premium: recipient disallows premium gifts")
	ErrPremiumGiftSelf            = errors.New("premium: gifting self is not allowed")
	ErrPremiumGiftMessageInvalid  = errors.New("premium: invalid gift message")
	ErrPremiumFormExpired         = errors.New("premium: payment form expired")
	ErrPremiumFormInvalid         = errors.New("premium: invalid payment form")
	ErrPremiumFormAmountChanged   = errors.New("premium: payment form amount changed")
	ErrPremiumInvoiceAlreadyPaid  = errors.New("premium: invoice already paid")
	ErrPremiumPaymentNotFound     = errors.New("premium: payment not found")
	ErrPremiumAlreadyRefunded     = errors.New("premium: payment already refunded")
	ErrPremiumExternalRefund      = errors.New("premium: external payment must be refunded by its provider")
)

func PremiumPurchaseDescription(kind PremiumPurchaseKind, months int, recipientID int64) string {
	if kind == PremiumPurchaseGift {
		return fmt.Sprintf("Premium gift: %d month(s) for user %d", months, recipientID)
	}
	return fmt.Sprintf("Premium subscription: %d month(s)", months)
}