using Dapper; using Npgsql; namespace KArtSell.Modules.IdentityAccess.ManageIdentityAndRoles.Features.RequestMfaSetup; public interface IRequestMfaSetupSql { Task<(Guid Id, string State, int Revision)> GetIdentityAsync(Guid identityId, CancellationToken ct); Task UpdateIdentityStateAsync(Guid identityId, string newState, int expectedRevision, CancellationToken ct); } public sealed class RequestMfaSetupSql : IRequestMfaSetupSql { private readonly Func> _connectionFactory; public RequestMfaSetupSql(Func> connectionFactory) { _connectionFactory = connectionFactory; } public async Task<(Guid Id, string State, int Revision)> GetIdentityAsync(Guid identityId, CancellationToken ct) { using var conn = await _connectionFactory(); const string sql = """ SELECT id, state, revision_version FROM identity.identity WHERE id = @identityId """; var row = await conn.QuerySingleOrDefaultAsync(sql, new { identityId }, commandTimeout: 5); if (row is null) throw new InvalidOperationException($"Identity {identityId} not found"); return ((Guid)row.id, (string)row.state, (int)row.revision_version); } public async Task UpdateIdentityStateAsync(Guid identityId, string newState, int expectedRevision, CancellationToken ct) { using var conn = await _connectionFactory(); const string sql = """ UPDATE identity.identity SET state = @newState, revision_version = revision_version + 1, updated_at = NOW(), published_at = NOW() WHERE id = @identityId AND revision_version = @expectedRevision """; var rowsAffected = await conn.ExecuteAsync(sql, new { identityId, newState, expectedRevision }, commandTimeout: 5); if (rowsAffected == 0) throw new InvalidOperationException("Optimistic concurrency violation: state changed"); } }