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:
Sacha Al Himdani
2026-06-29 20:42:50 +02:00
parent 4cdf8b7913
commit 1df4af4556
11 changed files with 944 additions and 91 deletions

View File

@@ -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
}

View File

@@ -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
},
)

View File

@@ -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
}