diff --git a/README.md b/README.md index 0111f73be..9567a3289 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,9 @@ the *"why?"* of this project. There's a live demo at [https://stats.arp242.net](https://stats.arp242.net). -Please consider [contributing financially][sponsor] if you're self-hosting -GoatCounter so I can pay my rent :-) GoatCounter is sponsored by a grant from -[NLnet's NGI Zero PET fund][nlnet]. +Please consider [contributing financially][sponsor] if you're using +goatcounter.com to pay for the server costs. -[nlnet]: https://nlnet.nl/project/GoatCounter/ [sponsor]: http://www.goatcounter.com/contribute [www]: https://www.goatcounter.com diff --git a/bosmang.go b/bosmang.go index 7e8661bfe..6e4e3abec 100644 --- a/bosmang.go +++ b/bosmang.go @@ -16,20 +16,16 @@ import ( "zgo.at/zdb" "zgo.at/zstd/zjson" "zgo.at/zstd/zruntime" - "zgo.at/zstd/ztype" ) type BosmangStat struct { - ID int64 `db:"site_id"` - Codes string `db:"codes"` - Stripe *string `db:"stripe"` - BillingAmount *string `db:"billing_amount"` - Email string `db:"email"` - CreatedAt time.Time `db:"created_at"` - Plan string `db:"plan"` - LastMonth int `db:"last_month"` - Total int `db:"total"` - Avg int `db:"avg"` + ID int64 `db:"site_id"` + Codes string `db:"codes"` + Email string `db:"email"` + CreatedAt time.Time `db:"created_at"` + LastMonth int `db:"last_month"` + Total int `db:"total"` + Avg int `db:"avg"` } type BosmangStats []BosmangStat @@ -40,14 +36,6 @@ func (a *BosmangStats) List(ctx context.Context) error { if err != nil { return errors.Wrap(err, "BosmangStats.List") } - - curr := strings.NewReplacer("EUR ", "€", "USD ", "$") - aa := *a - for i := range aa { - if aa[i].BillingAmount != nil { - aa[i].BillingAmount = ztype.Ptr[string](curr.Replace(*aa[i].BillingAmount)) - } - } return nil } @@ -55,7 +43,6 @@ type BosmangSiteStat struct { Account Site Sites Sites Users Users - Usage AccountUsage } // ByID gets stats for a single site. @@ -80,7 +67,6 @@ func (a *BosmangSiteStat) ByID(ctx context.Context, id int64) error { return errors.Wrap(err, "BosmangSiteStats.ByID") } - err = a.Usage.Get(WithSite(ctx, &a.Account)) return errors.Wrap(err, "BosmangSiteStats.ByID") } diff --git a/cmd/goatcounter/db.go b/cmd/goatcounter/db.go index 709045e2e..b4aaf896d 100644 --- a/cmd/goatcounter/db.go +++ b/cmd/goatcounter/db.go @@ -131,8 +131,8 @@ create and update commands: -access* Access to give this user: readonly Can't change any settings. - settings Can change settings, except billing and - site/user management. + settings Can change settings, except site/user + management. admin Full access. superuser Full access, including the "server management" page. @@ -701,11 +701,9 @@ func cmdDBSiteCreate(ctx context.Context, vhost, email, link, pwd string) error s := goatcounter.Site{ Code: "serve-" + zcrypto.Secret64(), Cname: &vhost, - Plan: goatcounter.PlanBusinessPlus, } if account.ID > 0 { - s.Parent, s.Settings, s.UserDefaults, s.Plan = - &account.ID, account.Settings, account.UserDefaults, goatcounter.PlanChild + s.Parent, s.Settings, s.UserDefaults = &account.ID, account.Settings, account.UserDefaults } err := s.Insert(ctx) if err != nil { diff --git a/cmd/goatcounter/saas.go b/cmd/goatcounter/saas.go index faa22eba3..3360416ba 100644 --- a/cmd/goatcounter/saas.go +++ b/cmd/goatcounter/saas.go @@ -15,8 +15,6 @@ import ( "zgo.at/zli" "zgo.at/zlog" "zgo.at/zstd/znet" - "zgo.at/zstd/zstring" - "zgo.at/zstripe" "zgo.at/zvalidate" ) @@ -41,19 +39,17 @@ func cmdSaas(f zli.Flags, ready chan<- struct{}, stop chan struct{}) error { var ( domain = f.String("goatcounter.localhost:8081,static.goatcounter.localhost:8081", "domain").Pointer() - stripe = f.String("", "stripe").Pointer() ) dbConnect, dbConn, dev, automigrate, listen, flagTLS, from, websocket, err := flagsServe(f, &v) if err != nil { return err } - return func(domain, stripe string) error { + return func(domain string) error { if flagTLS == "" { flagTLS = map[bool]string{true: "http", false: "acme"}[dev] } - flagStripe(stripe, &v) domain, domainStatic, domainCount, urlStatic := flagDomain(domain, &v) from = flagFrom(from, domain, &v) if !dev && domain != "goatcounter.com" { @@ -94,41 +90,7 @@ func cmdSaas(f zli.Flags, ready chan<- struct{}, stop chan struct{}) error { zlog.Printf("serving %q on %q; dev=%t", domain, listen, dev) ready <- struct{}{} }) - }(*domain, *stripe) -} - -func flagStripe(stripe string, v *zvalidate.Validator) { - if stripe == "" { - v.Required("-stripe", stripe) - return - } - - if zstring.ContainsAny(zlog.Config.Debug, "stripe", "all") { - zstripe.DebugURL = true - zstripe.DebugRespBody = true - zstripe.DebugReqBody = true - } - - zstripe.StripeVersion = "2020-08-27" - for _, k := range zstring.Fields(stripe, ":") { - switch { - case strings.HasPrefix(k, "sk_"): - zstripe.SecretKey = k - case strings.HasPrefix(k, "pk_"): - zstripe.PublicKey = k - case strings.HasPrefix(k, "whsec_"): - zstripe.SignSecret = k - } - } - if zstripe.SecretKey == "" { - v.Append("-stripe", "missing secret key (sk_)") - } - if zstripe.PublicKey == "" { - v.Append("-stripe", "missing public key (pk_)") - } - if zstripe.SignSecret == "" { - v.Append("-stripe", "missing signing secret (whsec_)") - } + }(*domain) } func flagDomain(domain string, v *zvalidate.Validator) (string, string, string, string) { diff --git a/cmd/goatcounter/saas_test.go b/cmd/goatcounter/saas_test.go index 2dc536992..18ae6b6d7 100644 --- a/cmd/goatcounter/saas_test.go +++ b/cmd/goatcounter/saas_test.go @@ -22,7 +22,6 @@ func TestSaas(t *testing.T) { "-debug=all", "-domain=goatcounter.com,a.a", "-listen=localhost:31874", - "-stripe=sk_test_x:pk_test_x:whsec_x", "-tls=http") }() <-ready diff --git a/cmd/goatcounter/serve.go b/cmd/goatcounter/serve.go index 8096a2ba2..90dca0450 100644 --- a/cmd/goatcounter/serve.go +++ b/cmd/goatcounter/serve.go @@ -211,7 +211,6 @@ func cmdServe(f zli.Flags, ready chan<- struct{}, stop chan struct{}) error { } return doServe(ctx, db, listen, listenTLS, tlsc, hosts, stop, func() { - banner() startupMsg(db) zlog.Printf("ready; serving %d sites on %q; dev=%t; sites: %s", len(cnames), listen, dev, strings.Join(cnames, ", ")) @@ -465,20 +464,6 @@ func lsSites(ctx context.Context) ([]string, error) { return cnames, nil } -func banner() { - fmt.Fprint(zli.Stdout, ` -┏━━━━━━━━━━━━━━━━━━━━━ Thank you for using GoatCounter! ━━━━━━━━━━━━━━━━━━━━━━┓ -┃ ┃ -┃ Great you're choosing to self-host GoatCounter! Please consider making a ┃ -┃ financial contribution according to your means if this is useful for you to ┃ -┃ ensure the long-term viability. Thank you :-) ┃ -┃ ┃ -┃ https://www.goatcounter.com/contribute ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -`) -} - func startupMsg(db zdb.DB) { var msg string err := db.Get(context.Background(), &msg, `select value from store where key='display-once'`) diff --git a/context.go b/context.go index 719a9e32f..eb9b8622b 100644 --- a/context.go +++ b/context.go @@ -69,8 +69,8 @@ func MustGetSite(ctx context.Context) *Site { return s } -// GetAccount gets this site's "account site" on which the users, billing, etc. -// are stored. +// GetAccount gets this site's "account site" on which the users, etc. are +// stored. func GetAccount(ctx context.Context) (*Site, error) { s := MustGetSite(ctx) if s.Parent == nil { diff --git a/cron/cron.go b/cron/cron.go index dd8584d9d..521436240 100644 --- a/cron/cron.go +++ b/cron/cron.go @@ -32,10 +32,8 @@ var Tasks = []Task{ {"vacuum pageviews (data retention)", DataRetention, 1 * time.Hour}, {"renew ACME certs", renewACME, 2 * time.Hour}, {"vacuum soft-deleted sites", vacuumDeleted, 12 * time.Hour}, - {"process scheduled plan changes", cancelPlan, 12 * time.Hour}, {"rm old exports", oldExports, 1 * time.Hour}, {"cycle sessions", sessions, 1 * time.Minute}, - {"report usage", reportUsage, 12 * time.Hour}, {"send email reports", EmailReports, 1 * time.Hour}, } diff --git a/cron/email_reports.go b/cron/email_reports.go index 56e556950..250c1aecc 100644 --- a/cron/email_reports.go +++ b/cron/email_reports.go @@ -139,7 +139,9 @@ func reportText(ctx context.Context, site goatcounter.Site, user goatcounter.Use return nil, nil, "", err } - diffs, err := args.Pages.DiffTotal(ctx, rng) + d := -rng.End.Sub(rng.Start) + prev := ztime.NewRange(rng.Start.Add(d)).To(rng.End.Add(d)) + diffs, err := args.Pages.Diff(ctx, rng, prev) if err != nil { return nil, nil, "", err } diff --git a/cron/tasks.go b/cron/tasks.go index ded5ce6fb..8fe483222 100644 --- a/cron/tasks.go +++ b/cron/tasks.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "os" - "strconv" "strings" "sync" "time" @@ -20,7 +19,6 @@ import ( "zgo.at/zdb" "zgo.at/zlog" "zgo.at/zstd/ztime" - "zgo.at/zstripe" ) func oldExports(ctx context.Context) error { @@ -243,79 +241,8 @@ func vacuumDeleted(ctx context.Context) error { return nil } -// Unset plans after cancellation -func cancelPlan(ctx context.Context) error { - var sites goatcounter.Sites - err := sites.ExpiredPlans(ctx) - if err != nil { - return errors.Errorf("cancelPlans: %w", err) - } - - for _, s := range sites { - s.BillingAmount = nil - s.Plan = goatcounter.PlanFree - s.PlanPending = nil - s.PlanCancelAt = nil - err := s.UpdateStripe(ctx) - if err != nil { - return err - } - } - return nil -} - func sessions(ctx context.Context) error { goatcounter.Memstore.EvictSessions() goatcounter.Memstore.RefreshSalt() return nil } - -// Report usage to Strip; the pricing plan is set up to use the highest value, -// so just report the current value. -func reportUsage(ctx context.Context) error { - var sites goatcounter.Sites - err := sites.UnscopedList(ctx) - if err != nil { - return err - } - - for _, s := range sites { - if s.ExtraPageviews == nil { - continue - } - if s.ExtraPageviewsSub == nil { - zlog.Errorf("ExtraPageviewsSub == nil for site %d", s.ID) - continue - } - - var usage goatcounter.AccountUsage - err := usage.Get(goatcounter.WithSite(ctx, &s)) - if err != nil { - zlog.Error(err) - continue - } - - if usage.Plan.MonthlyHits > usage.Total.ThisPeriod { - continue - } - - charge := (usage.Total.ThisPeriod - usage.Plan.MonthlyHits) / 10_000 - if *s.ExtraPageviews > 0 && float64(charge)*0.20 > float64(*s.ExtraPageviews) { - charge = int(float64(*s.ExtraPageviews) / 0.20) - fmt.Println("XX MAX", charge) - } - - zlog.Printf("reporting usage for %d: %d (€%.2f)", s.ID, charge, float64(charge)*0.20) - - _, err = zstripe.Request(nil, "POST", "/v1/subscription_items/"+*s.ExtraPageviewsSub+"/usage_records", zstripe.Body{ - "quantity": strconv.Itoa(charge), - "timestamp": strconv.FormatInt(ztime.Now().UTC().Unix(), 10), - "action": "set", - }.Encode()) - if err != nil { - zlog.Error(err) - } - } - - return nil -} diff --git a/cron/tasks_test.go b/cron/tasks_test.go index 9e5c85d61..c7d71ea83 100644 --- a/cron/tasks_test.go +++ b/cron/tasks_test.go @@ -19,8 +19,7 @@ import ( func TestDataRetention(t *testing.T) { ctx := gctest.DB(t) - site := goatcounter.Site{Code: "bbbb", Plan: goatcounter.PlanPersonal, - Settings: goatcounter.SiteSettings{DataRetention: 31}} + site := goatcounter.Site{Code: "bbbb", Settings: goatcounter.SiteSettings{DataRetention: 31}} err := site.Insert(ctx) if err != nil { t.Fatal(err) diff --git a/db/migrate/2022-02-16-1-rm-billing.sql b/db/migrate/2022-02-16-1-rm-billing.sql new file mode 100644 index 000000000..1fc2174d8 --- /dev/null +++ b/db/migrate/2022-02-16-1-rm-billing.sql @@ -0,0 +1,9 @@ +alter table sites drop column if exists plan; +alter table sites drop column if exists plan_pending; +alter table sites drop column if exists plan_cancel_at; +alter table sites drop column if exists stripe; +alter table sites drop column if exists billing_amount; +alter table sites drop column if exists billing_anchor; +alter table sites drop column if exists extra_pageviews; +alter table sites drop column if exists extra_pageviews_sub; +alter table sites drop column if exists notes; diff --git a/db/query/bosmang.List.sql b/db/query/bosmang.List.sql index a59910321..0c5a1b982 100644 --- a/db/query/bosmang.List.sql +++ b/db/query/bosmang.List.sql @@ -30,9 +30,6 @@ select grouped.total, grouped.codes, created_at, - billing_amount, - plan, - stripe, grouped.last_month, (coalesce(total, 0) / greatest(extract('days' from now() - created_at), 1) * 30.5)::int as avg from grouped diff --git a/db/schema.gotxt b/db/schema.gotxt index e46ff4788..17769cb8f 100644 --- a/db/schema.gotxt +++ b/db/schema.gotxt @@ -19,18 +19,6 @@ create table sites ( settings {{jsonb}} not null, user_defaults {{jsonb}} not null default '{}', received_data integer not null default 0, - - -- Billing related stuff. - plan varchar not null, - plan_pending varchar null, - plan_cancel_at timestamp, - stripe varchar null, - billing_amount varchar, - billing_anchor timestamp, - notes text not null default '', - extra_pageviews int, - extra_pageviews_sub varchar, - state varchar not null default 'a' check(state in ('a', 'd')), created_at timestamp not null {{check_timestamp "created_at"}}, updated_at timestamp {{check_timestamp "updated_at"}}, diff --git a/docs/sbom.markdown b/docs/sbom.markdown index 8cff5a2ce..84020b5ba 100644 --- a/docs/sbom.markdown +++ b/docs/sbom.markdown @@ -41,7 +41,6 @@ Direct dependencies: | zgo.at/zli | MIT | CLI conveniences | | zgo.at/zlog | MIT | Logging library. | | zgo.at/zstd | MIT | Extensions to stdlib. | -| zgo.at/zstripe | MIT | Stripe integration | | zgo.at/zvalidate | MIT | Validate values | Testing dependencies: diff --git a/gctest/gctest.go b/gctest/gctest.go index 3708b9460..0c426df48 100644 --- a/gctest/gctest.go +++ b/gctest/gctest.go @@ -107,7 +107,7 @@ func db(t testing.TB, storeFile bool) context.Context { } func initData(ctx context.Context, db zdb.DB, t testing.TB) context.Context { - site := goatcounter.Site{Code: "gctest", Cname: ztype.Ptr("gctest.localhost"), Plan: goatcounter.PlanFree} + site := goatcounter.Site{Code: "gctest", Cname: ztype.Ptr("gctest.localhost")} err := site.Insert(ctx) if err != nil { t.Fatalf("create site: %s", err) @@ -186,19 +186,12 @@ func Site(ctx context.Context, t *testing.T, site *goatcounter.Site, user *goatc if site.Code == "" { site.Code = "gctest-" + zcrypto.Secret64() } - if site.Plan == "" { - site.Plan = goatcounter.PlanFree - } err := site.Insert(ctx) if err != nil { t.Fatal(err) } ctx = goatcounter.WithSite(ctx, site) - err = site.UpdateStripe(ctx) - if err != nil { - t.Fatal(err) - } user.Site = site.ID if user.Email == "" { diff --git a/go.mod b/go.mod index f87af66d9..d3bbd945d 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,6 @@ require ( zgo.at/zlog v0.0.0-20211008102840-46c1167bf2a9 zgo.at/zprof v0.0.0-20211217104121-c3c12596d8f0 zgo.at/zstd v0.0.0-20220606095932-df1f8a2ae661 - zgo.at/zstripe v1.1.1-0.20210407063143-62ac9deebc08 zgo.at/ztpl v0.0.0-20211128061406-6ff34b1256c4 zgo.at/zvalidate v0.0.0-20211128195927-d13b18611e62 ) diff --git a/go.sum b/go.sum index 7ae24223a..826c2cda8 100644 --- a/go.sum +++ b/go.sum @@ -124,8 +124,6 @@ zgo.at/zstd v0.0.0-20210512041107-8951517febd3/go.mod h1:sQqrTxBwKW0nlwcOg9RxXB8 zgo.at/zstd v0.0.0-20220306174247-aa79e904bd64/go.mod h1:sQqrTxBwKW0nlwcOg9RxXB8ikY+NBciJnJRPOq/gEuY= zgo.at/zstd v0.0.0-20220606095932-df1f8a2ae661 h1:UME9B37TLF4J0/t2Wz/hilCgG/1xMuexuTP/NNEPKy4= zgo.at/zstd v0.0.0-20220606095932-df1f8a2ae661/go.mod h1:KYkLDHZN6APaRYHlL+hpwBJW/g9w+MLmXi4MKE5YteY= -zgo.at/zstripe v1.1.1-0.20210407063143-62ac9deebc08 h1:c5+QSfo9AQfeigy/GsUGu+cq5JHJ8qo9KiNRoLeqhX4= -zgo.at/zstripe v1.1.1-0.20210407063143-62ac9deebc08/go.mod h1:bIBaT9rsnc+uWMSf+6UXjDHhlwxLgcU7jahxUMU+j+k= zgo.at/ztpl v0.0.0-20211017232908-7dce3dc79277/go.mod h1:mhjSF7+pfwMjNhDgmw9Ywr1CixzeVWuvMjF7gLJT8Uw= zgo.at/ztpl v0.0.0-20211128061406-6ff34b1256c4 h1:/GHTN42or9pD7HVnlQohlC+QegKpwBn9hhE0Fa8zJPc= zgo.at/ztpl v0.0.0-20211128061406-6ff34b1256c4/go.mod h1:mhjSF7+pfwMjNhDgmw9Ywr1CixzeVWuvMjF7gLJT8Uw= diff --git a/handlers/api.go b/handlers/api.go index 5ff5a3182..48a3e1390 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -665,7 +665,6 @@ func (h api) siteCreate(w http.ResponseWriter, r *http.Request) error { } site.Parent = &Site(r.Context()).ID - site.Plan = goatcounter.PlanChild err = site.Insert(r.Context()) if err != nil { return err diff --git a/handlers/api_test.go b/handlers/api_test.go index 4f4483444..84765a548 100644 --- a/handlers/api_test.go +++ b/handlers/api_test.go @@ -445,26 +445,22 @@ func TestAPISitesCreate(t *testing.T) { }{ {false, `{"code":"apitest"}`, 200, func(s *goatcounter.Site) { s.Code = "apitest" - s.Parent = ztype.Ptr(int64(1)) - s.Plan = "child" + s.Parent = zint.NewPtr64(1).P }}, {true, `{"cname":"apitest.localhost"}`, 200, func(s *goatcounter.Site) { - s.Cname = ztype.Ptr("apitest.localhost") - s.Parent = ztype.Ptr(int64(1)) - s.Plan = "child" + s.Cname = ztype.Ptr("apitest.localhost").P + s.Parent = zint.NewPtr64(1).P s.CnameSetupAt = &now }}, // Ignore plan. - {false, `{"code":"apitest","plan":"personal"}`, 200, func(s *goatcounter.Site) { + {false, `{"code":"apitest"}`, 200, func(s *goatcounter.Site) { s.Code = "apitest" - s.Parent = ztype.Ptr(int64(1)) - s.Plan = "child" + s.Parent = zint.NewPtr64(1).P }}, - {true, `{"cname":"apitest.localhost","plan":"personal"}`, 200, func(s *goatcounter.Site) { - s.Cname = ztype.Ptr("apitest.localhost") - s.Parent = ztype.Ptr(int64(1)) - s.Plan = "child" + {true, `{"cname":"apitest.localhost"}`, 200, func(s *goatcounter.Site) { + s.Cname = ztype.Ptr("apitest.localhost").P + s.Parent = zint.NewPtr64(1).P s.CnameSetupAt = &now }}, } @@ -522,13 +518,11 @@ func TestAPISitesUpdate(t *testing.T) { }{ {false, "PATCH", `{}`, 200, func(s *goatcounter.Site) { s.Code = "gctest" - s.Cname = ztype.Ptr("gctest.localhost") - s.Plan = goatcounter.PlanFree + s.Cname = ztype.Ptr("gctest.localhost").P }}, {false, "POST", `{}`, 200, func(s *goatcounter.Site) { s.Code = "gctest" - //s.Cname = ztype.Ptr("gctest.localhost") - s.Plan = goatcounter.PlanFree + //s.Cname = ztype.Ptr("gctest.localhost").P }}, } diff --git a/handlers/backend.go b/handlers/backend.go index 799cfebd1..360e998ef 100644 --- a/handlers/backend.go +++ b/handlers/backend.go @@ -20,7 +20,6 @@ import ( "zgo.at/zlog" "zgo.at/zstd/zfs" "zgo.at/zstd/zstring" - "zgo.at/zstripe" ) func NewBackend(db zdb.DB, acmeh http.HandlerFunc, dev, goatcounterCom, websocket bool, domainStatic string, dashTimeout int) chi.Router { @@ -123,7 +122,6 @@ func (h backend) Mount(r chi.Router, db zdb.DB, dev bool, domainStatic string, d "X-Content-Type-Options": []string{"nosniff"}, } - // https://stripe.com/docs/security#content-security-policy ds := []string{header.CSPSourceSelf} if domainStatic != "" { ds = append(ds, domainStatic) @@ -131,15 +129,15 @@ func (h backend) Mount(r chi.Router, db zdb.DB, dev bool, domainStatic string, d header.SetCSP(headers, header.CSPArgs{ header.CSPDefaultSrc: {header.CSPSourceNone}, header.CSPImgSrc: append(ds, "data:"), - header.CSPScriptSrc: append(ds, "https://js.stripe.com"), + header.CSPScriptSrc: ds, header.CSPStyleSrc: append(ds, header.CSPSourceUnsafeInline), header.CSPFontSrc: ds, - header.CSPFormAction: {header.CSPSourceSelf, "https://billing.stripe.com"}, + header.CSPFormAction: {header.CSPSourceSelf}, // 'self' does not include websockets, and we need to use // "wss://domain.com"; this is difficult because of custom domains // and such, so just allow all websockets. - header.CSPConnectSrc: {header.CSPSourceSelf, "wss:", "https://api.stripe.com"}, - header.CSPFrameSrc: {header.CSPSourceSelf, "https://js.stripe.com", "https://hooks.stripe.com"}, + header.CSPConnectSrc: {header.CSPSourceSelf, "wss:"}, + header.CSPFrameSrc: {header.CSPSourceSelf}, header.CSPManifestSrc: ds, // Too much noise: header.CSPReportURI: {"/csp"}, }) @@ -161,9 +159,6 @@ func (h backend) Mount(r chi.Router, db zdb.DB, dev bool, domainStatic string, d } { af := a.With(loggedIn, addz18n()) - if zstripe.SecretKey != "" && zstripe.SignSecret != "" && zstripe.PublicKey != "" { - billing{}.mount(a, af) - } af.Get("/updates", zhttp.Wrap(h.updates)) settings{}.mount(af) diff --git a/handlers/backend_test.go b/handlers/backend_test.go index fc69a5ed8..89d0159c3 100644 --- a/handlers/backend_test.go +++ b/handlers/backend_test.go @@ -57,9 +57,6 @@ func TestBackendTpl(t *testing.T) { {"/api.html", "GoatCounter API documentation"}, {"/api2.html", " 0 { - err = site.ByID(r.Context(), int64(s.Metadata.SiteID)) - } - if err != nil { - return fmt.Errorf("whSubscriptionUpdated: cannot find Stripe customer %q (metadata: %d), %w", - s.Customer, s.Metadata.SiteID, err) - } - - site.PlanCancelAt = nil - if !s.CancelAt.IsZero() { - site.PlanCancelAt = &s.CancelAt.Time - } - - if site.Stripe == nil { - site.Stripe = &s.Customer - } - site.Plan = plan - site.BillingAmount = ztype.Ptr(fmt.Sprintf("%s %d", currency, amount)) - if s.BillingCycleAnchor.IsZero() { - site.BillingAnchor = nil - } else { - site.BillingAnchor = &s.BillingCycleAnchor.Time - } - return site.UpdateStripe(r.Context()) -} - -func (h billing) whSubscriptionDeleted(event zstripe.Event, w http.ResponseWriter, r *http.Request) error { - // I don't think we ever need to handle this, since cancellations are - // already pushed with an update. But log for now just in case. - fmt.Println(string(event.Data.Raw)) - return nil -} - -func (h billing) whCheckout(event zstripe.Event, w http.ResponseWriter, r *http.Request) error { - var s Session - err := json.Unmarshal(event.Data.Raw, &s) - if err != nil { - return err - } - - // No processing needed for one-time donations. - if strings.HasPrefix(s.ClientReferenceID, "one-time") { - bgrun.Run("email:donation", func() { - t := "New one-time donation: " + s.ClientReferenceID - blackmail.Send(t, - blackmail.From("GoatCounter Billing", "billing@goatcounter.com"), - blackmail.To("billing@goatcounter.com"), - blackmail.Bodyf(t)) - }) - return nil - } - - id, err := strconv.ParseInt(s.ClientReferenceID, 10, 64) - if err != nil { - return fmt.Errorf("ClientReferenceID: %w", err) - } - - var site goatcounter.Site - err = site.ByID(r.Context(), id) - if err != nil { - return err - } - - n := ztime.Now() - site.Stripe = &s.Customer - site.BillingAmount = ztype.Ptr(fmt.Sprintf("%s %d", strings.ToUpper(s.Currency), s.AmountTotal/100)) - site.BillingAnchor = &n - site.Plan = *site.PlanPending - site.PlanPending = nil - site.PlanCancelAt = nil - return site.UpdateStripe(r.Context()) -} - -func getPlan(ctx context.Context, s Subscription) (string, error) { - planID := s.Items.Data[0].Price.ID - for k, v := range stripePlans[goatcounter.Config(ctx).Dev] { - if v == planID { - return k, nil - } - } - return "", fmt.Errorf("unknown plan: %q", planID) -} diff --git a/handlers/billing_test.go b/handlers/billing_test.go deleted file mode 100644 index 9b3ebdf33..000000000 --- a/handlers/billing_test.go +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright © Martin Tournoij – This file is part of GoatCounter and published -// under the terms of a slightly modified EUPL v1.2 license, which can be found -// in the LICENSE file or at https://license.goatcounter.com - -package handlers - -import ( - "net/http/httptest" - "regexp" - "strings" - "testing" - "time" - - "github.com/PuerkitoBio/goquery" - "zgo.at/goatcounter/v2" - "zgo.at/goatcounter/v2/gctest" - "zgo.at/zdb" - "zgo.at/zstd/ztest" - "zgo.at/zstd/ztime" - "zgo.at/zstd/ztype" - "zgo.at/zstripe" -) - -func TestSettingsBilling(t *testing.T) { - tp := func(t time.Time) *time.Time { return &t } - ztime.SetNow(t, "2020-06-20") - tests := []struct { - name string - site *goatcounter.Site - want []string - }{ - { - "trial", - &goatcounter.Site{ - Plan: goatcounter.PlanTrial, - }, - []string{ - "Currently using the Free plan; the limits for this are", - "Your billing period starts at the 1st of every month", - "Subscribe to a plan", - - "!Your next invoice", - "!Manage subscription", - }, - }, - { - "free plan", - &goatcounter.Site{ - Plan: goatcounter.PlanFree, - }, - []string{ - "Currently using the Free plan; the limits for this are", - "Your billing period starts at the 1st of every month", - "Subscribe to a plan", - - "!Your next invoice", - "!Manage subscription", - }, - }, - { - "personal", - &goatcounter.Site{ - Plan: goatcounter.PlanPersonal, - Stripe: ztype.Ptr("cus_asd"), - BillingAmount: ztype.Ptr("EUR 2"), - BillingAnchor: tp(ztime.FromString("2020-06-18")), - }, - []string{ - "Currently using the Personal plan; the limits for this are", - "Your billing period starts at the 18th of every month", - "Your next invoice will be on Jul 18th", - "Manage subscription", - - "!Subscribe to a plan", - }, - }, - { - "starter subscription", - &goatcounter.Site{ - Plan: goatcounter.PlanStarter, - Stripe: ztype.Ptr("cus_abc"), - BillingAmount: ztype.Ptr("EUR 5"), - BillingAnchor: tp(ztime.FromString("2020-06-18")), - }, - []string{ - "Currently using the Starter plan; the limits for this are", - "Your billing period starts at the 18th of every month", - "Your next invoice will be on Jul 18th", - "Manage subscription", - - "!Subscribe to a plan", - }, - }, - - { - "scheduled cancel", - &goatcounter.Site{ - Plan: goatcounter.PlanStarter, - Stripe: ztype.Ptr("cus_abc"), - BillingAmount: ztype.Ptr("EUR 5"), - BillingAnchor: tp(ztime.FromString("2020-06-18")), - PlanCancelAt: tp(ztime.FromString("2020-07-18")), - }, - []string{ - "Currently using the Starter plan; the limits for this are", - "Your billing period starts at the 18th of every month", - "Manage subscription", - "scheduled to be cancelled on Jul 18th", - - "!Subscribe to a plan", - "!next invoice", - }, - }, - - { - "cancelled", - &goatcounter.Site{ - Stripe: ztype.Ptr("cus_abc"), - }, - []string{ - "Currently using the Free plan; the limits for this are", - "Your billing period starts at the 1st of every month", - "Subscribe to a plan", - - "!Your next invoice", - "!Manage subscription", - }, - }, - } - - zstripe.SecretKey, zstripe.SignSecret, zstripe.PublicKey = "x", "x", "x" - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := gctest.DB(t) - goatcounter.Config(ctx).GoatcounterCom = true - ctx = gctest.Site(ctx, t, tt.site, nil) - - r, rr := newLoginTest(t, ctx, "GET", "/billing", nil) - newBackend(zdb.MustGetDB(ctx)).ServeHTTP(rr, r) - ztest.Code(t, rr, 200) - - have := bodyText(t, rr) - for _, w := range tt.want { - if !matchText(have, w) { - t.Error(w) - } - } - if t.Failed() { - t.Log("\n" + have) - } - }) - } -} - -func bodyHtml(t *testing.T, rr *httptest.ResponseRecorder) string { - doc, err := goquery.NewDocumentFromReader(rr.Body) - if err != nil { - t.Fatal(err) - } - - sel := doc.Find(".page") - sel.Find("nav.tab-nav").Remove() // Settings nav - - b, err := sel.Html() - if err != nil { - t.Fatal(err) - } - return regexp.MustCompile(`\n\s*\n+`).ReplaceAllString(strings.TrimSpace(b), "\n") -} - -func bodyText(t *testing.T, rr *httptest.ResponseRecorder) string { - doc, err := goquery.NewDocumentFromReader(rr.Body) - if err != nil { - t.Fatal(err) - } - - sel := doc.Find(".page") - sel.Find("nav.tab-nav").Remove() // Settings nav - - return regexp.MustCompile(`\n\s*\n+`).ReplaceAllString(strings.TrimSpace(sel.Text()), "\n") -} - -func matchText(body, find string) bool { - not := strings.HasPrefix(find, "!") - if not { - find = find[1:] - } - - p := strings.ReplaceAll(regexp.QuoteMeta(strings.ToLower(find)), " ", `\s+`) - m := regexp.MustCompile(p).MatchString(strings.ToLower(body)) - if not { - m = !m - } - return m -} diff --git a/handlers/bosmang.go b/handlers/bosmang.go index a21ae45b7..dc8b094ec 100644 --- a/handlers/bosmang.go +++ b/handlers/bosmang.go @@ -5,8 +5,6 @@ package handlers import ( - "context" - "fmt" "net/http" "sort" "sync" @@ -47,7 +45,6 @@ func (h bosmang) mount(r chi.Router, db zdb.DB) { a.Get("/bosmang/sites", zhttp.Wrap(h.sites)) a.Get("/bosmang/sites/{id}", zhttp.Wrap(h.site)) - a.Post("/bosmang/sites/{id}/update-billing", zhttp.Wrap(h.updateBilling)) a.Post("/bosmang/sites/login/{id}", zhttp.Wrap(h.login)) } @@ -183,67 +180,6 @@ func (h bosmang) site(w http.ResponseWriter, r *http.Request) error { }{newGlobals(w, r), a}) } -func (h bosmang) updateBilling(w http.ResponseWriter, r *http.Request) error { - v := zvalidate.New() - id := v.Integer("id", chi.URLParam(r, "id")) - - var args struct { - Stripe string `json:"stripe"` - Amount string `json:"amount"` - Plan string `json:"plan"` - PlanPending string `json:"plan_pending"` - PlanCancelAt string `json:"plan_cancel_at"` - Notes string `json:"notes"` - } - _, err := zhttp.Decode(r, &args) - if err != nil { - zhttp.FlashError(w, err.Error()) - return zhttp.SeeOther(w, fmt.Sprintf("/bosmang/%d", id)) - } - - var site goatcounter.Site - err = site.ByID(r.Context(), id) - if err != nil { - zhttp.FlashError(w, err.Error()) - return zhttp.SeeOther(w, fmt.Sprintf("/bosmang/%d", id)) - } - - site.Stripe, site.BillingAmount, site.PlanPending, site.PlanCancelAt = nil, nil, nil, nil - - site.Plan = args.Plan - if args.Stripe != "" { - site.Stripe = &args.Stripe - } - if args.Amount != "" { - site.BillingAmount = &args.Amount - } - if args.PlanPending != "" { - site.PlanPending = &args.PlanPending - } - if args.PlanCancelAt != "" { - t, err := time.Parse("2006-01-02 15:04:05", args.PlanCancelAt) - if err != nil { - return err - } - site.PlanCancelAt = &t - } - - ctx := goatcounter.WithSite(goatcounter.CopyContextValues(r.Context()), &site) - err = zdb.TX(ctx, func(ctx context.Context) error { - err := site.UpdateStripe(ctx) - if err != nil { - return err - } - return zdb.Exec(ctx, `update sites set notes=? where site_id=?`, args.Notes, site.ID) - }) - if err != nil { - zhttp.FlashError(w, err.Error()) - return zhttp.SeeOther(w, fmt.Sprintf("/bosmang/%d", id)) - } - - return zhttp.SeeOther(w, fmt.Sprintf("/bosmang/%d", id)) -} - func (h bosmang) login(w http.ResponseWriter, r *http.Request) error { v := zvalidate.New() id := v.Integer("id", chi.URLParam(r, "id")) diff --git a/handlers/dashboard.go b/handlers/dashboard.go index be0714484..fe0d2ac79 100644 --- a/handlers/dashboard.go +++ b/handlers/dashboard.go @@ -194,10 +194,13 @@ func (h backend) dashboard(w http.ResponseWriter, r *http.Request) error { // Copy max and refs to pages; they're in separate "widgets" so they can run // in parallel. + // if pages := wid.Get("pages"); len(pages) > 0 { - max := wid.GetOne("max").(*widgets.Max) - for _, p := range pages { - p.(*widgets.Pages).Max = max.Max + if m := wid.GetOne("max"); m != nil { + max := m.(*widgets.Max) + for _, p := range pages { + p.(*widgets.Pages).Max = max.Max + } } } diff --git a/handlers/handlers.go b/handlers/handlers.go index 92b8ce555..0a1208a6e 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -17,7 +17,6 @@ import ( "zgo.at/zhttp/mware" "zgo.at/zlog" "zgo.at/zstd/zfs" - "zgo.at/zstripe" ) var rateLimits = struct { @@ -67,7 +66,6 @@ type Globals struct { StaticDomain string Domain string Version string - Billing bool GoatcounterCom bool Dev bool Port string @@ -90,7 +88,6 @@ func newGlobals(w http.ResponseWriter, r *http.Request) Globals { Static: goatcounter.Config(ctx).URLStatic, Domain: goatcounter.Config(ctx).Domain, Version: goatcounter.Version, - Billing: zstripe.SecretKey != "" && zstripe.SignSecret != "" && zstripe.PublicKey != "", GoatcounterCom: goatcounter.Config(ctx).GoatcounterCom, Dev: goatcounter.Config(ctx).Dev, Port: goatcounter.Config(ctx).Port, diff --git a/handlers/http_test.go b/handlers/http_test.go index 963d64a75..9284c1ae2 100644 --- a/handlers/http_test.go +++ b/handlers/http_test.go @@ -86,10 +86,6 @@ func TestMain(m *testing.M) { "settings_server.gohtml", "_dashboard_configure_widget.gohtml", "_user_dashboard_widget.gohtml", - - // TODO: hard to test; requires a browser as the JS generates secret - // Stripe stuff in JS. - "billing.gohtml", )) } diff --git a/handlers/settings.go b/handlers/settings.go index 2717db74c..2c60144f6 100644 --- a/handlers/settings.go +++ b/handlers/settings.go @@ -10,7 +10,6 @@ import ( "fmt" "io" "net/http" - "net/url" "os" "path/filepath" "runtime" @@ -32,10 +31,8 @@ import ( "zgo.at/zhttp/mware" "zgo.at/zlog" "zgo.at/zstd/zint" - "zgo.at/zstd/zjson" "zgo.at/zstd/zruntime" "zgo.at/zstd/ztime" - "zgo.at/zstripe" "zgo.at/zvalidate" ) @@ -308,7 +305,6 @@ func (h settings) sitesAdd(w http.ResponseWriter, r *http.Request) error { // Create new site. newSite.Parent = &account.ID - newSite.Plan = goatcounter.PlanChild newSite.Settings = Site(r.Context()).Settings err = zdb.TX(r.Context(), func(ctx context.Context) error { err = newSite.Insert(ctx) @@ -653,18 +649,6 @@ func (h settings) deleteDo(w http.ResponseWriter, r *http.Request) error { account := Account(r.Context()) - has, err := hasPlan(r.Context(), account) - if err != nil { - return err - } - if has { - zhttp.FlashError(w, T(r.Context(), "error/account-has-stripe-subscription|This account still has a Stripe subscription; cancel that first on the billing page.")) - q := url.Values{} - q.Set("reason", args.Reason) - q.Set("contact_me", fmt.Sprintf("%t", args.ContactMe)) - return zhttp.SeeOther(w, "/settings/delete?"+q.Encode()) - } - if args.Reason != "" { bgrun.Run("email:deletion", func() { contact := "false" @@ -688,39 +672,6 @@ func (h settings) deleteDo(w http.ResponseWriter, r *http.Request) error { return zhttp.SeeOther(w, "https://"+goatcounter.Config(r.Context()).Domain) } -func hasPlan(ctx context.Context, site *goatcounter.Site) (bool, error) { - if !goatcounter.Config(ctx).GoatcounterCom || site.Plan == goatcounter.PlanChild || !site.StripeCustomer() { - return false, nil - } - - var customer struct { - Subscriptions struct { - Data []struct { - CancelAtPeriodEnd bool `json:"cancel_at_period_end"` - CurrentPeriodEnd zjson.Timestamp `json:"current_period_end"` - Plan struct { - Quantity int `json:"quantity"` - } `json:"plan"` - } `json:"data"` - } `json:"subscriptions"` - } - _, err := zstripe.Request(&customer, "GET", - fmt.Sprintf("/v1/customers/%s", *site.Stripe), "") - if err != nil { - return false, err - } - - if len(customer.Subscriptions.Data) == 0 { - return false, nil - } - - if customer.Subscriptions.Data[0].CancelAtPeriodEnd { - return false, nil - } - - return true, nil -} - func (h settings) users(verr *zvalidate.Validator) zhttp.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) error { account := Account(r.Context()) diff --git a/handlers/settings_test.go b/handlers/settings_test.go index f37cef346..bd6fa3708 100644 --- a/handlers/settings_test.go +++ b/handlers/settings_test.go @@ -42,7 +42,6 @@ func TestSettingsTpl(t *testing.T) { ss := goatcounter.Site{ Code: "subsite", Parent: &one, - Plan: goatcounter.PlanChild, } err := ss.Insert(ctx) if err != nil { @@ -112,9 +111,9 @@ func TestSettingsSitesAdd(t *testing.T) { auth: true, wantFormCode: 303, want: ` - site_id code cname plan parent state - 1 gctes gctest.localhost personal NULL a - 2 serve add.example.com child 1 a`, + site_id code cname parent state + 1 gctes gctest.localhost NULL a + 2 serve add.example.com 1 a`, }, { name: "already exists for this account", @@ -123,7 +122,6 @@ func TestSettingsSitesAdd(t *testing.T) { Parent: ztype.Ptr(int64(1)), Cname: ztype.Ptr("add.example.com"), Code: "add", - Plan: goatcounter.PlanChild, } err := s.Insert(ctx) if err != nil { @@ -138,9 +136,9 @@ func TestSettingsSitesAdd(t *testing.T) { wantFormCode: 400, wantFormBody: "already exists", want: ` - site_id code cname plan parent state - 1 gctes gctest.localhost personal NULL a - 2 serve add.example.com child 1 a`, + site_id code cname parent state + 1 gctes gctest.localhost NULL a + 2 serve add.example.com 1 a`, }, { name: "already exists on other account", @@ -148,7 +146,6 @@ func TestSettingsSitesAdd(t *testing.T) { s := goatcounter.Site{ Cname: ztype.Ptr("add.example.com"), Code: "add", - Plan: goatcounter.PlanPersonal, } err := s.Insert(ctx) if err != nil { @@ -163,9 +160,9 @@ func TestSettingsSitesAdd(t *testing.T) { wantFormCode: 400, wantFormBody: "already exists", want: ` - site_id code cname plan parent state - 1 gctes gctest.localhost personal NULL a - 2 serve add.example.com personal NULL a`, + site_id code cname parent state + 1 gctes gctest.localhost NULL a + 2 serve add.example.com NULL a`, }, { name: "undelete", @@ -174,7 +171,6 @@ func TestSettingsSitesAdd(t *testing.T) { Parent: ztype.Ptr(int64(1)), Cname: ztype.Ptr("add.example.com"), Code: "add", - Plan: goatcounter.PlanChild, } err := s.Insert(ctx) if err != nil { @@ -192,9 +188,9 @@ func TestSettingsSitesAdd(t *testing.T) { auth: true, wantFormCode: 303, want: ` - site_id code cname plan parent state - 1 gctes gctest.localhost personal NULL a - 2 serve add.example.com child 1 a`, + site_id code cname parent state + 1 gctes gctest.localhost NULL a + 2 serve add.example.com 1 a`, }, { name: "undelete other account", @@ -202,7 +198,6 @@ func TestSettingsSitesAdd(t *testing.T) { s := goatcounter.Site{ Cname: ztype.Ptr("add.example.com"), Code: "add", - Plan: goatcounter.PlanPersonal, } err := s.Insert(ctx) if err != nil { @@ -221,15 +216,15 @@ func TestSettingsSitesAdd(t *testing.T) { wantFormCode: 400, wantFormBody: "already exists", want: ` - site_id code cname plan parent state - 1 gctes gctest.localhost personal NULL a - 2 serve add.example.com personal NULL d`, + site_id code cname parent state + 1 gctes gctest.localhost NULL a + 2 serve add.example.com NULL d`, }, } for _, tt := range tests { runTest(t, tt, func(t *testing.T, rr *httptest.ResponseRecorder, r *http.Request) { - have := zdb.DumpString(r.Context(), `select site_id, substr(code, 0, 6) as code, cname, plan, parent, state from sites`) + have := zdb.DumpString(r.Context(), `select site_id, substr(code, 0, 6) as code, cname, parent, state from sites`) if d := zdb.Diff(have, tt.want); d != "" { t.Error(d) } @@ -248,7 +243,6 @@ func TestSettingsSitesRemove(t *testing.T) { Parent: ztype.Ptr(int64(1)), Cname: ztype.Ptr("add.example.com"), Code: "add", - Plan: goatcounter.PlanChild, }).Insert(ctx) if err != nil { t.Fatal(err) @@ -261,9 +255,9 @@ func TestSettingsSitesRemove(t *testing.T) { auth: true, wantFormCode: 303, want: ` - site_id code cname plan parent state - 1 gctes gctest.localhost personal NULL a - 2 serve add.example.com child 1 d`, + site_id code cname parent state + 1 gctes gctest.localhost NULL a + 2 serve add.example.com 1 d`, }, { name: "remove self", @@ -275,8 +269,8 @@ func TestSettingsSitesRemove(t *testing.T) { auth: true, wantFormCode: 303, want: ` - site_id code cname plan parent state - 1 gctes gctest.localhost personal NULL d`, + site_id code cname parent state + 1 gctes gctest.localhost NULL d`, }, { name: "remove other account", @@ -284,7 +278,6 @@ func TestSettingsSitesRemove(t *testing.T) { s := goatcounter.Site{ Cname: ztype.Ptr("add.example.com"), Code: "add", - Plan: goatcounter.PlanPersonal, } err := s.Insert(ctx) if err != nil { @@ -298,15 +291,15 @@ func TestSettingsSitesRemove(t *testing.T) { auth: true, wantFormCode: 404, want: ` - site_id code cname plan parent state - 1 gctes gctest.localhost personal NULL a - 2 serve add.example.com personal NULL a`, + site_id code cname parent state + 1 gctes gctest.localhost NULL a + 2 serve add.example.com NULL a`, }, } for _, tt := range tests { runTest(t, tt, func(t *testing.T, rr *httptest.ResponseRecorder, r *http.Request) { - have := zdb.DumpString(r.Context(), `select site_id, substr(code, 0, 6) as code, cname, plan, parent, state from sites`) + have := zdb.DumpString(r.Context(), `select site_id, substr(code, 0, 6) as code, cname, parent, state from sites`) if d := zdb.Diff(have, tt.want); d != "" { t.Error(d) } diff --git a/handlers/website.go b/handlers/website.go index 8ccaf042c..c050207c3 100644 --- a/handlers/website.go +++ b/handlers/website.go @@ -30,7 +30,6 @@ import ( "zgo.at/zhttp/mware" "zgo.at/zlog" "zgo.at/zstd/zfs" - "zgo.at/zstripe" "zgo.at/zvalidate" ) @@ -293,7 +292,7 @@ func (h website) doSignup(w http.ResponseWriter, r *http.Request) error { return err } - site := goatcounter.Site{Code: args.Code, LinkDomain: args.LinkDomain, Plan: goatcounter.PlanTrial} + site := goatcounter.Site{Code: args.Code, LinkDomain: args.LinkDomain} user := goatcounter.User{Email: args.Email, Password: []byte(args.Password), Access: goatcounter.UserAccesses{"all": goatcounter.AccessAdmin}} @@ -535,11 +534,8 @@ func (h website) help(w http.ResponseWriter, r *http.Request) error { func (h website) contribute(w http.ResponseWriter, r *http.Request) error { return zhttp.Template(w, "contribute.gohtml", struct { Globals - Page string - MetaDesc string - StripePublicKey string - SKU string - FromWWW bool - }{newGlobals(w, r), "contribute", "Contribute – GoatCounter", - zstripe.PublicKey, stripePlans[goatcounter.Config(r.Context()).Dev]["donate"], h.fromWWW}) + Page string + MetaDesc string + FromWWW bool + }{newGlobals(w, r), "contribute", "Contribute – GoatCounter", h.fromWWW}) } diff --git a/handlers/website_test.go b/handlers/website_test.go index a94238dbd..cfb466e24 100644 --- a/handlers/website_test.go +++ b/handlers/website_test.go @@ -39,7 +39,7 @@ func TestWebsiteTpl(t *testing.T) { //{"/help", "I don’t see my pageviews?"}, {"/help/gdpr", "consult a lawyer"}, {"/contact", "Send message"}, - {"/contribute", "One-time donation"}, + {"/contribute", "Contribute"}, {"/api.html", "GoatCounter API documentation"}, {"/api2.html", " f.call()) - ;[page_dashboard, page_billing, page_settings_main, page_user_pref, page_user_dashboard, page_bosmang] + ;[page_dashboard, page_settings_main, page_user_pref, page_user_dashboard, page_bosmang] .forEach((f) => document.body.id === f.name.replace(/_/g, '-') && f.call()) }) @@ -153,81 +153,6 @@ }) } - var page_billing = function() { - // Pricing FAQ - $('#home-pricing-faq dt').on('click', function(e) { - var dd = $(e.target).next().addClass('cbox') - if (dd[0].style.height === 'auto') - dd.css({padding: '0', height: '0', marginBottom: '0'}) - else - dd.css({padding: '.3em 1em', height: 'auto', marginBottom: '1em'}) - }) - - // Extra pageviews. - $('#allow_extra').on('change', function(e) { - $('#extra-limit').css('display', this.checked ? '' : 'none') - $('#max_extra').trigger('change') - }).trigger('change') - $('#max_extra').on('change', function(e) { - var p = $('#n-pageviews'), - n = parseInt($(this).val(), 10) * 50000, - pv = {business: 500_000, businessplus: 1_000_000}[p.attr('data-plan')] || 100_000, - pn = {personal: 'Personal', personalplus: 'Starter', business: 'Business', businessplus: 'Business plus'}[p.attr('data-plan')] - - var t = $('#allow_extra').is(':checked') ? 'There is no limit on the number of pageviews.' : '' - if (n) - t = `Your limit will be ${format_int(pv+n)} pageviews (${format_int(pv)} from the ${pn} plan, plus ${format_int(n)} extra).` - p.html(t) - }).trigger('change') - - // Show/hide donation options. - $('.plan input').on('change', function() { - $('.free').css('display', $('input[name="plan"]:checked').val() === 'personal' ? '' : 'none') - }).trigger('change') - - var form = $('#billing-form'), - nodonate = false - form.find('button').on('click', function() { nodonate = this.id === 'nodonate' }) - - // Create new Stripe subscription. - form.on('submit', function(e) { - e.preventDefault() - - if (typeof(Stripe) === 'undefined') { - alert('Stripe JavaScript failed to load from "https://js.stripe.com/v3"; ' + - 'ensure this domain is allowed to load JavaScript and reload the page to try again.') - return - } - - var err = function(e) { $('#stripe-error').text(e); }, - plan = $('input[name="plan"]:checked').val(), - quantity = (plan === 'personal' ? (parseInt($('#quantity').val(), 10) || 0) : 1) - - if (!plan) - return alert('You need to select a plan') - - form.find('button[type="submit"]').attr('disabled', true).text('Redirecting...') - jQuery.ajax({ - url: '/billing/start', - method: 'POST', - data: {csrf: CSRF, plan: plan, quantity: quantity, nodonate: nodonate}, - success: function(data) { - if (data.no_stripe) - return location.reload() - Stripe(form.attr('data-key')).redirectToCheckout({sessionId: data.id}). - then(function(result) { err(result.error ? result.error.message : '') }) - }, - error: function(xhr, settings, e) { - err(err) - on_error(`/billing/start: csrf: ${csrf}; plan: ${plan}; q: ${quantity}; xhr: ${xhr}`) - }, - complete: function() { - form.find('button[type="submit"]').attr('disabled', false).text('Continue') - }, - }) - }) - } - var page_user_dashboard = function() { // Add new widget. $('.widget-add-new select').on('change', function(e) { diff --git a/public/script.js b/public/script.js index e06418fe4..a4657fc27 100644 --- a/public/script.js +++ b/public/script.js @@ -90,48 +90,6 @@ return obj; }; - var setup_donate = function() { - var form = document.getElementById('donate-form') - if (!form) - return; - - var err = function(e) { document.getElementById('stripe-error').innerText = e } - - var query = split_query(location.search) - if (query['return']) { - if (query['return'] !== 'success') - return err('Looks like there was an error in processing the payment :-(') - form.innerHTML = '

Thank you for your donation!

' - return; - } - - form.addEventListener('submit', function(e) { - e.preventDefault(); - - if (typeof(Stripe) === 'undefined') { - alert('Stripe JavaScript failed to load from "https://js.stripe.com/v3"; ' + - 'ensure this domain is allowed to load JavaScript and reload the page to try again.'); - return; - } - - var q = {five: 5, ten: 10, twenty: 20, fourty: 40}[document.activeElement.value] - if (!q) { - q = parseInt(document.getElementById('quantity').value, 10); - if (q % 5 !== 0) - return err('Amount must be in multiples of 5') - } - - Stripe(form.dataset.key).redirectToCheckout({ - items: [{sku: form.dataset.sku, quantity: q / 5}], - clientReferenceId: 'one-time ' + q, - successUrl: location.origin + '/contribute?return=success#donate', - cancelUrl: location.origin + '/contribute?return=cancel#donate', - }).then(function(result) { - err(result.error ? result.error.message : ''); - }); - }, false) - } - if (document.readyState === 'complete') init(); else diff --git a/settings.go b/settings.go index 7fff5e0d6..38759de64 100644 --- a/settings.go +++ b/settings.go @@ -54,7 +54,7 @@ var EmailReports = []int{EmailReportNever, EmailReportDaily, EmailReportWeekly, type ( // SiteSettings contains all the user-configurable settings for a site, with - // the exception of the domain and billing settings. + // the exception of the domain settings. // // This is stored as JSON in the database. SiteSettings struct { @@ -146,6 +146,18 @@ func defaultWidgetSettings(ctx context.Context) map[string]WidgetSettings { v.Range("limit_pages", int64(val.(float64)), 1, 100) }, }, + // "compare": WidgetSetting{ + // Type: "select", + // Value: "none", + // Label: z18n.T(ctx, "widget-setting/label/compare|Compare"), + // Help: z18n.T(ctx, "widget-setting/help/compare|Show comparison"), + // Options: [][2]string{ + // [2]string{"none", z18n.T(ctx, "widget-settings/none|None")}, + // [2]string{"period", z18n.T(ctx, "widget-settings/previous-period|Previous period")}, + // [2]string{"quarter", z18n.T(ctx, "widget-settings/previous-quarter|Previous quarter")}, + // [2]string{"year", z18n.T(ctx, "widget-settings/pervious-year|Previous year")}, + // }, + // }, "style": WidgetSetting{ Type: "select", Label: z18n.T(ctx, "widget-setting/label/chart-style|Chart style"), diff --git a/site.go b/site.go index 37c8f3725..7e0e61dd8 100644 --- a/site.go +++ b/site.go @@ -22,19 +22,6 @@ import ( "zgo.at/zstd/ztime" ) -// Plan column values. -const ( - PlanTrial = "trial" - PlanFree = "free" - PlanPersonal = "personal" - PlanStarter = "starter" - PlanBusiness = "business" - PlanBusinessPlus = "businessplus" - PlanChild = "child" -) - -var PlanCodes = []string{PlanTrial, PlanFree, PlanPersonal, PlanStarter, PlanBusiness, PlanBusinessPlus} - var reserved = []string{ "www", "mail", "smtp", "imap", "static", "admin", "ns1", "ns2", "m", "mobile", "api", @@ -64,34 +51,6 @@ type Site struct { // Site domain for linking (www.arp242.net). LinkDomain string `db:"link_domain" json:"link_domain"` - // Plan currently subscribed to. - Plan string `db:"plan" json:"plan,readonly"` - - // Plan this site tried to subscribe to, but payment hasn't been verified - // yet. - PlanPending *string `db:"plan_pending" json:"plan_pending,readonly"` - - // Stripe customer ID. - Stripe *string `db:"stripe" json:"stripe,readonly"` - - // When this plan is scheduled to be cancelled. - PlanCancelAt *time.Time `db:"plan_cancel_at" json:"plan_cancel_at,readonly"` - - // Amount is being paid for the plan. - BillingAmount *string `db:"billing_amount" json:"billing_amount,readonly"` - - // Maximum number of extra pageviews to charge for. - ExtraPageviews *int `db:"extra_pageviews" json:"extra_pageviews,readonly"` - - // subscription_item in Stripe for extra pageviews (si_ ...) - ExtraPageviewsSub *string `db:"extra_pageviews_sub" json:"-"` - - // "Anchor" for the billing period. - // - // This is the time someone subscribed to a plan; and their billing period - // will start on this day. - BillingAnchor *time.Time `db:"billing_anchor" json:"billing_anchor,readonly"` - Settings SiteSettings `db:"settings" json:"setttings"` UserDefaults UserSettings `db:"user_defaults" json:"user_defaults"` @@ -153,7 +112,6 @@ func (s *Site) Validate(ctx context.Context) error { v := NewValidate(ctx) if Config(ctx).GoatcounterCom { - v.Required("plan", s.Plan) v.Required("code", s.Code) v.Len("code", s.Code, 2, 50) v.Exclude("code", s.Code, reserved) @@ -163,19 +121,6 @@ func (s *Site) Validate(ctx context.Context) error { v.Required("state", s.State) v.Include("state", s.State, States) - if s.Parent == nil { - v.Include("plan", s.Plan, PlanCodes) - } else { - v.Include("plan", s.Plan, []string{PlanChild}) - } - - if s.PlanPending != nil { - v.Include("plan_pending", *s.PlanPending, PlanCodes) - if s.Parent != nil { - v.Append("plan_pending", "can't be set if there's a parent") - } - } - v.URL("link_domain", s.LinkDomain) v.Sub("settings", "", s.Settings.Validate(ctx)) @@ -200,10 +145,6 @@ func (s *Site) Validate(ctx context.Context) error { } } - if s.Stripe != nil && !strings.HasPrefix(*s.Stripe, "cus_") { - v.Append("stripe", "not a valid Stripe customer ID") - } - if s.Cname != nil { v.Len("cname", *s.Cname, 4, 255) v.Domain("cname", *s.Cname) @@ -241,8 +182,8 @@ func (s *Site) Insert(ctx context.Context) error { } s.ID, err = zdb.InsertID(ctx, "site_id", `insert into sites ( - parent, code, cname, link_domain, settings, user_defaults, plan, created_at, first_hit_at, cname_setup_at) values (?)`, - zdb.L{s.Parent, s.Code, s.Cname, s.LinkDomain, s.Settings, s.UserDefaults, s.Plan, s.CreatedAt, s.CreatedAt, s.CnameSetupAt}) + parent, code, cname, link_domain, settings, user_defaults, created_at, first_hit_at, cname_setup_at) values (?)`, + zdb.L{s.Parent, s.Code, s.Cname, s.LinkDomain, s.Settings, s.UserDefaults, s.CreatedAt, s.CreatedAt, s.CnameSetupAt}) if err != nil && zdb.ErrUnique(err) { return guru.New(400, "this site already exists: code or domain must be unique") } @@ -271,38 +212,12 @@ func (s *Site) Update(ctx context.Context) error { return nil } -// UpdateStripe sets the billing info. -func (s *Site) UpdateStripe(ctx context.Context) error { - if s.ID == 0 { - return errors.New("ID == 0") - } - s.Defaults(ctx) - err := s.Validate(ctx) - if err != nil { - return err - } - - err = zdb.Exec(ctx, `update sites - set stripe=?, plan=?, plan_pending=?, billing_amount=?, extra_pageviews=?, extra_pageviews_sub=?, billing_anchor=?, plan_cancel_at=?, updated_at=? - where site_id=?`, - s.Stripe, s.Plan, s.PlanPending, s.BillingAmount, s.ExtraPageviews, s.ExtraPageviewsSub, s.BillingAnchor, s.PlanCancelAt, s.UpdatedAt, s.ID) - if err != nil { - return errors.Wrap(err, "Site.UpdateStripe") - } - - s.ClearCache(ctx, false) - return nil -} - func (s *Site) UpdateParent(ctx context.Context, newParent *int64) error { if s.ID == 0 { return errors.New("ID == 0") } s.Parent = newParent - if newParent != nil { - s.Plan = PlanChild - } s.Defaults(ctx) err := s.Validate(ctx) @@ -326,8 +241,8 @@ func (s *Site) UpdateParent(ctx context.Context, newParent *int64) error { } return zdb.Exec(ctx, - `update sites set parent=?, plan=?, updated_at=? where site_id=?`, - s.Parent, s.Plan, s.UpdatedAt, s.ID) + `update sites set parent=?, updated_at=? where site_id=?`, + s.Parent, s.UpdatedAt, s.ID) }) if err != nil { return errors.Wrap(err, "Site.UpdateParent") @@ -495,12 +410,6 @@ func (s *Site) ByIDState(ctx context.Context, id int64, state string) error { return nil } -// ByStripe gets a site by the Stripe customer ID. -func (s *Site) ByStripe(ctx context.Context, stripe string) error { - err := zdb.Get(ctx, s, `select * from sites where stripe=$1`, stripe) - return errors.Wrapf(err, "Site.ByStripe %s", stripe) -} - // ByCode gets a site by code. func (s *Site) ByCode(ctx context.Context, code string) error { return errors.Wrapf(zdb.Get(ctx, s, @@ -622,45 +531,6 @@ func (s Site) IDOrParent() int64 { return s.ID } -//lint:ignore U1001 used in template (via ShowPayBanner) -var trialPeriod = time.Hour * 24 * 14 - -// ShowPayBanner determines if we should show a "please pay" banner for the -// customer. -// -//lint:ignore U1001 used in template. -func (s Site) ShowPayBanner(ctx context.Context) bool { - account := MustGetAccount(ctx) - return account.Plan == PlanTrial && -ztime.Now().Sub(account.CreatedAt.Add(trialPeriod)) < 0 -} - -// StripeCustomer reports if this Stripe column refers to a Stripe customer. -func (s Site) StripeCustomer() bool { - return s.Stripe != nil && !zstring.HasPrefixes(*s.Stripe, "cus_github", "cus_patreon_") -} - -// Subscribed reports if this customer is currently a paying customer subscribed -// to a plan. This may be payed outside of GoatCounter (e.g. GitHub). -func (s Site) Subscribed() bool { - return s.BillingAmount != nil -} - -// PayExternal gets the external payment name, or an empty string if there is -// none. -func (s Site) PayExternal() string { - if s.Stripe == nil { - return "" - } - - if strings.HasPrefix(*s.Stripe, "cus_github_") { - return "GitHub Sponsors" - } - if strings.HasPrefix(*s.Stripe, "cus_patreon_") { - return "Patreon" - } - return "" -} - // DeleteAll deletes all pageviews for this site, keeping the site itself and // user intact. func (s Site) DeleteAll(ctx context.Context) error { @@ -735,35 +605,6 @@ func (s Site) DeleteOlderThan(ctx context.Context, days int) error { }) } -func (s Site) BillingAnchorDay() int { - if s.BillingAnchor == nil { - return 1 - } - return s.BillingAnchor.Day() -} - -func (s Site) NextInvoice() time.Time { - if s.BillingAnchor == nil { - return ztime.Time{ztime.Now()}.AddPeriod(1, ztime.Month).StartOf(ztime.Month).Time - } - - diff := ztime.NewRange(*s.BillingAnchor).To(ztime.Now()).Diff(ztime.Month) - if ztime.Now().Day() > s.BillingAnchor.Day() { - diff.Months++ - } - n := ztime.AddPeriod(*s.BillingAnchor, diff.Months, ztime.Month) - return ztime.StartOf(n, ztime.Day) -} - -func (s Site) ThisBillingPeriod() ztime.Range { - return ztime.NewRange(ztime.AddPeriod(s.NextInvoice(), -1, ztime.Month)).To(ztime.Now()) -} - -func (s Site) PreviousBillingPeriod() ztime.Range { - c := s.ThisBillingPeriod() - return ztime.NewRange(ztime.AddPeriod(c.Start, -1, ztime.Month)).To(ztime.EndOf(c.Start.AddDate(0, 0, -1), ztime.Day)) -} - // Sites is a list of sites. type Sites []Site @@ -833,13 +674,6 @@ func (s *Sites) OldSoftDeleted(ctx context.Context) error { StateDeleted), "Sites.OldSoftDeleted") } -// ExpiredPlans finds all sites which have a plan that's expired. -func (s *Sites) ExpiredPlans(ctx context.Context) error { - err := zdb.Select(ctx, s, `/* Sites.ExpiredPlans */ - select * from sites where ? > plan_cancel_at`, ztime.Now()) - return errors.Wrap(err, "Sites.ExpiredPlans") -} - // Find sites: by ID if ident is a number, or by host if it's not. func (s *Sites) Find(ctx context.Context, ident []string) error { ids, strs := splitIntStr(ident) @@ -885,121 +719,3 @@ func (s *Sites) ListIDs(ctx context.Context, ids ...int64) error { StateActive, ids) return errors.Wrap(err, "Sites.ListIDs") } - -// Plan represents a plan people can subscribe to. -type Plan struct { - Code string - Name string - Price int - MaxHits int - MonthlyHits int -} - -type Plans []Plan - -func (p Plans) Find(s Site) Plan { - for _, pp := range p { - if pp.Code == s.Plan { - return pp - } - } - return Plan{} -} - -var planDetails = Plans{ - { - Code: PlanTrial, - Name: "Free", - MaxHits: 2_400_000, - MonthlyHits: 100_000, - }, { - Code: PlanFree, - Name: "Free", - MaxHits: 2_400_000, - MonthlyHits: 100_000, - }, { - Code: PlanPersonal, - Name: "Personal", - MaxHits: 2_400_000, - MonthlyHits: 100_000, - }, { - Code: PlanStarter, - Name: "Starter", - MaxHits: 4_800_000, - MonthlyHits: 100_000, - Price: 500, - }, { - Code: PlanBusiness, - Name: "Business", - MaxHits: 24_000_000, - MonthlyHits: 500_000, - Price: 1500, - }, { - Code: PlanBusinessPlus, - Name: "Business plus", - MaxHits: 0, - MonthlyHits: 1_000_000, - Price: 3000, - }, { - Code: PlanChild, - Name: "Child", - }, -} - -type AccountUsage struct { - Plan Plan - ThisStart, PrevStart, PrevEnd time.Time - Stats []AccountUsageStats - Total AccountUsageStats -} - -type AccountUsageStats struct { - Code string `db:"code"` - Total int `db:"total"` - ThisPeriod int `db:"this_period"` - PrevPeriod int `db:"prev_period"` -} - -func (a *AccountUsage) Get(ctx context.Context) error { - account, err := GetAccount(ctx) - if err != nil { - return fmt.Errorf("AccountUsage: %w", err) - } - a.Plan = planDetails.Find(*account) - - var sites Sites - err = sites.ForThisAccount(WithSite(ctx, account), false) - if err != nil { - return errors.Wrap(err, "AccountUsage.Get") - } - - a.ThisStart = ztime.AddPeriod(account.NextInvoice(), -1, ztime.Month) - a.PrevStart = ztime.AddPeriod(a.ThisStart, -1, ztime.Month) - a.PrevEnd = ztime.EndOf(a.ThisStart.AddDate(0, 0, -1), ztime.Day) - var query []string - for _, s := range sites { - query = append(query, fmt.Sprintf(`select - (select code from sites where site_id=%[1]d) as code, - (select coalesce(sum(total), 0) from hit_counts where site_id=%[1]d) as total, - (select coalesce(sum(total), 0) from hit_counts where site_id=%[1]d and hour>='%[2]s') as this_period, - (select coalesce(sum(total), 0) from hit_counts where site_id=%[1]d and hour>='%[3]s' and hour<'%[4]s') as prev_period`, - s.ID, - a.ThisStart.Format("2006-01-02 15:04:05"), - a.PrevStart.Format("2006-01-02 15:04:05"), - a.PrevEnd.Format("2006-01-02 15:04:05"), - )) - } - - err = zdb.Select(ctx, &a.Stats, - "/* AccountUsage.Get */\n"+strings.Join(query, "\nunion ")+"\norder by code asc") - if err != nil { - return errors.Wrap(err, "AccountUsage.Get") - } - - for _, s := range a.Stats { - a.Total.PrevPeriod += s.PrevPeriod - a.Total.ThisPeriod += s.ThisPeriod - a.Total.Total += s.Total - } - return nil -} diff --git a/site_test.go b/site_test.go index 634dac432..167b9df68 100644 --- a/site_test.go +++ b/site_test.go @@ -23,7 +23,7 @@ func TestGetAccount(t *testing.T) { t.Fatal() } - ctx2 := gctest.Site(ctx, t, &Site{Plan: PlanChild, Parent: &MustGetSite(ctx).ID}, nil) + ctx2 := gctest.Site(ctx, t, &Site{Parent: &MustGetSite(ctx).ID}, nil) a2 := MustGetAccount(ctx2) if a2.ID != MustGetSite(ctx).ID { t.Fatal() @@ -37,7 +37,7 @@ func TestGetAccount(t *testing.T) { func TestSiteInsert(t *testing.T) { ctx := gctest.DB(t) - s := Site{Code: "the-code", Plan: PlanPersonal} + s := Site{Code: "the-code"} err := s.Insert(ctx) if err != nil { t.Fatal(err) @@ -55,24 +55,24 @@ func TestSiteValidate(t *testing.T) { want map[string][]string }{ { - Site{Code: "hello-0", State: StateActive, Plan: PlanPersonal}, + Site{Code: "hello-0", State: StateActive}, nil, nil, }, { - Site{Code: "h€llo", State: StateActive, Plan: PlanPersonal}, + Site{Code: "h€llo", State: StateActive}, nil, map[string][]string{"code": {"must be a valid hostname: invalid character: '€'"}}, }, { - Site{Code: "hel_lo", State: StateActive, Plan: PlanPersonal}, + Site{Code: "hel_lo", State: StateActive}, nil, map[string][]string{"code": {"must be a valid hostname: invalid character: '_'"}}, }, { - Site{Code: "hello", State: StateActive, Plan: PlanPersonal}, + Site{Code: "hello", State: StateActive}, func(ctx context.Context) { - s := Site{Code: "hello", State: StateActive, Plan: PlanPersonal} + s := Site{Code: "hello", State: StateActive} err := s.Insert(ctx) if err != nil { panic(err) diff --git a/tpl/_backend_bottom.gohtml b/tpl/_backend_bottom.gohtml index 80cfe428e..9ffaf6d4b 100644 --- a/tpl/_backend_bottom.gohtml +++ b/tpl/_backend_bottom.gohtml @@ -1,15 +1,5 @@ {{- /* .page */}} {{template "_bottom_links.gohtml" .}} - {{/* - {{if and .User.ID .Billing (eq .Path "/") (.Site.ShowPayBanner .Context)}} -
-

{{.T `nav-bot/trial-expired|Hey hey; you’ve been using GoatCounter for more than 14 days.
- Please choose if you want to subscribe to a plan or continue with the free plan on the %[billing page].` - (tag "a" `href="/billing"`)}} -

-
- {{end}} - */}} ←︎ {{.T "top-nav/back|Back"}} {{else if has_prefix .Path "/settings/purge/confirm"}} ←︎ {{.T "top-nav/back|Back"}} - {{else if has_prefix .Path "/billing/"}} - ←︎ {{.T "top-nav/back|Back"}} {{else if has_prefix .Path "/i18n/"}} ←︎ {{.T "top-nav/back|Back"}} {{else if has_prefix .Path "/bosmang/"}} diff --git a/tpl/_pricing.gohtml b/tpl/_pricing.gohtml deleted file mode 100644 index ae16c2e49..000000000 --- a/tpl/_pricing.gohtml +++ /dev/null @@ -1,59 +0,0 @@ -{{if .Site}}
{{else}}
{{end}} - {{if .Site -}} - - {{- end}} - {{if .Site}}
{{else}}{{end}} diff --git a/tpl/_settings_nav.gohtml b/tpl/_settings_nav.gohtml index dfa5d65b5..b7e0a1145 100644 --- a/tpl/_settings_nav.gohtml +++ b/tpl/_settings_nav.gohtml @@ -6,9 +6,6 @@ {{if .User.AccessAdmin}} {{.T "link/users|Users"}} {{.T "link/sites|Sites"}} - {{if .Billing}} - {{.T "link/billing|Billing"}} - {{end}} {{if .GoatcounterCom}} {{.T "link/rm-account|Delete account"}} {{end}} diff --git a/tpl/billing.gohtml b/tpl/billing.gohtml deleted file mode 100644 index 36c695eab..000000000 --- a/tpl/billing.gohtml +++ /dev/null @@ -1,157 +0,0 @@ -{{template "_backend_top.gohtml" .}} -{{template "_settings_nav.gohtml" .}} - -
-
-

