From 6963a886023f64f86075812cb514373891296395 Mon Sep 17 00:00:00 2001 From: bronku Date: Sun, 8 Nov 2026 20:09:06 +0100 Subject: [PATCH] stripe --- go.mod | 7 ++- go.sum | 10 ++-- main.go | 14 +++-- orders/order.go | 113 +++++++++++++++++++++++++++++++++++-- orders/phone_validation.go | 2 +- 5 files changed, 129 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 2095e85..8452937 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,11 @@ module piekarnikgosipl go 1.26.2 -require github.com/a-h/templ v0.3.1020 +require ( + github.com/a-h/templ v0.3.1020 + github.com/stripe/stripe-go/v82 v82.5.1 + github.com/tdewolff/minify/v2 v2.24.13 +) require ( github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect @@ -14,7 +18,6 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/natefinch/atomic v1.0.1 // indirect - github.com/tdewolff/minify/v2 v2.24.13 // indirect github.com/tdewolff/parse/v2 v2.8.12 // indirect golang.org/x/mod v0.26.0 // indirect golang.org/x/net v0.51.0 // indirect diff --git a/go.sum b/go.sum index ad118b4..4ff3778 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -29,11 +27,15 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stripe/stripe-go/v82 v82.5.1 h1:05q6ZDKoe8PLMpQV072obF74HCgP4XJeJYoNuRSX2+8= +github.com/stripe/stripe-go/v82 v82.5.1/go.mod h1:majCQX6AfObAvJiHraPi/5udwHi4ojRvJnnxckvHrX8= github.com/tdewolff/minify/v2 v2.24.13 h1:xrcF7gKDnUszseEY9WX9mUlZII2v2Go/QAcAwRASw58= github.com/tdewolff/minify/v2 v2.24.13/go.mod h1:emvwoYeIl8bfAKqRU5ww95LX9Gpggpqv/naal9a8Yq0= github.com/tdewolff/parse/v2 v2.8.12 h1:5BBjfaCv482v3nltlS0u6wH1xJaxjR6ofDrWttNvROg= github.com/tdewolff/parse/v2 v2.8.12/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= +github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= +github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= @@ -42,10 +44,10 @@ golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main.go b/main.go index 88e8b51..e90a60a 100644 --- a/main.go +++ b/main.go @@ -207,9 +207,15 @@ func main() { if len(os.Args) > 1 { return } - server := http.ServeMux{} - server.Handle("/", http.FileServer(http.Dir("www"))) - server.HandleFunc("POST /order/", orders.OrderHandler) + + orders.InitStripe(os.Getenv("STRIPE_SECRET_KEY")) + + mux := http.ServeMux{} + + mux.Handle("/", http.FileServer(http.Dir("www"))) + mux.HandleFunc("POST /order/", orders.OrderHandler) + mux.HandleFunc("POST /webhook", orders.WebhookHandler(os.Getenv("STRIPE_WEBHOOK_SECRET"))) + log.Println("listening on :8000") - log.Fatal(http.ListenAndServe(":8000", &server)) + log.Fatal(http.ListenAndServe(":8000", &mux)) } diff --git a/orders/order.go b/orders/order.go index 50ccc7e..5a1e317 100644 --- a/orders/order.go +++ b/orders/order.go @@ -1,15 +1,27 @@ package orders import ( + "crypto/rand" + "encoding/hex" + "encoding/json" "errors" "fmt" + "io" "log" "math" "net/http" "piekarnikgosipl/data" "strconv" + "strings" + "unicode" + + "github.com/stripe/stripe-go/v82" + "github.com/stripe/stripe-go/v82/checkout/session" + "github.com/stripe/stripe-go/v82/webhook" ) +const retainer = 5000 // 50zł + type listItem struct { id string count int // count multiplied by 100 to allow for half item purchases @@ -21,6 +33,8 @@ type order struct { items []listItem } +func InitStripe(secret string) { stripe.Key = secret } + func parseFloat(input string) (int, error) { val, err := strconv.ParseFloat(input, 64) if err != nil { @@ -37,25 +51,31 @@ func parseFloat(input string) (int, error) { } return int(math.Round(val * 100)), nil +} +func validName(name string) bool { + if name == "" || len(name) > 100 { + return false + } + return strings.IndexFunc(name, unicode.IsControl) == -1 } func parseOrder(r *http.Request) (order, error) { var o order var err error var errorList []error - o.name = r.PostFormValue("name") - if o.name == "" { + o.name = strings.TrimSpace(r.PostFormValue("name")) + if !validName(o.name) { errorList = append(errorList, fmt.Errorf("Invalid name")) } - o.phone, err = parseAndValidatePolishMobile(r.PostFormValue("phone")) + o.phone, err = parsePhone(r.PostFormValue("phone")) if err != nil { errorList = append(errorList, err) } for _, item := range data.Swieta { count, err := parseFloat(r.PostFormValue(item.Id)) if err != nil { - errorList = append(errorList, fmt.Errorf("Can't parse the count for %v: %e", item.Name, err)) + errorList = append(errorList, fmt.Errorf("Can't parse the count for %v: %v", item.Name, err)) continue } if count%10 == 5 && !item.Halveable { @@ -67,8 +87,89 @@ func parseOrder(r *http.Request) (order, error) { return o, errors.Join(errorList...) } +func newOrderID() string { + b := make([]byte, 8) + rand.Read(b) + return hex.EncodeToString(b) +} + +func startCheckout(w http.ResponseWriter, r *http.Request, o order) { + id := newOrderID() + log.Println("ORDER_PENDING", id, o) + + s, err := session.New(&stripe.CheckoutSessionParams{ + Mode: stripe.String("payment"), + ClientReferenceID: stripe.String(id), + Locale: stripe.String("pl"), + LineItems: []*stripe.CheckoutSessionLineItemParams{{ + Quantity: stripe.Int64(1), + PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{ + Currency: stripe.String("pln"), + UnitAmount: stripe.Int64(retainer), + ProductData: &stripe.CheckoutSessionLineItemPriceDataProductDataParams{ + Name: stripe.String("Zaliczka na zamówienie " + id), + }, + }, + }}, + SuccessURL: stripe.String("https://piekarnikgosi.pl/dziekujemy"), + CancelURL: stripe.String("https://piekarnikgosi.pl/"), + }) + if err != nil { + log.Println("ORDER_STRIPE_ERROR", id, err) + http.Error(w, "payment error", http.StatusInternalServerError) + return + } + if r.Header.Get("HX-Request") == "true" { + w.Header().Set("HX-Redirect", s.URL) + w.WriteHeader(http.StatusOK) + return + } + http.Redirect(w, r, s.URL, http.StatusSeeOther) +} + func OrderHandler(w http.ResponseWriter, r *http.Request) { o, err := parseOrder(r) - log.Println(o, err) - fmt.Fprintf(w, "%v, %v", o, err) + if err != nil { + log.Println("ORDER_INVALID", o, err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + startCheckout(w, r, o) +} + +func WebhookHandler(secret string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 65536)) + if err != nil { + http.Error(w, "bad body", http.StatusBadRequest) + return + } + event, err := webhook.ConstructEventWithOptions(body, r.Header.Get("Stripe-Signature"), secret, + webhook.ConstructEventOptions{IgnoreAPIVersionMismatch: true}) + if err != nil { + http.Error(w, "bad signature", http.StatusBadRequest) + return + } + + if event.Type != "checkout.session.completed" && event.Type != "checkout.session.async_payment_succeeded" { + w.WriteHeader(http.StatusOK) + return + } + + var s stripe.CheckoutSession + if err := json.Unmarshal(event.Data.Raw, &s); err != nil { + http.Error(w, "bad event", http.StatusBadRequest) + return + } + + switch { + case s.PaymentStatus != stripe.CheckoutSessionPaymentStatusPaid: + log.Println("ORDER_NOT_YET_PAID", s.ClientReferenceID, s.ID, s.PaymentStatus) + case s.AmountTotal != retainer || s.Currency != stripe.CurrencyPLN: + log.Println("ORDER_PAYMENT_MISMATCH", s.ClientReferenceID, s.ID, s.AmountTotal, s.Currency) + default: + log.Println("ORDER_PAID", s.ClientReferenceID, s.ID) + } + w.WriteHeader(http.StatusOK) + } } diff --git a/orders/phone_validation.go b/orders/phone_validation.go index eff5c8e..ed6bbb1 100644 --- a/orders/phone_validation.go +++ b/orders/phone_validation.go @@ -29,7 +29,7 @@ func isValidPolishMobile(nineDigitNum string) bool { return validPrefixes[prefix2] } -func parseAndValidatePolishMobile(input string) (string, error) { +func parsePhone(input string) (string, error) { input = strings.TrimSpace(input) if strings.HasPrefix(input, "+48") {