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.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user