113 lines
3.6 KiB
Go
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)
|
|
}
|