@@ -66,6 +66,90 @@ func (r *attemptRepository) GetByID(ctx context.Context, id int64) (*models.Asse
6666 return & attempt , nil
6767}
6868
69+ func (r * attemptRepository ) GetByInviteID (ctx context.Context , inviteID int64 ) (* models.AssessmentAttempt , error ) {
70+ const query = `
71+ SELECT id, invite_id, started_at, completed_at, status, total_score, created_at, updated_at
72+ FROM assessment_attempts
73+ WHERE invite_id = $1
74+ LIMIT 1
75+ `
76+ var attempt models.AssessmentAttempt
77+ err := r .db .QueryRowContext (ctx , query , inviteID ).Scan (
78+ & attempt .ID ,
79+ & attempt .InviteID ,
80+ & attempt .StartedAt ,
81+ & attempt .CompletedAt ,
82+ & attempt .Status ,
83+ & attempt .TotalScore ,
84+ & attempt .CreatedAt ,
85+ & attempt .UpdatedAt ,
86+ )
87+ if errors .Is (err , sql .ErrNoRows ) {
88+ return nil , repositories .ErrNotFound
89+ }
90+ if err != nil {
91+ return nil , fmt .Errorf ("get assessment attempt by invite id: %w" , err )
92+ }
93+ return & attempt , nil
94+ }
95+
96+ func (r * attemptRepository ) GetAssessmentTemplateIDForAttempt (ctx context.Context , attemptID int64 ) (int64 , error ) {
97+ const query = `
98+ SELECT i.assessment_template_id
99+ FROM assessment_attempts a
100+ JOIN assessment_invites i ON i.id = a.invite_id
101+ WHERE a.id = $1
102+ `
103+ var assessmentID int64
104+ err := r .db .QueryRowContext (ctx , query , attemptID ).Scan (& assessmentID )
105+ if errors .Is (err , sql .ErrNoRows ) {
106+ return 0 , repositories .ErrNotFound
107+ }
108+ if err != nil {
109+ return 0 , fmt .Errorf ("get assessment template id for attempt: %w" , err )
110+ }
111+ return assessmentID , nil
112+ }
113+
114+ func (r * attemptRepository ) FinalizeAttempt (ctx context.Context , attemptID int64 , totalScore int , completedAt time.Time , attemptStatus string ) error {
115+ tx , err := r .db .BeginTx (ctx , nil )
116+ if err != nil {
117+ return fmt .Errorf ("finalize attempt begin tx: %w" , err )
118+ }
119+ defer func () { _ = tx .Rollback () }()
120+
121+ res , err := tx .ExecContext (ctx , `
122+ UPDATE assessment_attempts
123+ SET total_score = $2, completed_at = $3, status = $4, updated_at = NOW()
124+ WHERE id = $1 AND status = 'in_progress'
125+ ` , attemptID , totalScore , completedAt , attemptStatus )
126+ if err != nil {
127+ return fmt .Errorf ("finalize attempt update attempt: %w" , err )
128+ }
129+ n , err := res .RowsAffected ()
130+ if err != nil {
131+ return fmt .Errorf ("finalize attempt rows affected: %w" , err )
132+ }
133+ if n == 0 {
134+ return repositories .ErrNotFound
135+ }
136+
137+ _ , err = tx .ExecContext (ctx , `
138+ UPDATE assessment_invites i
139+ SET status = 'completed', updated_at = NOW()
140+ FROM assessment_attempts a
141+ WHERE i.id = a.invite_id AND a.id = $1
142+ ` , attemptID )
143+ if err != nil {
144+ return fmt .Errorf ("finalize attempt update invite: %w" , err )
145+ }
146+
147+ if err := tx .Commit (); err != nil {
148+ return fmt .Errorf ("finalize attempt commit: %w" , err )
149+ }
150+ return nil
151+ }
152+
69153func (r * attemptRepository ) ListByAssessmentID (ctx context.Context , assessmentID int64 ) ([]models.AssessmentAttempt , error ) {
70154 // Attempts link to assessments via assessment_invites.assessment_template_id.
71155 const query = `
@@ -105,6 +189,46 @@ func (r *attemptRepository) ListByAssessmentID(ctx context.Context, assessmentID
105189 return attempts , nil
106190}
107191
192+ func (r * attemptRepository ) ListWithCandidateEmailByAssessmentID (ctx context.Context , assessmentID int64 ) ([]models.AttemptWithCandidateEmail , error ) {
193+ const query = `
194+ SELECT a.id, a.invite_id, a.started_at, a.completed_at, a.status,
195+ a.total_score, a.created_at, a.updated_at,
196+ i.candidate_email
197+ FROM assessment_attempts a
198+ JOIN assessment_invites i ON i.id = a.invite_id
199+ WHERE i.assessment_template_id = $1
200+ ORDER BY a.id
201+ `
202+ rows , err := r .db .QueryContext (ctx , query , assessmentID )
203+ if err != nil {
204+ return nil , fmt .Errorf ("list assessment attempts with email: %w" , err )
205+ }
206+ defer rows .Close ()
207+
208+ var out []models.AttemptWithCandidateEmail
209+ for rows .Next () {
210+ var row models.AttemptWithCandidateEmail
211+ if err := rows .Scan (
212+ & row .ID ,
213+ & row .InviteID ,
214+ & row .StartedAt ,
215+ & row .CompletedAt ,
216+ & row .Status ,
217+ & row .TotalScore ,
218+ & row .CreatedAt ,
219+ & row .UpdatedAt ,
220+ & row .CandidateEmail ,
221+ ); err != nil {
222+ return nil , fmt .Errorf ("scan attempt with email: %w" , err )
223+ }
224+ out = append (out , row )
225+ }
226+ if err := rows .Err (); err != nil {
227+ return nil , fmt .Errorf ("iterate attempts with email: %w" , err )
228+ }
229+ return out , nil
230+ }
231+
108232func (r * attemptRepository ) UpdateStatus (ctx context.Context , id int64 , status string ) error {
109233 const query = `
110234 UPDATE assessment_attempts
0 commit comments