Reject signature and approval requests for people with ended contracts

Add server-side validation in BulkRequestSignatures, RequestSignature,
and RequestApproval to load the referenced profiles and verify none
have an ended contract before proceeding. Returns ErrProfileContractEnded
if a profile's contract_end_date is in the past, surfaced as a CONFLICT
GraphQL error in all three resolvers.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-04-15 14:00:10 +02:00
parent 524f209df7
commit 754dac9ec6
4 changed files with 58 additions and 1 deletions

View File

@@ -37,6 +37,7 @@ import { graphql } from "relay-runtime";
import { z } from "zod";
import type { SignatureDocumentsDialogMutation } from "#/__generated__/core/SignatureDocumentsDialogMutation.graphql";
import type { SignatureDocumentsDialogPeopleFragment$key } from "#/__generated__/core/SignatureDocumentsDialogPeopleFragment.graphql";
import type { SignatureDocumentsDialogPeopleQuery } from "#/__generated__/core/SignatureDocumentsDialogPeopleQuery.graphql";
import type { SignatureDocumentsDialogPeopleRefetchQuery } from "#/__generated__/core/SignatureDocumentsDialogPeopleRefetchQuery.graphql";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
@@ -203,7 +204,10 @@ function PeopleList({
hasNext,
loadNext,
isLoadingNext,
} = usePaginationFragment<SignatureDocumentsDialogPeopleRefetchQuery>(
} = usePaginationFragment<
SignatureDocumentsDialogPeopleRefetchQuery,
SignatureDocumentsDialogPeopleFragment$key
>(
signatureDocumentsDialogPeopleFragment,
data.organization,
);

View File

@@ -106,6 +106,18 @@ func (s *DocumentApprovalService) RequestApproval(
err := s.svc.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
profiles := &coredata.MembershipProfiles{}
if err := profiles.LoadByIDs(ctx, tx, s.svc.scope, req.ApproverIDs); err != nil {
return fmt.Errorf("cannot load approver profiles: %w", err)
}
now := time.Now()
for _, p := range *profiles {
if p.ContractEndDate != nil && p.ContractEndDate.Before(now) {
return &ErrProfileContractEnded{ProfileID: p.ID}
}
}
document := &coredata.Document{}
if err := document.LoadByID(ctx, tx, s.svc.scope, req.DocumentID); err != nil {
return fmt.Errorf("cannot load document: %w", err)

View File

@@ -80,6 +80,10 @@ type (
ErrDocumentVersionSignatureAlreadySigned struct {
}
ErrProfileContractEnded struct {
ProfileID gid.GID
}
CreateDocumentRequest struct {
OrganizationID gid.GID
Title string
@@ -217,6 +221,10 @@ func (e ErrDocumentVersionSignatureAlreadySigned) Error() string {
return "document version signature already signed"
}
func (e ErrProfileContractEnded) Error() string {
return fmt.Sprintf("cannot use profile %q: contract has ended", e.ProfileID)
}
func (s *DocumentService) Get(
ctx context.Context,
documentID gid.GID,
@@ -883,6 +891,18 @@ func (s *DocumentService) BulkRequestSignatures(
err := s.svc.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
profiles := &coredata.MembershipProfiles{}
if err := profiles.LoadByIDs(ctx, tx, s.svc.scope, req.SignatoryIDs); err != nil {
return fmt.Errorf("cannot load signatory profiles: %w", err)
}
now := time.Now()
for _, p := range *profiles {
if p.ContractEndDate != nil && p.ContractEndDate.Before(now) {
return &ErrProfileContractEnded{ProfileID: p.ID}
}
}
for _, documentID := range req.DocumentIDs {
documentVersion := &coredata.DocumentVersion{}
if err := documentVersion.LoadLatestVersion(ctx, tx, s.svc.scope, documentID); err != nil {
@@ -984,6 +1004,15 @@ func (s *DocumentService) RequestSignature(
return fmt.Errorf("cannot request signature for unpublished version")
}
profile := &coredata.MembershipProfile{}
if err := profile.LoadByID(ctx, tx, s.svc.scope, req.Signatory); err != nil {
return fmt.Errorf("cannot load signatory profile: %w", err)
}
if profile.ContractEndDate != nil && profile.ContractEndDate.Before(time.Now()) {
return &ErrProfileContractEnded{ProfileID: profile.ID}
}
var err error
signature, err = s.createSignatureRequestInTx(ctx, tx, req.DocumentVersionID, req.Signatory, false)
if err != nil {

View File

@@ -1156,6 +1156,10 @@ func (r *mutationResolver) RequestDocumentVersionApproval(ctx context.Context, i
return nil, gqlutils.Conflict(ctx, errNotDraft)
}
if errContractEnded, ok := errors.AsType[*probo.ErrProfileContractEnded](err); ok {
return nil, gqlutils.Conflict(ctx, errContractEnded)
}
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors)
}
@@ -1354,6 +1358,10 @@ func (r *mutationResolver) RequestSignature(ctx context.Context, input types.Req
return nil, gqlutils.Conflict(ctx, errArchived)
}
if errContractEnded, ok := errors.AsType[*probo.ErrProfileContractEnded](err); ok {
return nil, gqlutils.Conflict(ctx, errContractEnded)
}
r.logger.ErrorCtx(ctx, "cannot request signature", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
@@ -1387,6 +1395,10 @@ func (r *mutationResolver) BulkRequestSignatures(ctx context.Context, input type
},
)
if err != nil {
if errContractEnded, ok := errors.AsType[*probo.ErrProfileContractEnded](err); ok {
return nil, gqlutils.Conflict(ctx, errContractEnded)
}
r.logger.ErrorCtx(ctx, "cannot bulk request signatures", log.Error(err))
return nil, gqlutils.Internal(ctx)
}