- Add Square payment integration (mock + handlers + UI): terminal/online payments, refunds, tips, saved cards, webhooks. Build-tagged dev/prod clients. - Redesign booking flow: Step 4 conditional (deposit only), Step 5 confirmation screen with booking ID, auto-submit on transition. - Redesign schedule modal: 2x3 button grid with Pay Deposit/Pay Early logic. - Add deposit warning banner at Step 1 for users with outstanding deposits. - Fix weekday conversion bug: Go 0=Sunday vs DB 0=Monday mismatch in 6 locations. - Fix timezone bug: UTC vs London time in closing hours validation. - Fix frontend error parsing: plain text backend errors now displayed correctly. - Fix crypto.randomUUID fallback for environments without Web Crypto. - Add 7 new regression tests: closing hours, advance check, active booking limit, weekday conversion, UTC/London, deposit snapshot, exceptional hours. - Fix 3 flaky tests: dynamic dates instead of fixed, no-show timing.
79 lines
2.1 KiB
Go
79 lines
2.1 KiB
Go
package webhooks
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
)
|
|
|
|
type SquareWebhookEvent struct {
|
|
Type string `json:"type"`
|
|
EventID string `json:"event_id"`
|
|
CreatedAt string `json:"created_at"`
|
|
Data json.RawMessage `json:"data"`
|
|
LocationID string `json:"location_id"`
|
|
}
|
|
|
|
func HandleSquareWebhook(w http.ResponseWriter, r *http.Request) {
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
log.Printf("Failed to read webhook body: %v", err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer r.Body.Close()
|
|
|
|
signature := r.Header.Get("x-square-signature")
|
|
signingKey := os.Getenv("SQUARE_WEBHOOK_SIGNATURE_KEY")
|
|
|
|
if signingKey != "" && signature != "" {
|
|
if !verifySquareSignature(body, signature, signingKey) {
|
|
log.Printf("Invalid Square webhook signature")
|
|
http.Error(w, "Invalid signature", http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
var event SquareWebhookEvent
|
|
if err := json.Unmarshal(body, &event); err != nil {
|
|
log.Printf("Failed to parse webhook event: %v", err)
|
|
http.Error(w, "Invalid event", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
log.Printf("[SQUARE-WEBHOOK] Received event: %s", event.Type)
|
|
|
|
switch event.Type {
|
|
case "payment.updated":
|
|
handlePaymentUpdated(event.Data)
|
|
case "refund.updated":
|
|
handleRefundUpdated(event.Data)
|
|
case "dispute.created":
|
|
log.Printf("[SQUARE-WEBHOOK] Dispute created: %s", event.EventID)
|
|
default:
|
|
log.Printf("[SQUARE-WEBHOOK] Unknown event type: %s", event.Type)
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("ok"))
|
|
}
|
|
|
|
func verifySquareSignature(body []byte, signature, signingKey string) bool {
|
|
mac := hmac.New(sha256.New, []byte(signingKey))
|
|
mac.Write(body)
|
|
expected := hex.EncodeToString(mac.Sum(nil))
|
|
return hmac.Equal([]byte(signature), []byte(expected))
|
|
}
|
|
|
|
func handlePaymentUpdated(data json.RawMessage) {
|
|
log.Printf("[SQUARE-WEBHOOK] payment.updated: %s", string(data))
|
|
}
|
|
|
|
func handleRefundUpdated(data json.RawMessage) {
|
|
log.Printf("[SQUARE-WEBHOOK] refund.updated: %s", string(data))
|
|
} |