feat: add collaborative interview scheduling

Add candidate decisions, co-interviewer accounts, scheduled interview inbox and calendar views, candidate-safe meeting links, shared documents, and private live notes backed by RabbitMQ.

Extend the disposable integration environment to validate PostgreSQL migrations, seeded users, interview lifecycle behavior, and RabbitMQ fanout.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Daniel Noulet 2026-09-19 21:58:24 +02:00
parent fd06fbbb5d
commit 75e3c73a61
35 changed files with 3621 additions and 58 deletions

View file

@ -21,6 +21,7 @@ var (
ErrInvalidQuestion = errors.New("invalid question")
ErrValidation = errors.New("validation failed")
ErrInvalidState = errors.New("operation is not allowed in the current state")
ErrForbidden = errors.New("operation is forbidden")
)
const CandidateThreshold = 60.0
@ -60,6 +61,7 @@ type IInvitationService interface {
Accept(context.Context, string) (model.Interview, error)
SaveAnswer(context.Context, string, uint, string) error
Finish(context.Context, string) (model.Interview, error)
SetOutcome(context.Context, uint, uint, string) (model.Invitation, error)
}
type AuthService struct {
@ -460,6 +462,25 @@ func (s *InvitationService) Finish(ctx context.Context, token string) (model.Int
return s.GetInterview(ctx, token)
}
func (s *InvitationService) SetOutcome(
ctx context.Context, jobID, invitationID uint, outcome string,
) (model.Invitation, error) {
switch outcome {
case "pending":
case "passed", "failed":
invitation, err := s.store.GetInvitation(ctx, jobID, invitationID)
if err != nil {
return model.Invitation{}, err
}
if invitation.Status != "completed" {
return model.Invitation{}, fmt.Errorf("%w: assessment must be completed", ErrInvalidState)
}
default:
return model.Invitation{}, fmt.Errorf("%w: unsupported assessment outcome", ErrValidation)
}
return s.store.UpdateInvitationOutcome(ctx, jobID, invitationID, outcome)
}
func interviewExpired(interview model.Interview, now time.Time) bool {
return interview.Invitation.AcceptedAt != nil &&
!now.Before(interview.Invitation.AcceptedAt.Add(time.Duration(interview.Job.DurationMinutes)*time.Minute))