diff --git a/skouter.go b/skouter.go index 86bf325..1160d9b 100644 --- a/skouter.go +++ b/skouter.go @@ -571,6 +571,23 @@ func fetchFeesTemp(db *sql.DB, user int, branch int) ([]FeeTemplate, error) { return fees, nil } +func constructEvent(r *http.Request, key string) (*stripe.Event, error) { + b, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("io.ReadAll: %v", err) + return nil, err + } + + event, err := + webhook.ConstructEvent(b, r.Header.Get("Stripe-Signature"), key) + if err != nil { + log.Printf("webhook.ConstructEvent: %v", err) + return nil, err + } + + return &event, nil +} + // Fetch fees from the database func getFeesTemp(w http.ResponseWriter, db *sql.DB, r *http.Request) { var fees []FeeTemplate @@ -3037,6 +3054,28 @@ func createSubscription(cid string) (*stripe.Subscription, error) { return s, err } +func ( user *User ) SyncSub( sub *stripe.Subscription, db *sql.DB ) error { + var err error + + 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 { + err = user.Sub.insertSub(db) + } else { + user.Sub.updateSub(db) + } + + return err +} + // Creates a new subscription instance for a new user or retrieves the // existing instance if possible. It's main purpose is to supply a // client secret used for sending billing information to stripe. @@ -3164,25 +3203,14 @@ func invoiceFailed(w http.ResponseWriter, db *sql.DB, r *http.Request) { } // Important for catching subscription creation through Stripe dashboard -// although it already happens at subscribe(). It checks if the user already -// has a subscription and replaces those fields if necessary so a seperate -// subCreated() is not necessary. -func subUpdated(w http.ResponseWriter, db *sql.DB, r *http.Request) { - +// although it already happens at subscribe(). +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.SubUpdated) + var err error + + event, err := constructEvent(r, hookKeys.SubCreated) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) - log.Printf("webhook.ConstructEvent: %v", err) return } @@ -3191,7 +3219,7 @@ func subUpdated(w http.ResponseWriter, db *sql.DB, r *http.Request) { w.WriteHeader(http.StatusOK) if event.Type != "customer.subscription.created" { log.Println( - "Invalid event type sent to customer.subscription.created.") + "Invalid event type. Expecting customer.subscription.created.") return } @@ -3212,21 +3240,7 @@ func subUpdated(w http.ResponseWriter, db *sql.DB, r *http.Request) { 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 { - err = user.Sub.insertSub(db) - } else { - user.Sub.updateSub(db) - } + err = user.SyncSub(&sub, db) if err != nil { http.Error(w, err.Error(), 500) @@ -3236,31 +3250,71 @@ func subUpdated(w http.ResponseWriter, db *sql.DB, r *http.Request) { log.Println("User subscription created:", user.Id, sub.ID) } -// Handles changes to customer subscriptions sent by Stripe -func subDeleted(w http.ResponseWriter, db *sql.DB, r *http.Request) { +// Checks if the user already has a subscription and replaces those fields if +// necessary. +func subUpdated(w http.ResponseWriter, db *sql.DB, r *http.Request) { var sub stripe.Subscription - b, err := io.ReadAll(r.Body) + var err error + + event, err := constructEvent(r, hookKeys.SubUpdated) 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.SubUpdated) + // 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.updated" { + log.Println( + "Invalid event type sent. Expecting customer.subscription.updated.") + 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) + } + + err = user.SyncSub(&sub, db) + + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + log.Println("User subscription created:", user.Id, sub.ID) +} + +// Handles deleted subscriptions hooks sent by Stripe +func subDeleted(w http.ResponseWriter, db *sql.DB, r *http.Request) { + var sub stripe.Subscription + var err error + + event, err := constructEvent(r, hookKeys.SubDeleted) 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.updated" { + if event.Type != "customer.subscription.deleted" { log.Println( - "Invalid event type sent to customer.subscription.updated.") + "Invalid event type sent. Expecting customer.subscription.deleted.") return } @@ -3281,21 +3335,9 @@ func subDeleted(w http.ResponseWriter, db *sql.DB, r *http.Request) { 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) + user.Sub.Status = "canceled" - if user.Sub.Id != 0 { - err = user.Sub.insertSub(db) - } else { - user.Sub.updateSub(db) - } + err = user.SyncSub(&sub, db) if err != nil { http.Error(w, err.Error(), 500) @@ -3435,13 +3477,13 @@ func api(w http.ResponseWriter, r *http.Request) { invoiceFailed(w, db, r) case match(p, "/api/stripe/sub-created", &args) && r.Method == http.MethodPost: - subUpdated(w, db, r) + subCreated(w, db, r) case match(p, "/api/stripe/sub-updated", &args) && r.Method == http.MethodPost: subUpdated(w, db, r) - case match(p, "/api/stripe/sub-updated", &args) && + case match(p, "/api/stripe/sub-deleted", &args) && r.Method == http.MethodPost: - subUpdated(w, db, r) + subDeleted(w, db, r) default: http.Error(w, "Invalid route or token", 404) } @@ -3774,6 +3816,9 @@ func dev(args []string) { hookKeys = HookKeys{ InvoicePaid: os.Getenv("DEV_WEBHOOK_KEY"), InvoiceFailed: os.Getenv("DEV_WEBHOOK_KEY"), + SubCreated: os.Getenv("DEV_WEBHOOK_KEY"), + SubUpdated: os.Getenv("DEV_WEBHOOK_KEY"), + SubDeleted: os.Getenv("DEV_WEBHOOK_KEY"), } db, err := sql.Open("mysql",