mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
GET /v1/billing/plans answered one catalog on every host, and every brand's pay host reads it — so pay.lux.cloud, pay.lux.tel and pay.zoo.cloud listed Hanzo's ladder (Pro, Max, Team, rooms in the hanzo.ai app) under their own names. @hanzo/plans is the Hanzo product's catalog: it is on sale where the host resolves to Hanzo and nowhere else, and a brand that publishes no catalog sells nothing — an empty list, never somebody else's. - billing.Seller names the brand; Sells(host) reduces the host with the org endpoint's own table, so the brand a page wears and the plans it lists cannot disagree. An empty host is the deployment default, as there. - ReadPlans takes the host; ListPlans and GetPlan pass checkout.RequestHost. - BrandSlugForHost drops port and case as Resolve does. Co-authored-by: Hanzo Dev <[email protected]>
98 lines
2.8 KiB
Go
98 lines
2.8 KiB
Go
package billing
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"testing"
|
|
|
|
"github.com/hanzoai/commerce/util/test/ae"
|
|
)
|
|
|
|
// sameCents compares two optional annual prices by value; both-absent is equal.
|
|
func sameCents(a, b *int64) bool {
|
|
if a == nil || b == nil {
|
|
return a == b
|
|
}
|
|
return *a == *b
|
|
}
|
|
|
|
func centsText(p *int64) string {
|
|
if p == nil {
|
|
return "null"
|
|
}
|
|
return fmt.Sprint(*p)
|
|
}
|
|
|
|
// TestAnnualPriceIsTheCatalogs: GET /v1/billing/plans serves each plan's
|
|
// priceAnnual as the catalog states it — cents where it names a price, null
|
|
// where it names none. Advisory and dedicated are sold by the month only and
|
|
// enterprise is a sales call; all three were served as a $0 annual price, which
|
|
// on a plan that charges advertises a free year of it. Asked of the embed and of
|
|
// the seeded authority read back the way the endpoint reads it, as JSON.
|
|
func TestAnnualPriceIsTheCatalogs(t *testing.T) {
|
|
want := map[string]string{}
|
|
for _, f := range []struct {
|
|
fs interface{ ReadFile(string) ([]byte, error) }
|
|
path string
|
|
}{{subscriptionJSON, "plans/subscription.json"}, {dnsJSON, "plans/dns.json"}} {
|
|
raw, err := f.fs.ReadFile(f.path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", f.path, err)
|
|
}
|
|
var rows []canonicalPlan
|
|
if err := json.Unmarshal(raw, &rows); err != nil {
|
|
t.Fatalf("decode %s: %v", f.path, err)
|
|
}
|
|
for _, r := range rows {
|
|
want[r.ID] = "null"
|
|
if r.PriceAnnual != nil {
|
|
want[r.ID] = fmt.Sprint(int64(math.Round(*r.PriceAnnual * 100)))
|
|
}
|
|
}
|
|
}
|
|
for _, slug := range []string{"advisory", "dedicated", "enterprise"} {
|
|
if want[slug] != "null" {
|
|
t.Fatalf("the catalog states an annual price for %s (%s); this test asserts nothing about null", slug, want[slug])
|
|
}
|
|
}
|
|
|
|
served := func(name string, plans []PlanView) {
|
|
t.Helper()
|
|
body, err := json.Marshal(plans)
|
|
if err != nil {
|
|
t.Fatalf("%s: marshal: %v", name, err)
|
|
}
|
|
var rows []map[string]json.RawMessage
|
|
if err := json.Unmarshal(body, &rows); err != nil {
|
|
t.Fatalf("%s: unmarshal: %v", name, err)
|
|
}
|
|
if len(rows) != len(want) {
|
|
t.Fatalf("%s serves %d plans, the catalog lists %d", name, len(rows), len(want))
|
|
}
|
|
for _, row := range rows {
|
|
var slug string
|
|
_ = json.Unmarshal(row["slug"], &slug)
|
|
if got := string(row["priceAnnual"]); got != want[slug] {
|
|
t.Errorf("%s: %s priceAnnual = %s, the catalog states %s", name, slug, got, want[slug])
|
|
}
|
|
}
|
|
}
|
|
|
|
served("embed", catalog)
|
|
|
|
c := ae.NewContext()
|
|
defer c.Close()
|
|
if _, _, err := SeedPlans(c); err != nil {
|
|
t.Fatalf("seed: %v", err)
|
|
}
|
|
plans, err := ReadPlans(c, "", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("read plans: %v", err)
|
|
}
|
|
if _, ok := planAuthorityRows(c); !ok {
|
|
t.Fatal("the authority is empty after the seed, so the embed answered and the row path is untested")
|
|
}
|
|
served("authority", plans)
|
|
}
|