/
d3h5
/
Unistream.TestTask
Обзор
Документация
Войти
/
d3h5
/
Unistream.TestTask
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
Unistream.TestTask.DataAccessImpl/DataContexts/DataServices/MainDataService.cs
186 строк
9 KB
WORKSTATION-H\mrthe
Правки, отладка
03 июн 2025, 17:24
03 июн 2025, 17:24
012cad7
Код
Авторство
О чём код?
using Microsoft.EntityFrameworkCore; using Unistream.TestTask.DataAccessImpl.Extensions; using Unistream.TestTask.DataAccessInterfaces.DataContexts.DataServices; using Unistream.TestTask.DataAccessInterfaces.DataContexts.Entities; using Unistream.TestTask.Utils.Exceptions; namespace Unistream.TestTask.DataAccessImpl.DataContexts.DataServices; public class MainDataService(ApplicationDbContext applicationDbContext) : IMainDataService { private readonly ApplicationDbContext _applicationDbContext = applicationDbContext; /// <inheritdoc/> public async Task AddTransaction(TransactionEntity transaction, Operation type, CancellationToken cancellationToken = default) { transaction.InsertDateTime = DateTime.UtcNow; transaction.Status = Status.Created; try { _ = await _applicationDbContext.Transactions.AddAsync(transaction, cancellationToken); _ = await _applicationDbContext.SaveChangesAsync(cancellationToken); } catch (Exception ex) { // Может возникнуть ошибка, что транзакция уже существует с таким Id, была добавлена в другом потоке или инстансе сервиса. if (ex is DbUpdateException && await GetTransactionById(transaction.Id, cancellationToken) != null) { throw new TransactionNoAccessException($"Доступ к транзакции {transaction.Id} ограничен, она в обработке.", transaction.Id, null); } throw; } } /// <inheritdoc/> public async Task<TransactionEntity?> GetPrevTransaction(Guid clientId, long currentNonce, CancellationToken cancellationToken = default) { return await _applicationDbContext.Transactions .Where(t => t.ClientId == clientId && t.Nonce < currentNonce) .FirstOrDefaultAsync(cancellationToken); } /// <inheritdoc/> public async Task SetErrorTransaction(TransactionEntity transaction, CancellationToken cancellationToken = default) { transaction.Status = Status.Error; _ = await _applicationDbContext.SaveChangesAsync(cancellationToken); } /// <inheritdoc/> public async Task SetProcessingTransaction(TransactionEntity transaction, CancellationToken cancellationToken = default) { transaction.Status = Status.Processing; _ = await _applicationDbContext.SaveChangesAsync(cancellationToken); } /// <inheritdoc/> public async Task SetCompletedTransaction(TransactionEntity transaction, CancellationToken cancellationToken = default) { transaction.Status = Status.Completed; _ = await _applicationDbContext.SaveChangesAsync(cancellationToken); } public async Task SetTimeout(int sec, CancellationToken cancellationToken = default) { //_ = await _applicationDbContext.Database.ExecuteSqlRawAsync($"SET LOCAL lock_timeout TO @timeout", // new NpgsqlParameter("@timeout", $"{sec}s"), // cancellationToken //); _ = await _applicationDbContext.Database.ExecuteSqlRawAsync($"SET LOCAL lock_timeout TO '{sec}s'", cancellationToken); } /// <inheritdoc/> public async Task<TransactionEntity?> GetTransactionById(Guid id, CancellationToken cancellationToken = default) { return await _applicationDbContext.Transactions.FirstOrDefaultAsync(t => t.Id == id, cancellationToken); } /// <inheritdoc/> public async Task<TransactionEntity?> RevertTransaction(TransactionEntity transaction, CancellationToken cancellationToken = default) { // Сделаем нашу транзакцию отмененной transaction.Status = Status.Cancelled; // Получим баланс клиента перед добавлением транзакции var balance = await GetClientBalance(transaction.ClientId, cancellationToken); if (transaction.Operation == Operation.Credit && balance < transaction.Amount) { throw new NegativeBalanceException($"Баланс клиента не может стать меньше нуля, отмена транзакции {transaction.Id} невозможна.", transaction.Id, transaction.ClientId); } // Добавим новую транзакцию, которая будет отменять transaction var revertTransaction = new TransactionEntity { Id = Guid.NewGuid(), ClientId = transaction.ClientId, ParentTransactionId = transaction.Id, DateTime = DateTime.UtcNow, Operation = transaction.Operation == Operation.Credit ? Operation.Debit : Operation.Credit, InsertDateTime = DateTime.UtcNow, Amount = transaction.Amount, BalanceBefore = balance, BalanceAfter = balance - transaction.Amount, Status = Status.Reverse }; _ = await _applicationDbContext.Transactions.AddAsync(revertTransaction, cancellationToken); _ = await _applicationDbContext.SaveChangesAsync(cancellationToken); // Мы параноики, поэтому сразу после добавления транзакции получаем её из БД return await GetTransactionById(revertTransaction.Id, cancellationToken); } /// <inheritdoc/> public async Task<decimal> GetClientBalance(Guid id, CancellationToken cancellationToken = default) { return (await _applicationDbContext.Transactions .AsNoTracking() .Where(x => x.ClientId == id && (x.Status == Status.Completed || x.Status == Status.Reverse)) .OrderByDescending(t => t.Nonce) .FirstOrDefaultAsync(cancellationToken))? .BalanceAfter ?? 0; } /// <inheritdoc/> public async Task<decimal> GetClientBalance(Guid id, long maxNonce, CancellationToken cancellationToken = default) { return (await _applicationDbContext.Transactions .AsNoTracking() .Where(x => x.ClientId == id && (x.Status == Status.Completed || x.Status == Status.Reverse) && x.Nonce < maxNonce) .OrderByDescending(t => t.Nonce) .FirstOrDefaultAsync(cancellationToken))? .BalanceAfter ?? 0; } /// <inheritdoc/> public async Task<bool> CheckClient(Guid id, CancellationToken cancellationToken = default) { return await _applicationDbContext.Transactions .AsNoTracking() .AnyAsync(t => t.ClientId == id, cancellationToken); } /// <inheritdoc/> public async Task<TransactionEntity?> GetLastTransactionByClientId(Guid id, CancellationToken cancellationToken = default) { return await _applicationDbContext.Transactions .AsNoTracking() .Where(t => t.ClientId == id).OrderByDescending(t => t.DateTime) .FirstOrDefaultAsync(cancellationToken); } /// <inheritdoc/> public async Task BlockTransaction(TransactionEntity transaction, CancellationToken cancellationToken = default) { _ = await _applicationDbContext.Transactions .Where(x => x.ClientId == transaction.ClientId && x.Nonce >= transaction.Nonce) .ForUpdate() .FirstAsync(cancellationToken); } /// <inheritdoc/> public async Task BlockClient(Guid clientId, CancellationToken cancellationToken = default) { _ = await _applicationDbContext.Transactions .Where(x => x.ClientId == clientId) .ForUpdate() .OrderBy(x => x.Nonce) .FirstAsync(cancellationToken); } /// <inheritdoc/> public async Task<bool> IsBlockClient(Guid clientId, CancellationToken cancellationToken = default) { return !await _applicationDbContext.Transactions .Where(x => x.ClientId == clientId) .ForUpdateSkipLocked() .OrderBy(x => x.Nonce) .AnyAsync(cancellationToken); } }