Add document approval workflow

Introduce a complete approval system for document publishing. Document
versions can now require approval from selected reviewers before being
published, with automatic publishing once all approvers have approved.

- Add approval quorum and decision tables with backfill migration
- Implement request approval, approve, and reject flows with electronic
  signature support for approve decisions
- Add employee approvals page with dedicated tab and pending approvals view
- Add changelog field to publish and request approval flows
- Pre-select previous version's approvers in the publish dialog
- Show quorum approvers in document list with 100 approver hard limit
- Expose approval workflow through GraphQL, MCP, and CLI
- Remove legacy default approvers feature entirely
- Add comprehensive e2e test coverage for approval workflows

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-03-27 17:22:54 +01:00
parent 4a2d308da0
commit 999171a626
78 changed files with 6483 additions and 1600 deletions

View File

@@ -48,6 +48,7 @@ type (
CreateSignatureRequest struct {
OrganizationID gid.GID
DocumentType coredata.ElectronicSignatureDocumentType
DocumentName *string
FileID gid.GID
SignerEmail mail.Addr
ConsentText string // optional; required when DocumentType == OTHER
@@ -61,6 +62,18 @@ type (
SignerUA string
}
CreateAndAcceptSignatureRequest struct {
OrganizationID gid.GID
DocumentType coredata.ElectronicSignatureDocumentType
DocumentName *string
FileID gid.GID
SignerEmail mail.Addr
SignerFullName string
SignerIPAddr string
SignerUA string
ConsentText string
}
RecordEventRequest struct {
SignatureID gid.GID
EventType coredata.ElectronicSignatureEventType
@@ -161,6 +174,7 @@ func (s *Service) CreateSignature(
OrganizationID: req.OrganizationID,
Status: coredata.ElectronicSignatureStatusPending,
DocumentType: req.DocumentType,
DocumentName: req.DocumentName,
FileID: stampedFileID,
SignerEmail: req.SignerEmail.String(),
ConsentText: consentText,
@@ -178,6 +192,59 @@ func (s *Service) CreateSignature(
return sig, nil
}
func (s *Service) CreateAndAcceptSignature(
ctx context.Context,
conn pg.Conn,
req *CreateAndAcceptSignatureRequest,
) (*coredata.ElectronicSignature, error) {
sig, err := s.CreateSignature(
ctx,
conn,
&CreateSignatureRequest{
OrganizationID: req.OrganizationID,
DocumentType: req.DocumentType,
DocumentName: req.DocumentName,
FileID: req.FileID,
SignerEmail: req.SignerEmail,
ConsentText: req.ConsentText,
},
)
if err != nil {
return nil, fmt.Errorf("cannot create signature: %w", err)
}
now := time.Now()
scope := coredata.NewScopeFromObjectID(req.OrganizationID)
sig.SignerFullName = &req.SignerFullName
sig.SignerIPAddress = &req.SignerIPAddr
sig.SignerUserAgent = &req.SignerUA
sig.SignedAt = &now
sig.Status = coredata.ElectronicSignatureStatusAccepted
sig.UpdatedAt = now
if err := sig.Update(ctx, conn, scope); err != nil {
return nil, fmt.Errorf("cannot accept signature: %w", err)
}
if err := s.recordEvent(
ctx,
conn,
&RecordEventRequest{
SignatureID: sig.ID,
EventType: coredata.ElectronicSignatureEventTypeSignatureAccepted,
EventSource: coredata.ElectronicSignatureEventSourceServer,
ActorEmail: req.SignerEmail,
ActorIPAddr: req.SignerIPAddr,
ActorUA: req.SignerUA,
},
); err != nil {
return nil, fmt.Errorf("cannot record signature event: %w", err)
}
return sig, nil
}
func (s *Service) createStampedDocument(
ctx context.Context,
conn pg.Conn,