Add document webhook events
Add a resource-oriented set of webhook events for the document lifecycle. Each event carries the document plus only the sub-resource it concerns (version, signature or approval). Events: - document.created / updated / archived / unarchived / deleted - document.version.created / updated / published / rejected / deleted - document.version.signature.requested / signed / cancelled - document.version.approval.requested / approved / rejected / voided Wires the new types through the migration, Go enum, GraphQL schema, CLI, n8n nodes and the console webhooks settings UI. Signed-off-by: Sacha Al Himdani <sacha@probo.com>
This commit is contained in:
@@ -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"];
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
35
pkg/coredata/migrations/20260629T175459Z.sql
Normal file
35
pkg/coredata/migrations/20260629T175459Z.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
-- Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
--
|
||||
-- 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';
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
166
pkg/webhook/types/document.go
Normal file
166
pkg/webhook/types/document.go
Normal file
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user