Files
descrybe/apps/api/internal/billing/credit_packs.go
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

172 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package billing
import "strings"
// CreditsPerAIProduct is the typical wallet debit for one AI enhance
// (product_processing base + one openai_token_k pack when tokens ≤ 1000).
// Each content-language pass burns another ~CreditsPerAIProduct per product.
// Included monthly grants assume AssumedPrimaryContentLanguages only.
const CreditsPerAIProduct = 2
// AssumedPrimaryContentLanguages is how many content languages the included
// monthly grant is sized for. Extra languages → credit packs or BYOK.
const AssumedPrimaryContentLanguages = 1
// ScaleMaxProducts is the top self-serve SKU ceiling. Huge catalogs (1M+)
// belong on Enterprise (sales-led credits / BYOK), not public Scale.
const ScaleMaxProducts = 12_000
// PlanAICoverPercent is included monthly AI as a share of CreditSKUBase
// (primary language only). Paid public plans use ~50% cover so Starter stays
// lean (100 credits ≈ 50 AI products on a 100-SKU plan) and higher tiers
// scale with CreditSKUBase ≤ PlanMaxProducts — not a full-catalog AI bundle.
// Formula: credits = (CreditSKUBase × cover% / 100) × CreditsPerAIProduct.
func PlanAICoverPercent(planName string) int {
switch strings.ToLower(strings.TrimSpace(planName)) {
case "starter":
return 50 // 100 × 50% × 2 = 100
case "plus":
return 50 // 400 × 50% × 2 = 400
case "growth":
return 50 // 1,200 × 50% × 2 = 1,200
case "business":
return 50 // 4,000 × 50% × 2 = 4,000
case "scale":
return 50 // 12,000 × 50% × 2 = 12,000
case "enterprise":
return 50 // display ladder only; grant is EnterpriseUnlimitedCredits
default:
return 0
}
}
// PlanMaxProducts is the hard SKU ceiling for a public plan name.
// Slow retail ladder for small→mid shops; Scale reaches ScaleMaxProducts;
// Enterprise is unlimited (nil). Credits sized via CreditSKUBase (≤ MaxProducts).
func PlanMaxProducts(planName string) *int {
mp := func(n int) *int { return &n }
switch strings.ToLower(strings.TrimSpace(planName)) {
case "free":
return mp(50)
case "starter":
return mp(100)
case "plus":
return mp(400)
case "growth":
return mp(1_200)
case "business":
return mp(4_000)
case "scale":
return mp(ScaleMaxProducts)
default:
// Enterprise and unknown custom plans — unlimited SKU cap.
return nil
}
}
// CreditSKUBase is the catalog size used ONLY to size included monthly AI credits.
// May be smaller than PlanMaxProducts so large catalogs still get a bounded AI starter grant.
// Public paid ladder: base equals PlanMaxProducts (50% cover → half-catalog primary-lang AI).
func CreditSKUBase(planName string) int {
switch strings.ToLower(strings.TrimSpace(planName)) {
case "starter":
return 100
case "plus":
return 400
case "growth":
return 1_200
case "business":
return 4_000
case "scale":
return 12_000
default:
return 0
}
}
// MonthlyCreditsForSKUCover returns credits for coverPct% of skuBase at CreditsPerAIProduct each.
func MonthlyCreditsForSKUCover(skuBase, coverPct int) int {
if skuBase <= 0 || coverPct <= 0 {
return 0
}
if coverPct > 100 {
coverPct = 100
}
products := (skuBase * coverPct) / 100
return products * CreditsPerAIProduct * AssumedPrimaryContentLanguages
}
// MonthlyCreditsForPlan sizes the monthly grant from CreditSKUBase × PlanAICoverPercent.
// The maxProducts argument is ignored when CreditSKUBase is set (paid public ladder).
func MonthlyCreditsForPlan(planName string, maxProducts int) int {
base := CreditSKUBase(planName)
if base <= 0 {
base = maxProducts
}
return MonthlyCreditsForSKUCover(base, PlanAICoverPercent(planName))
}
// CreditPack is a one-time AI credit top-up sold via Stripe Checkout (mode=payment).
// These are additional Stripe Products with one-time Prices — not subscription add-ons.
type CreditPack struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Credits int `json:"credits"`
PriceUSD int `json:"price_usd"` // whole dollars for marketing UI
// Approx product×language passes at CreditsPerAIProduct.
AIProducts int `json:"ai_products"`
}
func packFromCredits(id, name, desc string, credits, priceUSD int) CreditPack {
return CreditPack{
ID: id,
Name: name,
Description: desc,
Credits: credits,
PriceUSD: priceUSD,
AIProducts: credits / CreditsPerAIProduct,
}
}
// DefaultCreditPacks is the public top-up ladder (credits-first sizing).
// Prices are balanced so small packs are not punitive $/credit vs larger ones,
// while staying expensive enough that packs cannot undercut plan upgrades or A1 (~€300).
func DefaultCreditPacks() []CreditPack {
return []CreditPack{
packFromCredits("tiny", "Nano pack", "Smoke tests / tiny fixes (25 credits ≈ 12 AI products)", 25, 29),
packFromCredits("small", "Starter pack", "Small top-up (65 credits ≈ 32 AI products)", 65, 59),
packFromCredits("medium", "Plus pack", "Burst top-up (200 credits ≈ 100 AI products)", 200, 149),
packFromCredits("large", "Growth pack", "Mid buffer (500 credits ≈ 250 AI products)", 500, 299),
packFromCredits("xl", "Catalog pack", "Catalog / re-run buffer (1,200 credits ≈ 600 AI products)", 1200, 599),
packFromCredits("xxl", "Business pack", "Large multi-language buffer (3,000 credits ≈ 1,500 AI products)", 3000, 1299),
packFromCredits("mega", "Scale pack", "Distributor / agency burst (8,000 credits ≈ 4,000 AI products)", 8000, 2999),
}
}
// CreditPackByID returns a pack from DefaultCreditPacks.
func CreditPackByID(id string) (CreditPack, bool) {
want := strings.ToLower(strings.TrimSpace(id))
for _, p := range DefaultCreditPacks() {
if p.ID == want {
return p, true
}
}
return CreditPack{}, false
}
// CreditPackPriceKey is the Stripe PriceIDs map key for a one-time pack (pack:<id>).
func CreditPackPriceKey(packID string) string {
return "pack:" + strings.ToLower(strings.TrimSpace(packID))
}
// CreditPackSettingsKey is the platformsettings Values key (stripe.price.pack.<id>).
func CreditPackSettingsKey(packID string) string {
return "stripe.price.pack." + strings.ToLower(strings.TrimSpace(packID))
}
// CreditPackEnvVar is the optional process-env fallback (STRIPE_PRICE_PACK_<ID>).
func CreditPackEnvVar(packID string) string {
return "STRIPE_PRICE_PACK_" + strings.ToUpper(strings.TrimSpace(packID))
}