From f2dd73def023ca67d2c36019c2e39be56db7c158 Mon Sep 17 00:00:00 2001
From: Immanuel Onyeka <immanuel@debian-BULLSEYE-live-builder-AMD64>
Date: Tue, 23 Jan 2024 13:40:19 -0500
Subject: [PATCH] Return payment intent for new or existing subs

---
 .../user/js/registration/registration.vue     |   8 +-
 migrations/0_29092022_setup_tables.sql        |   2 +
 skouter.go                                    | 180 +++++++++++++-----
 3 files changed, 137 insertions(+), 53 deletions(-)

diff --git a/grav-admin/user/js/registration/registration.vue b/grav-admin/user/js/registration/registration.vue
index 4a12fca..764de78 100644
--- a/grav-admin/user/js/registration/registration.vue
+++ b/grav-admin/user/js/registration/registration.vue
@@ -58,7 +58,6 @@ function create(user) {
 }
 
 function intent(user) {
-  console.log(user)
   
   return fetch(`/api/user/subscribe`,
 	    {method: 'POST',
@@ -69,7 +68,10 @@ function intent(user) {
     		},
         }).then(resp => {
             if (resp.ok) {
-              return resp.json().then(u => { err.value = "" })
+              return resp.json().then(u => {
+                err.value = ""
+                console.log(u)
+              })
             } else {
               resp.text().then( e => err.value = e)
             }
@@ -78,7 +80,7 @@ function intent(user) {
 
 onMounted(() => {
   getUser().then( u => {
-    intent(u)
+    if (u) intent(u)
   })
 })
 </script>
diff --git a/migrations/0_29092022_setup_tables.sql b/migrations/0_29092022_setup_tables.sql
index 7252f7f..08b8ab7 100644
--- a/migrations/0_29092022_setup_tables.sql
+++ b/migrations/0_29092022_setup_tables.sql
@@ -33,6 +33,7 @@ CREATE TABLE user (
 	address	INT NOT NULL,
 	password 		CHAR(64) NOT NULL,
 	verified 		BOOLEAN,
+	newsletter 		BOOLEAN DEFAULT 0,
 	branch_id 		INT DEFAULT NULL,
 	avatar	BLOB(102400) NOT NULL DEFAULT 0,
 	letterhead	BLOB(102400) NOT NULL DEFAULT 0,
@@ -62,6 +63,7 @@ CREATE TABLE subscription (
 	stripe_id 		VARCHAR(255) DEFAULT '',
 	user_id 		INT,
 	customer_id 	VARCHAR(255) NOT NULL,
+	client_secret 	VARCHAR(255) NOT NULL,
 	current_period_end 		INT DEFAULT 0,
 	current_period_start 		INT DEFAULT 0,
 	PRIMARY KEY (`id`),
diff --git a/skouter.go b/skouter.go
index 4cef825..7cc27c6 100644
--- a/skouter.go
+++ b/skouter.go
@@ -31,7 +31,8 @@ import (
 	"github.com/stripe/stripe-go/v76"
 	"github.com/stripe/stripe-go/v76/customer"
 	"github.com/stripe/stripe-go/v76/subscription"
-	// "github.com/stripe/stripe-go/v76/paymentintent"
+	"github.com/stripe/stripe-go/v76/invoice"
+	"github.com/stripe/stripe-go/v76/paymentintent"
 	"image"
 	_ "image/jpeg"
 	"image/png"
@@ -355,6 +356,25 @@ func match(path, pattern string, args *[]string) bool {
 	return true
 }
 
+func createToken(id int, role string) (*http.Cookie, error) {
+	token := jwt.NewWithClaims(jwt.SigningMethodHS256,
+		UserClaims{Id: id, Role: role,
+			Exp: time.Now().Add(time.Minute * 30).Format(time.UnixDate)})
+
+	tokenStr, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
+	if err != nil {
+		log.Println("Token could not be signed: ", err, tokenStr)
+		return nil, errors.New("Token generation error.")
+	}
+
+	cookie := http.Cookie{Name: "skouter",
+		Value:   tokenStr,
+		Path:    "/",
+		Expires: time.Now().Add(time.Hour * 24)}
+
+	return &cookie, nil
+}
+
 func (estimate *Estimate) makeResults() []Result {
 	var results []Result
 	amortize := func(principle float64, rate float64, periods float64) int {
@@ -866,27 +886,16 @@ func login(w http.ResponseWriter, db *sql.DB, r *http.Request) {
 		return
 	}
 
-	token := jwt.NewWithClaims(jwt.SigningMethodHS256,
-		UserClaims{Id: id, Role: role,
-			Exp: time.Now().Add(time.Minute * 30).Format(time.UnixDate)})
-
-	tokenStr, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
+	cookie, err := createToken(id, role)
 	if err != nil {
-		log.Println("Token could not be signed: ", err, tokenStr)
-		http.Error(w, "Token generation error.", http.StatusInternalServerError)
-		return
+		http.Error(w, err.Error(), 500)
 	}
-
-	cookie := http.Cookie{Name: "skouter",
-		Value:   tokenStr,
-		Path:    "/",
-		Expires: time.Now().Add(time.Hour * 24)}
-	http.SetCookie(w, &cookie)
-	_, err = w.Write([]byte(tokenStr))
+	
+	http.SetCookie(w, cookie)
+	_, err = w.Write([]byte(cookie.Value))
 	if err != nil {
-		http.Error(w,
-			"Could not complete token write.",
-			http.StatusInternalServerError)
+		http.Error(w, "Could not complete token write.", 
+		http.StatusInternalServerError)
 	}
 }
 
@@ -950,6 +959,7 @@ func guard(r *http.Request, required int) bool {
 	if roles[claims.Role] < required {
 		return false
 	}
+	
 
 	return true
 }
@@ -1251,7 +1261,8 @@ func querySub(db *sql.DB, id int) (Subscription, error) {
 	user_id,
 	customer_id,
 	current_period_end,
-	current_period_start
+	current_period_start,
+	client_secret
 	FROM subscription WHERE id = ?
 	`
 	row := db.QueryRow(query, id)
@@ -1262,6 +1273,7 @@ func querySub(db *sql.DB, id int) (Subscription, error) {
 		&s.CustomerId,
 		&s.End,
 		&s.Start,
+		&s.ClientSecret,
 	)
 
 	return s, err
@@ -1277,7 +1289,8 @@ func (user *User) querySub(db *sql.DB) error {
 	user_id,
 	customer_id,
 	current_period_end,
-	current_period_start
+	current_period_start,
+	client_secret
 	FROM subscription WHERE user_id = ?
 	`
 	row := db.QueryRow(query, user.Id)
@@ -1289,6 +1302,7 @@ func (user *User) querySub(db *sql.DB) error {
 		&user.Sub.CustomerId,
 		&user.Sub.End,
 		&user.Sub.Start,
+		&user.Sub.ClientSecret,
 	)
 
 	return err
@@ -1397,6 +1411,7 @@ func insertUser(db *sql.DB, user User) (int, error) {
 // Insert user returning it's ID or any error
 func (sub *Subscription) insertSub(db *sql.DB) (error) {
 	var query string
+	var row *sql.Row
 	var err error
 
 	query = `INSERT INTO subscription
@@ -1405,17 +1420,49 @@ func (sub *Subscription) insertSub(db *sql.DB) (error) {
 		user_id,
 		customer_id,
 		current_period_end,
-		current_period_start
+		current_period_start,
+		client_secret
 	)
-	VALUES (?, ?, ?, ?, ?)
+	VALUES (?, ?, ?, ?, ?, ?)
+	RETURNING id
 	`
-
-	_, err = db.Exec(query,
+	
+	row = db.QueryRow(query,
 		sub.StripeId,
 		sub.UserId,
 		sub.CustomerId,
 		sub.End,
 		sub.Start,
+		sub.ClientSecret,
+	)
+
+	err = row.Scan(&sub.Id)
+	return err
+}
+
+func (sub *Subscription) updateSubSecret(db *sql.DB) error {
+	var query string
+	var err error
+
+	query = `UPDATE subscription SET
+	client_secret = ?
+	WHERE id = ?
+	`
+	
+	s, err := subscription.Get(sub.StripeId, &stripe.SubscriptionParams{})
+	if err != nil { return err }
+	
+	i, err := invoice.Get(s.LatestInvoice.ID, &stripe.InvoiceParams{})
+	if err != nil { return err }
+	
+	p, err := paymentintent.Get(i.PaymentIntent.ID,
+	&stripe.PaymentIntentParams{})
+	if err != nil { return err }
+	
+
+	_, err = db.Exec(query,
+		p.ClientSecret,
+		sub.Id,
 	)
 
 	return err
@@ -1435,8 +1482,11 @@ func (user *User) updateCustomerId(db *sql.DB, cid string) (error) {
 		cid,
 		user.Id,
 	)
+	if err != nil { return err }
+	
+	user.CustomerId = cid
 
-	return err
+	return nil
 }
 
 func updateAddress(address Address, db *sql.DB) error {
@@ -2869,25 +2919,9 @@ stripe.Customer, error) {
 	return *result, err
 }
 
-func createSubscription(w http.ResponseWriter, db *sql.DB, r *http.Request) {
-	claims, err := getClaims(r)
-	user, err := queryUser(db, claims.Id)
-	if err != nil {
-		w.WriteHeader(422)
-		return
-	}
-	
-	var name string = user.FirstName + " " + user.LastName
-	
-	c, err := createCustomer(name, user.Email, user.Address)
-	
-	if err != nil {
-		http.Error(w, err.Error(), 422)
-		return
-	}
-	
-	err = user.updateCustomerId(db, c.ID)
-	
+// Initiates a new standard subscription using a given customer ID
+func createSubscription(cid string) (*stripe.Subscription, error) {
+
 	// Automatically save the payment method to the subscription
     // when the first payment is successful.
     paymentSettings := &stripe.SubscriptionPaymentSettingsParams{
@@ -2898,7 +2932,7 @@ func createSubscription(w http.ResponseWriter, db *sql.DB, r *http.Request) {
     // latest invoice and that invoice's payment_intent
     // so we can pass it to the front end to confirm the payment
     subscriptionParams := &stripe.SubscriptionParams{
-        Customer: stripe.String(c.ID),
+        Customer: stripe.String(cid),
         Items: []*stripe.SubscriptionItemsParams{
             {
                 Price: stripe.String("price_1OZLK9BPMoXn2pf9kuTAf8rs"),
@@ -2910,12 +2944,58 @@ func createSubscription(w http.ResponseWriter, db *sql.DB, r *http.Request) {
     subscriptionParams.AddExpand("latest_invoice.payment_intent")
     s, err := subscription.New(subscriptionParams)
     
-    if err != nil {
-		http.Error(w, err.Error(), 500)
+    return s, err
+}
+
+func subscribe(w http.ResponseWriter, db *sql.DB, r *http.Request) {
+	claims, err := getClaims(r)
+	user, err := queryUser(db, claims.Id)
+	if err != nil {
+		w.WriteHeader(422)
 		return
 	}
 	
-	json.NewEncoder(w).Encode(s)
+	user.querySub(db)
+	
+	var name string = user.FirstName + " " + user.LastName
+	
+	if user.CustomerId == "" {
+		c, err := createCustomer(name, user.Email, user.Address)
+		if err != nil {
+			http.Error(w, err.Error(), 500)
+			return
+		}
+	
+		err = user.updateCustomerId(db, c.ID)
+		if err != nil {
+			http.Error(w, err.Error(), 500)
+			return
+		}
+	}
+	
+	if user.Sub.Id == 0 {
+		s, err := createSubscription(user.CustomerId)
+		if err != nil {
+			http.Error(w, err.Error(), 500)
+			return
+		}
+		
+		user.Sub.UserId = user.Id
+		user.Sub.StripeId = s.ID
+		user.Sub.CustomerId = user.CustomerId
+		user.Sub.End = int(s.CurrentPeriodEnd)
+		user.Sub.Start = int(s.CurrentPeriodStart)
+		user.Sub.ClientSecret = s.LatestInvoice.PaymentIntent.ClientSecret
+		
+		user.Sub.insertSub(db)
+		json.NewEncoder(w).Encode(user.Sub)
+	} else {
+		json.NewEncoder(w).Encode(user.Sub)
+	}
+	
+	// Should check that subscription is still valid and has payment intent
+	// here
+	
 }
 
 func api(w http.ResponseWriter, r *http.Request) {
@@ -2991,7 +3071,7 @@ func api(w http.ResponseWriter, r *http.Request) {
 	case match(p, "/api/user/subscribe", &args) &&
 		r.Method == http.MethodPost &&
 		guard(r, 1):
-		createSubscription(w, db, r)
+		subscribe(w, db, r)
 	case match(p, "/api/fees", &args) &&
 		r.Method == http.MethodGet &&
 		guard(r, 1):