From 9e16843a29e1ea4029eb2b19e61a521c1b49aa3a Mon Sep 17 00:00:00 2001 From: bronku Date: Mon, 9 Nov 2026 16:29:52 +0100 Subject: [PATCH] dev env --- .gitignore | 1 + .zed/tasks.json | 11 +- css/swieta.css | 4 +- dev.sh | 29 ++++ main.go | 4 +- ...one_validation.go => field_validations.go} | 28 ++++ orders/order.go | 125 ++---------------- orders/stripe.go | 99 ++++++++++++++ 8 files changed, 178 insertions(+), 123 deletions(-) create mode 100755 dev.sh rename orders/{phone_validation.go => field_validations.go} (65%) create mode 100644 orders/stripe.go diff --git a/.gitignore b/.gitignore index 33c1459..f4fa39b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ www .DS_Store *_templ.go piekarnikgosi +.env diff --git a/.zed/tasks.json b/.zed/tasks.json index e917514..507be6c 100644 --- a/.zed/tasks.json +++ b/.zed/tasks.json @@ -1,7 +1,16 @@ [ { "label": "Run", - "command": "go tool templ generate && go run .", + "command": "./dev.sh", + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "shell": "system", + "save": "all", + }, + { + "label": "Build Static Site", + "command": "go tool templ generate && go run . -- build", "use_new_terminal": false, "allow_concurrent_runs": false, "reveal": "always", diff --git a/css/swieta.css b/css/swieta.css index c7b2123..80f18f1 100644 --- a/css/swieta.css +++ b/css/swieta.css @@ -4,8 +4,8 @@ --swieta-red: oklch(45% .15 25); --border-color: color-mix(in oklab, var(--swieta-green) 25%, transparent); - --swieta-pad: 2rem; /* outer: section padding, gaps between cards */ - --swieta-card-pad: 2rem; /* inner: padding inside cards and boxes */ + --swieta-pad: 2rem; + --swieta-card-pad: 2rem; } @media (max-width: 768px) { diff --git a/dev.sh b/dev.sh new file mode 100755 index 0000000..d408417 --- /dev/null +++ b/dev.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +exec 3< <(stripe listen --events checkout.session.completed,checkout.session.async_payment_succeeded --forward-to localhost:8000/webhook 2>&1) +CHILD_PID=$! +trap "kill -TERM $CHILD_PID 2>/dev/null; exec 3<&-" EXIT + +if [ -f .env ]; then + export $(cat .env | xargs) +fi +export DEV_ENV="TRUE" +export WEBHOOK_SECRET="" + +while IFS= read -r line <&3; do + matched_value=$(echo "$line" | grep -oP 'whsec_[a-zA-Z0-9]+') + if [ -n "$matched_value" ]; then + WEBHOOK_SECRET=$matched_value + break + fi +done + +exec 3<&- + +if [ -z "$WEBHOOK_SECRET" ]; then + echo "Error: can't get the webhook secret" + exit 1 +fi + +go tool templ generate +go run . diff --git a/main.go b/main.go index c46e93b..17b5ab9 100644 --- a/main.go +++ b/main.go @@ -210,13 +210,11 @@ func main() { return } - orders.InitStripe(os.Getenv("STRIPE_SECRET_KEY")) - mux := http.ServeMux{} mux.Handle("/", http.FileServer(http.Dir("www"))) mux.Handle("POST /order", orders.Limit(http.HandlerFunc(orders.OrderHandler), 10*time.Second, 5)) - mux.HandleFunc("POST /webhook", orders.WebhookHandler(os.Getenv("STRIPE_WEBHOOK_SECRET"))) + mux.HandleFunc("POST /webhook", orders.WebhookHandler) log.Println("listening on :8000") log.Fatal(http.ListenAndServe(":8000", &mux)) diff --git a/orders/phone_validation.go b/orders/field_validations.go similarity index 65% rename from orders/phone_validation.go rename to orders/field_validations.go index df8ea3c..b89ab47 100644 --- a/orders/phone_validation.go +++ b/orders/field_validations.go @@ -2,8 +2,11 @@ package orders import ( "fmt" + "math" "regexp" + "strconv" "strings" + "unicode" ) func isValidPolishMobile(nineDigitNum string) bool { @@ -55,3 +58,28 @@ func parsePhone(input string) (string, error) { return cleaned, nil } + +func parseFloat(input string) (int, error) { + val, err := strconv.ParseFloat(input, 64) + if err != nil { + return 0, fmt.Errorf("Nieprawidłowa wartość") + } + + if val < 0 || val > 100 { + return 0, fmt.Errorf("Wartość musi być między 0 a 100") + } + + doubled := val * 2 + if math.Abs(doubled-math.Round(doubled)) > 1e-9 { + return 0, fmt.Errorf("Wartość musi być wielokrotnością 0,5") + } + + return int(math.Round(val * 10)), nil +} + +func validName(name string) bool { + if name == "" || len(name) > 100 { + return false + } + return strings.IndexFunc(name, unicode.IsControl) == -1 +} diff --git a/orders/order.go b/orders/order.go index 7744509..fb5ee9b 100644 --- a/orders/order.go +++ b/orders/order.go @@ -3,61 +3,31 @@ 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 + count int // count multiplied by 10 to allow for half item purchases } type order struct { + id string name string phone string items []listItem } -func InitStripe(secret string) { stripe.Key = secret } - -func parseFloat(input string) (int, error) { - val, err := strconv.ParseFloat(input, 64) - if err != nil { - return 0, fmt.Errorf("Nieprawidłowa wartość") - } - - if val < 0 || val > 100 { - return 0, fmt.Errorf("Wartość musi być między 0 a 100") - } - - doubled := val * 2 - if math.Abs(doubled-math.Round(doubled)) > 1e-9 { - return 0, fmt.Errorf("Wartość musi być wielokrotnością 0,5") - } - - 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 newOrderID() string { + b := make([]byte, 8) + rand.Read(b) + return hex.EncodeToString(b) } func parseOrder(r *http.Request) (order, error) { @@ -90,55 +60,13 @@ func parseOrder(r *http.Request) (order, error) { } total += count * price } - if total < 5000 { + if total < 500 { errorList = append(errorList, fmt.Errorf("Minimalna wartość zamówienia wynosi 50zł")) } + o.id = newOrderID() 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) - - url := r.URL.RequestURI() - log.Println(url) - - 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?order=" + id), - CancelURL: stripe.String("https://piekarnikgosi.pl/"), - }) - if err != nil { - log.Println("ORDER_STRIPE_ERROR", id, err) - writeErrors(w, r, http.StatusInternalServerError, "Nie udało się przejść do płatności. Spróbuj ponownie lub zadzwoń: 690 214 515.") - 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) { r.Body = http.MaxBytesReader(w, r.Body, 16<<10) if err := r.ParseForm(); err != nil { @@ -153,40 +81,3 @@ func OrderHandler(w http.ResponseWriter, r *http.Request) { } 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/stripe.go b/orders/stripe.go new file mode 100644 index 0000000..8ce7ac2 --- /dev/null +++ b/orders/stripe.go @@ -0,0 +1,99 @@ +package orders + +import ( + "encoding/json" + "io" + "log" + "net/http" + "os" + + "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ł + +var siteUrl = "https://piekarnikgosi.pl" +var webhookSecret = "" + +func init() { + if os.Getenv("DEV_ENV") == "TRUE" { + siteUrl = "http://localhost:8000" + } + log.Println("the url is: ", siteUrl) + stripe.Key = os.Getenv("STRIPE_SECRET_KEY") + webhookSecret = os.Getenv("WEBHOOK_SECRET") +} + +func startCheckout(w http.ResponseWriter, r *http.Request, o order) { + log.Println("ORDER_PENDING", o) + + url := r.URL.RequestURI() + log.Println(url) + + s, err := session.New(&stripe.CheckoutSessionParams{ + Mode: stripe.String("payment"), + ClientReferenceID: stripe.String(o.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 " + o.id), + }, + }, + }}, + SuccessURL: stripe.String(siteUrl + "/dziekujemy?order=" + o.id), + CancelURL: stripe.String(siteUrl), + }) + if err != nil { + log.Println("ORDER_STRIPE_ERROR", o.id, err) + writeErrors(w, r, http.StatusInternalServerError, "Nie udało się przejść do płatności. Spróbuj ponownie lub zadzwoń: 690 214 515.") + 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 WebhookHandler(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"), webhookSecret, + 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) + +}