Browse Source

Add subscription creation webhook for Stripe

master
Immanuel Onyeka 1 year ago
parent
commit
7e919e1a41
2 changed files with 110 additions and 18 deletions
  1. +10
    -2
      migrations/0_29092022_setup_tables.sql
  2. +100
    -16
      skouter.go

+ 10
- 2
migrations/0_29092022_setup_tables.sql View File

@@ -45,9 +45,10 @@ CREATE TABLE user (
'Mortgage Broker', 'Mortgage Broker',
'Executive', 'Executive',
'Other') NOT NULL, 'Other') NOT NULL,
status ENUM('Trial', status ENUM('Unsubscribed',
'Trial',
'Free', 'Free',
'Subscribed', 'Subscriber',
'Branch', 'Branch',
'Admin') DEFAULT 'Trial', 'Admin') DEFAULT 'Trial',
role ENUM('User', 'Manager', 'Admin') NOT NULL, role ENUM('User', 'Manager', 'Admin') NOT NULL,
@@ -66,6 +67,13 @@ CREATE TABLE subscription (
price_id VARCHAR(255) NOT NULL, price_id VARCHAR(255) NOT NULL,
/* Key used by stripejs */ /* Key used by stripejs */
client_secret VARCHAR(255) NOT NULL, client_secret VARCHAR(255) NOT NULL,
status ENUM('incomplete',
'incomplete_expired',
'trialing',
'active',
'past_due',
'canceled',
'unpaid'),
payment_status VARCHAR(50) NOT NULL, payment_status VARCHAR(50) NOT NULL,
current_period_end INT DEFAULT 0, current_period_end INT DEFAULT 0,
current_period_start INT DEFAULT 0, current_period_start INT DEFAULT 0,


+ 100
- 16
skouter.go View File

@@ -75,6 +75,7 @@ type Subscription struct {
End int `json:"end"` End int `json:"end"`
ClientSecret string `json:"clientSecret,omitempty"` ClientSecret string `json:"clientSecret,omitempty"`
PaymentStatus string `json:"paymentStatus"` PaymentStatus string `json:"paymentStatus"`
Status string `json:"status"`
} }


type User struct { type User struct {
@@ -236,6 +237,9 @@ type Endpoint func(http.ResponseWriter, *sql.DB, *http.Request)
type HookKeys struct { type HookKeys struct {
InvoicePaid string InvoicePaid string
InvoiceFailed string InvoiceFailed string
SubCreated string
SubUpdated string
SubDeleted string
} }


var ( var (
@@ -265,6 +269,15 @@ var roles = map[string]int{
"Admin": 3, "Admin": 3,
} }


var statuses = map[string]int{
"Unsubscribed": 1,
"Trial": 2,
"Free": 3,
"Subscriber": 4,
"Branch": 5,
"Admin": 6,
}

var propertyTypes = []string{ var propertyTypes = []string{
"Single Detached", "Single Detached",
"Single Attached", "Single Attached",
@@ -283,6 +296,9 @@ var feeTypes = []string{
var hookKeys = HookKeys{ var hookKeys = HookKeys{
InvoicePaid: "", InvoicePaid: "",
InvoiceFailed: "", InvoiceFailed: "",
SubCreated: "",
SubUpdated: "",
SubDeleted: "",
} }


var standardPriceId = "price_1OZLK9BPMoXn2pf9kuTAf8rs" var standardPriceId = "price_1OZLK9BPMoXn2pf9kuTAf8rs"
@@ -1485,9 +1501,10 @@ func (sub *Subscription) insertSub(db *sql.DB) (error) {
current_period_end, current_period_end,
current_period_start, current_period_start,
client_secret, client_secret,
payment_status payment_status,
status
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id RETURNING id
` `
@@ -1500,6 +1517,7 @@ func (sub *Subscription) insertSub(db *sql.DB) (error) {
sub.Start, sub.Start,
sub.ClientSecret, sub.ClientSecret,
sub.PaymentStatus, sub.PaymentStatus,
sub.Status,
) )


err = row.Scan(&sub.Id) err = row.Scan(&sub.Id)
@@ -1511,7 +1529,7 @@ func (sub *Subscription) updateSub(db *sql.DB) error {
var err error var err error


query = `UPDATE subscription query = `UPDATE subscription
SET client_secret = ?, payment_status = ? SET client_secret = ?, payment_status = ?,
WHERE id = ? WHERE id = ?
` `
@@ -1585,7 +1603,7 @@ func updateAddress(address Address, db *sql.DB) error {
return err return err
} }


func updateUser(user User, db *sql.DB) error { func (user *User) update(db *sql.DB) error {
query := ` query := `
UPDATE user UPDATE user
SET SET
@@ -1647,7 +1665,7 @@ func setUser(user User, db *sql.DB) error {
return errors.New("Invalid role") return errors.New("Invalid role")
} }


err = updateUser(user, db) err = user.update(db)
if err != nil { if err != nil {
return err return err
} }
@@ -3069,23 +3087,19 @@ func subscribe(w http.ResponseWriter, db *sql.DB, r *http.Request) {
user.Sub.ClientSecret = s.LatestInvoice.PaymentIntent.ClientSecret user.Sub.ClientSecret = s.LatestInvoice.PaymentIntent.ClientSecret
user.Sub.PaymentStatus = string(s.LatestInvoice.PaymentIntent.Status) user.Sub.PaymentStatus = string(s.LatestInvoice.PaymentIntent.Status)
// Inserting from here is probably unnecessary and confusing because
// new subs are already handled by the stripe hook
err = user.Sub.insertSub(db) err = user.Sub.insertSub(db)
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
} }
json.NewEncoder(w).Encode(user.Sub)
} else { } else {
err = user.Sub.updateSub(db) // This should handle creating a new subscription when the old one
if err != nil { // has an incomplete_expired status and cannot be paid
http.Error(w, err.Error(), 500)
return
}
json.NewEncoder(w).Encode(user.Sub)
} }
// Should check that subscription is still valid and has payment intent json.NewEncoder(w).Encode(user.Sub)
// here
} }


@@ -3132,7 +3146,6 @@ func invoicePaid(w http.ResponseWriter, db *sql.DB, r *http.Request) {
} }
log.Println(user.Id, s.ID) log.Println(user.Id, s.ID)
} }