Plan info

-

Currently using the {{.Usage.Plan.Name}} plan; the limits - for this are {{nformat .Usage.Plan.MonthlyHits .User}} pageviews a - month, with {{if .Usage.Plan.MaxHits}}{{nformat .Usage.Plan.MaxHits .User}}{{else}}unlimited{{end}} total pageviews.

- - {{if and .Account.Subscribed .Account.PayExternal}} - The plan was set up through your contribution at {{.Account.PayExternal}}, and can’t be cancelled or changed here.
- Get in touch at support@goatcounter.com - if you want to change it or have any questions about it. - {{else if .Account.Subscribed}} -

- {{if .Account.PlanCancelAt}} - This subscription is scheduled to be cancelled on {{.Account.PlanCancelAt.Format "Jan"}} {{ord .Account.NextInvoice.Day}}. - {{else}} - Your next invoice will be on {{.Account.NextInvoice.Format "Jan"}} {{ord .Account.NextInvoice.Day}}. - {{end}} -

-
- - -
- {{end}} - - {{if .Account.Subscribed}} -
- -
Extra pageviews
- -

Additional pageviews can be added for €0.20 per 10k pageviews, - charged based on actual usage. So if you use 100,000 pageviews - then you’ll be charged €1 extra. Details

- -
-
- -
- -
-

