Files
at-container-registry/pkg/hold/billing/config.go
T

133 lines
3.7 KiB
Go

//go:build billing
package billing
import (
"fmt"
"os"
"go.yaml.in/yaml/v4"
)
// BillingConfig holds billing/Stripe settings parsed from the hold config YAML.
// The billing section is a top-level key in the YAML file, separate from quota.
type BillingConfig struct {
Enabled bool
Currency string
SuccessURL string
CancelURL string
// Tier-level billing info keyed by tier name (same keys as quota tiers).
Tiers map[string]BillingTierConfig
// Tier assigned to plankowner crew members.
PlankOwnerCrewTier string
}
// BillingTierConfig holds Stripe pricing for a single tier.
type BillingTierConfig struct {
Description string `yaml:"description,omitempty"`
StripePriceMonthly string `yaml:"stripe_price_monthly,omitempty"`
StripePriceYearly string `yaml:"stripe_price_yearly,omitempty"`
}
// billingYAML is the top-level YAML structure for extracting the billing section.
type billingYAML struct {
Billing rawBillingConfig `yaml:"billing"`
}
type rawBillingConfig struct {
Enabled bool `yaml:"enabled"`
Currency string `yaml:"currency,omitempty"`
SuccessURL string `yaml:"success_url,omitempty"`
CancelURL string `yaml:"cancel_url,omitempty"`
PlankOwnerCrewTier string `yaml:"plankowner_crew_tier,omitempty"`
Tiers map[string]BillingTierConfig `yaml:"tiers,omitempty"`
}
// LoadBillingConfig reads the hold config YAML and extracts billing fields.
// Returns (nil, nil) if the file is missing or billing is not enabled.
// Returns (nil, err) if the file exists with billing enabled but is misconfigured.
func LoadBillingConfig(configPath string) (*BillingConfig, error) {
if configPath == "" {
return nil, nil
}
data, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("failed to read config: %w", err)
}
return parseBillingConfig(data)
}
// parseBillingConfig extracts billing fields from hold config YAML bytes.
// Returns (nil, nil) if billing is not enabled.
// Returns (nil, err) if billing is enabled but misconfigured.
func parseBillingConfig(data []byte) (*BillingConfig, error) {
var raw billingYAML
if err := yaml.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
if !raw.Billing.Enabled {
return nil, nil
}
cfg := &BillingConfig{
Enabled: true,
Currency: raw.Billing.Currency,
SuccessURL: raw.Billing.SuccessURL,
CancelURL: raw.Billing.CancelURL,
PlankOwnerCrewTier: raw.Billing.PlankOwnerCrewTier,
Tiers: raw.Billing.Tiers,
}
if cfg.Tiers == nil {
cfg.Tiers = make(map[string]BillingTierConfig)
}
// Validate: billing enabled but no tiers have any Stripe prices configured
hasAnyPrice := false
for _, tier := range cfg.Tiers {
if tier.StripePriceMonthly != "" || tier.StripePriceYearly != "" {
hasAnyPrice = true
break
}
}
if !hasAnyPrice {
return nil, fmt.Errorf("billing is enabled but no tiers have Stripe prices configured")
}
return cfg, nil
}
// GetTierPricing returns billing info for a tier, or nil if not found.
func (c *BillingConfig) GetTierPricing(tierKey string) *BillingTierConfig {
if c == nil {
return nil
}
t, ok := c.Tiers[tierKey]
if !ok {
return nil
}
return &t
}
// GetTierByPriceID finds the tier key that contains the given Stripe price ID.
// Returns empty string if no match.
func (c *BillingConfig) GetTierByPriceID(priceID string) string {
if c == nil || priceID == "" {
return ""
}
for key, tier := range c.Tiers {
if tier.StripePriceMonthly == priceID || tier.StripePriceYearly == priceID {
return key
}
}
return ""
}