Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
172 lines
6.2 KiB
Go
172 lines
6.2 KiB
Go
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))
|
||
}
|