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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user