- - -
- {{end}} -
- -
-

Usage

-

Your billing period starts at the {{.Account.BillingAnchorDay | ord}} of every month, at which point the monthly limits reset.

- -

- Total pageviews stored: - {{if .Usage.Plan.MaxHits}} - {{percentage .Usage.Total.Total .Usage.Plan.MaxHits | printf "%.0f"}}% - ({{nformat .Usage.Total.Total .User}} out of {{nformat .Usage.Plan.MaxHits .User}}) - {{else}} - - - {{nformat .Usage.Total.Total .User}} (no limit) - {{end}} -
- Pageviews this billing period: - {{percentage .Usage.Total.ThisPeriod .Usage.Plan.MonthlyHits | printf "%.0f"}}% - ({{nformat .Usage.Total.ThisPeriod .User}} out of {{nformat .Usage.Plan.MonthlyHits .User}}) -
- Pageviews last billing period: - {{percentage .Usage.Total.PrevPeriod .Usage.Plan.MonthlyHits | printf "%.0f"}}% - ({{nformat .Usage.Total.PrevPeriod .User}}) -

- - {{if gt (len .Usage.Stats) 1}} - -
Breakdown by site
-
- - - - - - - - - - - {{range $u := .Usage.Stats}} - - - - - - - {{end}} -
SiteTotalThis billing periodPrevious billing period
{{$u.Code}}{{nformat $u.Total $.User}}{{nformat $u.ThisPeriod $.User}}{{nformat $u.PrevPeriod $.User}}
- {{end}} -
-
- -{{if .Account.Subscribed}} -

