mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
dispute.state.updated and dispute.state.changed both normalize to dispute.updated and both are clawback events. A dispute report carries the dispute's version and updated_at, and dispute.Report (embedded in the invoice and in the wallet payment's record, models/paymentorg) keeps the latest: a report older by version, else by updated_at, else by phase (inquiry, chargeback, decided), changes nothing, and WON stays final. An inquiry holds no funds: it takes nothing from a wallet, and on an invoice it is recorded while the period stays paid. A chargeback (EVIDENCE_REQUIRED, PROCESSING), or one lost or accepted, takes a wallet payment back once and unpays a period; a chargeback won gives the wallet payment back and restores the period. Co-authored-by: Hanzo Dev <[email protected]>
81 lines
2.6 KiB
Go
81 lines
2.6 KiB
Go
package square
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"testing"
|
|
)
|
|
|
|
// squareSign reproduces Square's documented webhook signing: base64 of
|
|
// HMAC-SHA256 over (notificationURL + rawBody) keyed by the signature key.
|
|
// See https://developer.squareup.com/docs/webhooks/step3validate
|
|
func squareSign(key, notificationURL string, body []byte) string {
|
|
mac := hmac.New(sha256.New, []byte(key))
|
|
mac.Write([]byte(notificationURL))
|
|
mac.Write(body)
|
|
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
func TestValidateWebhook_SignsOverURLPlusBody(t *testing.T) {
|
|
const (
|
|
key = "test-signature-key"
|
|
url = "https://commerce.hanzo.ai/v1/billing/webhooks/square"
|
|
)
|
|
body := []byte(`{"event_id":"evt_1","type":"payment.updated","data":{"type":"payment","id":"pay_1","object":{"id":"pay_1","status":"COMPLETED"}}}`)
|
|
|
|
sp := NewProcessor(Config{
|
|
AccessToken: "tok",
|
|
LocationID: "loc",
|
|
WebhookSecret: key,
|
|
WebhookURL: url,
|
|
Environment: "production",
|
|
})
|
|
|
|
// Correct signature (URL + body) must validate.
|
|
good := squareSign(key, url, body)
|
|
evt, err := sp.ValidateWebhook(nil, body, good)
|
|
if err != nil {
|
|
t.Fatalf("valid signature rejected: %v", err)
|
|
}
|
|
if evt == nil || evt.ID != "evt_1" || evt.Type != "payment.updated" {
|
|
t.Fatalf("unexpected parsed event: %+v", evt)
|
|
}
|
|
|
|
// Body-only signature (the old, wrong algorithm) must NOT validate —
|
|
// this is the regression guard for the real-callback bug.
|
|
bodyOnly := squareSign(key, "", body)
|
|
if _, err := sp.ValidateWebhook(nil, body, bodyOnly); err == nil {
|
|
t.Fatal("body-only signature was accepted; URL prefix not enforced")
|
|
}
|
|
|
|
// Wrong URL must NOT validate.
|
|
wrongURL := squareSign(key, "https://evil.example/webhook", body)
|
|
if _, err := sp.ValidateWebhook(nil, body, wrongURL); err == nil {
|
|
t.Fatal("signature for wrong URL was accepted")
|
|
}
|
|
}
|
|
|
|
func TestValidateWebhook_MissingURLFailsClosed(t *testing.T) {
|
|
sp := NewProcessor(Config{
|
|
AccessToken: "tok",
|
|
LocationID: "loc",
|
|
WebhookSecret: "k",
|
|
// WebhookURL intentionally empty.
|
|
Environment: "production",
|
|
})
|
|
if _, err := sp.ValidateWebhook(nil, []byte(`{}`), "anything"); err == nil {
|
|
t.Fatal("expected failure when webhook URL is unconfigured")
|
|
}
|
|
}
|
|
|
|
// Square names a dispute's change of state dispute.state.updated, and once named it
|
|
// dispute.state.changed; both are the one normalized dispute.updated.
|
|
func TestMapSquareEventType_DisputeStateBothNames(t *testing.T) {
|
|
for _, sq := range []string{"dispute.state.updated", "dispute.state.changed"} {
|
|
if got := mapSquareEventType(sq); got != "dispute.updated" {
|
|
t.Fatalf("mapSquareEventType(%q) = %q, want dispute.updated", sq, got)
|
|
}
|
|
}
|
|
}
|