carlos / platform Public

Version approved pooled awake allowances and preserve historical terms

claude agent, operated by paul@keymail.dev 5b1d7c02ff72c6c6c04106782c065e4d3d17d308 parent ef41648
20 files changed, +574 −65
  • cmd/console/billing.go +9 −5
  • cmd/console/billingterms_20261003_test.go +64 −0
  • implementation/carloku-paid-launch/stripe-provision.sh +2 −2
  • internal/console/billingcalculation_test.go +1 −1
  • internal/console/billingcurrency_test.go +4 −4
  • internal/console/billinghandlers.go +6 −1
  • internal/console/billingpage.go +11 −7
  • internal/console/billingpage_test.go +2 −2
  • internal/console/billingservice.go +11 −3
  • internal/console/billingtaxshape_fixtures_test.go +1 −1
  • internal/console/billingterms_20261003_test.go +84 −0
  • internal/plans/allowances_20261003_test.go +199 −0
  • internal/plans/plans-2026-10-03-v1.json +65 −0
  • internal/plans/plans.go +38 −10
  • internal/plans/plans_test.go +1 −1
  • internal/plans/pricebook.go +25 −17
  • internal/plans/pricebook.json +39 −0
  • internal/plans/pricebook_test.go +3 −6
  • internal/plans/provision_test.go +5 −1
  • internal/plans/public-pricebook.json +4 −4
