From 7388ab9c65d6fb6e0eff97547b409cfff8df420e Mon Sep 17 00:00:00 2001 From: Stanislaw <56738034+anvme@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:03:17 +0200 Subject: [PATCH] fix: prevent duplicate CSAT submissions under concurrency The pre-check on response_timestamp was a read-then-write race: concurrent submits all passed it and all wrote. Guard the UPDATE with response_timestamp IS NULL and treat a zero-row update as already submitted. --- internal/csat/csat.go | 7 ++++++- internal/csat/queries.sql | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/csat/csat.go b/internal/csat/csat.go index 34a8e6bf..0d0c01a9 100644 --- a/internal/csat/csat.go +++ b/internal/csat/csat.go @@ -106,11 +106,16 @@ func (m *Manager) UpdateResponse(uuid string, score int, feedback string, meta j meta = json.RawMessage(`{}`) } - _, err = m.q.Update.Exec(uuid, score, feedback, meta) + res, err := m.q.Update.Exec(uuid, score, feedback, meta) if err != nil { m.lo.Error("error updating CSAT", "error", err) return envelope.NewError(envelope.GeneralError, m.i18n.T("globals.messages.somethingWentWrong"), nil) } + // The UPDATE is guarded by response_timestamp IS NULL, so a concurrent + // request that already recorded a response updates zero rows here. + if n, _ := res.RowsAffected(); n == 0 { + return envelope.NewError(envelope.InputError, m.i18n.T("csat.alreadySubmitted"), nil) + } return nil } diff --git a/internal/csat/queries.sql b/internal/csat/queries.sql index faa2a6cd..b8e68d65 100644 --- a/internal/csat/queries.sql +++ b/internal/csat/queries.sql @@ -23,4 +23,4 @@ SET rating = $2, feedback = $3, meta = COALESCE($4::jsonb, '{}'), response_timestamp = NOW() -WHERE uuid = $1; +WHERE uuid = $1 AND response_timestamp IS NULL;