From a3fac4f543a4238656ab1ee0f4e762a1e118d18c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 21 Jul 2026 07:57:04 +0000 Subject: [PATCH] Add rights requests to webhooks Emit lifecycle events for rights requests created through either the console or compliance portal. Keep webhook subscription choices in sync across every API and client surface. Signed-off-by: Cursor Agent Co-authored-by: Sacha Al Himdani Signed-off-by: Cursor Agent --- .../settings/WebhooksSettingsPage.tsx | 3 + .../nodes/Probo/actions/webhook/events.ts | 3 + pkg/cmd/webhook/shared/events.go | 3 + pkg/coredata/migrations/20260721T075032Z.sql | 23 ++ pkg/coredata/webhook_event_type.go | 24 +- pkg/probo/rights_request_service.go | 38 +++ pkg/probo/rights_request_service_test.go | 224 ++++++++++++++++++ .../api/console/v1/graphql/webhook.graphql | 6 + pkg/server/api/mcp/v1/specification.yaml | 3 + pkg/trust/rights_request_service.go | 13 + pkg/trust/rights_request_service_test.go | 152 ++++++++++++ pkg/webhook/types/rights_request.go | 58 +++++ 12 files changed, 541 insertions(+), 9 deletions(-) create mode 100644 pkg/coredata/migrations/20260721T075032Z.sql create mode 100644 pkg/probo/rights_request_service_test.go create mode 100644 pkg/trust/rights_request_service_test.go create mode 100644 pkg/webhook/types/rights_request.go diff --git a/apps/console/src/pages/organizations/settings/WebhooksSettingsPage.tsx b/apps/console/src/pages/organizations/settings/WebhooksSettingsPage.tsx index 623f1b1f1..5f79f9119 100644 --- a/apps/console/src/pages/organizations/settings/WebhooksSettingsPage.tsx +++ b/apps/console/src/pages/organizations/settings/WebhooksSettingsPage.tsx @@ -170,6 +170,9 @@ const EVENT_TYPES = [ { value: "OBLIGATION_CREATED", label: "obligation:created" }, { value: "OBLIGATION_UPDATED", label: "obligation:updated" }, { value: "OBLIGATION_DELETED", label: "obligation:deleted" }, + { value: "RIGHT_REQUEST_CREATED", label: "right-request:created" }, + { value: "RIGHT_REQUEST_UPDATED", label: "right-request:updated" }, + { value: "RIGHT_REQUEST_DELETED", label: "right-request:deleted" }, { value: "DOCUMENT_CREATED", label: "document:created" }, { value: "DOCUMENT_UPDATED", label: "document:updated" }, { value: "DOCUMENT_ARCHIVED", label: "document:archived" }, diff --git a/packages/n8n-node/nodes/Probo/actions/webhook/events.ts b/packages/n8n-node/nodes/Probo/actions/webhook/events.ts index 27cfccc41..412a7aa35 100644 --- a/packages/n8n-node/nodes/Probo/actions/webhook/events.ts +++ b/packages/n8n-node/nodes/Probo/actions/webhook/events.ts @@ -42,6 +42,9 @@ export const WEBHOOK_EVENT_OPTIONS: INodePropertyOptions[] = [ { name: 'Obligation Created', value: 'OBLIGATION_CREATED' }, { name: 'Obligation Deleted', value: 'OBLIGATION_DELETED' }, { name: 'Obligation Updated', value: 'OBLIGATION_UPDATED' }, + { name: 'Right Request Created', value: 'RIGHT_REQUEST_CREATED' }, + { name: 'Right Request Deleted', value: 'RIGHT_REQUEST_DELETED' }, + { name: 'Right Request Updated', value: 'RIGHT_REQUEST_UPDATED' }, { name: 'Third Party Created', value: 'THIRD_PARTY_CREATED' }, { name: 'Third Party Deleted', value: 'THIRD_PARTY_DELETED' }, { name: 'Third Party Updated', value: 'THIRD_PARTY_UPDATED' }, diff --git a/pkg/cmd/webhook/shared/events.go b/pkg/cmd/webhook/shared/events.go index 814c2c4ac..ab1b46742 100644 --- a/pkg/cmd/webhook/shared/events.go +++ b/pkg/cmd/webhook/shared/events.go @@ -33,6 +33,9 @@ var ValidEvents = []string{ "OBLIGATION_CREATED", "OBLIGATION_UPDATED", "OBLIGATION_DELETED", + "RIGHT_REQUEST_CREATED", + "RIGHT_REQUEST_UPDATED", + "RIGHT_REQUEST_DELETED", "DOCUMENT_CREATED", "DOCUMENT_UPDATED", "DOCUMENT_ARCHIVED", diff --git a/pkg/coredata/migrations/20260721T075032Z.sql b/pkg/coredata/migrations/20260721T075032Z.sql new file mode 100644 index 000000000..a4a95f27e --- /dev/null +++ b/pkg/coredata/migrations/20260721T075032Z.sql @@ -0,0 +1,23 @@ +-- Copyright (c) 2026 Probo Inc . +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy +-- of this software and associated documentation files (the "Software"), to deal +-- in the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-- copies of the Software, and to permit persons to whom the Software is +-- furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in +-- all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. + +ALTER TYPE webhook_event_type ADD VALUE 'right-request:created'; +ALTER TYPE webhook_event_type ADD VALUE 'right-request:updated'; +ALTER TYPE webhook_event_type ADD VALUE 'right-request:deleted'; diff --git a/pkg/coredata/webhook_event_type.go b/pkg/coredata/webhook_event_type.go index c99ed7132..79ac026db 100644 --- a/pkg/coredata/webhook_event_type.go +++ b/pkg/coredata/webhook_event_type.go @@ -30,15 +30,18 @@ import ( type WebhookEventType string const ( - WebhookEventTypeThirdPartyCreated WebhookEventType = "third-party:created" - WebhookEventTypeThirdPartyUpdated WebhookEventType = "third-party:updated" - WebhookEventTypeThirdPartyDeleted WebhookEventType = "third-party:deleted" - WebhookEventTypeUserCreated WebhookEventType = "user:created" - WebhookEventTypeUserUpdated WebhookEventType = "user:updated" - WebhookEventTypeUserDeleted WebhookEventType = "user:deleted" - WebhookEventTypeObligationCreated WebhookEventType = "obligation:created" - WebhookEventTypeObligationUpdated WebhookEventType = "obligation:updated" - WebhookEventTypeObligationDeleted WebhookEventType = "obligation:deleted" + WebhookEventTypeThirdPartyCreated WebhookEventType = "third-party:created" + WebhookEventTypeThirdPartyUpdated WebhookEventType = "third-party:updated" + WebhookEventTypeThirdPartyDeleted WebhookEventType = "third-party:deleted" + WebhookEventTypeUserCreated WebhookEventType = "user:created" + WebhookEventTypeUserUpdated WebhookEventType = "user:updated" + WebhookEventTypeUserDeleted WebhookEventType = "user:deleted" + WebhookEventTypeObligationCreated WebhookEventType = "obligation:created" + WebhookEventTypeObligationUpdated WebhookEventType = "obligation:updated" + WebhookEventTypeObligationDeleted WebhookEventType = "obligation:deleted" + WebhookEventTypeRightRequestCreated WebhookEventType = "right-request:created" + WebhookEventTypeRightRequestUpdated WebhookEventType = "right-request:updated" + WebhookEventTypeRightRequestDeleted WebhookEventType = "right-request:deleted" WebhookEventTypeDocumentCreated WebhookEventType = "document:created" WebhookEventTypeDocumentUpdated WebhookEventType = "document:updated" @@ -81,6 +84,9 @@ func (v WebhookEventType) IsValid() bool { WebhookEventTypeObligationCreated, WebhookEventTypeObligationUpdated, WebhookEventTypeObligationDeleted, + WebhookEventTypeRightRequestCreated, + WebhookEventTypeRightRequestUpdated, + WebhookEventTypeRightRequestDeleted, WebhookEventTypeDocumentCreated, WebhookEventTypeDocumentUpdated, WebhookEventTypeDocumentArchived, diff --git a/pkg/probo/rights_request_service.go b/pkg/probo/rights_request_service.go index 3b42f8a67..2439fc28a 100644 --- a/pkg/probo/rights_request_service.go +++ b/pkg/probo/rights_request_service.go @@ -30,6 +30,8 @@ import ( "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/validator" + "go.probo.inc/probo/pkg/webhook" + webhooktypes "go.probo.inc/probo/pkg/webhook/types" ) type RightsRequestService struct { @@ -147,6 +149,17 @@ func (s *RightsRequestService) Create( return fmt.Errorf("cannot insert rights request: %w", err) } + if err := webhook.InsertData( + ctx, + conn, + scope, + request.OrganizationID, + coredata.WebhookEventTypeRightRequestCreated, + webhooktypes.NewRightsRequest(request), + ); err != nil { + return fmt.Errorf("cannot insert webhook event: %w", err) + } + return nil }, ) @@ -174,6 +187,8 @@ func (s *RightsRequestService) Update( return fmt.Errorf("cannot load rights request: %w", err) } + previousRequest := webhooktypes.NewRightsRequest(request) + if req.RequestType != nil { request.RequestType = *req.RequestType } @@ -208,6 +223,18 @@ func (s *RightsRequestService) Update( return fmt.Errorf("cannot update rights request: %w", err) } + if err := webhook.InsertUpdateData( + ctx, + conn, + scope, + request.OrganizationID, + coredata.WebhookEventTypeRightRequestUpdated, + webhooktypes.NewRightsRequest(request), + previousRequest, + ); err != nil { + return fmt.Errorf("cannot insert webhook event: %w", err) + } + return nil }, ) @@ -230,6 +257,17 @@ func (s *RightsRequestService) Delete( return fmt.Errorf("cannot load rights request: %w", err) } + if err := webhook.InsertData( + ctx, + conn, + scope, + request.OrganizationID, + coredata.WebhookEventTypeRightRequestDeleted, + webhooktypes.NewRightsRequest(request), + ); err != nil { + return fmt.Errorf("cannot insert webhook event: %w", err) + } + if err := request.Delete(ctx, conn, scope); err != nil { return fmt.Errorf("cannot delete rights request: %w", err) } diff --git a/pkg/probo/rights_request_service_test.go b/pkg/probo/rights_request_service_test.go new file mode 100644 index 000000000..aa6fdd5a2 --- /dev/null +++ b/pkg/probo/rights_request_service_test.go @@ -0,0 +1,224 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package probo + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.gearno.de/kit/pg" + "go.probo.inc/probo/internal/test" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" + webhooktypes "go.probo.inc/probo/pkg/webhook/types" +) + +func TestRightsRequestService_WebhookLifecycle(t *testing.T) { + t.Parallel() + + client := test.PGClient(t) + organizationID := insertRightsRequestWebhookOrganization(t, client) + scope := coredata.NewScope(organizationID.TenantID()) + insertRightsRequestWebhookSubscription(t, client, scope, organizationID) + + service := RightsRequestService{svc: &Service{pg: client}} + requestType := coredata.RightsRequestTypeAccess + requestState := coredata.RightsRequestStateTodo + dataSubject := "Jane Doe" + contact := "jane@example.com" + + rightsRequest, err := service.Create( + t.Context(), + scope, + &CreateRightsRequestRequest{ + OrganizationID: organizationID, + RequestType: &requestType, + RequestState: &requestState, + DataSubject: &dataSubject, + Contact: &contact, + }, + ) + require.NoError(t, err) + + created, createdFrom := loadRightsRequestWebhookData( + t, + client, + organizationID, + coredata.WebhookEventTypeRightRequestCreated, + ) + assert.Equal(t, rightsRequest.ID, created.ID) + assert.Equal(t, dataSubject, *created.DataSubject) + assert.Nil(t, createdFrom) + + updatedState := coredata.RightsRequestStateInProgress + updatedSubject := "Jane Smith" + rightsRequest, err = service.Update( + t.Context(), + scope, + &UpdateRightsRequestRequest{ + ID: rightsRequest.ID, + RequestState: &updatedState, + DataSubject: new(&updatedSubject), + }, + ) + require.NoError(t, err) + + updated, updatedFrom := loadRightsRequestWebhookData( + t, + client, + organizationID, + coredata.WebhookEventTypeRightRequestUpdated, + ) + require.NotNil(t, updatedFrom) + assert.Equal(t, updatedState, updated.RequestState) + assert.Equal(t, updatedSubject, *updated.DataSubject) + assert.Equal(t, requestState, updatedFrom.RequestState) + assert.Equal(t, dataSubject, *updatedFrom.DataSubject) + + err = service.Delete(t.Context(), scope, rightsRequest.ID) + require.NoError(t, err) + + deleted, deletedFrom := loadRightsRequestWebhookData( + t, + client, + organizationID, + coredata.WebhookEventTypeRightRequestDeleted, + ) + assert.Equal(t, rightsRequest.ID, deleted.ID) + assert.Equal(t, updatedState, deleted.RequestState) + assert.Nil(t, deletedFrom) +} + +func insertRightsRequestWebhookOrganization(t *testing.T, client *pg.Client) gid.GID { + t.Helper() + + tenantID := gid.NewTenantID() + organizationID := gid.New(tenantID, coredata.OrganizationEntityType) + now := time.Now() + + err := client.WithConn( + t.Context(), + func(ctx context.Context, conn pg.Querier) error { + _, err := conn.Exec( + ctx, + `INSERT INTO organizations (id, tenant_id, name, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)`, + organizationID.String(), + tenantID.String(), + "right-request-webhook-"+organizationID.String(), + now, + now, + ) + + return err + }, + ) + require.NoError(t, err) + + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + + _ = client.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + _, err := conn.Exec(ctx, "DELETE FROM organizations WHERE id = $1", organizationID.String()) + return err + }, + ) + }) + + return organizationID +} + +func insertRightsRequestWebhookSubscription( + t *testing.T, + client *pg.Client, + scope coredata.Scoper, + organizationID gid.GID, +) { + t.Helper() + + now := time.Now() + subscription := coredata.WebhookSubscription{ + ID: gid.New(organizationID.TenantID(), coredata.WebhookSubscriptionEntityType), + OrganizationID: organizationID, + EndpointURL: "https://example.test/webhook", + SelectedEvents: coredata.WebhookEventTypes{ + coredata.WebhookEventTypeRightRequestCreated, + coredata.WebhookEventTypeRightRequestUpdated, + coredata.WebhookEventTypeRightRequestDeleted, + }, + EncryptedSigningSecret: []byte("test-signing-secret"), + CreatedAt: now, + UpdatedAt: now, + } + + err := client.WithTx( + t.Context(), + func(ctx context.Context, tx pg.Tx) error { + return subscription.Insert(ctx, tx, scope) + }, + ) + require.NoError(t, err) +} + +func loadRightsRequestWebhookData( + t *testing.T, + client *pg.Client, + organizationID gid.GID, + eventType coredata.WebhookEventType, +) (*webhooktypes.RightsRequest, *webhooktypes.RightsRequest) { + t.Helper() + + var ( + data []byte + updatedFrom []byte + ) + + err := client.WithConn( + t.Context(), + func(ctx context.Context, conn pg.Querier) error { + return conn.QueryRow( + ctx, + `SELECT data, updated_from FROM webhook_data WHERE organization_id = $1 AND event_type = $2`, + organizationID.String(), + eventType.String(), + ).Scan(&data, &updatedFrom) + }, + ) + require.NoError(t, err) + + current := &webhooktypes.RightsRequest{} + require.NoError(t, json.Unmarshal(data, current)) + + if updatedFrom == nil { + return current, nil + } + + previous := &webhooktypes.RightsRequest{} + require.NoError(t, json.Unmarshal(updatedFrom, previous)) + + return current, previous +} diff --git a/pkg/server/api/console/v1/graphql/webhook.graphql b/pkg/server/api/console/v1/graphql/webhook.graphql index 00a09c06b..1152577d1 100644 --- a/pkg/server/api/console/v1/graphql/webhook.graphql +++ b/pkg/server/api/console/v1/graphql/webhook.graphql @@ -18,6 +18,12 @@ enum WebhookEventType @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeObligationUpdated") OBLIGATION_DELETED @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeObligationDeleted") + RIGHT_REQUEST_CREATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeRightRequestCreated") + RIGHT_REQUEST_UPDATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeRightRequestUpdated") + RIGHT_REQUEST_DELETED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeRightRequestDeleted") DOCUMENT_CREATED @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentCreated") DOCUMENT_UPDATED diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index e5b66b509..48f008a06 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -6577,6 +6577,9 @@ components: - "obligation:created" - "obligation:updated" - "obligation:deleted" + - "right-request:created" + - "right-request:updated" + - "right-request:deleted" go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.WebhookEventType WebhookEventStatus: diff --git a/pkg/trust/rights_request_service.go b/pkg/trust/rights_request_service.go index d9acd66d7..9c2b5741f 100644 --- a/pkg/trust/rights_request_service.go +++ b/pkg/trust/rights_request_service.go @@ -31,6 +31,8 @@ import ( "go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/probo" "go.probo.inc/probo/pkg/validator" + "go.probo.inc/probo/pkg/webhook" + webhooktypes "go.probo.inc/probo/pkg/webhook/types" ) // RightsRequestDeadlineDays is the number of days a portal-submitted data @@ -104,6 +106,17 @@ func (s *RightsRequestService) Create( return fmt.Errorf("cannot insert rights request: %w", err) } + if err := webhook.InsertData( + ctx, + tx, + scope, + request.OrganizationID, + coredata.WebhookEventTypeRightRequestCreated, + webhooktypes.NewRightsRequest(request), + ); err != nil { + return fmt.Errorf("cannot insert webhook event: %w", err) + } + return nil }, ) diff --git a/pkg/trust/rights_request_service_test.go b/pkg/trust/rights_request_service_test.go new file mode 100644 index 000000000..34e5d45c8 --- /dev/null +++ b/pkg/trust/rights_request_service_test.go @@ -0,0 +1,152 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package trust + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.gearno.de/kit/pg" + "go.probo.inc/probo/internal/test" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" + webhooktypes "go.probo.inc/probo/pkg/webhook/types" +) + +func TestRightsRequestService_CreateEnqueuesWebhook(t *testing.T) { + t.Parallel() + + client := test.PGClient(t) + organizationID := insertPortalRequestWebhookOrganization(t, client) + scope := coredata.NewScope(organizationID.TenantID()) + insertPortalRequestWebhookSubscription(t, client, scope, organizationID) + + service := RightsRequestService{svc: &Service{pg: client}} + dataSubject := "Jane Doe" + contact := "jane@example.com" + rightsRequest, err := service.Create( + t.Context(), + scope, + &CreateRightsRequest{ + OrganizationID: organizationID, + RequestType: coredata.RightsRequestTypeAccess, + DataSubject: &dataSubject, + Contact: contact, + }, + ) + require.NoError(t, err) + + var data []byte + + err = client.WithConn( + t.Context(), + func(ctx context.Context, conn pg.Querier) error { + return conn.QueryRow( + ctx, + `SELECT data FROM webhook_data WHERE organization_id = $1 AND event_type = $2`, + organizationID.String(), + coredata.WebhookEventTypeRightRequestCreated.String(), + ).Scan(&data) + }, + ) + require.NoError(t, err) + + payload := &webhooktypes.RightsRequest{} + require.NoError(t, json.Unmarshal(data, payload)) + assert.Equal(t, rightsRequest.ID, payload.ID) + assert.Equal(t, coredata.RightsRequestStateTodo, payload.RequestState) + assert.Equal(t, dataSubject, *payload.DataSubject) + assert.Equal(t, contact, *payload.Contact) + assert.NotNil(t, payload.Deadline) +} + +func insertPortalRequestWebhookOrganization(t *testing.T, client *pg.Client) gid.GID { + t.Helper() + + tenantID := gid.NewTenantID() + organizationID := gid.New(tenantID, coredata.OrganizationEntityType) + now := time.Now() + + err := client.WithConn( + t.Context(), + func(ctx context.Context, conn pg.Querier) error { + _, err := conn.Exec( + ctx, + `INSERT INTO organizations (id, tenant_id, name, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)`, + organizationID.String(), + tenantID.String(), + "portal-request-webhook-"+organizationID.String(), + now, + now, + ) + + return err + }, + ) + require.NoError(t, err) + + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + + _ = client.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + _, err := conn.Exec(ctx, "DELETE FROM organizations WHERE id = $1", organizationID.String()) + return err + }, + ) + }) + + return organizationID +} + +func insertPortalRequestWebhookSubscription( + t *testing.T, + client *pg.Client, + scope coredata.Scoper, + organizationID gid.GID, +) { + t.Helper() + + now := time.Now() + subscription := coredata.WebhookSubscription{ + ID: gid.New(organizationID.TenantID(), coredata.WebhookSubscriptionEntityType), + OrganizationID: organizationID, + EndpointURL: "https://example.test/webhook", + SelectedEvents: coredata.WebhookEventTypes{coredata.WebhookEventTypeRightRequestCreated}, + EncryptedSigningSecret: []byte("test-signing-secret"), + CreatedAt: now, + UpdatedAt: now, + } + + err := client.WithTx( + t.Context(), + func(ctx context.Context, tx pg.Tx) error { + return subscription.Insert(ctx, tx, scope) + }, + ) + require.NoError(t, err) +} diff --git a/pkg/webhook/types/rights_request.go b/pkg/webhook/types/rights_request.go new file mode 100644 index 000000000..ad4e67fe2 --- /dev/null +++ b/pkg/webhook/types/rights_request.go @@ -0,0 +1,58 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package types + +import ( + "time" + + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" +) + +type RightsRequest struct { + ID gid.GID `json:"id"` + OrganizationID gid.GID `json:"organizationId"` + RequestType coredata.RightsRequestType `json:"requestType"` + RequestState coredata.RightsRequestState `json:"requestState"` + DataSubject *string `json:"dataSubject"` + Contact *string `json:"contact"` + Details *string `json:"details"` + Deadline *time.Time `json:"deadline"` + ActionTaken *string `json:"actionTaken"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func NewRightsRequest(r *coredata.RightsRequest) *RightsRequest { + return &RightsRequest{ + ID: r.ID, + OrganizationID: r.OrganizationID, + RequestType: r.RequestType, + RequestState: r.RequestState, + DataSubject: r.DataSubject, + Contact: r.Contact, + Details: r.Details, + Deadline: r.Deadline, + ActionTaken: r.ActionTaken, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + } +}