fastpocket/Backend/main.go

585 lines
22 KiB
Go

package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/forms"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/plugins/jsvm"
"github.com/stripe/stripe-go/v78"
"github.com/stripe/stripe-go/v78/billingportal/session"
checkoutSession "github.com/stripe/stripe-go/v78/checkout/session"
"github.com/stripe/stripe-go/v78/customer"
"github.com/stripe/stripe-go/v78/webhook"
)
func coalesce(value *string, defaultValue string) string {
if value != nil {
return *value
}
return defaultValue
}
func int64ToISODate(timestamp int64) string {
// Convert the Unix timestamp to a time.Time
t := time.Unix(timestamp, 0)
// Format the time as an ISO 8601 date string (in UTC)
return t.Format(time.RFC3339)
}
func main() {
// Retreive stripe generated webhook secret
// SECRET_TXT, err := os.ReadFile("secret.txt")
// if err != nil {
// log.Fatal(err)
// }
// WHSEC := strings.ReplaceAll(string(SECRET_TXT), "\n", "")
// log.Print(WHSEC)
app := pocketbase.New()
// Retreive your STRIPE_SECRET_KEY from environment variables
stripe.Key = os.Getenv("STRIPE_SECRET_KEY")
stripeSuccessURL := os.Getenv("STRIPE_SUCCESS_URL")
stripeCancelURL := os.Getenv("STRIPE_CANCEL_URL")
stripeBillingReturnURL := os.Getenv("STRIPE_BILLING_RETURN_URL")
WHSEC := os.Getenv("STRIPE_WHSEC")
jsvm.MustRegister(app, jsvm.Config{
HooksWatch: true,
HooksPoolSize: 25,
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.POST("/create-checkout-session", func(c echo.Context) error {
// 1. Destructure the price and quantity from the POST body
body := c.Request().Body
defer body.Close()
payload, _ := io.ReadAll(body)
var data map[string]interface{}
json.Unmarshal([]byte(payload), &data)
price, _ := data["price"].(map[string]interface{})
quantity, _ := data["quantity"].(float64)
// 2. Get the user from pocketbase auth
token := c.Request().Header.Get("Authorization")
record, err := app.Dao().FindAuthRecordByToken(token, app.Settings().RecordAuthToken.Secret)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "Could not get user"})
}
// 3. Retrieve or create the customer in Stripe
existingCustomerRecord, err := app.Dao().FindFirstRecordByData("customer", "user_id", record.Id)
if err != nil {
//create new customer if none exists
customerEmail := record.GetString("email")
customerParams := &stripe.CustomerParams{
Email: &customerEmail,
Metadata: map[string]string{
"pocketbaseUUID": record.GetString("id"),
},
}
stripeCustomer, _ := customer.New(customerParams)
//upload customer to pocketbase
collection, err := app.Dao().FindCollectionByNameOrId("customer")
if err != nil {
return err
}
var form *forms.RecordUpsert
newCustomerRecord := models.NewRecord(collection)
if err == nil {
form = forms.NewRecordUpsert(app, newCustomerRecord)
}
form.LoadData(map[string]any{
"user_id": record.Id,
"stripe_customer_id": stripeCustomer.ID,
})
if err := form.Submit(); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "Could not create new customer"})
}
//Do Pricing New Customer
if price["type"] == "recurring" {
//create new session
lineParams := []*stripe.CheckoutSessionLineItemParams{
{
Price: stripe.String(price["id"].(string)),
Quantity: stripe.Int64(int64(quantity)),
},
}
customerUpdateParams := &stripe.CheckoutSessionCustomerUpdateParams{
Address: stripe.String("auto"),
}
subscriptionParams := &stripe.CheckoutSessionSubscriptionDataParams{
Metadata: map[string]string{},
}
sessionParams := &stripe.CheckoutSessionParams{
Customer: &stripeCustomer.ID,
PaymentMethodTypes: stripe.StringSlice([]string{"card"}),
BillingAddressCollection: stripe.String("required"),
CustomerUpdate: customerUpdateParams,
Mode: stripe.String("subscription"),
AllowPromotionCodes: stripe.Bool(true),
SuccessURL: &stripeSuccessURL,
CancelURL: &stripeCancelURL,
LineItems: lineParams,
SubscriptionData: subscriptionParams,
}
sesh, _ := checkoutSession.New(sessionParams)
return c.JSON(http.StatusOK, sesh)
} else if price["type"] == "one_time" {
//create new session
lineParams := []*stripe.CheckoutSessionLineItemParams{
{
Price: stripe.String(price["id"].(string)),
Quantity: stripe.Int64(int64(quantity)),
},
}
customerUpdateParams := &stripe.CheckoutSessionCustomerUpdateParams{
Address: stripe.String("auto"),
}
sessionParams := &stripe.CheckoutSessionParams{
Customer: &stripeCustomer.ID,
PaymentMethodTypes: stripe.StringSlice([]string{"card"}),
BillingAddressCollection: stripe.String("required"),
CustomerUpdate: customerUpdateParams,
Mode: stripe.String("payment"),
AllowPromotionCodes: stripe.Bool(true),
SuccessURL: &stripeSuccessURL,
CancelURL: &stripeCancelURL,
LineItems: lineParams,
}
sesh, _ := checkoutSession.New(sessionParams)
return c.JSON(http.StatusOK, sesh)
} else {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "Could not create new session"})
}
} else {
//Do Pricing Existing
if price["type"] == "recurring" {
//create new session
lineParams := []*stripe.CheckoutSessionLineItemParams{
{
Price: stripe.String(price["id"].(string)),
Quantity: stripe.Int64(int64(quantity)),
},
}
customerUpdateParams := &stripe.CheckoutSessionCustomerUpdateParams{
Address: stripe.String("auto"),
}
subscriptionParams := &stripe.CheckoutSessionSubscriptionDataParams{
Metadata: map[string]string{},
}
sessionParams := &stripe.CheckoutSessionParams{
Customer: stripe.String(existingCustomerRecord.GetString("stripe_customer_id")),
PaymentMethodTypes: stripe.StringSlice([]string{"card"}),
BillingAddressCollection: stripe.String("required"),
CustomerUpdate: customerUpdateParams,
Mode: stripe.String("subscription"),
AllowPromotionCodes: stripe.Bool(true),
SuccessURL: &stripeSuccessURL,
CancelURL: &stripeCancelURL,
LineItems: lineParams,
SubscriptionData: subscriptionParams,
}
sesh, _ := checkoutSession.New(sessionParams)
return c.JSON(http.StatusOK, sesh)
} else if price["type"] == "one_time" {
//create new session
lineParams := []*stripe.CheckoutSessionLineItemParams{
{
Price: stripe.String(price["id"].(string)),
Quantity: stripe.Int64(int64(quantity)),
},
}
customerUpdateParams := &stripe.CheckoutSessionCustomerUpdateParams{
Address: stripe.String("auto"),
}
sessionParams := &stripe.CheckoutSessionParams{
Customer: stripe.String(existingCustomerRecord.GetString("stripe_customer_id")),
PaymentMethodTypes: stripe.StringSlice([]string{"card"}),
BillingAddressCollection: stripe.String("required"),
CustomerUpdate: customerUpdateParams,
Mode: stripe.String("payment"),
AllowPromotionCodes: stripe.Bool(true),
SuccessURL: &stripeSuccessURL,
CancelURL: &stripeCancelURL,
LineItems: lineParams,
}
sesh, _ := checkoutSession.New(sessionParams)
return c.JSON(http.StatusOK, sesh)
} else {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "Could not create new session for stripe"})
}
}
})
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.POST("/create-portal-link", func(c echo.Context) error {
// 1. Get the user from pocketbase auth
token := c.Request().Header.Get("Authorization")
record, err := app.Dao().FindAuthRecordByToken(token, app.Settings().RecordAuthToken.Secret)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "Could not get user"})
}
// 2. Retrieve or create the customer in Stripe
existingCustomerRecord, err := app.Dao().FindFirstRecordByData("customer", "user_id", record.Id)
if err != nil {
//create new customer if none exists
customerParams := &stripe.CustomerParams{
Metadata: map[string]string{
"pocketbaseUUID": record.GetString("id"),
},
}
stripeCustomer, _ := customer.New(customerParams)
//upload customer to pocketbase
collection, err := app.Dao().FindCollectionByNameOrId("customer")
if err != nil {
return err
}
var form *forms.RecordUpsert
newCustomerRecord := models.NewRecord(collection)
if err == nil {
form = forms.NewRecordUpsert(app, newCustomerRecord)
}
form.LoadData(map[string]any{
"user_id": record.Id,
"stripe_customer_id": stripeCustomer.ID,
})
if err := form.Submit(); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "Could not create new customer"})
}
//create new session
sessionParams := &stripe.BillingPortalSessionParams{
Customer: stripe.String(stripeCustomer.ID),
ReturnURL: &stripeBillingReturnURL,
}
sesh, err := session.New(sessionParams)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "Could not create new session"})
} else {
return c.JSON(http.StatusOK, sesh)
}
} else {
//create new session
sessionParams := &stripe.BillingPortalSessionParams{
Customer: stripe.String(existingCustomerRecord.GetString("stripe_customer_id")),
ReturnURL: &stripeBillingReturnURL,
}
sesh, err := session.New(sessionParams)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "Could not create new session"})
} else {
return c.JSON(http.StatusOK, sesh)
}
}
})
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.POST("/stripe", func(c echo.Context) error {
// Read the request body into a byte slice
body := c.Request().Body
defer body.Close() // Close the body when done
payload, err := io.ReadAll(body)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "failed to read body"})
}
event := stripe.Event{}
err = json.Unmarshal(payload, &event)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "failed to parse JSON"})
}
signatureHeader := c.Request().Header.Get("Stripe-Signature")
event, err = webhook.ConstructEvent(payload, signatureHeader, WHSEC)
if err != nil {
failureMessage := fmt.Sprintf("webhook verification failed: payload=%q, signatureHeader=%q, err=%q",
payload, signatureHeader, err)
return c.JSON(http.StatusBadRequest, map[string]string{"failure": failureMessage})
}
switch event.Type {
case "product.created", "product.updated":
var product stripe.Product
err := json.Unmarshal(event.Data.Raw, &product)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "failed to marshall the stripe event"})
}
// Then define and call a func to handle the successful payment intent.
collection, err := app.Dao().FindCollectionByNameOrId("product")
if err != nil {
return err
}
existingRecord, err := app.Dao().FindFirstRecordByData("product", "product_id", product.ID)
record := models.NewRecord(collection)
var form *forms.RecordUpsert
if err == nil && existingRecord != nil {
// Existing record found, update it
// You might need to map data from product to your record
// Assuming UpdateRecord updates the existing record with new data
form = forms.NewRecordUpsert(app, existingRecord)
} else {
// Existing record not found, insert a new record
// You might need to map data from product to your record
// Assuming InsertRecord inserts a new record
form = forms.NewRecordUpsert(app, record)
}
form.LoadData(map[string]any{
"product_id": product.ID,
"active": product.Active,
"name": product.Name,
"description": coalesce(&product.Description, ""),
"metadata": product.Metadata,
})
// validate and submit (internally it calls app.Dao().SaveRecord(record) in a transaction)
if err := form.Submit(); err != nil {
return err
}
case "price.created", "price.updated":
var price stripe.Price
err := json.Unmarshal(event.Data.Raw, &price)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "failed to marshall the stripe event"})
}
// Then define and call a func to handle the successful payment intent.
collection, err := app.Dao().FindCollectionByNameOrId("price")
if err != nil {
return err
}
existingRecord, err := app.Dao().FindFirstRecordByData("product", "price_id", price.ID)
record := models.NewRecord(collection)
var form *forms.RecordUpsert
if err == nil && existingRecord != nil {
// Existing record found, update it
// You might need to map data from product to your record
// Assuming UpdateRecord updates the existing record with new data
form = forms.NewRecordUpsert(app, existingRecord)
} else {
// Existing record not found, insert a new record
// You might need to map data from product to your record
// Assuming InsertRecord inserts a new record
form = forms.NewRecordUpsert(app, record)
}
data := map[string]any{
"price_id": price.ID,
"product_id": price.Product.ID,
"active": price.Active,
"currency": price.Currency,
"description": price.Nickname,
"type": price.Type,
"unit_amount": price.UnitAmount,
"metadata": price.Metadata,
}
// Check if Recurring is not nil before accessing its fields
if price.Recurring != nil {
data["interval"] = price.Recurring.Interval
data["interval_count"] = price.Recurring.IntervalCount
data["trial_period_days"] = price.Recurring.TrialPeriodDays
}
form.LoadData(data)
// validate and submit (internally it calls app.Dao().SaveRecord(record) in a transaction)
if err := form.Submit(); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "failed to submit to pocketbase"})
}
case "customer.subscription.created", "customer.subscription.updated", "customer.subscription.deleted":
var subscription stripe.Subscription
err := json.Unmarshal(event.Data.Raw, &subscription)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "failed to marshall the stripe event"})
}
//Get customer's UUID from mapping table in order to update users billing address and payment method
existingCustomer, err := app.Dao().FindFirstRecordByData("customer", "stripe_customer_id", subscription.Customer.ID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "no customer"})
}
var uuid = existingCustomer.GetString("user_id")
collection, err := app.Dao().FindCollectionByNameOrId("subscription")
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "collection doesn't exist"})
}
//Update Subscription Details
existingRecord, err := app.Dao().FindFirstRecordByData("subscription", "subscription_id", subscription.ID)
record := models.NewRecord(collection)
var form *forms.RecordUpsert
if err == nil && existingRecord != nil {
// Existing record found, update it
// You might need to map data from product to your record
// Assuming UpdateRecord updates the existing record with new data
form = forms.NewRecordUpsert(app, existingRecord)
} else {
// Existing record not found, insert a new record
// You might need to map data from product to your record
// Assuming InsertRecord inserts a new record
form = forms.NewRecordUpsert(app, record)
}
form.LoadData(map[string]any{
"subscription_id": subscription.ID,
"user_id": uuid,
"metadata": subscription.Metadata,
"status": subscription.Status,
"price_id": subscription.Items.Data[0].Price.ID,
"quantity": subscription.Items.Data[0].Quantity,
"cancel_at_period_end": subscription.CancelAtPeriodEnd,
"cancel_at": int64ToISODate(subscription.CancelAt),
"canceled_at": int64ToISODate(subscription.CanceledAt),
"current_period_start": int64ToISODate(subscription.CurrentPeriodStart),
"current_period_end": int64ToISODate(subscription.CurrentPeriodEnd),
"created": int64ToISODate(subscription.Items.Data[0].Created),
"ended_at": int64ToISODate(subscription.EndedAt),
"trial_start": int64ToISODate(subscription.TrialStart),
"trial_end": int64ToISODate(subscription.TrialEnd),
})
if err := form.Submit(); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "couldn't submit subscription update"})
}
//Update User Details If Subscription Created
if event.Type == "customer.subscription.created" {
existingUserRecord, _ := app.Dao().FindFirstRecordByData("user", "id", uuid)
var userForm = forms.NewRecordUpsert(app, existingUserRecord)
userForm.LoadData(map[string]any{
"billing_address": subscription.DefaultPaymentMethod.Customer.Address,
"payment_method": subscription.DefaultPaymentMethod.Type,
})
// validate and submit (internally it calls app.Dao().SaveRecord(record) in a transaction)
if err := userForm.Submit(); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "couldn't submit user update"})
}
}
case "checkout.session.completed":
var session stripe.CheckoutSession
err := json.Unmarshal(event.Data.Raw, &session)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "failed to marshall the stripe event"})
}
if session.Mode == "subscription" {
//Get customer's UUID from mapping table in order to update users billing address and payment method
existingCustomer, err := app.Dao().FindFirstRecordByData("customer", "stripe_customer_id", session.Subscription.Customer.ID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "no customer"})
}
var uuid = existingCustomer.GetString("user_id")
collection, err := app.Dao().FindCollectionByNameOrId("subscription")
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "collection doesn't exist"})
}
//Update Subscription Details
existingRecord, err := app.Dao().FindFirstRecordByData("subscription", "subscription_id", session.Subscription.ID)
record := models.NewRecord(collection)
var form *forms.RecordUpsert
if err == nil && existingRecord != nil {
// Existing record found, update it
// You might need to map data from product to your record
// Assuming UpdateRecord updates the existing record with new data
form = forms.NewRecordUpsert(app, existingRecord)
} else {
// Existing record not found, insert a new record
// You might need to map data from product to your record
// Assuming InsertRecord inserts a new record
form = forms.NewRecordUpsert(app, record)
}
form.LoadData(map[string]any{
"subscription_id": session.Subscription.ID,
"user_id": uuid,
"metadata": session.Subscription.Metadata,
"status": session.Subscription.Status,
"price_id": session.Subscription.Items.Data[0].Price.ID,
"quantity": session.Subscription.Items.Data[0].Quantity,
"cancel_at_period_end": session.Subscription.CancelAtPeriodEnd,
"cancel_at": int64ToISODate(session.Subscription.CancelAt),
"canceled_at": int64ToISODate(session.Subscription.CanceledAt),
"current_period_start": int64ToISODate(session.Subscription.CurrentPeriodStart),
"current_period_end": int64ToISODate(session.Subscription.CurrentPeriodEnd),
"created": int64ToISODate(session.Subscription.Items.Data[0].Created),
"ended_at": int64ToISODate(session.Subscription.EndedAt),
"trial_start": int64ToISODate(session.Subscription.TrialStart),
"trial_end": int64ToISODate(session.Subscription.TrialEnd),
})
if err := form.Submit(); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "couldn't submit subscription update"})
}
//Update User Details
existingUserRecord, _ := app.Dao().FindFirstRecordByData("user", "id", uuid)
var userForm = forms.NewRecordUpsert(app, existingUserRecord)
userForm.LoadData(map[string]any{
"billing_address": session.Subscription.DefaultPaymentMethod.Customer.Address,
"payment_method": session.Subscription.DefaultPaymentMethod.Type,
})
// validate and submit (internally it calls app.Dao().SaveRecord(record) in a transaction)
if err := userForm.Submit(); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "couldn't submit user update"})
}
}
default:
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "didn't receive a valid event"})
}
return c.JSON(http.StatusOK, map[string]interface{}{"success": "data was received"})
} /* optional middlewares */)
return nil
})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}