diff --git a/apps/console/src/pages/organizations/settings/WebhooksSettingsPage.tsx b/apps/console/src/pages/organizations/settings/WebhooksSettingsPage.tsx index abc191c09..96f2b8fc8 100644 --- a/apps/console/src/pages/organizations/settings/WebhooksSettingsPage.tsx +++ b/apps/console/src/pages/organizations/settings/WebhooksSettingsPage.tsx @@ -164,6 +164,24 @@ const EVENT_TYPES = [ { value: "OBLIGATION_CREATED", label: "obligation:created" }, { value: "OBLIGATION_UPDATED", label: "obligation:updated" }, { value: "OBLIGATION_DELETED", label: "obligation:deleted" }, + { value: "DOCUMENT_CREATED", label: "document:created" }, + { value: "DOCUMENT_UPDATED", label: "document:updated" }, + { value: "DOCUMENT_ARCHIVED", label: "document:archived" }, + { value: "DOCUMENT_UNARCHIVED", label: "document:unarchived" }, + { value: "DOCUMENT_DELETED", label: "document:deleted" }, + { value: "DOCUMENT_VERSION_CREATED", label: "document-version:created" }, + { value: "DOCUMENT_VERSION_UPDATED", label: "document-version:updated" }, + { value: "DOCUMENT_VERSION_PUBLISHED", label: "document-version:published" }, + { value: "DOCUMENT_VERSION_REJECTED", label: "document-version:rejected" }, + { value: "DOCUMENT_VERSION_DELETED", label: "document-version:deleted" }, + { value: "DOCUMENT_VERSION_SIGNATURE_REQUESTED", label: "document-version-signature:requested" }, + { value: "DOCUMENT_VERSION_SIGNATURE_SIGNED", label: "document-version-signature:signed" }, + { value: "DOCUMENT_VERSION_SIGNATURE_CANCELLED", label: "document-version-signature:cancelled" }, + { value: "DOCUMENT_VERSION_APPROVAL_QUORUM_REQUESTED", label: "document-version-approval-quorum:requested" }, + { value: "DOCUMENT_VERSION_APPROVAL_QUORUM_UPDATED", label: "document-version-approval-quorum:updated" }, + { value: "DOCUMENT_VERSION_APPROVAL_QUORUM_APPROVED", label: "document-version-approval-quorum:approved" }, + { value: "DOCUMENT_VERSION_APPROVAL_QUORUM_REJECTED", label: "document-version-approval-quorum:rejected" }, + { value: "DOCUMENT_VERSION_APPROVAL_QUORUM_VOIDED", label: "document-version-approval-quorum:voided" }, ] as const; type WebhookEventType = (typeof EVENT_TYPES)[number]["value"]; diff --git a/packages/n8n-node/nodes/Probo/actions/webhook/events.ts b/packages/n8n-node/nodes/Probo/actions/webhook/events.ts index 92b8d96d0..4e30a4c14 100644 --- a/packages/n8n-node/nodes/Probo/actions/webhook/events.ts +++ b/packages/n8n-node/nodes/Probo/actions/webhook/events.ts @@ -15,6 +15,24 @@ import type { INodePropertyOptions } from 'n8n-workflow'; export const WEBHOOK_EVENT_OPTIONS: INodePropertyOptions[] = [ + { name: 'Document Archived', value: 'DOCUMENT_ARCHIVED' }, + { name: 'Document Created', value: 'DOCUMENT_CREATED' }, + { name: 'Document Deleted', value: 'DOCUMENT_DELETED' }, + { name: 'Document Unarchived', value: 'DOCUMENT_UNARCHIVED' }, + { name: 'Document Updated', value: 'DOCUMENT_UPDATED' }, + { name: 'Document Version Approval Quorum Approved', value: 'DOCUMENT_VERSION_APPROVAL_QUORUM_APPROVED' }, + { name: 'Document Version Approval Quorum Rejected', value: 'DOCUMENT_VERSION_APPROVAL_QUORUM_REJECTED' }, + { name: 'Document Version Approval Quorum Requested', value: 'DOCUMENT_VERSION_APPROVAL_QUORUM_REQUESTED' }, + { name: 'Document Version Approval Quorum Updated', value: 'DOCUMENT_VERSION_APPROVAL_QUORUM_UPDATED' }, + { name: 'Document Version Approval Quorum Voided', value: 'DOCUMENT_VERSION_APPROVAL_QUORUM_VOIDED' }, + { name: 'Document Version Created', value: 'DOCUMENT_VERSION_CREATED' }, + { name: 'Document Version Deleted', value: 'DOCUMENT_VERSION_DELETED' }, + { name: 'Document Version Published', value: 'DOCUMENT_VERSION_PUBLISHED' }, + { name: 'Document Version Rejected', value: 'DOCUMENT_VERSION_REJECTED' }, + { name: 'Document Version Signature Cancelled', value: 'DOCUMENT_VERSION_SIGNATURE_CANCELLED' }, + { name: 'Document Version Signature Requested', value: 'DOCUMENT_VERSION_SIGNATURE_REQUESTED' }, + { name: 'Document Version Signature Signed', value: 'DOCUMENT_VERSION_SIGNATURE_SIGNED' }, + { name: 'Document Version Updated', value: 'DOCUMENT_VERSION_UPDATED' }, { name: 'Obligation Created', value: 'OBLIGATION_CREATED' }, { name: 'Obligation Deleted', value: 'OBLIGATION_DELETED' }, { name: 'Obligation Updated', value: 'OBLIGATION_UPDATED' }, diff --git a/pkg/cmd/webhook/shared/events.go b/pkg/cmd/webhook/shared/events.go index 3e8b11e1a..71e2b9ce0 100644 --- a/pkg/cmd/webhook/shared/events.go +++ b/pkg/cmd/webhook/shared/events.go @@ -27,4 +27,22 @@ var ValidEvents = []string{ "OBLIGATION_CREATED", "OBLIGATION_UPDATED", "OBLIGATION_DELETED", + "DOCUMENT_CREATED", + "DOCUMENT_UPDATED", + "DOCUMENT_ARCHIVED", + "DOCUMENT_UNARCHIVED", + "DOCUMENT_DELETED", + "DOCUMENT_VERSION_CREATED", + "DOCUMENT_VERSION_UPDATED", + "DOCUMENT_VERSION_PUBLISHED", + "DOCUMENT_VERSION_REJECTED", + "DOCUMENT_VERSION_DELETED", + "DOCUMENT_VERSION_SIGNATURE_REQUESTED", + "DOCUMENT_VERSION_SIGNATURE_SIGNED", + "DOCUMENT_VERSION_SIGNATURE_CANCELLED", + "DOCUMENT_VERSION_APPROVAL_QUORUM_REQUESTED", + "DOCUMENT_VERSION_APPROVAL_QUORUM_UPDATED", + "DOCUMENT_VERSION_APPROVAL_QUORUM_APPROVED", + "DOCUMENT_VERSION_APPROVAL_QUORUM_REJECTED", + "DOCUMENT_VERSION_APPROVAL_QUORUM_VOIDED", } diff --git a/pkg/coredata/document.go b/pkg/coredata/document.go index b6e5e9e2e..f06888281 100644 --- a/pkg/coredata/document.go +++ b/pkg/coredata/document.go @@ -920,7 +920,9 @@ func (p *Documents) BulkArchive( scope Scoper, ) error { q := ` -UPDATE documents SET status = 'ARCHIVED', archived_at = @archived_at, trust_center_visibility = 'NONE' WHERE %s AND id = ANY(@document_ids) +UPDATE documents +SET status = 'ARCHIVED', archived_at = @archived_at, trust_center_visibility = 'NONE', updated_at = @updated_at +WHERE %s AND id = ANY(@document_ids) ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -929,9 +931,11 @@ UPDATE documents SET status = 'ARCHIVED', archived_at = @archived_at, trust_cent ids[i] = doc.ID } + now := time.Now() args := pgx.StrictNamedArgs{ "document_ids": ids, - "archived_at": time.Now(), + "archived_at": now, + "updated_at": now, } maps.Copy(args, scope.SQLArguments()) @@ -948,7 +952,7 @@ func (p *Documents) BulkUnarchive( scope Scoper, ) error { q := ` -UPDATE documents SET status = 'ACTIVE', archived_at = NULL WHERE %s AND id = ANY(@document_ids) +UPDATE documents SET status = 'ACTIVE', archived_at = NULL, updated_at = @updated_at WHERE %s AND id = ANY(@document_ids) ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -959,6 +963,7 @@ UPDATE documents SET status = 'ACTIVE', archived_at = NULL WHERE %s AND id = ANY args := pgx.StrictNamedArgs{ "document_ids": ids, + "updated_at": time.Now(), } maps.Copy(args, scope.SQLArguments()) diff --git a/pkg/coredata/migrations/20260629T175459Z.sql b/pkg/coredata/migrations/20260629T175459Z.sql new file mode 100644 index 000000000..cada47701 --- /dev/null +++ b/pkg/coredata/migrations/20260629T175459Z.sql @@ -0,0 +1,35 @@ +-- Copyright (c) 2026 Probo Inc . +-- +-- Permission to use, copy, modify, and/or distribute this software for any +-- purpose with or without fee is hereby granted, provided that the above +-- copyright notice and this permission notice appear in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +-- PERFORMANCE OF THIS SOFTWARE. + +ALTER TYPE webhook_event_type ADD VALUE 'document:created'; +ALTER TYPE webhook_event_type ADD VALUE 'document:updated'; +ALTER TYPE webhook_event_type ADD VALUE 'document:archived'; +ALTER TYPE webhook_event_type ADD VALUE 'document:unarchived'; +ALTER TYPE webhook_event_type ADD VALUE 'document:deleted'; + +ALTER TYPE webhook_event_type ADD VALUE 'document-version:created'; +ALTER TYPE webhook_event_type ADD VALUE 'document-version:updated'; +ALTER TYPE webhook_event_type ADD VALUE 'document-version:published'; +ALTER TYPE webhook_event_type ADD VALUE 'document-version:rejected'; +ALTER TYPE webhook_event_type ADD VALUE 'document-version:deleted'; + +ALTER TYPE webhook_event_type ADD VALUE 'document-version-signature:requested'; +ALTER TYPE webhook_event_type ADD VALUE 'document-version-signature:signed'; +ALTER TYPE webhook_event_type ADD VALUE 'document-version-signature:cancelled'; + +ALTER TYPE webhook_event_type ADD VALUE 'document-version-approval-quorum:requested'; +ALTER TYPE webhook_event_type ADD VALUE 'document-version-approval-quorum:updated'; +ALTER TYPE webhook_event_type ADD VALUE 'document-version-approval-quorum:approved'; +ALTER TYPE webhook_event_type ADD VALUE 'document-version-approval-quorum:rejected'; +ALTER TYPE webhook_event_type ADD VALUE 'document-version-approval-quorum:voided'; diff --git a/pkg/coredata/webhook_event_type.go b/pkg/coredata/webhook_event_type.go index 36fabedb1..45d6ef9b5 100644 --- a/pkg/coredata/webhook_event_type.go +++ b/pkg/coredata/webhook_event_type.go @@ -33,6 +33,28 @@ const ( WebhookEventTypeObligationCreated WebhookEventType = "obligation:created" WebhookEventTypeObligationUpdated WebhookEventType = "obligation:updated" WebhookEventTypeObligationDeleted WebhookEventType = "obligation:deleted" + + WebhookEventTypeDocumentCreated WebhookEventType = "document:created" + WebhookEventTypeDocumentUpdated WebhookEventType = "document:updated" + WebhookEventTypeDocumentArchived WebhookEventType = "document:archived" + WebhookEventTypeDocumentUnarchived WebhookEventType = "document:unarchived" + WebhookEventTypeDocumentDeleted WebhookEventType = "document:deleted" + + WebhookEventTypeDocumentVersionCreated WebhookEventType = "document-version:created" + WebhookEventTypeDocumentVersionUpdated WebhookEventType = "document-version:updated" + WebhookEventTypeDocumentVersionPublished WebhookEventType = "document-version:published" + WebhookEventTypeDocumentVersionRejected WebhookEventType = "document-version:rejected" + WebhookEventTypeDocumentVersionDeleted WebhookEventType = "document-version:deleted" + + WebhookEventTypeDocumentVersionSignatureRequested WebhookEventType = "document-version-signature:requested" + WebhookEventTypeDocumentVersionSignatureSigned WebhookEventType = "document-version-signature:signed" + WebhookEventTypeDocumentVersionSignatureCancelled WebhookEventType = "document-version-signature:cancelled" + + WebhookEventTypeDocumentVersionApprovalQuorumRequested WebhookEventType = "document-version-approval-quorum:requested" + WebhookEventTypeDocumentVersionApprovalQuorumUpdated WebhookEventType = "document-version-approval-quorum:updated" + WebhookEventTypeDocumentVersionApprovalQuorumApproved WebhookEventType = "document-version-approval-quorum:approved" + WebhookEventTypeDocumentVersionApprovalQuorumRejected WebhookEventType = "document-version-approval-quorum:rejected" + WebhookEventTypeDocumentVersionApprovalQuorumVoided WebhookEventType = "document-version-approval-quorum:voided" ) var ( @@ -52,7 +74,25 @@ func (v WebhookEventType) IsValid() bool { WebhookEventTypeUserDeleted, WebhookEventTypeObligationCreated, WebhookEventTypeObligationUpdated, - WebhookEventTypeObligationDeleted: + WebhookEventTypeObligationDeleted, + WebhookEventTypeDocumentCreated, + WebhookEventTypeDocumentUpdated, + WebhookEventTypeDocumentArchived, + WebhookEventTypeDocumentUnarchived, + WebhookEventTypeDocumentDeleted, + WebhookEventTypeDocumentVersionCreated, + WebhookEventTypeDocumentVersionUpdated, + WebhookEventTypeDocumentVersionPublished, + WebhookEventTypeDocumentVersionRejected, + WebhookEventTypeDocumentVersionDeleted, + WebhookEventTypeDocumentVersionSignatureRequested, + WebhookEventTypeDocumentVersionSignatureSigned, + WebhookEventTypeDocumentVersionSignatureCancelled, + WebhookEventTypeDocumentVersionApprovalQuorumRequested, + WebhookEventTypeDocumentVersionApprovalQuorumUpdated, + WebhookEventTypeDocumentVersionApprovalQuorumApproved, + WebhookEventTypeDocumentVersionApprovalQuorumRejected, + WebhookEventTypeDocumentVersionApprovalQuorumVoided: return true } diff --git a/pkg/probo/document_approval_service.go b/pkg/probo/document_approval_service.go index 694ed5cbd..b67266a1e 100644 --- a/pkg/probo/document_approval_service.go +++ b/pkg/probo/document_approval_service.go @@ -172,6 +172,8 @@ func (s *DocumentApprovalService) BulkPublishVersions( continue } + var requestedQuorum *coredata.DocumentVersionApprovalQuorum + if req.Minor { var err error @@ -191,9 +193,12 @@ func (s *DocumentApprovalService) BulkPublishVersions( approverIDs[i] = a.ApproverProfileID } - if _, err := s.RequestApprovalInTx(ctx, scope, tx, document, dv, approverIDs, &req.Changelog); err != nil { + quorum, err := s.RequestApprovalInTx(ctx, scope, tx, document, dv, approverIDs, &req.Changelog) + if err != nil { return fmt.Errorf("cannot request approval for %q: %w", documentID, err) } + + requestedQuorum = quorum } else { var err error @@ -206,6 +211,34 @@ func (s *DocumentApprovalService) BulkPublishVersions( publishedVersions = append(publishedVersions, dv) updatedDocuments = append(updatedDocuments, document) + + if requestedQuorum != nil { + if err := s.svc.Documents.emitDocumentEventInTx( + ctx, + scope, + tx, + dv.DocumentID, + coredata.WebhookEventTypeDocumentVersionApprovalQuorumRequested, + dv, + nil, + &requestedQuorum.ID, + ); err != nil { + return fmt.Errorf("cannot emit approval quorum requested webhook: %w", err) + } + } else { + if err := s.svc.Documents.emitDocumentEventInTx( + ctx, + scope, + tx, + dv.DocumentID, + coredata.WebhookEventTypeDocumentVersionPublished, + dv, + nil, + nil, + ); err != nil { + return fmt.Errorf("cannot emit version published webhook: %w", err) + } + } } return nil @@ -373,6 +406,31 @@ func (s *DocumentApprovalService) Approve( return fmt.Errorf("cannot check quorum approval: %w", err) } + if err := documentVersion.LoadByID(ctx, tx, scope, req.DocumentVersionID); err != nil { + return fmt.Errorf("cannot reload document version: %w", err) + } + + if err := quorum.LoadByID(ctx, tx, scope, quorum.ID); err != nil { + return fmt.Errorf("cannot reload approval quorum: %w", err) + } + + if quorum.Status == coredata.DocumentVersionApprovalQuorumStatusApproved { + return nil + } + + if err := s.svc.Documents.emitDocumentEventInTx( + ctx, + scope, + tx, + documentVersion.DocumentID, + coredata.WebhookEventTypeDocumentVersionApprovalQuorumUpdated, + documentVersion, + nil, + &quorum.ID, + ); err != nil { + return fmt.Errorf("cannot emit approval quorum updated webhook: %w", err) + } + return nil }, ) @@ -458,6 +516,32 @@ func (s *DocumentApprovalService) Reject( return fmt.Errorf("cannot update document version status: %w", err) } + if err := s.svc.Documents.emitDocumentEventInTx( + ctx, + scope, + tx, + documentVersion.DocumentID, + coredata.WebhookEventTypeDocumentVersionApprovalQuorumRejected, + documentVersion, + nil, + &quorum.ID, + ); err != nil { + return fmt.Errorf("cannot emit approval quorum rejected webhook: %w", err) + } + + if err := s.svc.Documents.emitDocumentEventInTx( + ctx, + scope, + tx, + documentVersion.DocumentID, + coredata.WebhookEventTypeDocumentVersionRejected, + documentVersion, + nil, + nil, + ); err != nil { + return fmt.Errorf("cannot emit document version rejected webhook: %w", err) + } + return nil }, ) @@ -536,6 +620,19 @@ func (s *DocumentApprovalService) VoidApproval( return fmt.Errorf("cannot update document version status: %w", err) } + if err := s.svc.Documents.emitDocumentEventInTx( + ctx, + scope, + tx, + documentVersion.DocumentID, + coredata.WebhookEventTypeDocumentVersionApprovalQuorumVoided, + documentVersion, + nil, + &quorum.ID, + ); err != nil { + return fmt.Errorf("cannot emit approval quorum voided webhook: %w", err) + } + return nil }, ) @@ -881,10 +978,37 @@ func (s *DocumentApprovalService) maybeApproveQuorum( return fmt.Errorf("cannot update quorum: %w", err) } - if err := s.publishVersion(ctx, scope, tx, quorum.VersionID); err != nil { + version, err := s.publishVersion(ctx, scope, tx, quorum.VersionID) + if err != nil { return fmt.Errorf("cannot publish version: %w", err) } + if err := s.svc.Documents.emitDocumentEventInTx( + ctx, + scope, + tx, + version.DocumentID, + coredata.WebhookEventTypeDocumentVersionApprovalQuorumApproved, + version, + nil, + &quorum.ID, + ); err != nil { + return fmt.Errorf("cannot emit approval quorum approved webhook: %w", err) + } + + if err := s.svc.Documents.emitDocumentEventInTx( + ctx, + scope, + tx, + version.DocumentID, + coredata.WebhookEventTypeDocumentVersionPublished, + version, + nil, + nil, + ); err != nil { + return fmt.Errorf("cannot emit document version published webhook: %w", err) + } + return nil } @@ -892,27 +1016,27 @@ func (s *DocumentApprovalService) publishVersion( ctx context.Context, scope coredata.Scoper, tx pg.Tx, versionID gid.GID, -) error { +) (*coredata.DocumentVersion, error) { version := &coredata.DocumentVersion{} if err := version.LoadByID(ctx, tx, scope, versionID); err != nil { - return fmt.Errorf("cannot load document version: %w", err) + return nil, fmt.Errorf("cannot load document version: %w", err) } document := &coredata.Document{} if err := document.LoadByID(ctx, tx, scope, version.DocumentID); err != nil { - return fmt.Errorf("cannot load document: %w", err) + return nil, fmt.Errorf("cannot load document: %w", err) } document.CurrentPublishedMajor = &version.Major document.CurrentPublishedMinor = &version.Minor if err := s.svc.Documents.finalizePublish(ctx, scope, tx, document, version, nil); err != nil { - return fmt.Errorf("cannot finalize publish: %w", err) + return nil, fmt.Errorf("cannot finalize publish: %w", err) } if err := s.svc.Documents.cancelPreviousMajorSignatureRequestsInTx(ctx, scope, tx, version.DocumentID, version.Major); err != nil { - return fmt.Errorf("cannot cancel signature requests from previous major versions: %w", err) + return nil, fmt.Errorf("cannot cancel signature requests from previous major versions: %w", err) } - return nil + return version, nil } diff --git a/pkg/probo/document_service.go b/pkg/probo/document_service.go index d55745cec..20d94bbb4 100644 --- a/pkg/probo/document_service.go +++ b/pkg/probo/document_service.go @@ -45,6 +45,8 @@ import ( "go.probo.inc/probo/pkg/pdfutils" "go.probo.inc/probo/pkg/prosemirror" "go.probo.inc/probo/pkg/validator" + "go.probo.inc/probo/pkg/webhook" + webhooktypes "go.probo.inc/probo/pkg/webhook/types" ) const DocumentSignatureConsentText = "By clicking \"Review and sign\", I consent to sign this document electronically and agree that my electronic signature has the same legal validity as a handwritten signature." @@ -607,6 +609,19 @@ func (s *DocumentService) PublishVersion( result.Document = document result.Version = version + if err := s.emitDocumentEventInTx( + ctx, + scope, + tx, + version.DocumentID, + coredata.WebhookEventTypeDocumentVersionPublished, + version, + nil, + nil, + ); err != nil { + return fmt.Errorf("cannot emit document version published webhook: %w", err) + } + return nil } @@ -619,6 +634,19 @@ func (s *DocumentService) PublishVersion( result.Document = document result.Version = version + if err := s.emitDocumentEventInTx( + ctx, + scope, + tx, + version.DocumentID, + coredata.WebhookEventTypeDocumentVersionPublished, + version, + nil, + nil, + ); err != nil { + return fmt.Errorf("cannot emit document version published webhook: %w", err) + } + return nil } @@ -661,6 +689,19 @@ func (s *DocumentService) PublishVersion( result.Version = dv result.Quorum = quorum + if err := s.emitDocumentEventInTx( + ctx, + scope, + tx, + dv.DocumentID, + coredata.WebhookEventTypeDocumentVersionApprovalQuorumRequested, + dv, + nil, + &quorum.ID, + ); err != nil { + return fmt.Errorf("cannot emit approval quorum requested webhook: %w", err) + } + return nil }, ) @@ -749,6 +790,23 @@ func (s *DocumentService) Create( } } + if err := s.emitDocumentEventInTx(ctx, scope, conn, documentID, coredata.WebhookEventTypeDocumentCreated, nil, nil, nil); err != nil { + return fmt.Errorf("cannot emit document created webhook: %w", err) + } + + if err := s.emitDocumentEventInTx( + ctx, + scope, + conn, + documentVersion.DocumentID, + coredata.WebhookEventTypeDocumentVersionCreated, + documentVersion, + nil, + nil, + ); err != nil { + return fmt.Errorf("cannot emit document version created webhook: %w", err) + } + return nil }, ) @@ -911,6 +969,19 @@ func (s *DocumentService) SignDocumentVersionByIdentity( return fmt.Errorf("cannot update document version signature: %w", err) } + if err := s.emitDocumentEventInTx( + ctx, + scope, + tx, + documentVersion.DocumentID, + coredata.WebhookEventTypeDocumentVersionSignatureSigned, + documentVersion, + documentVersionSignature, + nil, + ); err != nil { + return fmt.Errorf("cannot emit document version signature signed webhook: %w", err) + } + return nil }, ) @@ -1010,13 +1081,35 @@ func (s *DocumentService) BulkRequestSignatures( return &ErrDocumentVersionNotPublished{} } + document := &coredata.Document{} + if err := document.LoadByID(ctx, tx, scope, documentVersion.DocumentID); err != nil { + return fmt.Errorf("cannot load document %q: %w", documentVersion.DocumentID, err) + } + for _, signatoryID := range req.SignatoryIDs { - signature, err := s.createSignatureRequestInTx(ctx, scope, tx, documentVersion.ID, signatoryID) + signature, created, err := s.createSignatureRequestInTx(ctx, scope, tx, documentVersion.ID, signatoryID) if err != nil { return fmt.Errorf("cannot create signature request for document %q and signatory %q: %w", documentID, signatoryID, err) } signatures = append(signatures, signature) + + if !created { + continue + } + + if err := s.emitLoadedDocumentEventInTx( + ctx, + scope, + tx, + document, + coredata.WebhookEventTypeDocumentVersionSignatureRequested, + documentVersion, + signature, + nil, + ); err != nil { + return fmt.Errorf("cannot emit signature requested webhook: %w", err) + } } } @@ -1035,16 +1128,16 @@ func (s *DocumentService) createSignatureRequestInTx( tx pg.Tx, documentVersionID gid.GID, signatoryID gid.GID, -) (*coredata.DocumentVersionSignature, error) { +) (*coredata.DocumentVersionSignature, bool, error) { signatory := &coredata.MembershipProfile{} documentVersion := &coredata.DocumentVersion{} if err := documentVersion.LoadByID(ctx, tx, scope, documentVersionID); err != nil { - return nil, fmt.Errorf("cannot load document version: %w", err) + return nil, false, fmt.Errorf("cannot load document version: %w", err) } if err := signatory.LoadByID(ctx, tx, scope, signatoryID); err != nil { - return nil, fmt.Errorf("cannot load signatory: %w", err) + return nil, false, fmt.Errorf("cannot load signatory: %w", err) } // A signature applies to the whole major version: minor publishes keep it @@ -1056,11 +1149,11 @@ func (s *DocumentService) createSignatureRequestInTx( err := existingSignature.LoadByDocumentMajorAndSignatory(ctx, tx, scope, documentVersionID, signatoryID) if err == nil { - return existingSignature, nil + return existingSignature, false, nil } if !errors.Is(err, coredata.ErrResourceNotFound) { - return nil, fmt.Errorf("cannot load existing signature for signatory: %w", err) + return nil, false, fmt.Errorf("cannot load existing signature for signatory: %w", err) } documentVersionSignatureID := gid.New(scope.GetTenantID(), coredata.DocumentVersionSignatureEntityType) @@ -1078,10 +1171,10 @@ func (s *DocumentService) createSignatureRequestInTx( } if err := documentVersionSignature.Insert(ctx, tx, scope); err != nil { - return nil, fmt.Errorf("cannot insert document version signature: %w", err) + return nil, false, fmt.Errorf("cannot insert document version signature: %w", err) } - return documentVersionSignature, nil + return documentVersionSignature, true, nil } func (s *DocumentService) RequestSignature( @@ -1127,13 +1220,33 @@ func (s *DocumentService) RequestSignature( return &ErrProfileContractEnded{ProfileID: profile.ID} } - var err error + var ( + err error + created bool + ) - signature, err = s.createSignatureRequestInTx(ctx, scope, tx, req.DocumentVersionID, req.Signatory) + signature, created, err = s.createSignatureRequestInTx(ctx, scope, tx, req.DocumentVersionID, req.Signatory) if err != nil { return fmt.Errorf("cannot create signature request: %w", err) } + if !created { + return nil + } + + if err := s.emitDocumentEventInTx( + ctx, + scope, + tx, + documentVersion.DocumentID, + coredata.WebhookEventTypeDocumentVersionSignatureRequested, + documentVersion, + signature, + nil, + ); err != nil { + return fmt.Errorf("cannot emit document version signature requested webhook: %w", err) + } + return nil }, ) @@ -1240,6 +1353,162 @@ func (s *DocumentService) deleteDraftInTx( return nil } +// For deletion events this must be called before the document is soft-deleted, +// since Document.LoadByID filters out soft-deleted rows. +func (s *DocumentService) emitDocumentEventInTx( + ctx context.Context, scope coredata.Scoper, + tx pg.Tx, + documentID gid.GID, + eventType coredata.WebhookEventType, + version *coredata.DocumentVersion, + signature *coredata.DocumentVersionSignature, + quorumID *gid.GID, +) error { + document := &coredata.Document{} + if err := document.LoadByID(ctx, tx, scope, documentID); err != nil { + return fmt.Errorf("cannot load document for %q webhook: %w", eventType, err) + } + + return s.emitLoadedDocumentEventInTx(ctx, scope, tx, document, eventType, version, signature, quorumID) +} + +func (s *DocumentService) emitLoadedDocumentEventInTx( + ctx context.Context, scope coredata.Scoper, + tx pg.Tx, + document *coredata.Document, + eventType coredata.WebhookEventType, + version *coredata.DocumentVersion, + signature *coredata.DocumentVersionSignature, + quorumID *gid.GID, +) error { + subscriptions := coredata.WebhookSubscriptions{} + + exists, err := subscriptions.ExistsByOrganizationIDAndEventType(ctx, tx, scope, document.OrganizationID, eventType) + if err != nil { + return fmt.Errorf("cannot check webhook subscriptions for %q: %w", eventType, err) + } + + if !exists { + return nil + } + + var payload any + + switch { + case signature != nil: + payload = webhooktypes.NewDocumentVersionSignature(signature, version, document) + case quorumID != nil: + payload, err = s.loadDocumentApprovalQuorumForWebhook(ctx, scope, tx, *quorumID, version, document) + if err != nil { + return fmt.Errorf("cannot build approval quorum payload for %q webhook: %w", eventType, err) + } + case version != nil: + payload = webhooktypes.NewDocumentVersion(version, document) + default: + payload = webhooktypes.NewDocument(document) + } + + if err := webhook.InsertData( + ctx, + tx, + scope, + document.OrganizationID, + eventType, + payload, + ); err != nil { + return fmt.Errorf("cannot insert %q webhook event: %w", eventType, err) + } + + return nil +} + +func (s *DocumentService) emitDocumentLifecycleEventsInTx( + ctx context.Context, scope coredata.Scoper, + tx pg.Tx, + documentIDs []gid.GID, + eventType coredata.WebhookEventType, +) error { + if len(documentIDs) == 0 { + return nil + } + + documents := coredata.Documents{} + if err := documents.LoadByIDs(ctx, tx, scope, documentIDs); err != nil { + return fmt.Errorf("cannot load documents for %q webhook: %w", eventType, err) + } + + subscribed := make(map[gid.GID]bool) + + for _, document := range documents { + hasSubscription, cached := subscribed[document.OrganizationID] + if !cached { + subscriptions := coredata.WebhookSubscriptions{} + + exists, err := subscriptions.ExistsByOrganizationIDAndEventType(ctx, tx, scope, document.OrganizationID, eventType) + if err != nil { + return fmt.Errorf("cannot check webhook subscriptions for %q: %w", eventType, err) + } + + hasSubscription = exists + subscribed[document.OrganizationID] = exists + } + + if !hasSubscription { + continue + } + + if err := webhook.InsertData( + ctx, + tx, + scope, + document.OrganizationID, + eventType, + webhooktypes.NewDocument(document), + ); err != nil { + return fmt.Errorf("cannot insert %q webhook event: %w", eventType, err) + } + } + + return nil +} + +func (s *DocumentService) loadDocumentApprovalQuorumForWebhook( + ctx context.Context, scope coredata.Scoper, + tx pg.Tx, + quorumID gid.GID, + version *coredata.DocumentVersion, + document *coredata.Document, +) (*webhooktypes.DocumentApprovalQuorum, error) { + quorum := &coredata.DocumentVersionApprovalQuorum{} + if err := quorum.LoadByID(ctx, tx, scope, quorumID); err != nil { + return nil, fmt.Errorf("cannot load approval quorum for webhook: %w", err) + } + + decisions, err := page.LoadAll( + ctx, + page.OrderBy[coredata.DocumentVersionApprovalDecisionOrderField]{ + Field: coredata.DocumentVersionApprovalDecisionOrderFieldCreatedAt, + Direction: page.OrderDirectionAsc, + }, + func( + ctx context.Context, + cursor *page.Cursor[coredata.DocumentVersionApprovalDecisionOrderField], + ) ([]*coredata.DocumentVersionApprovalDecision, error) { + var batch coredata.DocumentVersionApprovalDecisions + if err := batch.LoadByQuorumID(ctx, tx, scope, quorumID, cursor, coredata.NewDocumentVersionApprovalDecisionFilter(nil)); err != nil { + return nil, fmt.Errorf("cannot load approval decisions: %w", err) + } + + return batch, nil + }, + ) + if err != nil { + return nil, fmt.Errorf("cannot load approval decisions for webhook: %w", err) + } + + return webhooktypes.NewDocumentApprovalQuorum(quorum, decisions, version, document), nil +} + func (s *DocumentService) SoftDelete( ctx context.Context, scope coredata.Scoper, documentID gid.GID, @@ -1249,6 +1518,10 @@ func (s *DocumentService) SoftDelete( return s.svc.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { + if err := s.emitDocumentEventInTx(ctx, scope, tx, documentID, coredata.WebhookEventTypeDocumentDeleted, nil, nil, nil); err != nil { + return fmt.Errorf("cannot emit document deleted webhook: %w", err) + } + if err := s.clearDocumentReferences(ctx, scope, tx, []gid.GID{documentID}); err != nil { return err } @@ -1271,6 +1544,10 @@ func (s *DocumentService) BulkSoftDelete( return s.svc.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { + if err := s.emitDocumentLifecycleEventsInTx(ctx, scope, tx, documentIDs, coredata.WebhookEventTypeDocumentDeleted); err != nil { + return fmt.Errorf("cannot emit document deleted webhooks: %w", err) + } + if err := s.clearDocumentReferences(ctx, scope, tx, documentIDs); err != nil { return err } @@ -1312,7 +1589,15 @@ func (s *DocumentService) BulkArchive( return err } - return documents.BulkArchive(ctx, tx, scope) + if err := documents.BulkArchive(ctx, tx, scope); err != nil { + return err + } + + if err := s.emitDocumentLifecycleEventsInTx(ctx, scope, tx, documentIDs, coredata.WebhookEventTypeDocumentArchived); err != nil { + return fmt.Errorf("cannot emit document archived webhooks: %w", err) + } + + return nil }, ) } @@ -1327,10 +1612,18 @@ func (s *DocumentService) BulkUnarchive( documents = append(documents, &coredata.Document{ID: documentID}) } - return s.svc.pg.WithConn( + return s.svc.pg.WithTx( ctx, - func(ctx context.Context, conn pg.Querier) error { - return documents.BulkUnarchive(ctx, conn, scope) + func(ctx context.Context, tx pg.Tx) error { + if err := documents.BulkUnarchive(ctx, tx, scope); err != nil { + return err + } + + if err := s.emitDocumentLifecycleEventsInTx(ctx, scope, tx, documentIDs, coredata.WebhookEventTypeDocumentUnarchived); err != nil { + return fmt.Errorf("cannot emit document unarchived webhooks: %w", err) + } + + return nil }, ) } @@ -1836,80 +2129,18 @@ func (s *DocumentService) Update( return fmt.Errorf("cannot update document: %w", err) } - // Handle draft version logic for title/content/classification/type changes. latestVersion := &coredata.DocumentVersion{} if err := latestVersion.LoadLatestVersion(ctx, tx, scope, req.DocumentID); err != nil { return fmt.Errorf("cannot load latest version: %w", err) } hasVersionChanges := req.Title != nil || req.Content != nil || req.Classification != nil || req.DocumentType != nil + docLevelChanged := req.TrustCenterVisibility != nil || req.DefaultApproverIDs != nil if req.Content != nil && document.WriteMode == coredata.DocumentWriteModeGenerated { return &ErrDocumentVersionGenerated{} } - if !hasVersionChanges { - if req.DefaultApproverIDs != nil { - defaultApprovers := &coredata.DocumentDefaultApprovers{} - if err := defaultApprovers.MergeByDocumentID(ctx, tx, scope, req.DocumentID, document.OrganizationID, *req.DefaultApproverIDs); err != nil { - return fmt.Errorf("cannot update default approvers: %w", err) - } - } - - return nil - } - - if latestVersion.Status == coredata.DocumentVersionStatusDraft { - // Draft exists: update it with any new values. - if err := s.updateVersionInTx(ctx, scope, tx, latestVersion, req.Content, req.Classification, req.DocumentType, req.Title); err != nil { - return err - } - - // If there is a published version and the draft matches it, delete the draft. - // Never delete the initial draft (v0.1) since there's nothing to fall back to. - if document.CurrentPublishedMajor != nil && (latestVersion.Major != 0 || latestVersion.Minor != 1) { - publishedVersion := &coredata.DocumentVersion{} - if err := publishedVersion.LoadByDocumentIDAndVersion( - ctx, - tx, - scope, - req.DocumentID, - *document.CurrentPublishedMajor, - *document.CurrentPublishedMinor, - ); err != nil { - return fmt.Errorf("cannot load published version: %w", err) - } - - if latestVersion.Title == publishedVersion.Title && - latestVersion.Content == publishedVersion.Content && - latestVersion.Classification == publishedVersion.Classification && - latestVersion.DocumentType == publishedVersion.DocumentType { - if err := s.deleteDraftInTx(ctx, scope, tx, latestVersion); err != nil { - return err - } - - resultVersion = nil - - return nil - } - } - - resultVersion = latestVersion - } else { - // No draft exists: create one. - draftVersion, err := s.createDraftInTx(ctx, scope, tx, document, latestVersion) - if err != nil { - return err - } - - if err := s.updateVersionInTx(ctx, scope, tx, draftVersion, req.Content, req.Classification, req.DocumentType, req.Title); err != nil { - return err - } - - resultVersion = draftVersion - draftCreated = true - } - if req.DefaultApproverIDs != nil { defaultApprovers := &coredata.DocumentDefaultApprovers{} if err := defaultApprovers.MergeByDocumentID(ctx, tx, scope, req.DocumentID, document.OrganizationID, *req.DefaultApproverIDs); err != nil { @@ -1917,6 +2148,88 @@ func (s *DocumentService) Update( } } + versionDeleted := false + + if hasVersionChanges { + if latestVersion.Status == coredata.DocumentVersionStatusDraft { + if err := s.updateVersionInTx(ctx, scope, tx, latestVersion, req.Content, req.Classification, req.DocumentType, req.Title); err != nil { + return err + } + + if document.CurrentPublishedMajor != nil && (latestVersion.Major != 0 || latestVersion.Minor != 1) { + publishedVersion := &coredata.DocumentVersion{} + if err := publishedVersion.LoadByDocumentIDAndVersion( + ctx, + tx, + scope, + req.DocumentID, + *document.CurrentPublishedMajor, + *document.CurrentPublishedMinor, + ); err != nil { + return fmt.Errorf("cannot load published version: %w", err) + } + + if latestVersion.Title == publishedVersion.Title && + latestVersion.Content == publishedVersion.Content && + latestVersion.Classification == publishedVersion.Classification && + latestVersion.DocumentType == publishedVersion.DocumentType { + if err := s.deleteDraftInTx(ctx, scope, tx, latestVersion); err != nil { + return err + } + + resultVersion = nil + versionDeleted = true + } + } + + if !versionDeleted { + resultVersion = latestVersion + } + } else { + draftVersion, err := s.createDraftInTx(ctx, scope, tx, document, latestVersion) + if err != nil { + return err + } + + if err := s.updateVersionInTx(ctx, scope, tx, draftVersion, req.Content, req.Classification, req.DocumentType, req.Title); err != nil { + return err + } + + resultVersion = draftVersion + draftCreated = true + } + + if versionDeleted { + if err := s.emitDocumentEventInTx( + ctx, + scope, + tx, + latestVersion.DocumentID, + coredata.WebhookEventTypeDocumentVersionDeleted, + latestVersion, + nil, + nil, + ); err != nil { + return fmt.Errorf("cannot emit document version deleted webhook: %w", err) + } + } else { + versionEvent := coredata.WebhookEventTypeDocumentVersionUpdated + if draftCreated { + versionEvent = coredata.WebhookEventTypeDocumentVersionCreated + } + + if err := s.emitDocumentEventInTx(ctx, scope, tx, resultVersion.DocumentID, versionEvent, resultVersion, nil, nil); err != nil { + return fmt.Errorf("cannot emit document version webhook: %w", err) + } + } + } + + if docLevelChanged { + if err := s.emitDocumentEventInTx(ctx, scope, tx, req.DocumentID, coredata.WebhookEventTypeDocumentUpdated, nil, nil, nil); err != nil { + return fmt.Errorf("cannot emit document updated webhook: %w", err) + } + } + return nil }, ) @@ -1957,7 +2270,24 @@ func (s *DocumentService) DeleteDraft( return &ErrDocumentDraftNotDeletable{} } - return s.deleteDraftInTx(ctx, scope, tx, latestVersion) + if err := s.deleteDraftInTx(ctx, scope, tx, latestVersion); err != nil { + return err + } + + if err := s.emitDocumentEventInTx( + ctx, + scope, + tx, + latestVersion.DocumentID, + coredata.WebhookEventTypeDocumentVersionDeleted, + latestVersion, + nil, + nil, + ); err != nil { + return fmt.Errorf("cannot emit document version deleted webhook: %w", err) + } + + return nil }, ) if err != nil { @@ -2013,6 +2343,10 @@ func (s *DocumentService) Archive( return fmt.Errorf("cannot archive document: %w", err) } + if err := s.emitDocumentEventInTx(ctx, scope, tx, documentID, coredata.WebhookEventTypeDocumentArchived, nil, nil, nil); err != nil { + return fmt.Errorf("cannot emit document archived webhook: %w", err) + } + return nil }, ) @@ -2049,6 +2383,10 @@ func (s *DocumentService) Unarchive( return fmt.Errorf("cannot unarchive document: %w", err) } + if err := s.emitDocumentEventInTx(ctx, scope, tx, documentID, coredata.WebhookEventTypeDocumentUnarchived, nil, nil, nil); err != nil { + return fmt.Errorf("cannot emit document unarchived webhook: %w", err) + } + return nil }, ) @@ -2097,6 +2435,19 @@ func (s *DocumentService) CancelSignatureRequest( return fmt.Errorf("cannot delete document version signature: %w", err) } + if err := s.emitDocumentEventInTx( + ctx, + scope, + tx, + documentVersion.DocumentID, + coredata.WebhookEventTypeDocumentVersionSignatureCancelled, + documentVersion, + documentVersionSignature, + nil, + ); err != nil { + return fmt.Errorf("cannot emit document version signature cancelled webhook: %w", err) + } + return nil }, ) diff --git a/pkg/probo/generated_document_service.go b/pkg/probo/generated_document_service.go index bfb9dab84..109dbbdb5 100644 --- a/pkg/probo/generated_document_service.go +++ b/pkg/probo/generated_document_service.go @@ -3214,6 +3214,8 @@ func (s *GeneratedDocumentService) publishOrRequestApproval( ) error { previousVersion := &coredata.DocumentVersion{} + isFirstVersion := false + err := previousVersion.LoadLatestVersion(ctx, tx, scope, document.ID) switch { case err == nil: @@ -3222,6 +3224,7 @@ func (s *GeneratedDocumentService) publishOrRequestApproval( version.DocumentType = previousVersion.DocumentType case errors.Is(err, coredata.ErrResourceNotFound): // First publish: keep the caller-provided defaults. + isFirstVersion = true default: return fmt.Errorf("cannot load previous document version: %w", err) } @@ -3276,10 +3279,30 @@ func (s *GeneratedDocumentService) publishOrRequestApproval( return fmt.Errorf("cannot save default approvers: %w", err) } - if _, err := s.svc.DocumentApprovals.RequestApprovalInTx(ctx, scope, tx, document, version, approverIDs, nil); err != nil { + quorum, err := s.svc.DocumentApprovals.RequestApprovalInTx(ctx, scope, tx, document, version, approverIDs, nil) + if err != nil { return fmt.Errorf("cannot request approval: %w", err) } + if isFirstVersion { + if err := s.svc.Documents.emitDocumentEventInTx(ctx, scope, tx, document.ID, coredata.WebhookEventTypeDocumentCreated, nil, nil, nil); err != nil { + return fmt.Errorf("cannot emit document created webhook: %w", err) + } + } + + if err := s.svc.Documents.emitDocumentEventInTx( + ctx, + scope, + tx, + version.DocumentID, + coredata.WebhookEventTypeDocumentVersionApprovalQuorumRequested, + version, + nil, + &quorum.ID, + ); err != nil { + return fmt.Errorf("cannot emit approval quorum requested webhook: %w", err) + } + return nil } @@ -3297,5 +3320,24 @@ func (s *GeneratedDocumentService) publishOrRequestApproval( } } + if isFirstVersion { + if err := s.svc.Documents.emitDocumentEventInTx(ctx, scope, tx, document.ID, coredata.WebhookEventTypeDocumentCreated, nil, nil, nil); err != nil { + return fmt.Errorf("cannot emit document created webhook: %w", err) + } + } + + if err := s.svc.Documents.emitDocumentEventInTx( + ctx, + scope, + tx, + version.DocumentID, + coredata.WebhookEventTypeDocumentVersionPublished, + version, + nil, + nil, + ); err != nil { + return fmt.Errorf("cannot emit document version published webhook: %w", err) + } + return nil } diff --git a/pkg/server/api/console/v1/graphql/webhook.graphql b/pkg/server/api/console/v1/graphql/webhook.graphql index c012f8898..00a09c06b 100644 --- a/pkg/server/api/console/v1/graphql/webhook.graphql +++ b/pkg/server/api/console/v1/graphql/webhook.graphql @@ -18,6 +18,42 @@ enum WebhookEventType @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeObligationUpdated") OBLIGATION_DELETED @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeObligationDeleted") + DOCUMENT_CREATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentCreated") + DOCUMENT_UPDATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentUpdated") + DOCUMENT_ARCHIVED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentArchived") + DOCUMENT_UNARCHIVED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentUnarchived") + DOCUMENT_DELETED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentDeleted") + DOCUMENT_VERSION_CREATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentVersionCreated") + DOCUMENT_VERSION_UPDATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentVersionUpdated") + DOCUMENT_VERSION_PUBLISHED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentVersionPublished") + DOCUMENT_VERSION_REJECTED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentVersionRejected") + DOCUMENT_VERSION_DELETED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentVersionDeleted") + DOCUMENT_VERSION_SIGNATURE_REQUESTED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentVersionSignatureRequested") + DOCUMENT_VERSION_SIGNATURE_SIGNED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentVersionSignatureSigned") + DOCUMENT_VERSION_SIGNATURE_CANCELLED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentVersionSignatureCancelled") + DOCUMENT_VERSION_APPROVAL_QUORUM_REQUESTED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentVersionApprovalQuorumRequested") + DOCUMENT_VERSION_APPROVAL_QUORUM_UPDATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentVersionApprovalQuorumUpdated") + DOCUMENT_VERSION_APPROVAL_QUORUM_APPROVED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentVersionApprovalQuorumApproved") + DOCUMENT_VERSION_APPROVAL_QUORUM_REJECTED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentVersionApprovalQuorumRejected") + DOCUMENT_VERSION_APPROVAL_QUORUM_VOIDED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeDocumentVersionApprovalQuorumVoided") } enum WebhookEventStatus diff --git a/pkg/webhook/types/document.go b/pkg/webhook/types/document.go new file mode 100644 index 000000000..30ac95ec7 --- /dev/null +++ b/pkg/webhook/types/document.go @@ -0,0 +1,166 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package types + +import ( + "time" + + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" +) + +type Document struct { + ID gid.GID `json:"id"` + OrganizationID gid.GID `json:"organizationId"` + Title string `json:"title"` + DocumentType coredata.DocumentType `json:"documentType"` + Status coredata.DocumentStatus `json:"status"` + TrustCenterVisibility coredata.TrustCenterVisibility `json:"trustCenterVisibility"` + CurrentPublishedMajor *int `json:"currentPublishedMajor"` + CurrentPublishedMinor *int `json:"currentPublishedMinor"` + ArchivedAt *time.Time `json:"archivedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type DocumentVersion struct { + ID gid.GID `json:"id"` + DocumentID gid.GID `json:"documentId"` + Title string `json:"title"` + Major int `json:"major"` + Minor int `json:"minor"` + Classification coredata.DocumentClassification `json:"classification"` + DocumentType coredata.DocumentType `json:"documentType"` + Changelog string `json:"changelog"` + Status coredata.DocumentVersionStatus `json:"status"` + PublishedAt *time.Time `json:"publishedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Document *Document `json:"document"` +} + +type DocumentVersionSignature struct { + ID gid.GID `json:"id"` + DocumentVersionID gid.GID `json:"documentVersionId"` + State coredata.DocumentVersionSignatureState `json:"state"` + SignedBy gid.GID `json:"signedBy"` + SignedAt *time.Time `json:"signedAt"` + RequestedAt time.Time `json:"requestedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Version *DocumentVersion `json:"version"` +} + +type DocumentApprovalQuorum struct { + ID gid.GID `json:"id"` + VersionID gid.GID `json:"versionId"` + Status coredata.DocumentVersionApprovalQuorumStatus `json:"status"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Decisions []*DocumentApprovalDecision `json:"decisions"` + Version *DocumentVersion `json:"version"` +} + +type DocumentApprovalDecision struct { + ID gid.GID `json:"id"` + ApproverID gid.GID `json:"approverId"` + State coredata.DocumentVersionApprovalDecisionState `json:"state"` + Comment *string `json:"comment"` + DecidedAt *time.Time `json:"decidedAt"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func NewDocument(d *coredata.Document) *Document { + return &Document{ + ID: d.ID, + OrganizationID: d.OrganizationID, + Title: d.Title, + DocumentType: d.DocumentType, + Status: d.Status, + TrustCenterVisibility: d.TrustCenterVisibility, + CurrentPublishedMajor: d.CurrentPublishedMajor, + CurrentPublishedMinor: d.CurrentPublishedMinor, + ArchivedAt: d.ArchivedAt, + CreatedAt: d.CreatedAt, + UpdatedAt: d.UpdatedAt, + } +} + +func NewDocumentVersion(v *coredata.DocumentVersion, d *coredata.Document) *DocumentVersion { + return &DocumentVersion{ + ID: v.ID, + DocumentID: v.DocumentID, + Title: v.Title, + Major: v.Major, + Minor: v.Minor, + Classification: v.Classification, + DocumentType: v.DocumentType, + Changelog: v.Changelog, + Status: v.Status, + PublishedAt: v.PublishedAt, + CreatedAt: v.CreatedAt, + UpdatedAt: v.UpdatedAt, + Document: NewDocument(d), + } +} + +func NewDocumentVersionSignature( + s *coredata.DocumentVersionSignature, + v *coredata.DocumentVersion, + d *coredata.Document, +) *DocumentVersionSignature { + return &DocumentVersionSignature{ + ID: s.ID, + DocumentVersionID: s.DocumentVersionID, + State: s.State, + SignedBy: s.SignedBy, + SignedAt: s.SignedAt, + RequestedAt: s.RequestedAt, + CreatedAt: s.CreatedAt, + UpdatedAt: s.UpdatedAt, + Version: NewDocumentVersion(v, d), + } +} + +func NewDocumentApprovalQuorum( + quorum *coredata.DocumentVersionApprovalQuorum, + decisions coredata.DocumentVersionApprovalDecisions, + v *coredata.DocumentVersion, + d *coredata.Document, +) *DocumentApprovalQuorum { + decisionPayloads := make([]*DocumentApprovalDecision, 0, len(decisions)) + for _, decision := range decisions { + decisionPayloads = append(decisionPayloads, &DocumentApprovalDecision{ + ID: decision.ID, + ApproverID: decision.ApproverID, + State: decision.State, + Comment: decision.Comment, + DecidedAt: decision.DecidedAt, + CreatedAt: decision.CreatedAt, + UpdatedAt: decision.UpdatedAt, + }) + } + + return &DocumentApprovalQuorum{ + ID: quorum.ID, + VersionID: quorum.VersionID, + Status: quorum.Status, + CreatedAt: quorum.CreatedAt, + UpdatedAt: quorum.UpdatedAt, + Decisions: decisionPayloads, + Version: NewDocumentVersion(v, d), + } +}