From 9f31c024f54ab822c145700c3decc8fc777aab7d Mon Sep 17 00:00:00 2001 From: Immanuel Onyeka Date: Sun, 2 Jun 2024 13:53:03 -0400 Subject: [PATCH] Add code for users to unsubscribe from settings Also embeds /assets, /migrations, and views/ directories in Go binary to make deployment simpler. --- components/settings.vue | 9 ++- components/unsubscribe.vue | 5 +- migrations/0_29092022_setup_tables.sql | 1 + skouter.go | 84 +++++++++++++++++++++++--- 4 files changed, 85 insertions(+), 14 deletions(-) diff --git a/components/settings.vue b/components/settings.vue index 5cc1411..0053b56 100644 --- a/components/settings.vue +++ b/components/settings.vue @@ -54,7 +54,9 @@

Subscriptions

- + - + diff --git a/components/unsubscribe.vue b/components/unsubscribe.vue index 4096333..9ab7986 100644 --- a/components/unsubscribe.vue +++ b/components/unsubscribe.vue @@ -11,11 +11,10 @@ diff --git a/migrations/0_29092022_setup_tables.sql b/migrations/0_29092022_setup_tables.sql index 14956eb..73d9a0b 100644 --- a/migrations/0_29092022_setup_tables.sql +++ b/migrations/0_29092022_setup_tables.sql @@ -77,6 +77,7 @@ CREATE TABLE subscription ( payment_status VARCHAR(50) NOT NULL, current_period_end INT DEFAULT 0, current_period_start INT DEFAULT 0, + cancel_at_end BOOL DEFAULT 0, PRIMARY KEY (`id`), FOREIGN KEY (user_id) REFERENCES user(id) ); diff --git a/skouter.go b/skouter.go index cb00c81..074f187 100644 --- a/skouter.go +++ b/skouter.go @@ -35,6 +35,7 @@ import ( "image" _ "image/jpeg" "image/png" + "embed" ) type Config struct { @@ -71,6 +72,7 @@ type Subscription struct { PriceId string `json:"priceId"` Start int `json:"start"` End int `json:"end"` + CancelAtEnd bool `json:"cancelAtEnd"` ClientSecret string `json:"clientSecret,omitempty"` PaymentStatus string `json:"paymentStatus"` Status string `json:"status"` @@ -302,6 +304,15 @@ var hookKeys = HookKeys{ SubDeleted: "", } +//go:embed assets +var assets embed.FS + +//go:embed migrations +var migrations embed.FS + +//go:embed views +var views embed.FS + var standardPriceId = "price_1OZLK9BPMoXn2pf9kuTAf8rs" // Used to validate claim in JWT token body. Checks if user id is greater than @@ -366,7 +377,7 @@ func cachePdf(name string) Page { "views/report/summary.tpl", "views/report/comparison.tpl"} - tpl := template.Must(template.New("master.tpl").Funcs(fm).ParseFiles(p...)) + tpl := template.Must(template.New("master.tpl").Funcs(fm).ParseFS(views, p...)) return Page{tpl: tpl, Title: "", Name: name} } @@ -1395,8 +1406,10 @@ func querySub(db *sql.DB, id int) (Subscription, error) { customer_id, current_period_end, current_period_start, + cancel_at_end, client_secret, - payment_status + payment_status, + status FROM subscription WHERE id = ? ` row := db.QueryRow(query, id) @@ -1408,8 +1421,10 @@ func querySub(db *sql.DB, id int) (Subscription, error) { &s.CustomerId, &s.End, &s.Start, + &s.CancelAtEnd, &s.ClientSecret, &s.PaymentStatus, + &s.Status, ) return s, err @@ -1520,7 +1535,7 @@ func insertUser(db *sql.DB, user User) (int, error) { ) VALUES (?, ?, ?, sha2(?, 256), ?, ?, ?, ?, ?, ?, CASE @b := ? WHEN 0 THEN NULL ELSE @b END, - ?, NOW(), NOW()) + ?, ?, NOW(), NOW()) RETURNING id ` @@ -1564,11 +1579,12 @@ func (sub *Subscription) insertSub(db *sql.DB) (error) { price_id, current_period_end, current_period_start, + cancel_at_end, client_secret, payment_status, status ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id ` @@ -1579,6 +1595,7 @@ func (sub *Subscription) insertSub(db *sql.DB) (error) { sub.PriceId, sub.End, sub.Start, + sub.CancelAtEnd, sub.ClientSecret, sub.PaymentStatus, sub.Status, @@ -1598,8 +1615,9 @@ func (sub *Subscription) updateSub(db *sql.DB) error { @b := ? WHEN 0 THEN current_period_end ELSE @b END, current_period_start = CASE @c := ? WHEN 0 THEN current_period_start ELSE @c END, - payment_status = CASE @d := ? WHEN '' THEN client_secret ELSE @d END, - status = CASE @e := ? WHEN '' THEN client_secret ELSE @e END + payment_status = CASE @d := ? WHEN '' THEN payment_status ELSE @d END, + status = CASE @e := ? WHEN '' THEN status ELSE @e END, + cancel_at_end = ? WHERE id = ? ` @@ -1609,8 +1627,10 @@ func (sub *Subscription) updateSub(db *sql.DB) error { sub.Start, sub.PaymentStatus, sub.Status, + sub.CancelAtEnd, sub.Id, ) + if err != nil { return err } return err @@ -3162,6 +3182,17 @@ func createTrialSubscription(cid string) (*stripe.Subscription, error) { return s, err } +// Cancel or uncancel subscription at the end of the current billing cycle. +func (sub *Subscription) CancelSubscription(cancel bool) (*stripe.Subscription, error) { + + subscriptionParams := &stripe.SubscriptionParams{ + CancelAtPeriodEnd: stripe.Bool(cancel), + } + s, err := subscription.Update(sub.StripeId, subscriptionParams) + + return s, err +} + func ( user *User ) SyncSub( sub *stripe.Subscription, db *sql.DB ) error { var err error @@ -3174,6 +3205,7 @@ func ( user *User ) SyncSub( sub *stripe.Subscription, db *sql.DB ) error { user.Sub.ClientSecret = sub.LatestInvoice.PaymentIntent.ClientSecret user.Sub.PaymentStatus = string(sub.LatestInvoice.PaymentIntent.Status) user.Sub.Status = string(sub.Status) + user.Sub.CancelAtEnd = sub.CancelAtPeriodEnd if user.Sub.Id != 0 { err = user.Sub.insertSub(db) @@ -3312,6 +3344,36 @@ func trialSubscribe(w http.ResponseWriter, db *sql.DB, r *http.Request) { json.NewEncoder(w).Encode(user.Sub) } +// Sets Stripe subscription to cancel automatically at end of current period, +// and updates subscription's 'CancelAtEnd' to true. If user's subscription is +// already canceled, it will uncancel the subscription. +func unsubscribe(w http.ResponseWriter, db *sql.DB, r *http.Request) { + stripe.Key = os.Getenv("STRIPE_SECRET_KEY") + claims, err := getClaims(r) + user, err := queryUser(db, claims.Id) + + if err != nil { + w.WriteHeader(422) + return + } + + user.querySub(db) + _, err = user.Sub.CancelSubscription(!user.Sub.CancelAtEnd) + + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + user.Sub.CancelAtEnd = !user.Sub.CancelAtEnd + err = user.Sub.updateSub(db) + + if err != nil { + http.Error(w, err.Error(), 500) + return + } +} + // A successful subscription payment should be confirmed by Stripe and // Updated through this hook. func invoicePaid(w http.ResponseWriter, db *sql.DB, r *http.Request) { @@ -3698,6 +3760,10 @@ func api(w http.ResponseWriter, r *http.Request) { r.Method == http.MethodPost && guard(r, 1): subscribe(w, db, r) + case match(p, "/api/user/unsubscribe", &args) && + r.Method == http.MethodGet && + guard(r, 1): + unsubscribe(w, db, r) case match(p, "/api/user/trial", &args) && r.Method == http.MethodPost && guard(r, 1): @@ -3813,7 +3879,7 @@ func serve() { } func dbReset(db *sql.DB) { - b, err := os.ReadFile("migrations/reset.sql") + b, err := migrations.ReadFile("migrations/reset.sql") if err != nil { log.Fatal(err) } @@ -3823,7 +3889,7 @@ func dbReset(db *sql.DB) { log.Fatal(err) } - b, err = os.ReadFile("migrations/0_29092022_setup_tables.sql") + b, err = migrations.ReadFile("migrations/0_29092022_setup_tables.sql") if err != nil { log.Fatal(err) } @@ -4135,7 +4201,7 @@ func dev(args []string) { } func check(args []string) { - files := http.FileServer(http.Dir("")) + files := http.FileServerFS(assets) http.Handle("/assets/", files) http.HandleFunc("/", checkPdf)