diff --git a/api/api.go b/api/api.go index 8fa1cd6d..206399c0 100644 --- a/api/api.go +++ b/api/api.go @@ -9,6 +9,7 @@ import ( "github.com/String-xyz/string-api/api/handler" "github.com/String-xyz/string-api/api/middleware" + "github.com/String-xyz/string-api/config" "github.com/jmoiron/sqlx" "github.com/labstack/echo/v4" @@ -117,5 +118,5 @@ func cardRoute(services service.Services, e *echo.Echo) { func webhookRoute(services service.Services, e *echo.Echo) { handler := handler.NewWebhook(e, services.Webhook) - handler.RegisterRoutes(e.Group("/webhooks"), middleware.VerifyWebhookPayload()) + handler.RegisterRoutes(e.Group("/webhooks"), middleware.VerifyWebhookPayload(config.Var.PERSONA_WEBHOOK_SECRET_KEY, config.Var.CHECKOUT_WEBHOOK_SECRET_KEY)) } diff --git a/api/handler/webhook.go b/api/handler/webhook.go index af397ab8..63423170 100644 --- a/api/handler/webhook.go +++ b/api/handler/webhook.go @@ -11,7 +11,8 @@ import ( ) type Webhook interface { - Handle(c echo.Context) error + HandleCheckout(c echo.Context) error + HandlePersona(c echo.Context) error RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) } @@ -24,22 +25,38 @@ func NewWebhook(route *echo.Echo, service service.Webhook) Webhook { return &webhook{service, nil} } -func (w webhook) Handle(c echo.Context) error { +func (w webhook) HandleCheckout(c echo.Context) error { cxt := c.Request().Context() body, err := io.ReadAll(c.Request().Body) if err != nil { return httperror.BadRequest400(c, "Failed to read body") } - err = w.service.Handle(cxt, body) + err = w.service.Handle(cxt, body, service.WebhookTypeCheckout) if err != nil { - return httperror.Internal500(c, "Failed to handle webhook") + return httperror.Internal500(c, "Failed to handle checkout webhook") + } + + return nil +} + +func (w webhook) HandlePersona(c echo.Context) error { + cxt := c.Request().Context() + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return httperror.BadRequest400(c, "Failed to read body") + } + + err = w.service.Handle(cxt, body, service.WebhookTypePersona) + if err != nil { + return httperror.Internal500(c, "Failed to handle persona webhook") } return nil } func (w webhook) RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) { - g.POST("/checkout", w.Handle, ms...) w.group = g + g.POST("/checkout", w.HandleCheckout, ms...) + g.POST("/persona", w.HandlePersona, ms...) } diff --git a/api/middleware/middleware.go b/api/middleware/middleware.go index d7a83ee3..8c802191 100644 --- a/api/middleware/middleware.go +++ b/api/middleware/middleware.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "encoding/hex" "io" + "strings" libcommon "github.com/String-xyz/go-lib/v2/common" "github.com/String-xyz/go-lib/v2/httperror" @@ -73,7 +74,6 @@ func APIKeySecretAuth(service service.Auth) echo.MiddlewareFunc { // TODO: Validate platformId c.Set("platformId", platformId) - return true, nil }, } @@ -102,13 +102,27 @@ func Georestrict(service service.Geofencing) echo.MiddlewareFunc { } } -func VerifyWebhookPayload() echo.MiddlewareFunc { - secretKey := config.Var.WEBHOOK_SECRET_KEY - +func VerifyWebhookPayload(pskey string, ckoskey string) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - signatureHeader := c.Request().Header.Get("Cko-Signature") + var signatureHeaderName string + var secretKey string + var validateFunc func([]byte, string, string) bool + + switch c.Path() { + case "/webhooks/checkout": + signatureHeaderName = "Cko-Signature" + secretKey = ckoskey + validateFunc = validateSignatureCheckout + case "/webhooks/persona": + signatureHeaderName = "Persona-Signature" + secretKey = pskey + validateFunc = validateSignaturePersona + default: + return httperror.BadRequest400(c, "Invalid path") + } + signatureHeader := c.Request().Header.Get(signatureHeaderName) body, err := io.ReadAll(c.Request().Body) if err != nil { return httperror.BadRequest400(c, "Failed to read body") @@ -116,16 +130,7 @@ func VerifyWebhookPayload() echo.MiddlewareFunc { c.Request().Body = io.NopCloser(bytes.NewBuffer(body)) - mac := hmac.New(sha256.New, []byte(secretKey)) - mac.Write(body) - expectedMAC := mac.Sum(nil) - - receivedMAC, err := hex.DecodeString(signatureHeader) - if err != nil { - return httperror.BadRequest400(c, "Failed to decode signature") - } - - if !hmac.Equal(receivedMAC, expectedMAC) { + if !validateFunc(body, signatureHeader, secretKey) { return httperror.Unauthorized401(c, "Failed to verify payload") } @@ -133,3 +138,41 @@ func VerifyWebhookPayload() echo.MiddlewareFunc { } } } + +func validateSignatureCheckout(body []byte, signature string, secretKey string) bool { + mac := hmac.New(sha256.New, []byte(secretKey)) + mac.Write(body) + expectedMAC := mac.Sum(nil) + + receivedMAC, err := hex.DecodeString(signature) + if err != nil { + return false + } + + return hmac.Equal(receivedMAC, expectedMAC) +} + +func validateSignaturePersona(body []byte, signatureHeader string, secretKey string) bool { + parts := strings.Split(signatureHeader, ",") + var timestamp, signature string + for _, part := range parts { + if strings.HasPrefix(part, "t=") { + timestamp = strings.TrimPrefix(part, "t=") + } else if strings.HasPrefix(part, "v1=") { + signature = strings.TrimPrefix(part, "v1=") + } + } + + macData := timestamp + "." + string(body) + + mac := hmac.New(sha256.New, []byte(secretKey)) + mac.Write([]byte(macData)) + expectedMAC := mac.Sum(nil) + + receivedMAC, err := hex.DecodeString(signature) + if err != nil { + return false + } + + return hmac.Equal(expectedMAC, receivedMAC) +} diff --git a/api/middleware/middleware_test.go b/api/middleware/middleware_test.go index 2a547057..82f66a19 100644 --- a/api/middleware/middleware_test.go +++ b/api/middleware/middleware_test.go @@ -1,90 +1,88 @@ package middleware import ( - "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" + "fmt" "net/http" "net/http/httptest" + "strings" "testing" - env "github.com/String-xyz/go-lib/v2/config" - "github.com/String-xyz/string-api/config" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) -func init() { - env.LoadEnv(&config.Var, "../../.env") -} - func TestVerifyWebhookPayload(t *testing.T) { - secretKey := config.Var.WEBHOOK_SECRET_KEY + checkoutSecretKey := "checkout_secret_key" + personaSecretKey := "persona_secret_key" - // We'll test with two cases tests := []struct { - name string - giveBody []byte - giveMAC string - wantHTTPCode int + name string + path string + signatureKey string + signatureName string + secretKey string + expectCode int }{ { - // This test case provides a valid body and MAC - name: "Valid MAC", - giveBody: []byte("Hello, World!"), - giveMAC: ComputeMAC([]byte("Hello, World!"), secretKey), - wantHTTPCode: http.StatusOK, + name: "Test unauthorized access due to invalid signature for Checkout", + path: "/webhooks/checkout", + signatureKey: "invalid_signature", + signatureName: "Cko-Signature", + secretKey: checkoutSecretKey, + expectCode: http.StatusUnauthorized, + }, + { + name: "Test successful access for Checkout", + path: "/webhooks/checkout", + signatureKey: computeHmacSha256("hello", checkoutSecretKey), + signatureName: "Cko-Signature", + secretKey: checkoutSecretKey, + expectCode: http.StatusOK, + }, + { + name: "Test unauthorized access due to invalid signature for Persona", + path: "/webhooks/persona", + signatureKey: "t=1629478952,v1=invalid_signature", + signatureName: "Persona-Signature", + secretKey: personaSecretKey, + expectCode: http.StatusUnauthorized, }, { - // This test case provides an invalid MAC - name: "Invalid MAC", - giveBody: []byte("Hello, World!"), - giveMAC: ComputeMAC([]byte("Bye, World!"), secretKey), - wantHTTPCode: http.StatusUnauthorized, + name: "Test successful access for Persona", + path: "/webhooks/persona", + signatureKey: fmt.Sprintf("t=1629478952,v1=%s", computeHmacSha256("1629478952.hello", personaSecretKey)), + signatureName: "Persona-Signature", + secretKey: personaSecretKey, + expectCode: http.StatusOK, }, } - // Let's iterate over our test cases for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Instantiate Echo e := echo.New() - - // Our middleware under test - middleware := VerifyWebhookPayload() - - // Mock a request - req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(tt.giveBody)) - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - req.Header.Set("Cko-Signature", tt.giveMAC) - - // Mock a response recorder + req := httptest.NewRequest(echo.POST, "/", strings.NewReader("hello")) + req.Header.Set(tt.signatureName, tt.signatureKey) rec := httptest.NewRecorder() - - // Create a context for our request c := e.NewContext(req, rec) + c.SetPath(tt.path) - // Mock a next function - next := func(c echo.Context) error { - return c.String(http.StatusOK, "OK") - } - - // Call our middleware - err := middleware(next)(c) - - // There should be no error returned - assert.NoError(t, err) + middleware := VerifyWebhookPayload(personaSecretKey, checkoutSecretKey) + middleware(func(c echo.Context) error { + return c.String(http.StatusOK, "Test") + })(c) - // Check if the status code is what we expect - assert.Equal(t, tt.wantHTTPCode, rec.Code) + assert.Equal(t, tt.expectCode, rec.Code) }) } } -// Helper function to compute the MAC of a given body and secret -func ComputeMAC(body []byte, secret string) string { - mac := hmac.New(sha256.New, []byte(secret)) - mac.Write(body) - return hex.EncodeToString(mac.Sum(nil)) +// Utility function to compute HMAC for testing +func computeHmacSha256(message string, secret string) string { + key := []byte(secret) + h := hmac.New(sha256.New, key) + h.Write([]byte(message)) + return hex.EncodeToString(h.Sum(nil)) } diff --git a/config/config.go b/config/config.go index 92c12ed2..c0cd7079 100644 --- a/config/config.go +++ b/config/config.go @@ -1,56 +1,57 @@ package config type vars struct { - AWS_ACCT string `required:"false"` - AWS_ACCESS_KEY_ID string `required:"false"` - AWS_SECRET_ACCESS_KEY string `required:"false"` - DEBUG_MODE string `required:"false"` - SERVICE_NAME string `required:"false"` - STRING_HOTWALLET_ADDRESS string `required:"false"` - CARD_FAIL_PROBABILITY string `required:"false"` - BASE_URL string `required:"true"` - ENV string `required:"true"` - PORT string `required:"true"` - COINCAP_API_URL string `required:"true"` - COINGECKO_API_URL string `required:"true"` - OWLRACLE_API_URL string `required:"true"` - OWLRACLE_API_KEY string `required:"true"` - OWLRACLE_API_SECRET string `required:"true"` - AWS_REGION string `required:"true"` - AWS_KMS_KEY_ID string `required:"true"` - CHECKOUT_PUBLIC_KEY string `required:"true"` - CHECKOUT_SECRET_KEY string `required:"true"` - CHECKOUT_ENV string `required:"true"` - EVM_PRIVATE_KEY string `required:"true"` - DB_NAME string `required:"true"` - DB_USERNAME string `required:"true"` - DB_PASSWORD string `required:"true"` - DB_HOST string `required:"true"` - DB_PORT string `required:"true"` - REDIS_PASSWORD string `required:"true"` - REDIS_HOST string `required:"true"` - REDIS_PORT string `required:"true"` - JWT_SECRET_KEY string `required:"true"` - UNIT21_API_KEY string `required:"true"` - UNIT21_ENV string `required:"true"` - UNIT21_ORG_NAME string `required:"true"` - UNIT21_RTR_URL string `required:"true"` - TWILIO_ACCOUNT_SID string `required:"true"` - TWILIO_AUTH_TOKEN string `required:"true"` - TWILIO_SMS_SID string `required:"true"` - TEAM_PHONE_NUMBERS string `required:"true"` - STRING_ENCRYPTION_KEY string `required:"true"` - SENDGRID_API_KEY string `required:"true"` - FINGERPRINT_API_KEY string `required:"true"` - FINGERPRINT_API_URL string `required:"true"` - STRING_INTERNAL_ID string `required:"true"` - STRING_WALLET_ID string `required:"true"` - STRING_BANK_ID string `required:"true"` - AUTH_EMAIL_ADDRESS string `required:"true"` - RECEIPTS_EMAIL_ADDRESS string `required:"true"` - WEBHOOK_SECRET_KEY string `required:"true"` - SLACK_WEBHOOK_URL string `required:"true"` - PERSONA_API_KEY string `required:"true"` + AWS_ACCT string `required:"false"` + AWS_ACCESS_KEY_ID string `required:"false"` + AWS_SECRET_ACCESS_KEY string `required:"false"` + DEBUG_MODE string `required:"false"` + SERVICE_NAME string `required:"false"` + STRING_HOTWALLET_ADDRESS string `required:"false"` + CARD_FAIL_PROBABILITY string `required:"false"` + BASE_URL string `required:"true"` + ENV string `required:"true"` + PORT string `required:"true"` + COINCAP_API_URL string `required:"true"` + COINGECKO_API_URL string `required:"true"` + OWLRACLE_API_URL string `required:"true"` + OWLRACLE_API_KEY string `required:"true"` + OWLRACLE_API_SECRET string `required:"true"` + AWS_REGION string `required:"true"` + AWS_KMS_KEY_ID string `required:"true"` + CHECKOUT_PUBLIC_KEY string `required:"true"` + CHECKOUT_WEBHOOK_SECRET_KEY string `required:"true"` + CHECKOUT_SECRET_KEY string `required:"true"` + CHECKOUT_ENV string `required:"true"` + EVM_PRIVATE_KEY string `required:"true"` + DB_NAME string `required:"true"` + DB_USERNAME string `required:"true"` + DB_PASSWORD string `required:"true"` + DB_HOST string `required:"true"` + DB_PORT string `required:"true"` + REDIS_PASSWORD string `required:"true"` + REDIS_HOST string `required:"true"` + REDIS_PORT string `required:"true"` + JWT_SECRET_KEY string `required:"true"` + UNIT21_API_KEY string `required:"true"` + UNIT21_ENV string `required:"true"` + UNIT21_ORG_NAME string `required:"true"` + UNIT21_RTR_URL string `required:"true"` + TWILIO_ACCOUNT_SID string `required:"true"` + TWILIO_AUTH_TOKEN string `required:"true"` + TWILIO_SMS_SID string `required:"true"` + TEAM_PHONE_NUMBERS string `required:"true"` + STRING_ENCRYPTION_KEY string `required:"true"` + SENDGRID_API_KEY string `required:"true"` + FINGERPRINT_API_KEY string `required:"true"` + FINGERPRINT_API_URL string `required:"true"` + STRING_INTERNAL_ID string `required:"true"` + STRING_WALLET_ID string `required:"true"` + STRING_BANK_ID string `required:"true"` + AUTH_EMAIL_ADDRESS string `required:"true"` + RECEIPTS_EMAIL_ADDRESS string `required:"true"` + SLACK_WEBHOOK_URL string `required:"true"` + PERSONA_API_KEY string `required:"true"` + PERSONA_WEBHOOK_SECRET_KEY string `required:"true"` } var Var vars diff --git a/pkg/internal/checkout/events.go b/pkg/internal/checkout/event.go similarity index 100% rename from pkg/internal/checkout/events.go rename to pkg/internal/checkout/event.go diff --git a/pkg/internal/checkout/slack.go b/pkg/internal/checkout/slack.go index 7bd4aa93..294e1049 100644 --- a/pkg/internal/checkout/slack.go +++ b/pkg/internal/checkout/slack.go @@ -5,8 +5,9 @@ import ( "encoding/json" "net/http" - "github.com/String-xyz/string-api/config" "github.com/rs/zerolog/log" + + "github.com/String-xyz/string-api/config" ) type SlackMessage struct { diff --git a/pkg/internal/persona/account.go b/pkg/internal/persona/account.go index daf4b313..07c2856f 100644 --- a/pkg/internal/persona/account.go +++ b/pkg/internal/persona/account.go @@ -17,6 +17,10 @@ type Account struct { Attributes AccountAttributes `json:"attributes"` } +func (a Account) GetType() string { + return a.Type +} + type AccountCreate struct { Attributes CommonFields `json:"attributes"` } diff --git a/pkg/internal/persona/event.go b/pkg/internal/persona/event.go new file mode 100644 index 00000000..ae089bc4 --- /dev/null +++ b/pkg/internal/persona/event.go @@ -0,0 +1,74 @@ +package persona + +import ( + "encoding/json" + "time" + + "github.com/String-xyz/go-lib/v2/common" + "github.com/cockroachdb/errors" +) + +type EventType string + +const ( + EventTypeAccountCreated = EventType("account.created") + EventTypeInquiryCreated = EventType("inquiry.created") + EventTypeInquiryStarted = EventType("inquiry.started") + EventTypeInquiryCompleted = EventType("inquiry.completed") + EventTypeVerificationCreated = EventType("verification.created") + EventTypeVerificationPassed = EventType("verification.passed") + EventTypeVerificationFailed = EventType("verification.failed") +) + +type PayloadData interface { + GetType() string +} + +type EventPayload struct { + Data json.RawMessage `json:"data"` +} + +type Event struct { + Id string `json:"id"` + Type string `json:"type"` + Attributes EventAttributes `json:"attributes"` +} + +type EventAttributes struct { + Name EventType `json:"name"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Payload EventPayload `json:"payload"` +} + +func (a EventAttributes) GetType() EventType { + return a.Name +} + +func (a EventAttributes) GetPayloadData() (PayloadData, error) { + switch a.GetType() { + case EventTypeAccountCreated: + var data Account + err := json.Unmarshal(a.Payload.Data, &data) + if err != nil { + return nil, common.StringError(err) + } + return data, nil + case EventTypeInquiryCreated, EventTypeInquiryStarted, EventTypeInquiryCompleted: + var data Inquiry + err := json.Unmarshal(a.Payload.Data, &data) + if err != nil { + return nil, common.StringError(err) + } + return data, nil + case EventTypeVerificationCreated, EventTypeVerificationPassed, EventTypeVerificationFailed: + var data Verification + err := json.Unmarshal(a.Payload.Data, &data) + if err != nil { + return nil, common.StringError(err) + } + return data, nil + default: + return nil, common.StringError(errors.Newf("unknown event type: %s", a.GetType())) + } +} diff --git a/pkg/internal/persona/inquiries.go b/pkg/internal/persona/inquiries.go index 71795e77..1e58fa0c 100644 --- a/pkg/internal/persona/inquiries.go +++ b/pkg/internal/persona/inquiries.go @@ -38,6 +38,10 @@ type Inquiry struct { Relationships Relationships `json:"relationships"` } +func (i Inquiry) GetType() string { + return i.Type +} + type InquiryResponse struct { Data Inquiry `json:"data"` Included []Included `json:"included"` diff --git a/pkg/internal/persona/types.go b/pkg/internal/persona/types.go index 84b804a1..575ffa0b 100644 --- a/pkg/internal/persona/types.go +++ b/pkg/internal/persona/types.go @@ -1,6 +1,8 @@ package persona -import "time" +import ( + "time" +) type IdValue struct { Type string `json:"type"` diff --git a/pkg/internal/persona/verification.go b/pkg/internal/persona/verification.go index 6b8efa93..9681a527 100644 --- a/pkg/internal/persona/verification.go +++ b/pkg/internal/persona/verification.go @@ -39,6 +39,10 @@ type Verification struct { Relationships Relationships `json:"relationships"` } +func (v Verification) GetType() string { + return v.Type +} + type VerificationResponse struct { Data Verification `json:"data"` } diff --git a/pkg/service/persona_webhook.go b/pkg/service/persona_webhook.go new file mode 100644 index 00000000..4b13e116 --- /dev/null +++ b/pkg/service/persona_webhook.go @@ -0,0 +1,67 @@ +package service + +import ( + "context" + "encoding/json" + + "github.com/String-xyz/go-lib/v2/common" + "github.com/cockroachdb/errors" + "github.com/rs/zerolog/log" + + "github.com/String-xyz/string-api/pkg/internal/persona" +) + +type personaWebhook struct{} + +func (p personaWebhook) Handle(ctx context.Context, data []byte) error { + event := persona.Event{} + err := json.Unmarshal(data, &event) + if err != nil { + return common.StringError(errors.Newf("error unmarshalling webhook event: %v", err)) + } + return p.processEvent(ctx, event) +} + +func (p personaWebhook) processEvent(ctx context.Context, event persona.Event) error { + payload, err := event.Attributes.GetPayloadData() + if err != nil { + return common.StringError(errors.Newf("error getting payload data: %v", err)) + } + switch event.Attributes.Name { + case persona.EventTypeAccountCreated: + return p.account(ctx, payload, event.Attributes.Name) + case persona.EventTypeInquiryCreated, persona.EventTypeInquiryStarted, persona.EventTypeInquiryCompleted: + return p.inquiry(ctx, payload, event.Attributes.Name) + case persona.EventTypeVerificationCreated, persona.EventTypeVerificationPassed, persona.EventTypeVerificationFailed: + return p.verification(ctx, payload, event.Attributes.Name) + default: + return common.StringError(errors.Newf("unknown event type: %s", event.Attributes.Name)) + } +} + +func (p personaWebhook) account(ctx context.Context, payload persona.PayloadData, eventType persona.EventType) error { + account, ok := payload.(persona.Account) + if !ok { + return common.StringError(errors.New("error casting payload to account")) + } + log.Info().Interface("account", account).Msg("account event") + return nil +} + +func (p personaWebhook) inquiry(ctx context.Context, payload persona.PayloadData, eventType persona.EventType) error { + inquiry, ok := payload.(persona.Inquiry) + if !ok { + return common.StringError(errors.New("error casting payload to inquiry")) + } + log.Info().Interface("inquiry", inquiry).Msg("inquiry event") + return nil +} + +func (p personaWebhook) verification(ctx context.Context, payload persona.PayloadData, eventType persona.EventType) error { + verification, ok := payload.(persona.Verification) + if !ok { + return common.StringError(errors.New("error casting payload to verification")) + } + log.Info().Interface("verification", verification).Msg("verification event") + return nil +} diff --git a/pkg/service/persona_webhook_test.go b/pkg/service/persona_webhook_test.go new file mode 100644 index 00000000..513a9537 --- /dev/null +++ b/pkg/service/persona_webhook_test.go @@ -0,0 +1,40 @@ +package service + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/String-xyz/string-api/pkg/test/data" +) + +func TestHandle(t *testing.T) { + webhook := personaWebhook{} + + tests := []struct { + name string + json string + err error + }{ + { + name: "Test Account event", + json: data.PersonAccountJSON, + err: nil, + }, + { + name: "Test Inquiry event", + json: data.PersonInquiryJSON, + err: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + err := webhook.Handle(context.Background(), []byte(tt.json)) + + assert.Equal(t, tt.err, err) + }) + } +} diff --git a/pkg/service/webhook.go b/pkg/service/webhook.go index 90e264a3..f3b1de59 100644 --- a/pkg/service/webhook.go +++ b/pkg/service/webhook.go @@ -4,23 +4,36 @@ import ( "encoding/json" "github.com/String-xyz/go-lib/v2/common" - "github.com/String-xyz/string-api/pkg/internal/checkout" "github.com/cockroachdb/errors" "github.com/rs/zerolog/log" "golang.org/x/net/context" + + "github.com/String-xyz/string-api/pkg/internal/checkout" +) + +type WebhookType string + +const ( + WebhookTypePersona WebhookType = "persona" + WebhookTypeCheckout WebhookType = "checkout" ) type Webhook interface { - Handle(ctx context.Context, data []byte) error + Handle(ctx context.Context, data []byte, webhook WebhookType) error } -type webhook struct{} +type webhook struct { + person personaWebhook +} func NewWebhook() Webhook { - return &webhook{} + return &webhook{personaWebhook{}} } -func (w webhook) Handle(ctx context.Context, data []byte) error { +func (w webhook) Handle(ctx context.Context, data []byte, webhook WebhookType) error { + if webhook == WebhookTypePersona { + return w.person.Handle(ctx, data) + } event := checkout.WebhookEvent{} err := json.Unmarshal(data, &event) if err != nil { diff --git a/pkg/service/webhook_test.go b/pkg/service/webhook_test.go index e6216ecb..ef820d76 100644 --- a/pkg/service/webhook_test.go +++ b/pkg/service/webhook_test.go @@ -13,25 +13,25 @@ import ( func TestPaymentAuthorized(t *testing.T) { json := data.AuhorizationApprovedJSON - err := NewWebhook().Handle(context.Background(), []byte(json)) + err := NewWebhook().Handle(context.Background(), []byte(json), WebhookTypeCheckout) assert.NoError(t, err) } func TestPaymentDeclined(t *testing.T) { json := data.AuhorizationDeclinedJSON - err := NewWebhook().Handle(context.Background(), []byte(json)) + err := NewWebhook().Handle(context.Background(), []byte(json), WebhookTypeCheckout) assert.NoError(t, err) } func TestPaymentCaptured(t *testing.T) { json := data.PaymentCapturedJSON - err := NewWebhook().Handle(context.Background(), []byte(json)) + err := NewWebhook().Handle(context.Background(), []byte(json), WebhookTypeCheckout) assert.NoError(t, err) } func TestPaymentApproved(t *testing.T) { json := data.PaymentApprovedJSON - err := NewWebhook().Handle(context.Background(), []byte(json)) + err := NewWebhook().Handle(context.Background(), []byte(json), WebhookTypeCheckout) assert.NoError(t, err) } diff --git a/pkg/test/data/test_data.go b/pkg/test/data/test_data.go index 01ac770d..29a7eb57 100644 --- a/pkg/test/data/test_data.go +++ b/pkg/test/data/test_data.go @@ -217,3 +217,291 @@ var PaymentCapturedJSON = `{ } } }` + +var PersonAccountJSON = ` +{ + "type": "event", + "id": "evt_Ej9ZbvZGjn11CyudXiHqV7tN", + "attributes": { + "name": "account.created", + "created-at": "2023-07-25T19:59:14.691Z", + "redacted-at": null, + "payload": { + "data": { + "type": "account", + "id": "act_ze7eJgwEHbkx1iiUiu42G2ey", + "attributes": { + "reference-id": null, + "created-at": "2023-07-25T19:59:14.000Z", + "updated-at": "2023-07-25T19:59:14.000Z", + "redacted-at": null, + "fields": { + "name": { + "type": "hash", + "value": { + "first": { + "type": "string", + "value": "Mister" + }, + "middle": { + "type": "string", + "value": null + }, + "last": { + "type": "string", + "value": "Tester" + } + } + }, + "address": { + "type": "hash", + "value": { + "street-1": { + "type": "string", + "value": null + }, + "street-2": { + "type": "string", + "value": null + }, + "subdivision": { + "type": "string", + "value": null + }, + "city": { + "type": "string", + "value": null + }, + "postal-code": { + "type": "string", + "value": null + }, + "country-code": { + "type": "string", + "value": null + } + } + }, + "identification-numbers": { + "type": "array", + "value": [] + }, + "birthdate": { + "type": "date", + "value": null + }, + "phone-number": { + "type": "string", + "value": null + }, + "email-address": { + "type": "string", + "value": null + }, + "selfie-photo": { + "type": "file", + "value": null + } + }, + "name-first": "Mister", + "name-middle": null, + "name-last": "Tester", + "phone-number": null, + "email-address": null, + "address-street-1": null, + "address-street-2": null, + "address-city": null, + "address-subdivision": null, + "address-postal-code": null, + "country-code": null, + "birthdate": null, + "social-security-number": null, + "tags": [], + "identification-numbers": {} + } + } + } + } +} +` +var PersonInquiryJSON = ` +{ + "type": "event", + "id": "evt_gEmTS7n2t3hHe4m2UYykLAym", + "attributes": { + "name": "inquiry.created", + "created-at": "2023-07-25T19:32:25.582Z", + "redacted-at": null, + "payload": { + "data": { + "type": "inquiry", + "id": "inq_bo5P7Ea1grrZc68Rg3mpFi2K", + "attributes": { + "status": "created", + "reference-id": null, + "note": null, + "behaviors": { + "request-spoof-attempts": null, + "user-agent-spoof-attempts": null, + "distraction-events": null, + "hesitation-baseline": null, + "hesitation-count": null, + "hesitation-time": null, + "shortcut-copies": null, + "shortcut-pastes": null, + "autofill-cancels": null, + "autofill-starts": null, + "devtools-open": null, + "completion-time": null, + "hesitation-percentage": null, + "behavior-threat-level": null + }, + "tags": [], + "creator": "API", + "reviewer-comment": null, + "created-at": "2023-07-25T19:32:25.000Z", + "started-at": null, + "completed-at": null, + "failed-at": null, + "marked-for-review-at": null, + "decisioned-at": null, + "expired-at": null, + "redacted-at": null, + "previous-step-name": null, + "next-step-name": "start_biometric_80e902_start", + "name-first": null, + "name-middle": null, + "name-last": null, + "birthdate": null, + "address-street-1": null, + "address-street-2": null, + "address-city": null, + "address-subdivision": null, + "address-subdivision-abbr": null, + "address-postal-code": null, + "address-postal-code-abbr": null, + "social-security-number": null, + "identification-number": null, + "email-address": null, + "phone-number": null, + "fields": { + "phone-number": { + "type": "string", + "value": null + }, + "selected-country-code": { + "type": "string", + "value": "US" + }, + "current-government-id": { + "type": "government_id", + "value": null + }, + "selected-id-class": { + "type": "string", + "value": null + }, + "address-street-1": { + "type": "string", + "value": null + }, + "address-street-2": { + "type": "string", + "value": null + }, + "address-city": { + "type": "string", + "value": null + }, + "address-subdivision": { + "type": "string", + "value": null + }, + "address-postal-code": { + "type": "string", + "value": null + }, + "address-country-code": { + "type": "string", + "value": null + }, + "birthdate": { + "type": "date", + "value": null + }, + "email-address": { + "type": "string", + "value": null + }, + "identification-class": { + "type": "string", + "value": null + }, + "identification-number": { + "type": "string", + "value": null + }, + "name-first": { + "type": "string", + "value": null + }, + "name-middle": { + "type": "string", + "value": null + }, + "name-last": { + "type": "string", + "value": null + }, + "current-selfie": { + "type": "selfie", + "value": null + } + } + }, + "relationships": { + "account": { + "data": { + "type": "account", + "id": "act_ndJNqdhWNi44S4Twf4bqzod1" + } + }, + "template": { + "data": null + }, + "inquiry-template": { + "data": { + "type": "inquiry-template", + "id": "itmpl_z2so7W2bCFHELp2dhxqqQjGy" + } + }, + "inquiry-template-version": { + "data": { + "type": "inquiry-template-version", + "id": "itmplv_CQbgqFNNgwrGe6jAB496VXpd" + } + }, + "reviewer": { + "data": null + }, + "reports": { + "data": [] + }, + "verifications": { + "data": [] + }, + "sessions": { + "data": [] + }, + "documents": { + "data": [] + }, + "selfies": { + "data": [] + } + } + } + } + } +} +`