Skip to content

Commit

Permalink
feature: categorize data (#36)
Browse files Browse the repository at this point in the history
* feat: add FileId entity and change dbContext

* feat: change add-account API and add 2 APIs which handle categorized-get and categorized-delete for account

* feat: change add-transaction API and add 2 APIs which handle categorized-get and categorized-delete for transaction

* feat: add file-id-controller

* fix: fix unit-test
  • Loading branch information
amiralirahimii authored Sep 8, 2024
1 parent 37297a3 commit bb4d055
Show file tree
Hide file tree
Showing 25 changed files with 538 additions and 93 deletions.
2 changes: 2 additions & 0 deletions src/Application/Interfaces/Repositories/IAccountRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ public interface IAccountRepository
Task<Account?> GetByIdAsync(long accountId);
Task<List<Account>> GetAllAccounts();
Task<List<long>> GetAllIdsAsync();
Task<List<Account>> GetByFileIdAsync(long fileId);
Task DeleteByFileIdAsync(long fileId);
}
11 changes: 11 additions & 0 deletions src/Application/Interfaces/Repositories/IFileIdRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Domain.Entities;

namespace Application.Interfaces.Repositories;

public interface IFileIdRepository
{
Task<bool> IdExistsAsync(long fileId);
Task AddAsync(FileId fileId);
Task DeleteByIdAsync(long fileId);
Task<List<FileId>> GetAllIdsAsync();
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public interface ITransactionRepository
Task<List<Transaction>> GetBySourceAccountId(long accountId);
Task<List<Transaction>> GetByDestinationAccountId(long accountId);
Task<List<long>> GetAllIdsAsync();
Task<List<Transaction>> GetByFileIdAsync(long fileId);
Task DeleteByFileIdAsync(long fileId);
}
4 changes: 3 additions & 1 deletion src/Application/Interfaces/Services/IAccountService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ namespace Application.Interfaces.Services;

public interface IAccountService
{
Task<Result> AddAccountsFromCsvAsync(string filePath);
Task<Result> AddAccountsFromCsvAsync(string filePath, long fileId);
Task<Result<Account>> GetAccountByIdAsync(long accountId);
Task<Result<List<Account>>> GetAllAccountsAsync();
Task<Result<List<Account>>> GetAccountsByFileIdAsync(long fileId);
Task<Result> DeleteAccountsByFileIdAsync(long fileId);
}
4 changes: 3 additions & 1 deletion src/Application/Interfaces/Services/ITransactionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ namespace Application.Interfaces.Services;

public interface ITransactionService
{
Task<Result> AddTransactionsFromCsvAsync(string filePath);
Task<Result> AddTransactionsFromCsvAsync(string filePath, long fileId);
Task<Result<List<Transaction>>> GetAllTransactionsAsync();
Task<Result<List<GetTransactionsByAccountIdResponse>>> GetTransactionsByAccountIdAsync(long accountId);
Task<Result<List<Transaction>>> GetTransactionsByFileIdAsync(long fileId);
Task<Result> DeleteTransactionsByFileIdAsync(long fileId);
}
5 changes: 3 additions & 2 deletions src/Application/Mappers/AccountMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Application.Mappers;

