using Data.SeaHavenIndustries; using Microsoft.EntityFrameworkCore; using SeaHaven.DataServices.Dto; using SeaHaven.DataServices.Interfaces; namespace SeaHaven.DataServices.Implementation { public class TeamMemberInviteDataService : ITeamMemberInviteDataService { private readonly ApplicationDbContext _context; public TeamMemberInviteDataService(ApplicationDbContext context) { _context = context; } public async Task AddAsync(TeamMemberInvite invite, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(invite); _context.TeamMemberInvites.Add(invite); await _context.SaveChangesAsync(cancellationToken); } public async Task ReplaceOpenForUserAsync( TeamMemberInvite invite, DateTime now, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(invite); await using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken); await _context.TeamMemberInvites .Where(existing => existing.UserId == invite.UserId && existing.UsedAt == null && existing.RevokedAt == null) .ExecuteUpdateAsync( setters => setters.SetProperty(existing => existing.RevokedAt, now), cancellationToken); _context.TeamMemberInvites.Add(invite); await _context.SaveChangesAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); } public Task GetByTokenHashAsync(string tokenHash, CancellationToken cancellationToken) { return _context.TeamMemberInvites .AsNoTracking() .Where(invite => invite.TokenHash == tokenHash) .Select(invite => new TeamMemberInviteData { Id = invite.Id, UserId = invite.UserId, ExpiresAt = invite.ExpiresAt, UsedAt = invite.UsedAt, RevokedAt = invite.RevokedAt, CodeHash = invite.CodeHash, CodeSalt = invite.CodeSalt, CodeExpiresAt = invite.CodeExpiresAt, CodeFailedAttempts = invite.CodeFailedAttempts, CodeSentAt = invite.CodeSentAt, CodeSendCount = invite.CodeSendCount, EmailConfirmedAt = invite.EmailConfirmedAt }) .SingleOrDefaultAsync(cancellationToken); } public async Task TryStartCodeAsync( StartTeamMemberInviteCodeCommand command, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(command); var updated = await OpenInvite(command.InviteId, command.Now) .Where(invite => invite.CodeSendCount < command.MaxSends && (invite.CodeSentAt == null || invite.CodeSentAt <= command.LastSentNoLaterThan)) .ExecuteUpdateAsync( setters => setters .SetProperty(invite => invite.CodeHash, command.CodeHash) .SetProperty(invite => invite.CodeSalt, command.CodeSalt) .SetProperty(invite => invite.CodeExpiresAt, command.CodeExpiresAt) .SetProperty(invite => invite.CodeFailedAttempts, 0) .SetProperty(invite => invite.CodeSentAt, command.Now) .SetProperty(invite => invite.CodeSendCount, invite => invite.CodeSendCount + 1), cancellationToken); return updated == 1; } public async Task TryReserveCodeAttemptAsync( int inviteId, int maxAttempts, DateTime now, CancellationToken cancellationToken) { var updated = await OpenInvite(inviteId, now) .Where(invite => invite.CodeHash != null && invite.CodeExpiresAt > now && invite.CodeFailedAttempts < maxAttempts) .ExecuteUpdateAsync( setters => setters.SetProperty( invite => invite.CodeFailedAttempts, invite => invite.CodeFailedAttempts + 1), cancellationToken); return updated == 1; } public async Task TryConfirmEmailAsync( int inviteId, string codeHash, DateTime now, CancellationToken cancellationToken) { var updated = await OpenInvite(inviteId, now) .Where(invite => invite.CodeHash == codeHash && invite.CodeExpiresAt > now) .ExecuteUpdateAsync( setters => setters .SetProperty(invite => invite.EmailConfirmedAt, now) .SetProperty(invite => invite.CodeHash, (string?)null) .SetProperty(invite => invite.CodeSalt, (string?)null) .SetProperty(invite => invite.CodeExpiresAt, (DateTime?)null) .SetProperty(invite => invite.CodeFailedAttempts, 0), cancellationToken); return updated == 1; } public async Task TryClaimAsync(int inviteId, DateTime now, CancellationToken cancellationToken) { var updated = await OpenInvite(inviteId, now) .Where(invite => invite.EmailConfirmedAt != null) .ExecuteUpdateAsync( setters => setters.SetProperty(invite => invite.UsedAt, now), cancellationToken); return updated == 1; } private IQueryable OpenInvite(int inviteId, DateTime now) { return _context.TeamMemberInvites.Where(invite => invite.Id == inviteId && invite.UsedAt == null && invite.RevokedAt == null && invite.ExpiresAt > now); } } }