Billing FAQ

-{{template "_billing_help.gohtml" .}} - -{{else}} - - -
-

Subscribe to a plan

-
- - - {{- template "_pricing.gohtml" . -}} - -
- Optional donation -

GoatCounter is free for personal non-commercial use, but a small - monthly donation is encouraged so I can pay my rent and such 😅 Even - just a small €1/month would be greatly appreciated!

- - /month - -

Other ways to contribute:

- -
- -
- - {{if not .Account.Stripe}} -

- {{end}} - -

- You’ll be asked for credit card details on the next page if you choose to donate.
- Contact if you need a payment option other than credit card (e.g. IBAN transfer). -

-
-
-
-

Pricing FAQ

- {{template "_billing_help.gohtml" .}} -
- -{{end}} - -{{template "_backend_bottom.gohtml" .}} diff --git a/tpl/bosmang_site.gohtml b/tpl/bosmang_site.gohtml index 5581ed729..c3f2de79e 100644 --- a/tpl/bosmang_site.gohtml +++ b/tpl/bosmang_site.gohtml @@ -2,66 +2,6 @@

Sites

-
- -
- Set plan - - {{if .Stat.Account.Stripe}} - {{- if has_prefix .Stat.Account.Stripe "cus_github" -}} - GitHub - {{- else -}} - Stripe - {{end}} - {{end}} - - - - - - - - - - - - - - - - - - - - - -
- -
-
-
diff --git a/tpl/bosmang_sites.gohtml b/tpl/bosmang_sites.gohtml index 7190111fd..195077a33 100644 --- a/tpl/bosmang_sites.gohtml +++ b/tpl/bosmang_sites.gohtml @@ -12,13 +12,11 @@ input { float: right; padding: .4em !important; } .c { white-space: normal; } .s a { display: block; text-align: right; } -