public static class AccountMapper
{
public static Account ToAccount(this AccountCsvModel csvModel)
public static Account ToAccount(this AccountCsvModel csvModel, long fileId)
{
return new Account
{
Expand All @@ -18,7 +18,8 @@ public static Account ToAccount(this AccountCsvModel csvModel)
BranchName = csvModel.BranchName,
OwnerName = csvModel.OwnerName,
OwnerLastName = csvModel.OwnerLastName,
OwnerId = csvModel.OwnerId
OwnerId = csvModel.OwnerId,
FileId = fileId
};
}
}
5 changes: 3 additions & 2 deletions src/Application/Mappers/TransactionMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace Application.Mappers;
public static class TransactionMapper
{
public static Transaction ToTransaction(this TransactionCsvModel csvModel)
public static Transaction ToTransaction(this TransactionCsvModel csvModel, long fileId)
{
var date = DateOnly.Parse(csvModel.Date).ToDateTime(TimeOnly.Parse(csvModel.Time));
var utcDate = DateTime.SpecifyKind(date, DateTimeKind.Utc);
Expand All @@ -16,7 +16,8 @@ public static Transaction ToTransaction(this TransactionCsvModel csvModel)
Amount = csvModel.Amount,
Date = utcDate,
Type = csvModel.Type,
TrackingId = csvModel.TrackingId
TrackingId = csvModel.TrackingId,
FileId = fileId
};
}
}
51 changes: 47 additions & 4 deletions src/Application/Services/DomainService/AccountService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,34 @@ public class AccountService : IAccountService
{
private readonly IAccountRepository _accountRepository;
private readonly IFileReaderService _fileReaderService;
private readonly IFileIdRepository _fileIdRepository;

public AccountService(IAccountRepository accountRepository, IFileReaderService fileReaderService)
public AccountService(IAccountRepository accountRepository, IFileReaderService fileReaderService, IFileIdRepository fileIdRepository)
{
_accountRepository = accountRepository;
_fileReaderService = fileReaderService;
_fileIdRepository = fileIdRepository;
}

public async Task<Result> AddAccountsFromCsvAsync(string filePath)
public async Task<Result> AddAccountsFromCsvAsync(string filePath, long fileId)
{
try
{
var accountCsvModels = _fileReaderService.ReadFromFile<AccountCsvModel>(filePath);

var accounts = accountCsvModels
.Select(csvModel => csvModel.ToAccount())
.Select(csvModel => csvModel.ToAccount(fileId))
.ToList();

var existingAccountIds = await _accountRepository.GetAllIdsAsync();
var newAccounts = accounts.Where(a => !existingAccountIds.Contains(a.AccountId)).ToList();


var fileAlreadyExists = await _fileIdRepository.IdExistsAsync(fileId);
if (fileAlreadyExists)
{
return Result.Fail("File-Id already exists");
}
await _fileIdRepository.AddAsync(new FileId { Id = fileId });
await _accountRepository.CreateBulkAsync(newAccounts);
return Result.Ok();
}
Expand Down Expand Up @@ -72,4 +80,39 @@ public async Task<Result<List<Account>>> GetAllAccountsAsync()
return Result<List<Account>>.Fail($"An unexpected error occurred: {ex.Message}");
}
}

public async Task<Result<List<Account>>> GetAccountsByFileIdAsync(long fileId)
{
try
{
if (!await _fileIdRepository.IdExistsAsync(fileId))
{
return Result<List<Account>>.Fail("File-Id not found");
}
var accounts = await _accountRepository.GetByFileIdAsync(fileId);
return Result<List<Account>>.Ok(accounts);
}
catch (Exception ex)
{
return Result<List<Account>>.Fail($"An unexpected error occurred: {ex.Message}");
}
}

public async Task<Result> DeleteAccountsByFileIdAsync(long fileId)
{
try
{
if (!await _fileIdRepository.IdExistsAsync(fileId))
{
return Result<List<Account>>.Fail("File-Id not found");
}
await _accountRepository.DeleteByFileIdAsync(fileId);
await _fileIdRepository.DeleteByIdAsync(fileId);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail($"An unexpected error occurred: {ex.Message}");
}
}
}
61 changes: 53 additions & 8 deletions src/Application/Services/DomainService/TransactionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,60 @@ public class TransactionService : ITransactionService
private readonly ITransactionRepository _transactionRepository;
private readonly IFileReaderService _fileReaderService;
private readonly IAccountRepository _accountRepository;

public TransactionService(ITransactionRepository transactionRepository, IFileReaderService fileReaderService, IAccountRepository accountRepository)
private readonly IFileIdRepository _fileIdRepository;
public TransactionService(ITransactionRepository transactionRepository,
IFileReaderService fileReaderService,
IAccountRepository accountRepository,
IFileIdRepository fileIdRepository)
{
_transactionRepository = transactionRepository;
_fileReaderService = fileReaderService;
_accountRepository = accountRepository;
_fileIdRepository = fileIdRepository;
}

private async Task<List<long>> ValidateTransactionCsvModelsAsync(List<TransactionCsvModel> transactionCsvModels)
private async Task<List<long>> ValidateTransactionCsvModelsAsync(List<TransactionCsvModel> transactionCsvModels, long fileId)
{
var invalidTransactionIds = new List<long>();
foreach (var transactionCsvModel in transactionCsvModels)
{
var sourceAccount = await _accountRepository.GetByIdAsync(transactionCsvModel.SourceAccount);
var destinationAccount = await _accountRepository.GetByIdAsync(transactionCsvModel.DestinationAccount);
bool isValidSourceAccount = (sourceAccount != null) && (sourceAccount.FileId == fileId);
bool isValidDestinationAccount = (destinationAccount != null) && (destinationAccount.FileId == fileId);
bool isValidDate = DateOnly.TryParseExact(transactionCsvModel.Date, "MM/dd/yyyy", null, System.Globalization.DateTimeStyles.None, out var date);
bool isValidTime = TimeOnly.TryParse(transactionCsvModel.Time, out var time);
if(sourceAccount == null || destinationAccount == null || !isValidDate || !isValidTime)
if(!isValidSourceAccount || !isValidDestinationAccount || !isValidDate || !isValidTime)
{
invalidTransactionIds.Add(transactionCsvModel.TransactionId);
}
}
return invalidTransactionIds;
}

