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

@ -85,11 +85,64 @@ func InvitationToWeb(value servicemodel.Invitation, frontendURL string) webmodel
ID: value.ID, JobID: value.JobID, ParticipantID: value.ParticipantID,
ParticipantName: value.ParticipantName, ParticipantEmail: value.ParticipantEmail,
Token: value.Token, Status: value.Status,
Outcome: value.Outcome,
ParticipantURL: frontendURL + "/participant/" + value.Token,
AcceptedAt: value.AcceptedAt, CompletedAt: value.CompletedAt, CreatedAt: value.CreatedAt,
}
}
func UserToWeb(value servicemodel.User) webmodel.User {
return webmodel.User{
ID: value.ID, Email: value.Email, DisplayName: value.DisplayName, Role: value.Role,
}
}
func ScheduledInterviewToWeb(
value servicemodel.ScheduledInterview, frontendURL string,
) webmodel.ScheduledInterviewResponse {
attendees := make([]webmodel.InterviewAttendeeResponse, len(value.Attendees))
for i, attendee := range value.Attendees {
attendees[i] = InterviewAttendeeToWeb(attendee)
}
notes := make([]webmodel.InterviewNoteResponse, len(value.Notes))
for i, note := range value.Notes {
notes[i] = InterviewNoteToWeb(note)
}
return webmodel.ScheduledInterviewResponse{
ID: value.ID, JobID: value.JobID, JobTitle: value.Job.Title,
ParticipantID: value.ParticipantID,
ParticipantName: value.Participant.FirstName + " " + value.Participant.LastName,
StartsAt: value.StartsAt, Location: value.Location, CandidateToken: value.CandidateToken,
CandidateURL: frontendURL + "/participant/meeting/" + value.CandidateToken,
SharedDocument: value.SharedDocument, Status: value.Status,
Attendees: attendees, Notes: notes,
CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt,
}
}
func ParticipantMeetingToWeb(value servicemodel.ScheduledInterview) webmodel.ParticipantMeetingResponse {
return webmodel.ParticipantMeetingResponse{
ID: value.ID, JobID: value.JobID, JobTitle: value.Job.Title,
ParticipantName: value.Participant.FirstName + " " + value.Participant.LastName,
StartsAt: value.StartsAt, Location: value.Location,
SharedDocument: value.SharedDocument, Status: value.Status,
}
}
func InterviewAttendeeToWeb(value servicemodel.InterviewAttendee) webmodel.InterviewAttendeeResponse {
return webmodel.InterviewAttendeeResponse{
UserID: value.UserID, DisplayName: value.User.DisplayName,
Email: value.User.Email, Status: value.Status,
}
}
func InterviewNoteToWeb(value servicemodel.InterviewNote) webmodel.InterviewNoteResponse {
return webmodel.InterviewNoteResponse{
ID: value.ID, AuthorUserID: value.AuthorID, AuthorName: value.Author.DisplayName,
Body: value.Body, CreatedAt: value.CreatedAt,
}
}
func InterviewToWeb(value servicemodel.Interview, frontendURL string) webmodel.InterviewResponse {
questions := make([]webmodel.InterviewQuestionResponse, len(value.Job.Questions))
for i, question := range value.Job.Questions {