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

@@ -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"];

View File

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

View File

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

View File

@@ -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())

View 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';

View File

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

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
}

View File

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

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