diff --git a/cmd/console/billing.go b/cmd/console/billing.go
index c5f97932f..f4ff5ce31 100644
--- a/cmd/console/billing.go
+++ b/cmd/console/billing.go
@@ -131,8 +131,7 @@ func billingConfig(getenv func(string) string) (*console.BillingConfig, error) {
used := 0
// catalog checks one book's keys (or the legacy keys, for book ""):
// all present, or, when it is not required, all absent.
- catalog := func(book string, required bool) bool {
- terms := plans.PriceTerms{TermsVersion: plans.CurrentTermsVersion, PriceBook: book}
+ catalog := func(book, version string, required bool) bool {
sold := []plans.Currency{""}
if book != plans.LegacyPriceBook {
var err error
@@ -144,7 +143,7 @@ func billingConfig(getenv func(string) string) (*console.BillingConfig, error) {
for _, plan := range paid {
for _, band := range bands {
for _, currency := range sold {
- key := terms.TermsVersion + "/" + plan + "/" + band
+ key := version + "/" + plan + "/" + band
if book != plans.LegacyPriceBook {
key = book + "/" + plan + "/" + band + "/" + string(currency)
}
@@ -175,11 +174,16 @@ func billingConfig(getenv func(string) string) (*console.BillingConfig, error) {
return true
}
for _, book := range priceBooks() {
- if !catalog(book, book == cfg.PriceBook) {
+ if !catalog(book, "", book == cfg.PriceBook) {
return nil, errBillingConfiguration
}
}
- if !catalog(plans.LegacyPriceBook, false) || used != len(cfg.Prices) {
+ for _, version := range plans.TermsVersions() {
+ if !catalog(plans.LegacyPriceBook, version, false) {
+ return nil, errBillingConfiguration
+ }
+ }
+ if used != len(cfg.Prices) {
return nil, errBillingConfiguration
}
return cfg, nil
diff --git a/cmd/console/billingterms_20261003_test.go b/cmd/console/billingterms_20261003_test.go
new file mode 100644
index 000000000..5b4363ac3
--- /dev/null
+++ b/cmd/console/billingterms_20261003_test.go
@@ -0,0 +1,64 @@
+package main
+
+import (
+ "encoding/json"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/plans"
+)
+
+func TestBillingConfigHistoricalLegacyCatalogs(t *testing.T) {
+ const historical = "2026-09-07-v1"
+ for _, book := range []string{"2026-10-01-draft1", plans.CurrentPriceBook} {
+ for _, kind := range []string{"absent", "historical", "both", "partial historical", "partial current", "unknown terms", "shared price", "bad historical price"} {
+ t.Run(book+"/"+kind, func(t *testing.T) {
+ env := billingEnvFixture(t)
+ env["CONSOLE_BILLING_PRICE_BOOK"] = book
+ prices := map[string]string{}
+ for key, id := range billingPricesFixture(false) {
+ prices[strings.Replace(key, plans.CurrentPriceBook, book, 1)] = id
+ }
+ add := func(version string) {
+ for _, plan := range []string{"personal", "community", "business"} {
+ for _, band := range []string{"A", "B", "C", "D"} {
+ prices[version+"/"+plan+"/"+band] = "price_" + version + plan + band
+ }
+ }
+ }
+ if kind != "absent" {
+ add(historical)
+ }
+ valid := kind == "absent" || kind == "historical" || kind == "both"
+ switch kind {
+ case "both":
+ add(plans.CurrentTermsVersion)
+ case "partial historical":
+ delete(prices, historical+"/personal/A")
+ case "partial current":
+ prices[plans.CurrentTermsVersion+"/personal/A"] = "price_partial"
+ case "unknown terms":
+ add("2030-01-01-v1")
+ case "shared price":
+ add(plans.CurrentTermsVersion)
+ prices[plans.CurrentTermsVersion+"/personal/A"] = prices[historical+"/personal/A"]
+ case "bad historical price":
+ prices[historical+"/personal/A"] = "wrong_id"
+ }
+ raw, err := json.Marshal(prices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ env["CONSOLE_STRIPE_PRICES"] = string(raw)
+ cfg, err := billingConfig(func(key string) string { return env[key] })
+ if valid {
+ if err != nil || cfg == nil || len(cfg.Prices) != len(prices) {
+ t.Fatalf("retained catalogs refused: %v", err)
+ }
+ } else if err == nil || cfg != nil {
+ t.Fatal("invalid retained catalog accepted")
+ }
+ })
+ }
+ }
+}
diff --git a/implementation/carloku-paid-launch/stripe-provision.sh b/implementation/carloku-paid-launch/stripe-provision.sh
index f21fbd409..7836aa472 100755
--- a/implementation/carloku-paid-launch/stripe-provision.sh
+++ b/implementation/carloku-paid-launch/stripe-provision.sh
@@ -39,12 +39,12 @@
#
set -euo pipefail
-readonly TERMS="2026-09-07-v1"
+readonly TERMS="2026-10-03-v1"
# The price book (internal/plans/pricebook.json). Prices are per book, per
# currency, and the console keys CONSOLE_STRIPE_PRICES by it. A book whose
# amounts are not approved for sale is a draft; the console refuses a draft
# book in live mode, and so does this script.
-readonly BOOK="2026-10-01-draft1"
+readonly BOOK="2026-10-03-draft1"
readonly BOOK_STATUS="draft"
readonly CURRENCIES=(usd eur gbp)
readonly API_VERSION="2025-02-24.acacia"
diff --git a/internal/console/billingcalculation_test.go b/internal/console/billingcalculation_test.go
index 5b614f3b9..32516e6e8 100644
--- a/internal/console/billingcalculation_test.go
+++ b/internal/console/billingcalculation_test.go
@@ -18,7 +18,7 @@ import (
func calculationFixture(plan string, quantity int64) (usage.Statement, []plans.Assignment) {
start := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)
st := usage.Statement{V: 1, Account: "acme", Month: "2026-09", Meters: map[string]float64{}, DaysInMonth: 30, Closed: true, UpdatedAt: start.AddDate(0, 1, 3)}
- return st, []plans.Assignment{{Revision: 1, EffectiveAt: start, TermsVersion: plans.CurrentTermsVersion, Plan: plan, Band: plans.BandA, Quantity: quantity, Status: "active", PaidThrough: start.AddDate(1, 0, 0)}}
+ return st, []plans.Assignment{{Revision: 1, EffectiveAt: start, TermsVersion: "2026-09-07-v1", Plan: plan, Band: plans.BandA, Quantity: quantity, Status: "active", PaidThrough: start.AddDate(1, 0, 0)}}
}
func TestBillingCalculationBusinessMillionWrites(t *testing.T) {
diff --git a/internal/console/billingcurrency_test.go b/internal/console/billingcurrency_test.go
index c35475960..a79810e5b 100644
--- a/internal/console/billingcurrency_test.go
+++ b/internal/console/billingcurrency_test.go
@@ -1285,7 +1285,7 @@ func TestLegacyCalculationBytesAreUnchanged(t *testing.T) {
t.Fatal("fixture: the golden bytes and digest disagree")
}
statement := usage.Statement{V: 1, Account: "bab", Month: "2026-09", Meters: map[string]float64{usage.MeterInstanceHours: 1000, usage.MeterStorageGBMonth: 40}, DaysInMonth: 30, Closed: true}
- calc, err := priceBillingCalculation(statement, monthHistory(plans.LegacyPriceTerms(plans.CurrentTermsVersion), false))
+ calc, err := priceBillingCalculation(statement, monthHistory(plans.LegacyPriceTerms("2026-09-07-v1"), false))
if err != nil {
t.Fatal(err)
}
@@ -1307,7 +1307,7 @@ func TestLegacyCalculationBytesAreUnchanged(t *testing.T) {
// bytes a legacy month gets from this build. An assignment handed in
// with an explicit "eur", which the store never produces, marshals
// with that field and nothing else different: the money is the same.
- history := monthHistory(plans.LegacyPriceTerms(plans.CurrentTermsVersion), false)
+ history := monthHistory(plans.LegacyPriceTerms("2026-09-07-v1"), false)
history[0].Currency = plans.EUR
spelled, err := priceBillingCalculation(statement, history)
if err != nil {
@@ -2629,7 +2629,7 @@ func TestBillingInclusionsFollowTheQuotedBand(t *testing.T) {
b := newBillingBrowser(t, fx)
m, _ := b.page("?country=IE")
for _, want := range []string{
- "One member. Each month: 85 awake hours, 2 GB storage, 2 GB transfer.",
+ "One member. Each month: 100 awake hours, 2 GB storage, 2 GB transfer.",
"Per member, minimum 10. Each member adds, per month: 17 awake hours, 0.5 GB storage, 0.4 GB transfer.",
"Per member, minimum 10. Each member adds, per month: 170 awake hours, 5 GB storage, 3 GB transfer, 30,000 replication writes. Priority support.",
} {
@@ -2640,7 +2640,7 @@ func TestBillingInclusionsFollowTheQuotedBand(t *testing.T) {
m, _ = b.page("?country=IN&currency=gbp")
for _, want := range []string{
"<strong>£7.70</strong> a year",
- "One member. Each month: 29.75 awake hours, 0.7 GB storage, 0.7 GB transfer.",
+ "One member. Each month: 35 awake hours, 0.7 GB storage, 0.7 GB transfer.",
"Each member adds, per month: 5.95 awake hours, 0.175 GB storage, 0.14 GB transfer.",
"Each member adds, per month: 59.5 awake hours, 1.75 GB storage, 1.05 GB transfer, 10,500 replication writes.",
} {
diff --git a/internal/console/billinghandlers.go b/internal/console/billinghandlers.go
index 1133845d4..62036d106 100644
--- a/internal/console/billinghandlers.go
+++ b/internal/console/billinghandlers.go
@@ -60,7 +60,12 @@ func (s *Server) handleBillingCheckout(w http.ResponseWriter, r *http.Request) {
return
}
back := "/a/" + url.PathEscape(acct.Sqid) + "/billing"
- if r.PostForm.Get("consent") != plans.CurrentTermsVersion {
+ book := plans.LegacyPriceBook
+ if s.billing != nil {
+ book = s.billing.cfg.PriceBook
+ }
+ version, err := plans.TermsVersionForBook(book)
+ if err != nil || r.PostForm.Get("consent") != version {
http.Redirect(w, r, back+"?error=unavailable", http.StatusSeeOther)
return
}
diff --git a/internal/console/billingpage.go b/internal/console/billingpage.go
index 8887eba0b..282233dca 100644
--- a/internal/console/billingpage.go
+++ b/internal/console/billingpage.go
@@ -167,7 +167,10 @@ func (s *Server) handleBillingPage(w http.ResponseWriter, r *http.Request) {
TermsVersion: plans.CurrentTermsVersion,
MinQuantity: 10,
}
- if def, err := plans.Lookup(plans.CurrentTermsVersion, plans.PlanCommunity); err == nil {
+ if s.billing != nil {
+ v.TermsVersion, _ = plans.TermsVersionForBook(s.billing.cfg.PriceBook)
+ }
+ if def, err := plans.Lookup(v.TermsVersion, plans.PlanCommunity); err == nil {
v.MinQuantity = def.MinQuantity
}
switch e := r.URL.Query().Get("error"); e {
@@ -253,7 +256,7 @@ func (s *Server) handleBillingPage(w http.ResponseWriter, r *http.Request) {
}
if hit == nil {
v.QuoteError = true
- } else if pct, ok := billingBandPercent(hit.Band); ok {
+ } else if pct, ok := billingBandPercent(v.TermsVersion, hit.Band); ok {
hit.Selected = true
band, country = hit.Band, hit.ISO2
v.Quote = &billingQuote{ISO2: hit.ISO2, Country: hit.Name, Percent: pct}
@@ -322,7 +325,8 @@ func (s *Server) pageCurrency(r *http.Request, v *billingPageView, country strin
chosen = string(c)
}
}
- terms := plans.PriceTerms{TermsVersion: plans.CurrentTermsVersion, PriceBook: book, Currency: plans.Currency(chosen)}
+ version, _ := plans.TermsVersionForBook(book)
+ terms := plans.PriceTerms{TermsVersion: version, PriceBook: book, Currency: plans.Currency(chosen)}
if terms.Legacy() {
terms.Currency = ""
v.CurrencyChosen, v.CurrencyHeld = false, false
@@ -377,12 +381,12 @@ func (s *Server) billingCountries() []billingCountry {
// billingBandPercent derives a band's percentage of the base price from the
// terms themselves (Personal at the band against Personal at band A), so the
// page can never state a factor the checkout would not charge.
-func billingBandPercent(band string) (int64, bool) {
- at, err := plans.Quote(plans.CurrentTermsVersion, plans.PlanPersonal, band, 1)
+func billingBandPercent(version, band string) (int64, bool) {
+ at, err := plans.Quote(version, plans.PlanPersonal, band, 1)
if err != nil {
return 0, false
}
- base, err := plans.Quote(plans.CurrentTermsVersion, plans.PlanPersonal, plans.BandA, 1)
+ base, err := plans.Quote(version, plans.PlanPersonal, plans.BandA, 1)
if err != nil || base.AnnualPriceMicroEUR <= 0 {
return 0, false
}
@@ -395,7 +399,7 @@ func billingBandPercent(band string) (int64, bool) {
func billingPlanRows(terms plans.PriceTerms, band string, members int64) []billingPlanRow {
var rows []billingPlanRow
for _, plan := range billingPaidPlans {
- def, err := plans.Lookup(plans.CurrentTermsVersion, plan)
+ def, err := plans.Lookup(terms.TermsVersion, plan)
if err != nil {
continue
}
diff --git a/internal/console/billingpage_test.go b/internal/console/billingpage_test.go
index cbc2b7cbd..a6a6de5eb 100644
--- a/internal/console/billingpage_test.go
+++ b/internal/console/billingpage_test.go
@@ -313,11 +313,11 @@ func TestBillingPagePriceFormatting(t *testing.T) {
t.Fatalf("community at D, 3 members (billed as 10) = %+v", rows[1])
}
for band, want := range map[string]int64{plans.BandA: 100, plans.BandB: 75, plans.BandC: 50, plans.BandD: 35} {
- if got, ok := billingBandPercent(band); !ok || got != want {
+ if got, ok := billingBandPercent(plans.CurrentTermsVersion, band); !ok || got != want {
t.Errorf("billingBandPercent(%s) = %d %v, want %d", band, got, ok, want)
}
}
- if _, ok := billingBandPercent("Z"); ok {
+ if _, ok := billingBandPercent(plans.CurrentTermsVersion, "Z"); ok {
t.Error("unknown band priced")
}
}
diff --git a/internal/console/billingservice.go b/internal/console/billingservice.go
index 5247b5e63..2e0dec5a7 100644
--- a/internal/console/billingservice.go
+++ b/internal/console/billingservice.go
@@ -104,11 +104,15 @@ func newBillingRuntime(cfg *BillingConfig) (*billingRuntime, error) {
if cfg.QuantityFence != "" && cfg.QuantityFence != billingQuantityFenceVerified {
return nil, errBillingUnavailable
}
+ termsVersion, err := plans.TermsVersionForBook(cfg.PriceBook)
+ if err != nil {
+ return nil, err
+ }
for country, band := range cfg.CountryBands {
if len(country) != 2 || strings.ToUpper(country) != country {
return nil, errBillingUnavailable
}
- if _, err := plans.Quote(plans.CurrentTermsVersion, plans.PlanPersonal, band, 1); err != nil {
+ if _, err := plans.Quote(termsVersion, plans.PlanPersonal, band, 1); err != nil {
return nil, err
}
}
@@ -481,7 +485,11 @@ func (s *Server) startBillingCheckoutQuoted(ctx context.Context, acct *Account,
if err != nil {
return "", err
}
- def, err := plans.Lookup(plans.CurrentTermsVersion, plan)
+ termsVersion, err := plans.TermsVersionForBook(b.cfg.PriceBook)
+ if err != nil {
+ return "", err
+ }
+ def, err := plans.Lookup(termsVersion, plan)
if err != nil || plan == plans.PlanFree {
return "", errBillingUnavailable
}
@@ -502,7 +510,7 @@ func (s *Server) startBillingCheckoutQuoted(ctx context.Context, acct *Account,
}
return "", err
}
- terms := plans.PriceTerms{TermsVersion: plans.CurrentTermsVersion, PriceBook: b.cfg.PriceBook, Currency: chosen}
+ terms := plans.PriceTerms{TermsVersion: termsVersion, PriceBook: b.cfg.PriceBook, Currency: chosen}
if terms.Legacy() {
terms.Currency = ""
}
diff --git a/internal/console/billingtaxshape_fixtures_test.go b/internal/console/billingtaxshape_fixtures_test.go
index a5173fd23..0f3647f54 100644
--- a/internal/console/billingtaxshape_fixtures_test.go
+++ b/internal/console/billingtaxshape_fixtures_test.go
@@ -49,7 +49,7 @@ func taxShapeInputs() map[string]struct {
st usage.Statement
h []plans.Assignment
}
- terms := plans.PriceTerms{TermsVersion: plans.CurrentTermsVersion, PriceBook: plans.CurrentPriceBook, Currency: plans.USD}
+ terms := plans.PriceTerms{TermsVersion: "2026-09-07-v1", PriceBook: "2026-10-01-draft1", Currency: plans.USD}
sep15 := time.Date(2026, 9, 15, 0, 0, 0, 0, time.UTC)
out := map[string]cs{}
st, h := calculationFixture(plans.PlanPersonal, 1)
diff --git a/internal/console/billingterms_20261003_test.go b/internal/console/billingterms_20261003_test.go
new file mode 100644
index 000000000..45fc1174e
--- /dev/null
+++ b/internal/console/billingterms_20261003_test.go
@@ -0,0 +1,84 @@
+package console
+
+import (
+ "errors"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/plans"
+)
+
+func TestAllowanceTermsFollowConfiguredBookThroughCheckout(t *testing.T) {
+ for _, book := range []string{"2026-10-01-draft1", plans.CurrentPriceBook} {
+ t.Run(book, func(t *testing.T) {
+ fx, f, acct := newCurrencyFixture(t)
+ version, err := plans.TermsVersionForBook(book)
+ if err != nil {
+ t.Fatal(err)
+ }
+ fx.srv.billing.cfg.PriceBook = book
+ for id, price := range f.prices {
+ metadata := price["metadata"].(map[string]string)
+ metadata["terms_version"], metadata["price_book"] = version, book
+ terms := plans.PriceTerms{TermsVersion: version, PriceBook: book, Currency: plans.Currency(metadata["currency"])}
+ fx.srv.billing.cfg.Prices[billingPriceKey(terms, metadata["plan"], metadata["band"])] = id
+ }
+ cfg := fx.srv.billing.cfg
+ cfg.SecretKey = "rk_test_mock"
+ if _, err := newBillingRuntime(&cfg); err != nil {
+ t.Fatalf("startup refused known book: %v", err)
+ }
+ browser := newBillingBrowser(t, fx)
+ page, fields := browser.page("?country=IE")
+ if !strings.Contains(page, `name="consent" value="`+version+`"`) {
+ t.Fatalf("page consent does not name selected book terms %s", version)
+ }
+ rows := billingPlanRows(plans.PriceTerms{TermsVersion: version, PriceBook: book, Currency: plans.EUR}, plans.BandA, 1)
+ want := "85"
+ if version == plans.CurrentTermsVersion {
+ want = "100"
+ }
+ if len(rows) != 3 || rows[0].Hours != want {
+ t.Fatalf("selected allowance %+v", rows)
+ }
+ fields.Set("plan", plans.PlanPersonal)
+ wrong := plans.CurrentTermsVersion
+ if wrong == version {
+ wrong = "2026-09-07-v1"
+ }
+ fields.Set("consent", wrong)
+ before := f.requests
+ response := browser.post("/billing/checkout", fields)
+ if response.Code != http.StatusSeeOther || !strings.HasSuffix(response.Header().Get("Location"), "?error=unavailable") || f.requests != before {
+ t.Fatalf("wrong terms consent was not refused locally: %d %s", response.Code, response.Header().Get("Location"))
+ }
+ fields.Set("consent", version)
+ response = browser.post("/billing/checkout", fields)
+ if response.Code != http.StatusSeeOther || !strings.HasPrefix(response.Header().Get("Location"), "https://checkout.stripe.com/") {
+ t.Fatalf("matching book consent failed: %d %s", response.Code, response.Header().Get("Location"))
+ }
+ intent, err := fx.st.billingCheckoutIntent(acct.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if intent.Terms != version || intent.PriceBook != book {
+ t.Fatalf("intent lost configured terms %+v", intent)
+ }
+ form := f.forms[len(f.forms)-1]
+ if form.Get("metadata[terms_version]") != version || form.Get("subscription_data[metadata][terms_version]") != version {
+ t.Fatalf("provider request lost version %v", form)
+ }
+ })
+ }
+}
+
+func TestAllowanceTermsStartupRejectsUnknownBook(t *testing.T) {
+ fx, _, _ := newCurrencyFixture(t)
+ cfg := fx.srv.billing.cfg
+ cfg.PriceBook = "unknown"
+ cfg.SecretKey = "rk_test_mock"
+ if _, err := newBillingRuntime(&cfg); !errors.Is(err, plans.ErrUnknownPriceBook) {
+ t.Fatalf("unknown configured book: %v", err)
+ }
+}
diff --git a/internal/plans/allowances_20261003_test.go b/internal/plans/allowances_20261003_test.go
new file mode 100644
index 000000000..c6f2ca604
--- /dev/null
+++ b/internal/plans/allowances_20261003_test.go
@@ -0,0 +1,199 @@
+package plans
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "reflect"
+ "testing"
+ "time"
+)
+
+func TestApprovedAllowancesCurrentAndHistorical(t *testing.T) {
+ for _, version := range []string{v1, CurrentTermsVersion} {
+ for i, band := range bandOrder {
+ free, err := Quote(version, PlanFree, band, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ wantFree := int64(100 * 3600)
+ wantPersonal := []int64{306000, 229500, 153000, 107100}
+ if version == CurrentTermsVersion {
+ wantFree = 216000
+ wantPersonal = []int64{360000, 270000, 180000, 126000}
+ }
+ if free.Allowances.AwakeSeconds != wantFree {
+ t.Fatalf("%s %s free %+v", version, band, free)
+ }
+ personal, err := Quote(version, PlanPersonal, band, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if personal.Allowances.AwakeSeconds != wantPersonal[i] {
+ t.Fatalf("%s %s personal %+v", version, band, personal)
+ }
+ for _, plan := range []string{PlanFree, PlanPersonal} {
+ if _, err := Quote(version, plan, band, 2); !errors.Is(err, ErrInvalidQuantity) {
+ t.Fatalf("%s quantity widened: %v", plan, err)
+ }
+ }
+ }
+ }
+ for _, plan := range []string{PlanFree, PlanPersonal, PlanCommunity, PlanBusiness} {
+ old, _ := Lookup(v1, plan)
+ current, _ := Lookup(CurrentTermsVersion, plan)
+ current.TermsVersion = old.TermsVersion
+ current.ResourcesPerMember.AwakeSeconds = old.ResourcesPerMember.AwakeSeconds
+ if !reflect.DeepEqual(old, current) {
+ t.Fatalf("unapproved plan change: %s", plan)
+ }
+ if plan == PlanCommunity || plan == PlanBusiness {
+ for _, band := range bandOrder {
+ for _, qty := range []int64{10, 37, 1_000_000} {
+ before, _ := Quote(v1, plan, band, qty)
+ after, _ := Quote(CurrentTermsVersion, plan, band, qty)
+ after.TermsVersion = before.TermsVersion
+ if !reflect.DeepEqual(before, after) {
+ t.Fatalf("pool changed %s %s %d", plan, band, qty)
+ }
+ }
+ }
+ }
+ }
+ // Usage is one monthly account total, including bursts exceeding two hours.
+ free, _ := Quote(CurrentTermsVersion, PlanFree, BandD, 1)
+ usage := Resources{AwakeSeconds: 24*3600 + 24*3600 + 12*3600}
+ over, err := CalculateOverage(CurrentTermsVersion, PlanFree, usage, free.Allowances)
+ if err != nil || over.AwakeMicroEUR != 0 {
+ t.Fatalf("monthly pool split into daily limits: %+v %v", over, err)
+ }
+ raw, _ := json.Marshal(free.Allowances)
+ var axes map[string]int64
+ if err := json.Unmarshal(raw, &axes); err != nil {
+ t.Fatal(err)
+ }
+ if len(axes) != 4 || axes["awake_seconds"] != 216000 {
+ t.Fatalf("allowance shape %s", raw)
+ }
+}
+
+func TestApprovedAllowanceBooksKeepHistoricalExport(t *testing.T) {
+ const oldBook = "2026-10-01-draft1"
+ before, err := PublicPriceBook(oldBook)
+ if err != nil {
+ t.Fatal(err)
+ }
+ hash := sha256.Sum256(before)
+ if hex.EncodeToString(hash[:]) != "d91d5a48669c1df853dda48e6ffec97e39b72279acc851d538468cdef1d478d2" {
+ t.Fatalf("historical public book changed: %x", hash)
+ }
+ old := priceBooks[oldBook]
+ next := priceBooks[CurrentPriceBook]
+ oldCopy, nextCopy := *old, *next
+ nextCopy.Version = oldCopy.Version
+ nextCopy.TermsVersion = oldCopy.TermsVersion
+ if !reflect.DeepEqual(oldCopy, nextCopy) {
+ t.Fatal("book changed beyond identifiers")
+ }
+ raw, err := json.Marshal(booksDoc{Books: []bookDoc{*old, *next}})
+ if err != nil {
+ t.Fatal(err)
+ }
+ decoded, err := decodePriceBooks(raw, termsByVersion, CountryBands())
+ if err != nil || len(decoded) != 2 {
+ t.Fatalf("roundtrip %v", err)
+ }
+ for _, book := range []string{oldBook, CurrentPriceBook} {
+ version, err := TermsVersionForBook(book)
+ if err != nil {
+ t.Fatal(err)
+ }
+ expected := v1
+ if book == CurrentPriceBook {
+ expected = CurrentTermsVersion
+ }
+ if version != expected {
+ t.Fatalf("book routing %s -> %s", book, version)
+ }
+ for _, currency := range allCurrencies {
+ term := PriceTerms{TermsVersion: version, PriceBook: book, Currency: currency}
+ q, err := PriceFor(term, PlanPersonal, BandA, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := int64(306000)
+ if version == CurrentTermsVersion {
+ want = 360000
+ }
+ if q.Allowances.AwakeSeconds != want {
+ t.Fatalf("book allowance %+v", q)
+ }
+ wrong := v1
+ if version == v1 {
+ wrong = CurrentTermsVersion
+ }
+ term.TermsVersion = wrong
+ if !errors.Is(term.Validate(), ErrUnknownTermsVersion) {
+ t.Fatal("mismatched book and terms accepted")
+ }
+ }
+ }
+ if version, err := TermsVersionForBook(""); err != nil || version != CurrentTermsVersion {
+ t.Fatalf("new legacy quote default %s %v", version, err)
+ }
+ if _, err := TermsVersionForBook("unknown"); !errors.Is(err, ErrUnknownPriceBook) {
+ t.Fatal(err)
+ }
+ if ok, err := BookIsApproved(CurrentPriceBook); err != nil || ok {
+ t.Fatal("unapproved currency amounts activated")
+ }
+}
+
+func TestApprovedAllowancesHistoricalMonth(t *testing.T) {
+ for _, tc := range []struct {
+ terms, book string
+ awake int64
+ }{{v1, "", 306000}, {v1, "2026-10-01-draft1", 306000}, {CurrentTermsVersion, CurrentPriceBook, 360000}} {
+ a := Assignment{EffectiveAt: time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC), Revision: 1, TermsVersion: tc.terms, Plan: PlanPersonal, Band: BandA, Quantity: 1, Status: "active", PaidThrough: time.Date(2027, 9, 1, 0, 0, 0, 0, time.UTC), PriceBook: tc.book}
+ if tc.book != "" {
+ a.Currency = EUR
+ }
+ month, err := AllowanceForMonth("2026-09", []Assignment{a})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if month.Resources.AwakeSeconds != tc.awake || month.TermsVersion != tc.terms {
+ t.Fatalf("historical month reinterpreted: %+v", month)
+ }
+ over, err := OverageFor(month.PriceTerms(), PlanPersonal, Resources{AwakeSeconds: 100 * 3600}, month.Resources)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := int64(150000)
+ if tc.terms == CurrentTermsVersion {
+ want = 0
+ }
+ if over.AwakeMicro != want {
+ t.Fatalf("historical overage %+v", over)
+ }
+ }
+}
+
+func TestApprovedAllowanceTermsGolden(t *testing.T) {
+ hash := sha256.Sum256(currentPlansJSON)
+ if hex.EncodeToString(hash[:]) != "e746d6b3c88d988d01e2043b1adb80fe2edfc51e7133631025d16893805fc7d4" {
+ t.Fatalf("versioned approved allowance terms changed: %x", hash)
+ }
+}
+
+func TestApprovedAllowanceTermsVersionsRetained(t *testing.T) {
+ versions := TermsVersions()
+ if !reflect.DeepEqual(versions, []string{v1, CurrentTermsVersion}) {
+ t.Fatalf("supported terms %v", versions)
+ }
+ versions[0] = "changed"
+ if got := TermsVersions(); got[0] != v1 {
+ t.Fatal("caller changed embedded terms list")
+ }
+}
diff --git a/internal/plans/plans-2026-10-03-v1.json b/internal/plans/plans-2026-10-03-v1.json
new file mode 100644
index 000000000..a22e4d81f
--- /dev/null
+++ b/internal/plans/plans-2026-10-03-v1.json
@@ -0,0 +1,65 @@
+{
+ "version": "2026-10-03-v1",
+ "band_factors_bps": {
+ "A": 10000,
+ "B": 7500,
+ "C": 5000,
+ "D": 3500
+ },
+ "overage_rates_micro_eur": {
+ "awake_hour": 10000,
+ "storage_gb_month": 106000,
+ "transfer_gb": 414000,
+ "writes_1000": 23000
+ },
+ "plans": {
+ "free": {
+ "annual_base_micro_eur": 0,
+ "min_quantity": 1,
+ "max_quantity": 1,
+ "band_scaled": false,
+ "resources_per_member": {
+ "awake_seconds": 216000,
+ "storage_byte_months": 250000000,
+ "transfer_bytes": 1000000000,
+ "writes": 0
+ }
+ },
+ "personal": {
+ "annual_base_micro_eur": 24000000,
+ "min_quantity": 1,
+ "max_quantity": 1,
+ "band_scaled": true,
+ "resources_per_member": {
+ "awake_seconds": 360000,
+ "storage_byte_months": 2000000000,
+ "transfer_bytes": 2000000000,
+ "writes": 0
+ }
+ },
+ "community": {
+ "annual_base_micro_eur": 5000000,
+ "min_quantity": 10,
+ "max_quantity": 1000000,
+ "band_scaled": true,
+ "resources_per_member": {
+ "awake_seconds": 61200,
+ "storage_byte_months": 500000000,
+ "transfer_bytes": 400000000,
+ "writes": 0
+ }
+ },
+ "business": {
+ "annual_base_micro_eur": 50000000,
+ "min_quantity": 10,
+ "max_quantity": 1000000,
+ "band_scaled": true,
+ "resources_per_member": {
+ "awake_seconds": 612000,
+ "storage_byte_months": 5000000000,
+ "transfer_bytes": 3000000000,
+ "writes": 30000
+ }
+ }
+ }
+}
diff --git a/internal/plans/plans.go b/internal/plans/plans.go
index 6d2bd8a14..806cc90d9 100644
--- a/internal/plans/plans.go
+++ b/internal/plans/plans.go
@@ -19,11 +19,15 @@ import (
"io"
"math"
"math/big"
+ "sort"
)
//go:embed plans.json
var plansJSON []byte
+//go:embed plans-2026-10-03-v1.json
+var currentPlansJSON []byte
+
// Stable, terse technical error codes. Never user-facing prose.
var (
ErrUnknownTermsVersion = errors.New("plans: unknown_terms_version")
@@ -51,7 +55,7 @@ const (
BandD = "D"
// CurrentTermsVersion is the terms version embedded by this build.
- CurrentTermsVersion = "2026-09-07-v1"
+ CurrentTermsVersion = "2026-10-03-v1"
)
const (
@@ -130,14 +134,35 @@ type termsDoc struct {
Plans map[string]planTerms `json:"plans"`
}
-var terms termsDoc
+var termsByVersion = loadTerms()
-func init() {
- t, err := decodeTerms(plansJSON)
- if err != nil {
- panic("plans: invalid embedded plans.json: " + err.Error())
+func loadTerms() map[string]*termsDoc {
+ out := make(map[string]*termsDoc)
+ for _, raw := range [][]byte{plansJSON, currentPlansJSON} {
+ t, err := decodeTerms(raw)
+ if err != nil {
+ panic("plans: invalid embedded plans.json: " + err.Error())
+ }
+ if out[t.Version] != nil {
+ panic(ErrUnknownTermsVersion)
+ }
+ out[t.Version] = t
+ }
+ if out[CurrentTermsVersion] == nil {
+ panic(ErrUnknownTermsVersion)
}
- terms = *t
+ return out
+}
+
+// TermsVersions lists all embedded terms versions in sorted order. Historical
+// purchases keep their version, so older versions remain available.
+func TermsVersions() []string {
+ out := make([]string, 0, len(termsByVersion))
+ for version := range termsByVersion {
+ out = append(out, version)
+ }
+ sort.Strings(out)
+ return out
}
// decodeTerms strictly decodes and validates a terms document: unknown
@@ -247,7 +272,8 @@ type PlanDef struct {
// scaling or quantity pooling. It errors on an unknown terms version or
// plan key rather than guessing.
func Lookup(version, plan string) (*PlanDef, error) {
- if version != terms.Version {
+ terms, ok := termsByVersion[version]
+ if !ok {
return nil, ErrUnknownTermsVersion
}
pt, ok := terms.Plans[plan]
@@ -286,7 +312,8 @@ type QuoteResult struct {
// per-member base resource times quantity times the band factor, never a
// per-member figure.
func Quote(version, plan, band string, qty int64) (*QuoteResult, error) {
- if version != terms.Version {
+ terms, ok := termsByVersion[version]
+ if !ok {
return nil, ErrUnknownTermsVersion
}
pt, ok := terms.Plans[plan]
@@ -362,7 +389,8 @@ type OverageResult struct {
// object-store egress, never counted twice). Excess below zero on any axis
// prices as zero on that axis, never a credit.
func CalculateOverage(version, plan string, usage, allowance Resources) (*OverageResult, error) {
- if version != terms.Version {
+ terms, ok := termsByVersion[version]
+ if !ok {
return nil, ErrUnknownTermsVersion
}
if _, ok := terms.Plans[plan]; !ok {
diff --git a/internal/plans/plans_test.go b/internal/plans/plans_test.go
index ac8c67a67..9892c9c34 100644
--- a/internal/plans/plans_test.go
+++ b/internal/plans/plans_test.go
@@ -8,7 +8,7 @@ import (
"testing"
)
-const v1 = CurrentTermsVersion
+const v1 = "2026-09-07-v1"
// --- Published examples (2026-09-07-carloku-pricing-plans-design.md) ---
diff --git a/internal/plans/pricebook.go b/internal/plans/pricebook.go
index 6ae5212ae..ab494d768 100644
--- a/internal/plans/pricebook.go
+++ b/internal/plans/pricebook.go
@@ -60,7 +60,7 @@ const (
// existed: none. It means EUR, from the terms' own figures.
LegacyPriceBook = ""
// CurrentPriceBook is the book new purchases are priced from.
- CurrentPriceBook = "2026-10-01-draft1"
+ CurrentPriceBook = "2026-10-03-draft1"
BookDraft = "draft"
BookApproved = "approved"
@@ -107,17 +107,6 @@ type booksDoc struct {
var priceBooks map[string]*bookDoc
func init() {
- // The terms are needed to validate a book, and package-level init
- // order across files is not something to lean on: decode them here if
- // plans.go's init has not run yet.
- t := terms
- if t.Version == "" {
- decoded, err := decodeTerms(plansJSON)
- if err != nil {
- panic("plans: invalid embedded plans.json: " + err.Error())
- }
- t = *decoded
- }
// Likewise the countries a book may name.
known := countryBandsByISO2
if known == nil {
@@ -127,7 +116,7 @@ func init() {
}
known = decoded
}
- books, err := decodePriceBooks(priceBookJSON, &t, known)
+ books, err := decodePriceBooks(priceBookJSON, termsByVersion, known)
if err != nil {
panic("plans: invalid embedded pricebook.json: " + err.Error())
}
@@ -146,7 +135,7 @@ func init() {
// may give a currency only to a country that has a band: a code that is
// not one (a typo, "UK" for "GB") would otherwise load, and that
// country's buyers would silently get the default.
-func decodePriceBooks(data []byte, t *termsDoc, knownCountries map[string]string) (map[string]*bookDoc, error) {
+func decodePriceBooks(data []byte, versions map[string]*termsDoc, knownCountries map[string]string) (map[string]*bookDoc, error) {
// encoding/json keeps the last of two equal keys and says nothing. In
// a price list that is one price silently replacing another.
if err := rejectDuplicateKeys(data); err != nil {
@@ -173,6 +162,10 @@ func decodePriceBooks(data []byte, t *termsDoc, knownCountries map[string]string
if out[b.Version] != nil {
return nil, fmt.Errorf("book %q appears twice", b.Version)
}
+ t, ok := versions[b.TermsVersion]
+ if !ok {
+ return nil, ErrUnknownTermsVersion
+ }
if err := validateBook(b, t, knownCountries); err != nil {
return nil, fmt.Errorf("book %q: %w", b.Version, err)
}
@@ -447,6 +440,20 @@ func BookIsApproved(book string) (bool, error) {
return b.Status == BookApproved, nil
}
+// TermsVersionForBook selects the terms for a new quote from book. An empty
+// book selects current terms for a new legacy-EUR quote. Stored purchases
+// retain their own TermsVersion and must not use this default.
+func TermsVersionForBook(book string) (string, error) {
+ if book == LegacyPriceBook {
+ return CurrentTermsVersion, nil
+ }
+ b, err := lookupBook(book)
+ if err != nil {
+ return "", err
+ }
+ return b.TermsVersion, nil
+}
+
// PriceBooks lists every price book this build has, sorted. A purchase
// recorded under a book stays priced from it for as long as it lasts, so a
// book that has ever been sold from must stay in the build.
@@ -504,7 +511,7 @@ func (t PriceTerms) Money() Currency {
// Validate checks the identity names terms and a book this build has and a
// currency that book sells in.
func (t PriceTerms) Validate() error {
- if t.TermsVersion != terms.Version {
+ if termsByVersion[t.TermsVersion] == nil {
return ErrUnknownTermsVersion
}
if t.Legacy() {
@@ -588,7 +595,7 @@ type Charges struct {
func (t PriceTerms) overageRates() (OverageRates, int64) {
if t.Legacy() {
- return terms.OverageRatesMicroEUR, invoiceFloorMicroEUR
+ return termsByVersion[t.TermsVersion].OverageRatesMicroEUR, invoiceFloorMicroEUR
}
b := priceBooks[t.PriceBook]
return b.OverageRatesMicro[t.Currency], b.OverageFloorMicro[t.Currency]
@@ -602,7 +609,7 @@ func OverageFor(t PriceTerms, plan string, usage, allowance Resources) (*Charges
if err := t.Validate(); err != nil {
return nil, err
}
- if _, ok := terms.Plans[plan]; !ok {
+ if _, ok := termsByVersion[t.TermsVersion].Plans[plan]; !ok {
return nil, ErrUnknownPlan
}
if err := requireNonNegative(usage); err != nil {
@@ -729,6 +736,7 @@ func PublicPriceBook(book string) ([]byte, error) {
if err != nil {
return nil, err
}
+ terms := termsByVersion[b.TermsVersion]
out := publicBook{
Schema: 1, PriceBook: b.Version, TermsVersion: b.TermsVersion, Status: b.Status, DefaultCurrency: b.DefaultCurrency,
CountryCurrency: map[string]Currency{}, CountryBand: CountryBands(), BandFactorsBps: map[string]int64{}, Overage: map[Currency]publicOverage{},
diff --git a/internal/plans/pricebook.json b/internal/plans/pricebook.json
index fcaf227f5..a8ad3ced1 100644
--- a/internal/plans/pricebook.json
+++ b/internal/plans/pricebook.json
@@ -38,6 +38,45 @@
"gbp": {"awake_hour": 9000, "storage_gb_month": 95400, "transfer_gb": 372600, "writes_1000": 20700}
},
"overage_floor_micro": {"usd": 1200000, "eur": 1000000, "gbp": 900000}
+ },
+ {
+ "version": "2026-10-03-draft1",
+ "terms_version": "2026-10-03-v1",
+ "status": "draft",
+ "default_currency": "usd",
+ "currencies": ["usd", "eur", "gbp"],
+ "country_currency": {
+ "eur": ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE"],
+ "gbp": ["GB"]
+ },
+ "annual_base_minor": {
+ "personal": {"usd": 2800, "eur": 2400, "gbp": 2200},
+ "community": {"usd": 600, "eur": 500, "gbp": 450},
+ "business": {"usd": 6000, "eur": 5000, "gbp": 4500}
+ },
+ "annual_unit_minor": {
+ "personal": {
+ "usd": {"A": 2800, "B": 2100, "C": 1400, "D": 980},
+ "eur": {"A": 2400, "B": 1800, "C": 1200, "D": 840},
+ "gbp": {"A": 2200, "B": 1650, "C": 1100, "D": 770}
+ },
+ "community": {
+ "usd": {"A": 600, "B": 450, "C": 300, "D": 210},
+ "eur": {"A": 500, "B": 375, "C": 250, "D": 175},
+ "gbp": {"A": 450, "B": 338, "C": 225, "D": 158}
+ },
+ "business": {
+ "usd": {"A": 6000, "B": 4500, "C": 3000, "D": 2100},
+ "eur": {"A": 5000, "B": 3750, "C": 2500, "D": 1750},
+ "gbp": {"A": 4500, "B": 3375, "C": 2250, "D": 1575}
+ }
+ },
+ "overage_rates_micro": {
+ "usd": {"awake_hour": 12000, "storage_gb_month": 127200, "transfer_gb": 496800, "writes_1000": 27600},
+ "eur": {"awake_hour": 10000, "storage_gb_month": 106000, "transfer_gb": 414000, "writes_1000": 23000},
+ "gbp": {"awake_hour": 9000, "storage_gb_month": 95400, "transfer_gb": 372600, "writes_1000": 20700}
+ },
+ "overage_floor_micro": {"usd": 1200000, "eur": 1000000, "gbp": 900000}
}
]
}
diff --git a/internal/plans/pricebook_test.go b/internal/plans/pricebook_test.go
index 0aace1e62..86939cf7d 100644
--- a/internal/plans/pricebook_test.go
+++ b/internal/plans/pricebook_test.go
@@ -430,7 +430,7 @@ func TestAllowanceForMonthKeepsOneCurrency(t *testing.T) {
// a frozen month's calculation holds its assignments, and its digest is of
// those bytes.
func TestLegacyAssignmentMarshalsAsBefore(t *testing.T) {
- a := Assignment{EffectiveAt: time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC), Revision: 3, TermsVersion: CurrentTermsVersion, Plan: PlanPersonal, Band: BandB, Quantity: 1, Status: "active", PaidThrough: time.Date(2027, 9, 1, 0, 0, 0, 0, time.UTC)}
+ a := Assignment{EffectiveAt: time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC), Revision: 3, TermsVersion: v1, Plan: PlanPersonal, Band: BandB, Quantity: 1, Status: "active", PaidThrough: time.Date(2027, 9, 1, 0, 0, 0, 0, time.UTC)}
raw, err := json.Marshal(a)
if err != nil {
t.Fatal(err)
@@ -439,7 +439,7 @@ func TestLegacyAssignmentMarshalsAsBefore(t *testing.T) {
if string(raw) != before {
t.Fatalf("a legacy assignment marshals as\n%s\nand used to marshal as\n%s", raw, before)
}
- m := MonthAllowance{Month: "2026-09", TermsVersion: CurrentTermsVersion, Assignments: []Assignment{a}}
+ m := MonthAllowance{Month: "2026-09", TermsVersion: v1, Assignments: []Assignment{a}}
raw, _ = json.Marshal(m)
if strings.Contains(string(raw), "currency") || strings.Contains(string(raw), "price_book") {
t.Fatalf("a legacy month allowance gained a field: %s", raw)
@@ -449,10 +449,7 @@ func TestLegacyAssignmentMarshalsAsBefore(t *testing.T) {
// What a book must be before it is trusted. Each case is the embedded
// book with one thing wrong.
func TestPriceBookValidation(t *testing.T) {
- tm, err := decodeTerms(plansJSON)
- if err != nil {
- t.Fatal(err)
- }
+ tm := termsByVersion
countries := CountryBands()
if _, err := decodePriceBooks(priceBookJSON, tm, countries); err != nil {
t.Fatalf("the embedded book: %v", err)
diff --git a/internal/plans/provision_test.go b/internal/plans/provision_test.go
index 8eca60097..6532e724e 100644
--- a/internal/plans/provision_test.go
+++ b/internal/plans/provision_test.go
@@ -44,6 +44,10 @@ func TestStripeProvisionScriptMatchesThePriceBook(t *testing.T) {
if book == nil || book[1] != CurrentPriceBook {
t.Fatalf("the script provisions book %v; the console sells from %s", book, CurrentPriceBook)
}
+ version := regexp.MustCompile(`(?m)^readonly TERMS="([^"]+)"`).FindStringSubmatch(script)
+ if version == nil || version[1] != CurrentTermsVersion {
+ t.Fatalf("script terms %v; current %s", version, CurrentTermsVersion)
+ }
status := regexp.MustCompile(`(?m)^readonly BOOK_STATUS="([^"]+)"`).FindStringSubmatch(script)
approved, err := BookIsApproved(CurrentPriceBook)
if err != nil {
@@ -54,7 +58,7 @@ func TestStripeProvisionScriptMatchesThePriceBook(t *testing.T) {
}
want := map[string]int64{}
for _, plan := range paidPlans {
- qty := terms.Plans[plan].MinQuantity
+ qty := termsByVersion[CurrentTermsVersion].Plans[plan].MinQuantity
for _, band := range bandOrder {
for _, c := range priceBooks[CurrentPriceBook].Currencies {
p, err := PriceFor(PriceTerms{TermsVersion: CurrentTermsVersion, PriceBook: CurrentPriceBook, Currency: c}, plan, band, qty)
diff --git a/internal/plans/public-pricebook.json b/internal/plans/public-pricebook.json
index 9e53c66d0..8d325d7f3 100644
--- a/internal/plans/public-pricebook.json
+++ b/internal/plans/public-pricebook.json
@@ -1,7 +1,7 @@
{
"schema": 1,
- "price_book": "2026-10-01-draft1",
- "terms_version": "2026-09-07-v1",
+ "price_book": "2026-10-03-draft1",
+ "terms_version": "2026-10-03-v1",
"status": "draft",
"default_currency": "usd",
"currencies": [
@@ -285,7 +285,7 @@
}
},
"resources_per_member": {
- "awake_seconds": 360000,
+ "awake_seconds": 216000,
"storage_byte_months": 250000000,
"transfer_bytes": 1000000000,
"writes": 0
@@ -317,7 +317,7 @@
}
},
"resources_per_member": {
- "awake_seconds": 306000,
+ "awake_seconds": 360000,
"storage_byte_months": 2000000000,
"transfer_bytes": 2000000000,
"writes": 0