func invoiceFailed(w http.ResponseWriter, db *sql.DB, r *http.Request) { func invoiceFailed(w http.ResponseWriter, db *sql.DB, r *http.Request) {
@@ -3154,6 +3167,77 @@ func invoiceFailed(w http.ResponseWriter, db *sql.DB, r *http.Request) {
log.Println(event.Data) log.Println(event.Data)
} }


// A successful subscription payment should be confirmed by Stripe and
// Updated through this hook.
func subCreated(w http.ResponseWriter, db *sql.DB, r *http.Request) {

var sub stripe.Subscription
b, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("io.ReadAll: %v", err)
return
}
event, err := webhook.ConstructEvent(b,
r.Header.Get("Stripe-Signature"),
hookKeys.SubCreated)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("webhook.ConstructEvent: %v", err)
return
}
// OK should be sent before any processing to confirm with Stripe that
// the hook was received
w.WriteHeader(http.StatusOK)
if event.Type != "customer.subscription.created" {
log.Println(
"Invalid event type sent to customer.subscription.created.")
return
}
json.Unmarshal(event.Data.Raw, &sub)
log.Println(event.Type, sub.ID, sub.Customer.ID)
user, err := queryCustomer(db, sub.Customer.ID)
if err != nil {
log.Printf("Could not query customer: %v", err)
return
}
if statuses[user.Status] < 5 && sub.Status == "trialing" {
user.Status = "Trial"
user.update(db)
} else if sub.Status != "active" {
user.Status = "Unsubscribed"
user.update(db)
}
user.Sub.UserId = user.Id
user.Sub.StripeId = sub.ID
user.Sub.CustomerId = user.CustomerId
user.Sub.PriceId = standardPriceId
user.Sub.End = int(sub.CurrentPeriodEnd)
user.Sub.Start = int(sub.CurrentPeriodStart)
user.Sub.ClientSecret = sub.LatestInvoice.PaymentIntent.ClientSecret
user.Sub.PaymentStatus = string(sub.LatestInvoice.PaymentIntent.Status)
user.Sub.Status = string(sub.Status)
if user.Sub.Id > 0 {
user.Sub.updateSub(db)
} else {
err = user.Sub.insertSub(db)
}
if err != nil {
http.Error(w, err.Error(), 500)
return
}
log.Println("User subscription created:", user.Id, sub.ID)
}

func api(w http.ResponseWriter, r *http.Request) { func api(w http.ResponseWriter, r *http.Request) {
var args []string var args []string


@@ -3443,7 +3527,7 @@ func seedUsers(db *sql.DB, addresses []Address, branches []Branch) []User {
users[i].Password = "test123" users[i].Password = "test123"
users[i].Verified = true users[i].Verified = true
users[i].Title = "Loan Officer" users[i].Title = "Loan Officer"
users[i].Status = "Subscribed" users[i].Status = "Subscriber"
users[i].Role = "User" users[i].Role = "User"
} }




||||||
x
 
000:0
Loading…
Cancel
Save