Files
enkly-ai/services/billing/internal/domain/subscription.go
enkly 79ce8dc4c0
Some checks failed
Go CI Pipeline / build_and_test (push) Failing after 22m0s
Init Project
2026-10-30 18:06:55 +01:00

113 lines
3.6 KiB
Go

// Package domain is the Billing bounded context's entities, plan
// catalog and repository interfaces — transplanted from
// internal/billing/plan.go (pure data, unchanged) plus the shape of
// internal/db's subscriptions.go results.
package domain
import (
"context"
"errors"
"strings"
"time"
)
var ErrNotFound = errors.New("billing: not found")
// Plan identifies one of enkly's subscription tiers. Stored verbatim in
// subscriptions.plan — see migrations/0001_init.sql's CHECK constraint.
type Plan string
const (
PlanFree Plan = "free"
PlanBasic Plan = "basic"
PlanPro Plan = "pro"
PlanPremium Plan = "premium"
)
// PlanInfo describes one plan: its display name/price and the env var
// holding its Stripe Price ID (empty for Free).
type PlanInfo struct {
Plan Plan
Name string
PriceEUR int
PriceEnvVar string
DailyMessageLimit int // 0 = unlimited
}
// Catalog lists every plan, in display order — identical to the
// monolith's billing.Catalog.
var Catalog = []PlanInfo{
{Plan: PlanFree, Name: "Free", PriceEUR: 0, PriceEnvVar: "", DailyMessageLimit: 20},
{Plan: PlanBasic, Name: "Basic", PriceEUR: 5, PriceEnvVar: "STRIPE_PRICE_BASIC", DailyMessageLimit: 100},
{Plan: PlanPro, Name: "Pro", PriceEUR: 8, PriceEnvVar: "STRIPE_PRICE_PRO", DailyMessageLimit: 400},
{Plan: PlanPremium, Name: "Premium", PriceEUR: 12, PriceEnvVar: "STRIPE_PRICE_PREMIUM", DailyMessageLimit: 0},
}
func Get(plan string) PlanInfo {
for _, p := range Catalog {
if string(p.Plan) == plan {
return p
}
}
return Catalog[0]
}
func Valid(plan string) bool {
for _, p := range Catalog {
if string(p.Plan) == plan {
return true
}
}
return false
}
func IsPriceID(v string) bool { return strings.HasPrefix(v, "price_") && len(v) > len("price_") }
// Rank returns plan's position in the catalog, so an upgrade can be
// told from a downgrade. Unknown plans rank as Free.
func Rank(plan Plan) int {
for i, p := range Catalog {
if p.Plan == plan {
return i
}
}
return 0
}
// Subscription is one user's billing state — models.Subscription minus
// json tags.
type Subscription struct {
UserID int64
Plan string
Status string
StripeCustomerID string
StripeSubscriptionID string
StripePriceID string
CurrentPeriodEnd time.Time
CancelAtPeriodEnd bool
CreatedAt time.Time
UpdatedAt time.Time
}
// SubscriptionRepository persists subscriptions, mirroring
// internal/db/subscriptions.go's functions one-for-one.
type SubscriptionRepository interface {
GetOrCreate(ctx context.Context, userID int64) (*Subscription, error)
SetStripeCustomer(ctx context.Context, userID int64, customerID string) error
ApplyStripeSubscription(ctx context.Context, customerID, subscriptionID, priceID, plan, status string, periodEnd time.Time, cancelAtPeriodEnd bool) error
ApplyStripeSubscriptionForUser(ctx context.Context, userID int64, customerID, subscriptionID, priceID, plan, status string, periodEnd time.Time, cancelAtPeriodEnd bool) error
CancelByStripeSubscriptionID(ctx context.Context, subscriptionID string) error
}
// StripeEventRepository is the webhook idempotency log.
type StripeEventRepository interface {
// RecordIfNew reports whether eventID hadn't been seen before.
RecordIfNew(ctx context.Context, eventID, eventType string) (firstTime bool, err error)
}
// UsageRepository is the per-user, per-UTC-day message counter.
type UsageRepository interface {
DailyUsage(ctx context.Context, userID int64) (int, error)
IncrementDailyUsage(ctx context.Context, userID int64) (int, error)
}