From 7dfaa0c92d913c807a3c210e732afc0d34f3a122 Mon Sep 17 00:00:00 2001 From: sobel-stripe Date: Wed, 17 Apr 2019 10:36:15 -0400 Subject: [PATCH] Add Go Echo server (#63) * page loads * inventory and creation * shipping update * add the fetching * version * webhooks * readme * integrate PR feedback --- server/go/README.md | 47 +++++++ server/go/app.go | 210 +++++++++++++++++++++++++++++++ server/go/config/config.go | 43 +++++++ server/go/go.mod | 13 ++ server/go/go.sum | 23 ++++ server/go/inventory/inventory.go | 67 ++++++++++ server/go/inventory/shipping.go | 35 ++++++ server/go/payments/payments.go | 113 +++++++++++++++++ server/go/setup/setup.go | 145 +++++++++++++++++++++ server/go/webhooks/webhooks.go | 55 ++++++++ 10 files changed, 751 insertions(+) create mode 100644 server/go/README.md create mode 100644 server/go/app.go create mode 100644 server/go/config/config.go create mode 100644 server/go/go.mod create mode 100644 server/go/go.sum create mode 100644 server/go/inventory/inventory.go create mode 100644 server/go/inventory/shipping.go create mode 100644 server/go/payments/payments.go create mode 100644 server/go/setup/setup.go create mode 100644 server/go/webhooks/webhooks.go diff --git a/server/go/README.md b/server/go/README.md new file mode 100644 index 00000000..8ad76828 --- /dev/null +++ b/server/go/README.md @@ -0,0 +1,47 @@ +# Stripe Payments Demo - Go Server + +This demo uses a simple [Echo](https://echo.labstack.com/) application as the server. + +## Payments Integration + +- [`app.go`](app.go) contains the routes that interface with Stripe to create charges and receive webhook events. + +## Requirements + +Youโ€™ll need the following: + +- [Go 1.11 or later](https://golang.org/doc/install) (for module support) +- Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay). +- Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free!) + +## Getting Started + +Copy the example environment variables file `.env.example` from the root of the repo into your own environment file called `.env`: + +``` +cp .env.example .env +``` + +Run the application from this directory (after running `cd server/go`): + +``` +go run app.go -root-directory=$(realpath ../..) +``` + +You should now see it running on [`http://localhost:4567/`](http://localhost:4567/) + +### Testing Webhooks + +If you want to test [receiving webhooks](https://stripe.com/docs/webhooks), we recommend using ngrok to expose your local server. + +First [download ngrok](https://ngrok.com) and start your Echo application. + +[Run ngrok](https://ngrok.com/docs). Assuming your Echo application is running on the default port 4567, you can simply run ngrok in your Terminal in the directory where you downloaded ngrok: + +``` +ngrok http 4567 +``` + +ngrok will display a UI in your terminal telling you the new forwarding address for your Echo app. Use this URL as the URL to be called in your developer [webhooks panel.](https://dashboard.stripe.com/account/webhooks) + +Don't forget to append `/webhook` when you set up your Stripe webhook URL in the Dashboard. Example URL to be called: `https://75795038.ngrok.io/webhook`. diff --git a/server/go/app.go b/server/go/app.go new file mode 100644 index 00000000..055fbe05 --- /dev/null +++ b/server/go/app.go @@ -0,0 +1,210 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "net/http" + "os" + "path" + + "github.com/joho/godotenv" + "github.com/labstack/echo" + "github.com/labstack/echo/middleware" + "github.com/labstack/gommon/log" + "github.com/stripe/stripe-go" + "github.com/stripe/stripe-go/webhook" + + "github.com/stripe/stripe-payments-demo/config" + "github.com/stripe/stripe-payments-demo/inventory" + "github.com/stripe/stripe-payments-demo/payments" + "github.com/stripe/stripe-payments-demo/setup" + "github.com/stripe/stripe-payments-demo/webhooks" +) + +func main() { + rootDirectory := flag.String("root-directory", "", "Root directory of the stripe-payments-demo repository") + flag.Parse() + + if *rootDirectory == "" { + panic("-root-directory is a required argument") + } + + err := godotenv.Load(path.Join(*rootDirectory, ".env")) + if err != nil { + panic(fmt.Sprintf("error loading .env: %v", err)) + } + + stripe.Key = os.Getenv("STRIPE_SECRET_KEY") + if stripe.Key == "" { + panic("STRIPE_SECRET_KEY must be in environment") + } + + publicDirectory := path.Join(*rootDirectory, "public") + e := buildEcho(publicDirectory) + e.Logger.Fatal(e.Start(":4567")) +} + +type listing struct { + Data interface{} `json:"data"` +} + +func buildEcho(publicDirectory string) *echo.Echo { + e := echo.New() + e.Use(middleware.Logger()) + e.Logger.SetLevel(log.DEBUG) + + e.File("/", path.Join(publicDirectory, "index.html")) + e.File("/.well-known/apple-developer-merchantid-domain-association", path.Join(publicDirectory, ".well-known/apple-developer-merchantid-domain-association")) + e.Static("/javascripts", path.Join(publicDirectory, "javascripts")) + e.Static("/stylesheets", path.Join(publicDirectory, "stylesheets")) + e.Static("/images", path.Join(publicDirectory, "images")) + + e.GET("/config", func(c echo.Context) error { + return c.JSON(http.StatusOK, config.Default()) + }) + + e.GET("/products", func(c echo.Context) error { + products, err := inventory.ListProducts() + if err != nil { + return err + } + + if !setup.ExpectedProductsExist(products) { + err := setup.CreateData() + if err != nil { + return err + } + + products, err = inventory.ListProducts() + if err != nil { + return err + } + } + + return c.JSON(http.StatusOK, listing{products}) + }) + + e.GET("/product/:product_id/skus", func(c echo.Context) error { + skus, err := inventory.ListSKUs(c.Param("product_id")) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, listing{skus}) + }) + + e.GET("/products/:product_id", func(c echo.Context) error { + product, err := inventory.RetrieveProduct(c.Param("product_id")) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, product) + }) + + e.POST("/payment_intents", func(c echo.Context) error { + r := new(payments.IntentCreationRequest) + err := c.Bind(r) + if err != nil { + return err + } + + pi, err := payments.CreateIntent(r) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, map[string]*stripe.PaymentIntent{ + "paymentIntent": pi, + }) + }) + + e.POST("/payment_intents/:id/shipping_change", func(c echo.Context) error { + r := new(payments.IntentShippingChangeRequest) + err := c.Bind(r) + if err != nil { + return err + } + + pi, err := payments.UpdateShipping(c.Param("id"), r) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, map[string]*stripe.PaymentIntent{ + "paymentIntent": pi, + }) + }) + + e.GET("/payment_intents/:id/status", func(c echo.Context) error { + pi, err := payments.RetrieveIntent(c.Param("id")) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, map[string]map[string]string{ + "paymentIntent": { + "status": string(pi.Status), + }, + }) + }) + + e.POST("/webhook", func(c echo.Context) error { + request := c.Request() + payload, err := ioutil.ReadAll(request.Body) + if err != nil { + return err + } + + var event stripe.Event + + webhookSecret := os.Getenv("STRIPE_WEBHOOK_SECRET") + if webhookSecret != "" { + event, err = webhook.ConstructEvent(payload, request.Header.Get("Stripe-Signature"), webhookSecret) + if err != nil { + return err + } + } else { + err := json.Unmarshal(payload, &event) + if err != nil { + return err + } + } + + objectType := event.Data.Object["object"].(string) + + var handled bool + switch objectType { + case "payment_intent": + var pi *stripe.PaymentIntent + err = json.Unmarshal(event.Data.Raw, &pi) + if err != nil { + return err + } + + handled, err = webhooks.HandlePaymentIntent(event, pi) + case "source": + var source *stripe.Source + err := json.Unmarshal(event.Data.Raw, &source) + if err != nil { + return err + } + + handled, err = webhooks.HandleSource(event, source) + } + + if err != nil { + return err + } + + if !handled { + fmt.Printf("๐Ÿ”” Webhook received and not handled! %s\n", event.Type) + } + + return nil + }) + + return e +} diff --git a/server/go/config/config.go b/server/go/config/config.go new file mode 100644 index 00000000..dd3dc0ea --- /dev/null +++ b/server/go/config/config.go @@ -0,0 +1,43 @@ +package config + +import ( + "os" + "strings" + + "github.com/stripe/stripe-payments-demo/inventory" +) + +type Config struct { + StripePublishableKey string `json:"stripePublishableKey"` + StripeCountry string `json:"stripeCountry"` + Country string `json:"country"` + Currency string `json:"currency"` + PaymentMethods []string `json:"paymentMethods"` + + ShippingOptions []inventory.ShippingOption `json:"shippingOptions"` +} + +func PaymentMethods() []string { + paymentMethodsString := os.Getenv("PAYMENT_METHODS") + if paymentMethodsString == "" { + return []string{"card"} + } else { + return strings.Split(paymentMethodsString, ", ") + } +} + +func Default() Config { + stripeCountry := os.Getenv("STRIPE_ACCOUNT_COUNTRY") + if stripeCountry == "" { + stripeCountry = "US" + } + + return Config{ + StripePublishableKey: os.Getenv("STRIPE_PUBLISHABLE_KEY"), + StripeCountry: stripeCountry, + Country: "US", + Currency: "eur", + PaymentMethods: PaymentMethods(), + ShippingOptions: inventory.ShippingOptions(), + } +} diff --git a/server/go/go.mod b/server/go/go.mod new file mode 100644 index 00000000..7033fc32 --- /dev/null +++ b/server/go/go.mod @@ -0,0 +1,13 @@ +module github.com/stripe/stripe-payments-demo + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/joho/godotenv v1.3.0 // indirect + github.com/labstack/echo v3.3.10+incompatible // indirect + github.com/labstack/gommon v0.2.8 // indirect + github.com/mattn/go-colorable v0.1.1 // indirect + github.com/mattn/go-isatty v0.0.7 // indirect + github.com/stripe/stripe-go v60.5.0+incompatible + github.com/valyala/fasttemplate v1.0.1 // indirect + golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a // indirect +) diff --git a/server/go/go.sum b/server/go/go.sum new file mode 100644 index 00000000..3c250f7c --- /dev/null +++ b/server/go/go.sum @@ -0,0 +1,23 @@ +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= +github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= +github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0= +github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= +github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/stripe/stripe-go v60.5.0+incompatible h1:nxxftMdmgWA4sbtxXPCk+/Fl+20McZZZl3nPr8KOfW0= +github.com/stripe/stripe-go v60.5.0+incompatible/go.mod h1:A1dQZmO/QypXmsL0T8axYZkSN/uA/T/A64pfKdBAMiY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a h1:Igim7XhdOpBnWPuYJ70XcNpq8q3BCACtVgNfoJxOV7g= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/server/go/inventory/inventory.go b/server/go/inventory/inventory.go new file mode 100644 index 00000000..880dc6e4 --- /dev/null +++ b/server/go/inventory/inventory.go @@ -0,0 +1,67 @@ +package inventory + +import ( + "fmt" + + "github.com/stripe/stripe-go" + "github.com/stripe/stripe-go/product" + "github.com/stripe/stripe-go/sku" +) + +type Item struct { + Parent string `json:"parent"` + Quantity int64 `json:"quantity"` +} + +func ListProducts() ([]*stripe.Product, error) { + products := []*stripe.Product{} + + params := &stripe.ProductListParams{} + params.Filters.AddFilter("limit", "", "3") + i := product.List(params) + for i.Next() { + products = append(products, i.Product()) + } + + err := i.Err() + if err != nil { + return nil, fmt.Errorf("inventory: error listing products: %v", err) + } + + return products, nil +} + +func RetrieveProduct(productID string) (*stripe.Product, error) { + return product.Get(productID, nil) +} + +func ListSKUs(productID string) ([]*stripe.SKU, error) { + skus := []*stripe.SKU{} + + params := &stripe.SKUListParams{} + params.Filters.AddFilter("limit", "", "1") + i := sku.List(params) + for i.Next() { + skus = append(skus, i.SKU()) + } + + err := i.Err() + if err != nil { + return nil, fmt.Errorf("inventory: error listing SKUs: %v", err) + } + + return skus, nil + +} + +func CalculatePaymentAmount(items []Item) (int64, error) { + total := int64(0) + for _, item := range items { + sku, err := sku.Get(item.Parent, nil) + if err != nil { + return 0, fmt.Errorf("inventory: error getting SKU for price: %v", err) + } + total += sku.Price * item.Quantity + } + return total, nil +} diff --git a/server/go/inventory/shipping.go b/server/go/inventory/shipping.go new file mode 100644 index 00000000..2b22be32 --- /dev/null +++ b/server/go/inventory/shipping.go @@ -0,0 +1,35 @@ +package inventory + +type ShippingOption struct { + ID string `json:"id"` + Label string `json:"label"` + Detail string `json:"detail"` + Amount int64 `json:"amount"` +} + +func ShippingOptions() []ShippingOption { + return []ShippingOption{ + { + ID: "free", + Label: "Free Shipping", + Detail: "Delivery within 5 days", + Amount: 0, + }, + { + ID: "express", + Label: "Express Shipping", + Detail: "Next day delivery", + Amount: 500, + }, + } +} + +func GetShippingCost(optionID string) (int64, bool) { + for _, option := range ShippingOptions() { + if option.ID == optionID { + return option.Amount, true + } + } + + return 0, false +} diff --git a/server/go/payments/payments.go b/server/go/payments/payments.go new file mode 100644 index 00000000..744bd028 --- /dev/null +++ b/server/go/payments/payments.go @@ -0,0 +1,113 @@ +package payments + +import ( + "fmt" + + "github.com/stripe/stripe-go" + "github.com/stripe/stripe-go/paymentintent" + + "github.com/stripe/stripe-payments-demo/config" + "github.com/stripe/stripe-payments-demo/inventory" +) + +type IntentCreationRequest struct { + Currency string `json:"currency"` + Items []inventory.Item `json:"items"` +} + +type IntentShippingChangeRequest struct { + Items []inventory.Item `json:"items"` + ShippingOption inventory.ShippingOption `json:"shippingOption"` +} + +func CreateIntent(r *IntentCreationRequest) (*stripe.PaymentIntent, error) { + amount, err := inventory.CalculatePaymentAmount(r.Items) + if err != nil { + return nil, fmt.Errorf("payments: error computing payment amount: %v", err) + } + + params := &stripe.PaymentIntentParams{ + Amount: stripe.Int64(amount), + Currency: stripe.String(r.Currency), + PaymentMethodTypes: paymentMethodTypes(), + } + pi, err := paymentintent.New(params) + if err != nil { + return nil, fmt.Errorf("payments: error creating payment intent: %v", err) + } + + return pi, nil +} + +func RetrieveIntent(paymentIntent string) (*stripe.PaymentIntent, error) { + pi, err := paymentintent.Get(paymentIntent, nil) + if err != nil { + return nil, fmt.Errorf("payments: error fetching payment intent: %v", err) + } + + return pi, nil +} + +func ConfirmIntent(paymentIntent string, source *stripe.Source) error { + pi, err := paymentintent.Get(paymentIntent, nil) + if err != nil { + return fmt.Errorf("payments: error fetching payment intent for confirmation: %v", err) + } + + if pi.Status != "requires_payment_method" { + return fmt.Errorf("payments: PaymentIntent already has a status of %s", pi.Status) + } + + params := &stripe.PaymentIntentConfirmParams{ + Source: stripe.String(source.ID), + } + _, err = paymentintent.Confirm(pi.ID, params) + if err != nil { + return fmt.Errorf("payments: error confirming PaymentIntent: %v", err) + } + + return nil +} + +func CancelIntent(paymentIntent string) error { + _, err := paymentintent.Cancel(paymentIntent, nil) + if err != nil { + return fmt.Errorf("payments: error canceling PaymentIntent: %v", err) + } + + return nil +} + +func UpdateShipping(paymentIntent string, r *IntentShippingChangeRequest) (*stripe.PaymentIntent, error) { + amount, err := inventory.CalculatePaymentAmount(r.Items) + if err != nil { + return nil, fmt.Errorf("payments: error computing payment amount: %v", err) + } + + shippingCost, ok := inventory.GetShippingCost(r.ShippingOption.ID) + if !ok { + return nil, fmt.Errorf("payments: no cost found for shipping id %q", r.ShippingOption.ID) + } + amount += shippingCost + + params := &stripe.PaymentIntentParams{ + Amount: stripe.Int64(amount), + } + pi, err := paymentintent.Update(paymentIntent, params) + if err != nil { + return nil, fmt.Errorf("payments: error updating payment intent: %v", err) + } + + return pi, nil +} + +func paymentMethodTypes() []*string { + types := config.PaymentMethods() + + out := make([]*string, len(types)) + for i := range types { + out[i] = &types[i] + } + + return out +} diff --git a/server/go/setup/setup.go b/server/go/setup/setup.go new file mode 100644 index 00000000..40f0769e --- /dev/null +++ b/server/go/setup/setup.go @@ -0,0 +1,145 @@ +package setup + +import ( + "fmt" + + "github.com/stripe/stripe-go" + "github.com/stripe/stripe-go/product" + "github.com/stripe/stripe-go/sku" +) + +func CreateData() error { + err := createProducts() + if err != nil { + return fmt.Errorf("setup: error creating products: %v", err) + } + + err = createSKUs() + if err != nil { + return fmt.Errorf("setup: error creating products: %v", err) + } + + return nil +} + +func ExpectedProductsExist(existingProducts []*stripe.Product) bool { + expectedProductIDs := []string{"increment", "shirt", "pins"} + + if len(expectedProductIDs) != len(existingProducts) { + return false + } + + existingProductIDs := map[string]bool{} + for _, product := range existingProducts { + existingProductIDs[product.ID] = true + } + + for _, productID := range expectedProductIDs { + if !existingProductIDs[productID] { + return false + } + } + + return true +} + +func createProducts() error { + paramses := []*stripe.ProductParams{ + { + ID: stripe.String("increment"), + Type: stripe.String(string(stripe.ProductTypeGood)), + Name: stripe.String("Increment Magazine"), + Attributes: []*string{ + stripe.String("issue"), + }, + }, + { + ID: stripe.String("pins"), + Type: stripe.String(string(stripe.ProductTypeGood)), + Name: stripe.String("Stripe Pins"), + Attributes: []*string{ + stripe.String("set"), + }, + }, + { + ID: stripe.String("shirt"), + Type: stripe.String(string(stripe.ProductTypeGood)), + Name: stripe.String("Stripe Shirt"), + Attributes: []*string{ + stripe.String("size"), + stripe.String("gender"), + }, + }, + } + + for _, params := range paramses { + _, err := product.New(params) + if err != nil { + stripeErr, ok := err.(*stripe.Error) + if ok && stripeErr.Code == "resource_already_exists" { + // This is fine โ€” we expect this to be idempotent. + } else { + return err + } + } + } + + return nil +} + +func createSKUs() error { + paramses := []*stripe.SKUParams{ + { + ID: stripe.String("increment-03"), + Product: stripe.String("increment"), + Attributes: map[string]string{ + "issue": "Issue #3 โ€œDevelopmentโ€", + }, + Price: stripe.Int64(399), + Currency: stripe.String(string(stripe.CurrencyUSD)), + Inventory: &stripe.InventoryParams{ + Type: stripe.String(string(stripe.SKUInventoryTypeInfinite)), + }, + }, + { + ID: stripe.String("shirt-small-woman"), + Product: stripe.String("shirt"), + Attributes: map[string]string{ + "size": "Small Standard", + "gender": "Woman", + }, + Price: stripe.Int64(999), + Currency: stripe.String(string(stripe.CurrencyUSD)), + Inventory: &stripe.InventoryParams{ + Type: stripe.String(string(stripe.SKUInventoryTypeInfinite)), + }, + }, + { + ID: stripe.String("pins-collector"), + Product: stripe.String("pins"), + Attributes: map[string]string{ + "set": "Collector Set", + }, + Price: stripe.Int64(799), + Currency: stripe.String(string(stripe.CurrencyUSD)), + Inventory: &stripe.InventoryParams{ + Quantity: stripe.Int64(500), + Type: stripe.String(string(stripe.SKUInventoryTypeFinite)), + }, + }, + } + + for _, params := range paramses { + _, err := sku.New(params) + if err != nil { + stripeErr, ok := err.(*stripe.Error) + if ok && stripeErr.Code == "resource_already_exists" { + // This is fine โ€” we expect this to be idempotent. + } else { + return err + } + } + } + + return nil +} diff --git a/server/go/webhooks/webhooks.go b/server/go/webhooks/webhooks.go new file mode 100644 index 00000000..7b54a99c --- /dev/null +++ b/server/go/webhooks/webhooks.go @@ -0,0 +1,55 @@ +package webhooks + +import ( + "fmt" + + "github.com/stripe/stripe-go" + + "github.com/stripe/stripe-payments-demo/payments" +) + +func HandlePaymentIntent(event stripe.Event, pi *stripe.PaymentIntent) (bool, error) { + switch event.Type { + case "payment_intent.succeeded": + fmt.Printf("๐Ÿ”” Webhook received! Payment for PaymentIntent %s succeeded\n", pi.ID) + return true, nil + + case "payment_intent.payment_failed": + if pi.LastPaymentError.PaymentMethod != nil { + fmt.Printf( + "๐Ÿ”” Webhook received! Payment on %s %s for PaymentIntent %s failed\n", + "payment_intent", + pi.LastPaymentError.PaymentMethod.ID, + pi.ID, + ) + } else { + fmt.Printf( + "๐Ÿ”” Webhook received! Payment on %s %s for PaymentIntent %s failed\n", + "source", + pi.LastPaymentError.Source.ID, + pi.ID, + ) + } + + return true, nil + + default: + return false, nil + } +} + +func HandleSource(event stripe.Event, source *stripe.Source) (bool, error) { + paymentIntent := source.Metadata["paymentIntent"] + if paymentIntent == "" { + return false, nil + } + + if source.Status == "chargeable" { + fmt.Printf("๐Ÿ”” Webhook received! The source %s is chargeable\n", source.ID) + return true, payments.ConfirmIntent(paymentIntent, source) + } else if source.Status == "failed" || source.Status == "canceled" { + return true, payments.CancelIntent(paymentIntent) + } + + return false, nil +}