Signups

-{{/*

Income

${{.TotalUSD}} GitHub + €{{.TotalEUR}} Stripe + $24 Patreon ≈ €{{.TotalEarnings}}

*/}}

Sites

@@ -28,28 +26,15 @@ input { float: right; padding: .4em !important; } - - {{range $s := .Stats}} - + - - {{end}} diff --git a/tpl/contribute.gohtml b/tpl/contribute.gohtml index 5eec1ee9c..b51252978 100644 --- a/tpl/contribute.gohtml +++ b/tpl/contribute.gohtml @@ -1,51 +1,9 @@ {{template "_top.gohtml" .}}

Contribute financially

-

I encourage everyone to self-host GoatCounter if they have the inclination to -do so, which is why it's 100% Open Source (or “Free”) software.

- -

I work on this full-time: it's not a side-project I work on in spare time, -it's my means of living. Please consider making a financial contribution if this -is useful for you to ensure the long-term viability.

- -

Thank you :-)

- -

Ways to contribute:

- -
    -
  • GitHub sponsor – includes a one-time donation option.
  • -
  • Subscribe to a plan on www.goatcounter.com
  • -
- - - -{{if .FromWWW}} -

You can also send a one-time donation with the form below; the payments are -processed by Stripe (you will need a Credit Card). You can also use -GitHub sponsors to make a -one-time donation.