public async Task<Result> AddTransactionsFromCsvAsync(string filePath)
public async Task<Result> AddTransactionsFromCsvAsync(string filePath, long fileId)
{
try
{
var transactionCsvModels = _fileReaderService.ReadFromFile<TransactionCsvModel>(filePath);
var invalidTransactionCsvModels = await ValidateTransactionCsvModelsAsync(transactionCsvModels);
var invalidTransactionCsvModels = await ValidateTransactionCsvModelsAsync(transactionCsvModels, fileId);
var transactions = transactionCsvModels
.Where(csvModel => !invalidTransactionCsvModels.Contains(csvModel.TransactionId))
.Select(csvModel => csvModel.ToTransaction())
.Select(csvModel => csvModel.ToTransaction(fileId))
.ToList();

var existingTransactionsIds = await _transactionRepository.GetAllIdsAsync();
var newTransactions = transactions.Where(t => !existingTransactionsIds.Contains(t.TransactionId)).ToList();

var fileAlreadyExists = await _fileIdRepository.IdExistsAsync(fileId);
if (!fileAlreadyExists)
{
return Result.Fail("File-Id do not exist");
}
await _transactionRepository.CreateBulkAsync(newTransactions);
return Result.Ok(invalidTransactionCsvModels.Count == 0
? "All transactions were added successfully."
: $"Some transactions were not added because of invalid data: {string.Join(", ", invalidTransactionCsvModels)}");
: $"{invalidTransactionCsvModels.Count} transactions were not added because of invalid data: {string.Join(", ", invalidTransactionCsvModels)}");
}
catch (Exception ex)
{
Expand Down Expand Up @@ -121,4 +132,38 @@ public async Task<Result<List<GetTransactionsByAccountIdResponse>>> GetTransacti
return Result<List<GetTransactionsByAccountIdResponse>>.Fail($"An error occurred: {ex.Message}");
}
}

public async Task<Result<List<Transaction>>> GetTransactionsByFileIdAsync(long fileId)
{
try
{
if (!await _fileIdRepository.IdExistsAsync(fileId))
{
return Result<List<Transaction>>.Fail("File-Id not found");
}
var transactions = await _transactionRepository.GetByFileIdAsync(fileId);
return Result<List<Transaction>>.Ok(transactions);
}
catch (Exception ex)
{
return Result<List<Transaction>>.Fail($"An error occurred: {ex.Message}");
}
}

public async Task<Result> DeleteTransactionsByFileIdAsync(long fileId)
{
try
{
if (!await _fileIdRepository.IdExistsAsync(fileId))
{
return Result.Fail("File-Id not found");
}
await _transactionRepository.DeleteByFileIdAsync(fileId);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail($"An error occurred: {ex.Message}");
}
}
}
2 changes: 2 additions & 0 deletions src/Domain/Entities/Account.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public class Account
[MaxLength(50)]
public string OwnerLastName { get; set; } = String.Empty;
public long OwnerId { get; set; }
public long FileId { get; set; }
public FileId? File { get; set; }

public List<Transaction> SourceTransactions { get; set; } = new();
public List<Transaction> DestinationTransactions { get; set; } = new();
Expand Down
11 changes: 11 additions & 0 deletions src/Domain/Entities/FileId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Domain.Entities;

[Table("FileIds")]
public class FileId
{
[Key]
public long Id { get; set; }
}
2 changes: 2 additions & 0 deletions src/Domain/Entities/Transaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ public class Transaction
[MaxLength(50)]
public string Type { get; set; } = String.Empty;
public long TrackingId { get; set; }
public long FileId { get; set; }
public FileId? File { get; set; }
}
13 changes: 13 additions & 0 deletions src/Infrastructure/Data/ApplicationDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> dbConte
{
public DbSet<Account> Accounts { get; set; }
public DbSet<Transaction> Transactions { get; set; }
public DbSet<FileId> Files { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
Expand All @@ -34,5 +35,17 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.WithMany(a => a.DestinationTransactions)
.HasForeignKey(t => t.DestinationAccountId)
.OnDelete(DeleteBehavior.Restrict);

modelBuilder.Entity<Transaction>()
.HasOne(t => t.File)
.WithMany()
.HasForeignKey(t => t.FileId)
.OnDelete(DeleteBehavior.Restrict);

modelBuilder.Entity<Account>()
.HasOne(t => t.File)
.WithMany()
.HasForeignKey(t => t.FileId)
.OnDelete(DeleteBehavior.Restrict);
}
}
Loading

0 comments on commit bb4d055

Please sign in to comment.