- - - -
-
- - - - -
- -
- - - - (in increments of €5) -
- -{{else}} -

You can use the form at - goatcounter.com/contribute or - GitHub sponsors to make a one-time donation.

-{{end}} +

Servers aren't free, and running goatcounter.com isn't free either. Please +consider contributing on GitHub +sponsor. You can do a one-time contribution or set up a recurring monthly +contribution.

{{template "_bottom.gohtml" .}} diff --git a/tpl/help/domains.markdown b/tpl/help/domains.markdown index fd0f7cc67..630a862a6 100644 --- a/tpl/help/domains.markdown +++ b/tpl/help/domains.markdown @@ -6,8 +6,8 @@ recorded as `/path`. This might be improved at some point in the future; the options right now are: 1. Create a new site for every domain; this is a completely separate site which - has the same user, login, plan, etc. You will need to use a different site - for every (sub)domain. + has the same user, login, etc. You will need to use a different site for + every (sub)domain. 2. If you want everything in a single overview then you can add the domain to the path, instead of just sending the path: diff --git a/tpl/help/privacy.markdown b/tpl/help/privacy.markdown index bd348d423..dcac67382 100644 --- a/tpl/help/privacy.markdown +++ b/tpl/help/privacy.markdown @@ -25,10 +25,6 @@ other methods.

No information is shared with third parties.

Using the GoatCounter.com service

-

Billing is handled by Stripe, and all -billing information is stored and handled by Stripe. See the -Stripe Privacy Policy.

-

An email address is required to use the GoatCounter.com service. We also use cookies to:

  • remember that you’re logged in to your account between visits;
  • diff --git a/tpl/home.gohtml b/tpl/home.gohtml index 150b7eb1d..cf9c8dd3e 100644 --- a/tpl/home.gohtml +++ b/tpl/home.gohtml @@ -108,16 +108,17 @@

    Pricing

    -{{template "_pricing.gohtml" .}} -
    -
    Additional pageviews can be added for €0.20 per 10k pageviews, charged based on actual usage.
    -
    -
    -

    Pricing FAQ

    - {{template "_billing_help.gohtml" .}} -
    -
    - Sign up +
    +

    GoatCounter.com is currently offered for free for reasonable public + usage. Running your personal website or small-to-medium business on it is + fine, but sending millions of pageviews/day isn’t.

    + +

    You can self-host GoatCounter + easily if you want to use it for more serious purposes.

    + +

    Donations are accepted via + Github Sponsors + to cover server costs.


    diff --git a/tpl/settings_sites.gohtml b/tpl/settings_sites.gohtml index 3a225ffcc..f65464b4e 100644 --- a/tpl/settings_sites.gohtml +++ b/tpl/settings_sites.gohtml @@ -5,7 +5,7 @@ {{.T `p/add-goatcounter-to-multiple-websites|

    Add GoatCounter to multiple websites by creating new sites. All sites - will share the same plan, users, and logins, but are otherwise completely + will share the same users, and logins, but are otherwise completely separate. The current site’s settings are copied on creation, but are independent afterwards.

    diff --git a/tpl/settings_users_form.gohtml b/tpl/settings_users_form.gohtml index ccc9e7b36..7081b6956 100644 --- a/tpl/settings_users_form.gohtml +++ b/tpl/settings_users_form.gohtml @@ -6,7 +6,7 @@ + {{t .Context "label/change-settings-limited|Can change settings, except site/user management"}} {{if $all}} diff --git a/tpl/signup.gohtml b/tpl/signup.gohtml index 629b35980..1d5b96afa 100644 --- a/tpl/signup.gohtml +++ b/tpl/signup.gohtml @@ -2,8 +2,6 @@

    Sign up for GoatCounter

    -

    The first 14 days are always free, after that commercial users must purchase a plan.

    -

    You can use an account on any number of sites/domains; see Settingssites for separating them out.

Avg. Site CodesPlan Created at
{{nformat $s.Total $.User}} {{nformat $s.LastMonth $.User}} {{nformat $s.Avg $.User}} {{$s.ID}} {{$s.Codes}} - {{- if $s.Stripe -}} - {{- if has_prefix $s.Stripe "cus_github" -}} - - {{- else if $s.Stripe -}} - - {{- end -}} - {{- end -}} - {{- if eq $s.Plan "free" -}}z{{end}}{{$s.Plan}}{{if $s.Stripe}}{{end}} - {{if $s.BillingAmount}}{{$s.BillingAmount}}{{end}} {{tformat $s.CreatedAt "" $.User}}