sync full source from private myoffice
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
namespace MyOffice.Services.Account;
|
||||
|
||||
using Data.Models.Accounts;
|
||||
|
||||
/// <summary>
|
||||
/// Account access checks shared by service mutations (motions, settings, invites).
|
||||
/// </summary>
|
||||
public static class AccountAcl
|
||||
{
|
||||
public static AccountAccess? GetUserAccess(Account account, Guid userId) =>
|
||||
account.AccessRights?.FirstOrDefault(x => x.UserId == userId);
|
||||
|
||||
public static bool CanWrite(Account account, Guid userId) =>
|
||||
GetUserAccess(account, userId)?.IsAllowWrite == true;
|
||||
|
||||
public static bool CanManage(Account account, Guid userId) =>
|
||||
GetUserAccess(account, userId)?.IsAllowManage == true;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace MyOffice.Services.Account;
|
||||
|
||||
/// <summary>
|
||||
/// Rules for counterparty (transfer) motion amounts when balancing between accounts.
|
||||
/// </summary>
|
||||
public static class AccountMotionBalancing
|
||||
{
|
||||
/// <summary>
|
||||
/// When the primary motion is income (Plus != 0), balancing is expense on the other account.
|
||||
/// Otherwise balancing is income on the other account.
|
||||
/// </summary>
|
||||
public static (decimal Plus, decimal Minus) ResolveAmounts(decimal primaryPlus, decimal amountBalancing)
|
||||
{
|
||||
if (primaryPlus != 0)
|
||||
{
|
||||
return (0, amountBalancing);
|
||||
}
|
||||
|
||||
return (amountBalancing, 0);
|
||||
}
|
||||
|
||||
public static string BalancingItemName(string accountName) => $"+{accountName}";
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
namespace MyOffice.Services.Account;
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Core;
|
||||
using Core.Extensions;
|
||||
using Data.Models.Accounts;
|
||||
using Domain;
|
||||
|
||||
public partial class AccountService
|
||||
{
|
||||
public async Task<Exec<AccountAccessInviteDto, AccessInviteStatus>> AccessInviteAsync(
|
||||
Guid userId,
|
||||
Guid accountId,
|
||||
string email,
|
||||
bool isAllowWrite,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (email == null)
|
||||
throw new ArgumentNullException(nameof(email));
|
||||
|
||||
var result = new Exec<AccountAccessInviteDto, AccessInviteStatus>(AccessInviteStatus.success);
|
||||
|
||||
var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken);
|
||||
if (account == null)
|
||||
{
|
||||
return result.Set(AccessInviteStatus.account_not_found);
|
||||
}
|
||||
|
||||
if (!CanManage(account, userId))
|
||||
{
|
||||
return result.Set(AccessInviteStatus.forbidden);
|
||||
}
|
||||
|
||||
if (account.AccessRights!.Any(x => x.User!.Email.EqualsIgnoreCase(email)))
|
||||
{
|
||||
return result.Set(AccessInviteStatus.access_exists);
|
||||
}
|
||||
|
||||
var access = await _accountAccessInviteRepository.GetAsync(userId, email, cancellationToken);
|
||||
if (access != null)
|
||||
{
|
||||
return result.Set(AccessInviteStatus.invite_exists);
|
||||
}
|
||||
|
||||
var invite = new AccountAccessInvite
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
UserId = userId,
|
||||
AccountId = account.Id,
|
||||
Email = email.SafeTrim()!,
|
||||
IsAllowWrite = isAllowWrite,
|
||||
};
|
||||
|
||||
await _accountAccessInviteRepository.AddAsync(invite, cancellationToken);
|
||||
|
||||
return result.Set(_mapper.Map<AccountAccessInviteDto>(invite));
|
||||
}
|
||||
|
||||
public async Task<Exec<AccountDto, GeneralExecStatus>> AccessUpdateAsync(
|
||||
Guid userId,
|
||||
Guid accountId,
|
||||
List<AccountAccessDto> accesses,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (accesses == null)
|
||||
throw new ArgumentNullException(nameof(accesses));
|
||||
|
||||
var result = new Exec<AccountDto, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken);
|
||||
if (account == null)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
if (!CanManage(account, userId))
|
||||
{
|
||||
return result.Set(GeneralExecStatus.forbidden);
|
||||
}
|
||||
|
||||
foreach (var access in account.AccessRights!)
|
||||
{
|
||||
var newAccess = accesses.FirstOrDefault(x => x.UserId == access.UserId);
|
||||
if (newAccess != null && newAccess.IsAllowWrite != access.IsAllowWrite)
|
||||
{
|
||||
access.IsAllowWrite = newAccess.IsAllowWrite;
|
||||
await _accountAccessRepository.UpdateAsync(access, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
account = await _accountRepository.GetAsync(userId, accountId, cancellationToken);
|
||||
|
||||
return result.Set(_mapper.Map<AccountDto>(account));
|
||||
}
|
||||
|
||||
public async Task<Exec<AccountDto, GeneralExecStatus>> AccessDeleteAsync(
|
||||
Guid userId,
|
||||
Guid accountId,
|
||||
Guid accessUserId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new Exec<AccountDto, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken);
|
||||
if (account == null || !account.AccessRights!.Any() || account.AccessRights!.Count() == 1)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
if (!CanManage(account, userId))
|
||||
{
|
||||
return result.Set(GeneralExecStatus.forbidden);
|
||||
}
|
||||
|
||||
var access = account.AccessRights!.FirstOrDefault(x => x.UserId == accessUserId);
|
||||
if (access == null)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
if (access.OwnerId == access.UserId)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
await _accountAccessRepository.DeleteAsync(access, cancellationToken);
|
||||
|
||||
account = await _accountRepository.GetAsync(userId, accountId, cancellationToken);
|
||||
|
||||
return result.Set(_mapper.Map<AccountDto>(account));
|
||||
|
||||
}
|
||||
|
||||
public async Task<List<AccountAccessInviteDto>> InvitesGetAsync(string email, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var invites = await _accountAccessInviteRepository.GetActiveAsync(email, cancellationToken);
|
||||
|
||||
return _mapper.Map<List<AccountAccessInviteDto>>(invites);
|
||||
}
|
||||
|
||||
public async Task<Exec<AccountDto, InviteAcceptStatus>> InviteAcceptAsync(
|
||||
Guid userId,
|
||||
Guid id,
|
||||
string name,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new Exec<AccountDto, InviteAcceptStatus>(InviteAcceptStatus.success);
|
||||
|
||||
var invite = await _accountAccessInviteRepository.GetAsync(id, cancellationToken);
|
||||
if (invite == null
|
||||
|| !invite.Email.EqualsIgnoreCase(_contextProvider.User.Email)
|
||||
|| invite.AcceptedOn.HasValue
|
||||
|| invite.RejectedOn.HasValue
|
||||
)
|
||||
{
|
||||
return result.Set(InviteAcceptStatus.invite_not_found);
|
||||
}
|
||||
|
||||
var account = await _accountRepository.GetAsync(invite.UserId, invite.AccountId, cancellationToken);
|
||||
if (account == null)
|
||||
{
|
||||
return result.Set(InviteAcceptStatus.account_not_found);
|
||||
}
|
||||
|
||||
if (account.AccessRights!.Any(x => x.UserId == userId))
|
||||
{
|
||||
result.Set(InviteAcceptStatus.already_accepted);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _accountAccessRepository.AddAsync(new AccountAccess
|
||||
{
|
||||
UserId = userId,
|
||||
AccountId = account.Id,
|
||||
OwnerId = invite.UserId,
|
||||
Name = name,
|
||||
IsAllowRead = true,
|
||||
IsAllowWrite = invite.IsAllowWrite,
|
||||
IsAllowManage = false,
|
||||
Type = AccountAccessTypeEnum.external,
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
invite.AcceptedOn = DateTime.UtcNow;
|
||||
await _accountAccessInviteRepository.UpdateAsync(invite, cancellationToken);
|
||||
|
||||
account = await _accountRepository.GetAsync(userId, account.Id, cancellationToken);
|
||||
|
||||
return result.Set(_mapper.Map<AccountDto>(account));
|
||||
}
|
||||
|
||||
public async Task<Exec<AccountAccessInviteDto, GeneralExecStatus>> InviteRejectAsync(
|
||||
Guid id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new Exec<AccountAccessInviteDto, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var invite = await _accountAccessInviteRepository.GetAsync(id, cancellationToken);
|
||||
if (invite == null
|
||||
|| !invite.Email.EqualsIgnoreCase(_contextProvider.User.Email)
|
||||
|| invite.AcceptedOn.HasValue
|
||||
|| invite.RejectedOn.HasValue
|
||||
)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
invite.RejectedOn = DateTime.UtcNow;
|
||||
await _accountAccessInviteRepository.UpdateAsync(invite, cancellationToken);
|
||||
|
||||
return result.Set(_mapper.Map<AccountAccessInviteDto>(invite));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace MyOffice.Services.Account;
|
||||
|
||||
using Data.Models.Accounts;
|
||||
|
||||
public partial class AccountService
|
||||
{
|
||||
private static AccountAccess? GetUserAccess(Account account, Guid userId) =>
|
||||
AccountAcl.GetUserAccess(account, userId);
|
||||
|
||||
private static bool CanWrite(Account account, Guid userId) =>
|
||||
AccountAcl.CanWrite(account, userId);
|
||||
|
||||
private static bool CanManage(Account account, Guid userId) =>
|
||||
AccountAcl.CanManage(account, userId);
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
namespace MyOffice.Services.Account;
|
||||
|
||||
using MyOffice.Core;
|
||||
using MyOffice.Core.Extensions;
|
||||
using MyOffice.Data.Models.Accounts;
|
||||
using MyOffice.Services.Account.Domain;
|
||||
|
||||
public partial class AccountService
|
||||
{
|
||||
public async Task<List<AccountDto>> GetAllAccountsAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var accounts = await _accountRepository.GetAllAsync(userId, cancellationToken);
|
||||
|
||||
return accounts.ToDto<AccountDto>(_mapper);
|
||||
}
|
||||
|
||||
public async Task<List<AccountDto>> GetByCategoryAsync(Guid userId, Guid categoryId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var accounts = await _accountRepository.GetByCategoryAsync(userId, categoryId, cancellationToken);
|
||||
|
||||
return accounts.ToDto<AccountDto>(_mapper);
|
||||
}
|
||||
|
||||
public async Task<List<AccountDto>> FindAccountsAsync(Guid userId, string term, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var accounts = await _accountRepository.FindAccountsAsync(userId, term, cancellationToken);
|
||||
|
||||
return accounts.ToDto<AccountDto>(_mapper);
|
||||
}
|
||||
|
||||
public List<AccountDetailedDto> GetByCategoryDetailed(Guid userId, Guid categoryId)
|
||||
{
|
||||
return _accountRepository
|
||||
.GetByCategoryDetailed(userId, categoryId)
|
||||
.ToDto<AccountDetailedDto>(_mapper);
|
||||
}
|
||||
|
||||
public async Task<List<AccountDetailedDto>> GetByCategoryDetailedAsync(
|
||||
Guid userId,
|
||||
Guid categoryId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var list = await _accountRepository.GetByCategoryDetailedAsync(userId, categoryId, cancellationToken);
|
||||
return list.ToDto<AccountDetailedDto>(_mapper);
|
||||
}
|
||||
|
||||
public Exec<AccountDetailedDto, GeneralExecStatus> GetByIdDetailed(Guid userId, Guid id)
|
||||
{
|
||||
var result = new Exec<AccountDetailedDto, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var account = _accountRepository.GetByIdDetailed(userId, id);
|
||||
if (account == null)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
return result.Set(account.ToDto<AccountDetailedDto>(_mapper));;
|
||||
}
|
||||
|
||||
public async Task<Exec<AccountDetailedDto, GeneralExecStatus>> GetByIdDetailedAsync(
|
||||
Guid userId,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var result = new Exec<AccountDetailedDto, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var account = await _accountRepository.GetByIdDetailedAsync(userId, id, cancellationToken);
|
||||
if (account == null)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
return result.Set(account.ToDto<AccountDetailedDto>(_mapper));
|
||||
}
|
||||
|
||||
public async Task<Exec<AccountDto, AccountAddStatus>> AccountAddAsync(
|
||||
Guid userId,
|
||||
AccountAdd input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (input == null)
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
|
||||
var result = new Exec<AccountDto, AccountAddStatus>(AccountAddStatus.success);
|
||||
|
||||
var category = await _accountCategoryRepository.GetAsync(userId, input.CategoryId, cancellationToken);
|
||||
if (category == null)
|
||||
{
|
||||
return result.Set(AccountAddStatus.category_not_found);
|
||||
}
|
||||
var currency = await _currencyRepository.GetByGlobalCurrencyAsync(userId, input.CurrencyId, cancellationToken);
|
||||
if (currency == null)
|
||||
{
|
||||
return result.Set(AccountAddStatus.currency_not_found);
|
||||
}
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = input.Name,
|
||||
CurrencyGlobalId = currency.CurrencyGlobalId,
|
||||
OwnerId = userId,
|
||||
};
|
||||
|
||||
account.Categories = new List<AccountAccountCategory>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AccountId = account.Id,
|
||||
CategoryId = category.Id,
|
||||
}
|
||||
};
|
||||
account.AccessRights = new List<AccountAccess>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AccountId = account.Id,
|
||||
UserId = userId,
|
||||
OwnerId = userId,
|
||||
IsAllowManage = true,
|
||||
IsAllowRead = true,
|
||||
IsAllowWrite = true,
|
||||
}
|
||||
};
|
||||
|
||||
if (!await _accountRepository.AddAsync(account, cancellationToken))
|
||||
{
|
||||
return result.Set(AccountAddStatus.failure);
|
||||
}
|
||||
// TODO: Add category refactoring
|
||||
if (!await _accountAccountCategoryRepository.AddAsync(account.Categories.FirstOrDefault()!, cancellationToken))
|
||||
{
|
||||
return result.Set(AccountAddStatus.failure);
|
||||
}
|
||||
// TODO: Add access refactoring
|
||||
if (!await _accountAccessRepository.AddAsync(account.AccessRights.FirstOrDefault()!, cancellationToken))
|
||||
{
|
||||
return result.Set(AccountAddStatus.failure);
|
||||
}
|
||||
|
||||
// TODO: Add account update AccessRights ???
|
||||
var access = account.AccessRights!.FirstOrDefault(x => x.UserId == userId);
|
||||
if (access != null && access.Type.ToString() != input.Type)
|
||||
{
|
||||
if (Enum.TryParse<AccountAccessTypeEnum>(input.Type, out var enumType))
|
||||
{
|
||||
access.Type = enumType;
|
||||
await _accountAccessRepository.UpdateAsync(access, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
return result.Set(account.ToDto<AccountDto>(_mapper));
|
||||
}
|
||||
|
||||
public async Task<Exec<AccountDto, GeneralExecStatus>> AccountDeleteAsync(
|
||||
Guid userId,
|
||||
string id,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var result = new Exec<AccountDto, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var account = await _accountRepository.GetAsync(userId, id.AsGuid(), cancellationToken);
|
||||
if (account == null)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
if (!CanManage(account, userId))
|
||||
{
|
||||
return result.Set(GeneralExecStatus.forbidden);
|
||||
}
|
||||
|
||||
if (account.Motions!.Any())
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
await _accountRepository.DeleteAsync(account, cancellationToken);
|
||||
result.Set(account.ToDto<AccountDto>(_mapper));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Exec<AccountDto, AccountEditStatus>> AccountUpdateAsync(
|
||||
Guid userId,
|
||||
Guid accountId,
|
||||
AccountEdit input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (input == null)
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
|
||||
var result = new Exec<AccountDto, AccountEditStatus>(AccountEditStatus.success);
|
||||
|
||||
AccountCategory? category = null;
|
||||
if (input.CategoryId.HasValue)
|
||||
{
|
||||
category = await _accountCategoryRepository.GetAsync(userId, input.CategoryId.Value, cancellationToken);
|
||||
if (category == null)
|
||||
{
|
||||
return result.Set(AccountEditStatus.category_not_found);
|
||||
}
|
||||
}
|
||||
|
||||
var currency = await _currencyRepository.GetByGlobalCurrencyAsync(userId, input.CurrencyId, cancellationToken);
|
||||
if (currency == null)
|
||||
{
|
||||
return result.Set(AccountEditStatus.currency_not_found);
|
||||
}
|
||||
|
||||
var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken);
|
||||
if (account == null)
|
||||
{
|
||||
return result.Set(AccountEditStatus.not_found);
|
||||
}
|
||||
|
||||
if (!CanManage(account, userId))
|
||||
{
|
||||
return result.Set(AccountEditStatus.forbidden);
|
||||
}
|
||||
|
||||
account.Name = input.Name;
|
||||
if (account.CurrencyGlobalId != currency.CurrencyGlobalId)
|
||||
{
|
||||
account.CurrencyGlobalId = currency.CurrencyGlobalId;
|
||||
}
|
||||
|
||||
AccountAccountCategory? addedLink = null;
|
||||
if (category != null && account.Categories!.All(x => x.CategoryId != category.Id))
|
||||
{
|
||||
addedLink = new AccountAccountCategory
|
||||
{
|
||||
AccountId = account.Id,
|
||||
CategoryId = category.Id,
|
||||
Category = category,
|
||||
};
|
||||
await _accountAccountCategoryRepository.AddAsync(addedLink, cancellationToken);
|
||||
}
|
||||
|
||||
if (!await _accountRepository.UpdateAsync(account, cancellationToken))
|
||||
{
|
||||
return result.Set(AccountEditStatus.failure);
|
||||
}
|
||||
|
||||
var access = account.AccessRights!.FirstOrDefault(x => x.UserId == userId);
|
||||
if (access != null && access.Type.ToString() != input.Type)
|
||||
{
|
||||
if (Enum.TryParse<AccountAccessTypeEnum>(input.Type, out var enumType))
|
||||
{
|
||||
access.Type = enumType;
|
||||
await _accountAccessRepository.UpdateAsync(access, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
// AddAsync detaches the link and does not refresh Account.Categories — keep response in sync.
|
||||
if (addedLink != null && (account.Categories?.All(x => x.CategoryId != addedLink.CategoryId) ?? true))
|
||||
{
|
||||
account.Categories = (account.Categories ?? Enumerable.Empty<AccountAccountCategory>())
|
||||
.Append(addedLink)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return result.Set(account.ToDto<AccountDto>(_mapper));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
namespace MyOffice.Services.Account;
|
||||
|
||||
using MyOffice.Core;
|
||||
using MyOffice.Data.Models.Accounts;
|
||||
using MyOffice.Services.Account.Domain;
|
||||
|
||||
public partial class AccountService
|
||||
{
|
||||
public async Task<List<AccountCategoryDto>> GetAllCategoriesAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var categories = await _accountCategoryRepository.GetAllAsync(userId, cancellationToken);
|
||||
|
||||
return _mapper.Map<List<AccountCategoryDto>>(categories);
|
||||
}
|
||||
|
||||
public async Task<Exec<AccountCategoryDto, GeneralExecStatus>> GetCategoryAsync(
|
||||
Guid userId,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new Exec<AccountCategoryDto, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var category = await _accountCategoryRepository.GetAsync(userId, id, cancellationToken);
|
||||
|
||||
if (category == null)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
return result.Set(_mapper.Map<AccountCategoryDto>(category));
|
||||
}
|
||||
|
||||
public async Task<Exec<AccountCategoryDto, GeneralExecStatus>> CategoryAddAsync(
|
||||
Guid userId,
|
||||
AccountCategoryDto input,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (input == null)
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
|
||||
var result = new Exec<AccountCategoryDto, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var category = new AccountCategory
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Name = input.Name,
|
||||
};
|
||||
|
||||
if (!await _accountCategoryRepository.AddAsync(category, cancellationToken))
|
||||
{
|
||||
return result.Set(GeneralExecStatus.failure);
|
||||
}
|
||||
|
||||
return result.Set(_mapper.Map<AccountCategoryDto>(category));
|
||||
}
|
||||
|
||||
public async Task<Exec<AccountCategoryDto, GeneralExecStatus>> CategoryUpdateAsync(
|
||||
Guid userId,
|
||||
Guid id,
|
||||
AccountCategoryDto input,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (input == null)
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
|
||||
var result = new Exec<AccountCategoryDto, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var exists = await _accountCategoryRepository.GetAsync(userId, id, cancellationToken);
|
||||
if (exists == null)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
exists.Name = input.Name;
|
||||
|
||||
if (!await _accountCategoryRepository.UpdateAsync(exists, cancellationToken))
|
||||
{
|
||||
return result.Set(GeneralExecStatus.failure);
|
||||
}
|
||||
|
||||
return result.Set(_mapper.Map<AccountCategoryDto>(exists));
|
||||
}
|
||||
|
||||
public async Task<Exec<AccountCategoryDto, AccountCategoryRemoveResult>> CategoryRemoveAsync(
|
||||
Guid userId,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new Exec<AccountCategoryDto, AccountCategoryRemoveResult>(AccountCategoryRemoveResult.success);
|
||||
|
||||
var exists = await _accountCategoryRepository.GetAsync(userId, id, cancellationToken);
|
||||
if (exists == null)
|
||||
{
|
||||
return result.Set(AccountCategoryRemoveResult.not_found);
|
||||
}
|
||||
|
||||
if (exists.Accounts?.Any() == true)
|
||||
{
|
||||
return result.Set(AccountCategoryRemoveResult.accounts_exists);
|
||||
}
|
||||
if (!await _accountCategoryRepository.RemoveAsync(exists, cancellationToken))
|
||||
{
|
||||
return result.Set(AccountCategoryRemoveResult.failure);
|
||||
}
|
||||
|
||||
return result.Set(_mapper.Map<AccountCategoryDto>(exists));
|
||||
}
|
||||
|
||||
public async Task<Exec<AccountDto, GeneralExecStatus>> AccountCategoryRemoveAsync(
|
||||
Guid userId,
|
||||
Guid accountId,
|
||||
Guid categoryId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new Exec<AccountDto, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken);
|
||||
if (account == null)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
if (!CanManage(account, userId))
|
||||
{
|
||||
return result.Set(GeneralExecStatus.forbidden);
|
||||
}
|
||||
|
||||
var links = await _accountAccountCategoryRepository.GetAsync(userId, accountId, categoryId, cancellationToken);
|
||||
if (links.Count == 0)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
foreach (var link in links)
|
||||
{
|
||||
await _accountAccountCategoryRepository.RemoveAsync(link, cancellationToken);
|
||||
}
|
||||
|
||||
// Account stays tracked after Remove; filter in-memory so the response is not stale.
|
||||
account.Categories = account.Categories?
|
||||
.Where(c => c.CategoryId != categoryId)
|
||||
.ToList();
|
||||
|
||||
return result.Set(account.ToDto<AccountDto>(_mapper));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
namespace MyOffice.Services.Account;
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Core;
|
||||
using Core.Extensions;
|
||||
using Data.Models.Accounts;
|
||||
using Domain;
|
||||
using Item.Domain;
|
||||
|
||||
public partial class AccountService
|
||||
{
|
||||
public async Task<Exec<List<MotionDto>, MotionAddStatus>> MotionAddAsync(
|
||||
Guid userId,
|
||||
Guid accountId,
|
||||
MotionAddUpdate motion,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (motion == null)
|
||||
throw new ArgumentNullException(nameof(motion));
|
||||
if (motion.Item == null)
|
||||
throw new ArgumentNullException(nameof(motion.Item));
|
||||
|
||||
var result = new Exec<List<MotionDto>, MotionAddStatus>(MotionAddStatus.success);
|
||||
|
||||
var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken);
|
||||
if (account == null)
|
||||
{
|
||||
return result.Set(MotionAddStatus.account_not_found);
|
||||
}
|
||||
|
||||
if (!CanWrite(account, userId))
|
||||
{
|
||||
return result.Set(MotionAddStatus.forbidden);
|
||||
}
|
||||
|
||||
var itemExec = await _itemService.GetOrCreateAsync(userId, motion.Item, cancellationToken);
|
||||
if (itemExec.Status != ItemGetOrAddResult.success)
|
||||
{
|
||||
return result.Set(MotionAddStatus.failure);
|
||||
}
|
||||
|
||||
var motionDb = new Motion
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
DateTime = motion.Date,
|
||||
AccountId = account.Id,
|
||||
ItemId = itemExec.Result!.Id,
|
||||
Description = motion.Description,
|
||||
AmountPlus = motion.Plus,
|
||||
AmountMinus = motion.Minus,
|
||||
};
|
||||
|
||||
if (!await _motionRepository.AddAsync(motionDb, cancellationToken))
|
||||
{
|
||||
return result.Set(MotionAddStatus.failure);
|
||||
}
|
||||
|
||||
result.Set(new List<MotionDto>());
|
||||
result.Result!.Add(_mapper.Map<MotionDto>(motionDb));
|
||||
|
||||
if (motion.AccountId.IsPresent() && motion.AmountBalancing != 0)
|
||||
{
|
||||
var accountBalancing = await _accountRepository.GetAsync(
|
||||
userId,
|
||||
motion.AccountId!.AsGuid(),
|
||||
cancellationToken);
|
||||
if (accountBalancing != null && CanWrite(accountBalancing, userId))
|
||||
{
|
||||
var balancingName = AccountMotionBalancing.BalancingItemName(account.Name);
|
||||
var itemBalancingExec = await _itemService.GetOrCreateAsync(
|
||||
userId,
|
||||
balancingName,
|
||||
cancellationToken);
|
||||
var (plus, minus) = AccountMotionBalancing.ResolveAmounts(motion.Plus, motion.AmountBalancing);
|
||||
var motionBalancing = new Motion
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
DateTime = motion.Date,
|
||||
AccountId = accountBalancing.Id,
|
||||
ItemId = itemBalancingExec.Result!.Id,
|
||||
Description = motion.Description,
|
||||
AmountPlus = plus,
|
||||
AmountMinus = minus,
|
||||
};
|
||||
|
||||
if (await _motionRepository.AddAsync(motionBalancing, cancellationToken))
|
||||
{
|
||||
result.Result!.Add(_mapper.Map<MotionDto>(motionBalancing));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
motionDb.Item = itemExec.Result!;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Exec<MotionDto, MotionUpdateStatus>> MotionUpdateAsync(
|
||||
Guid userId,
|
||||
Guid accountId,
|
||||
Guid motionId,
|
||||
MotionAddUpdate motion,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (motion == null)
|
||||
throw new ArgumentNullException(nameof(motion));
|
||||
if (motion.Item == null)
|
||||
throw new ArgumentNullException(nameof(motion.Item));
|
||||
|
||||
var result = new Exec<MotionDto, MotionUpdateStatus>(MotionUpdateStatus.success);
|
||||
|
||||
var exists = await _motionRepository.GetAsync(userId, motionId, cancellationToken);
|
||||
if (exists == null || exists.AccountId != accountId)
|
||||
{
|
||||
return result.Set(MotionUpdateStatus.not_found);
|
||||
}
|
||||
|
||||
var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken);
|
||||
if (account == null)
|
||||
{
|
||||
return result.Set(MotionUpdateStatus.not_found);
|
||||
}
|
||||
|
||||
if (!CanWrite(account, userId))
|
||||
{
|
||||
return result.Set(MotionUpdateStatus.forbidden);
|
||||
}
|
||||
|
||||
var itemExec = await _itemService.GetOrCreateAsync(userId, motion.Item, cancellationToken);
|
||||
if (itemExec.Status != ItemGetOrAddResult.success)
|
||||
{
|
||||
return result.Set(MotionUpdateStatus.failure);
|
||||
}
|
||||
|
||||
exists.Item.Id = itemExec.Result!.Id;
|
||||
exists.DateTime = motion.Date;
|
||||
exists.Description = motion.Description;
|
||||
exists.AmountPlus = motion.Plus;
|
||||
exists.AmountMinus = motion.Minus;
|
||||
|
||||
if (!await _motionRepository.UpdateAsync(exists, cancellationToken))
|
||||
{
|
||||
return result.Set(MotionUpdateStatus.failure);
|
||||
}
|
||||
|
||||
return result.Set(_mapper.Map<MotionDto>(exists));
|
||||
}
|
||||
|
||||
public async Task<Exec<List<MotionDto>, GeneralExecStatus>> GetMotionsAsync(
|
||||
Guid userId,
|
||||
Guid accountId,
|
||||
DateTime dateFrom,
|
||||
DateTime dateTo,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var result = new Exec<List<MotionDto>, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken);
|
||||
if (account == null)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
var motions = await _motionRepository.GetByAccountAsync(
|
||||
accountId,
|
||||
dateFrom.ToUtc(),
|
||||
dateTo.ToUtc(),
|
||||
cancellationToken);
|
||||
|
||||
return result.Set(_mapper.Map<List<MotionDto>>(motions));
|
||||
}
|
||||
|
||||
public async Task<Exec<MotionDto, MotionDeleteStatus>> MotionRemoveAsync(
|
||||
Guid userId,
|
||||
Guid accountId,
|
||||
Guid motionId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var result = new Exec<MotionDto, MotionDeleteStatus>(MotionDeleteStatus.success);
|
||||
|
||||
var exists = await _motionRepository.GetAsync(userId, motionId, cancellationToken);
|
||||
if (exists == null || exists.AccountId != accountId)
|
||||
{
|
||||
return result.Set(MotionDeleteStatus.not_found);
|
||||
}
|
||||
|
||||
var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken);
|
||||
if (account == null)
|
||||
{
|
||||
return result.Set(MotionDeleteStatus.not_found);
|
||||
}
|
||||
|
||||
if (!CanWrite(account, userId))
|
||||
{
|
||||
return result.Set(MotionDeleteStatus.forbidden);
|
||||
}
|
||||
|
||||
if (!await _motionRepository.RemoveAsync(exists, cancellationToken))
|
||||
{
|
||||
return result.Set(MotionDeleteStatus.failure);
|
||||
}
|
||||
|
||||
return result.Set(_mapper.Map<MotionDto>(exists));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace MyOffice.Services.Account;
|
||||
|
||||
using AutoMapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Identity;
|
||||
using Data.Repositories.Account;
|
||||
using Data.Repositories.Currency;
|
||||
using Data.Repositories.Item;
|
||||
using Item;
|
||||
|
||||
public partial class AccountService
|
||||
{
|
||||
private ILogger<AccountService> _logger;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IAccountCategoryRepository _accountCategoryRepository;
|
||||
private readonly IAccountAccessRepository _accountAccessRepository;
|
||||
private readonly IAccountAccessInviteRepository _accountAccessInviteRepository;
|
||||
private readonly IAccountRepository _accountRepository;
|
||||
private readonly ICurrencyRepository _currencyRepository;
|
||||
private readonly IAccountAccountCategoryRepository _accountAccountCategoryRepository;
|
||||
private readonly IMotionRepository _motionRepository;
|
||||
private readonly IItemRepository _itemRepository;
|
||||
private readonly IItemGlobalRepository _itemGlobalRepository;
|
||||
private readonly ItemService _itemService;
|
||||
private readonly IContextProvider _contextProvider;
|
||||
|
||||
public AccountService(
|
||||
ILogger<AccountService> logger,
|
||||
IMapper mapper,
|
||||
IAccountCategoryRepository accountCategoryRepository,
|
||||
IAccountAccessRepository accountAccessRepository,
|
||||
IAccountAccessInviteRepository accountAccessInviteRepository,
|
||||
IAccountRepository accountRepository,
|
||||
ICurrencyRepository currencyRepository,
|
||||
IAccountAccountCategoryRepository accountAccountCategoryRepository,
|
||||
IMotionRepository motionRepository,
|
||||
IItemRepository itemRepository,
|
||||
IItemGlobalRepository itemGlobalRepository,
|
||||
ItemService itemService,
|
||||
IContextProvider contextProvider
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_mapper = mapper;
|
||||
_accountCategoryRepository = accountCategoryRepository;
|
||||
_accountAccessRepository = accountAccessRepository;
|
||||
_accountAccessInviteRepository = accountAccessInviteRepository;
|
||||
_accountRepository = accountRepository;
|
||||
_currencyRepository = currencyRepository;
|
||||
_accountAccountCategoryRepository = accountAccountCategoryRepository;
|
||||
_motionRepository = motionRepository;
|
||||
_itemRepository = itemRepository;
|
||||
_itemGlobalRepository = itemGlobalRepository;
|
||||
_itemService = itemService;
|
||||
_contextProvider = contextProvider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
public enum AccessInviteStatus
|
||||
{
|
||||
success,
|
||||
account_not_found,
|
||||
access_exists,
|
||||
invite_exists,
|
||||
forbidden,
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
using AutoMapper;
|
||||
using Data.Models.Accounts;
|
||||
using MyOffice.Services.Identity;
|
||||
|
||||
public class AccountAccessDto
|
||||
{
|
||||
public Guid AccountId { get; set; }
|
||||
public AccountDto? Account { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User can access to account
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
public UserDto? User { get; set; }
|
||||
|
||||
public bool IsAllowRead { get; set; }
|
||||
public bool IsAllowWrite { get; set; }
|
||||
public bool IsAllowManage { get; set; }
|
||||
public bool IsOwner { get; set; }
|
||||
public AccountAccessTypeEnum Type { get; set; }
|
||||
/// <summary>
|
||||
/// Who add access
|
||||
/// </summary>
|
||||
public Guid OwnerId { get; set; }
|
||||
public UserDto? Owner { get; set; }
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
public class AccountAccessDtoProfile : Profile
|
||||
{
|
||||
public AccountAccessDtoProfile()
|
||||
{
|
||||
CreateMap<AccountAccess, AccountAccessDto>()
|
||||
.AfterMap<AccountAccessDtoMappingAction>()
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
public class AccountAccessDtoMappingAction : IMappingAction<AccountAccess, AccountAccessDto>
|
||||
{
|
||||
private readonly IContextProvider _contextProvider;
|
||||
public AccountAccessDtoMappingAction(IContextProvider contextProvider)
|
||||
{
|
||||
_contextProvider = contextProvider;
|
||||
}
|
||||
|
||||
public void Process(AccountAccess source, AccountAccessDto destination, ResolutionContext context)
|
||||
{
|
||||
destination.IsOwner = destination.OwnerId == _contextProvider.UserId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/// <see cref="MyOffice.Data.Models.Accounts.AccountAccessInvite"/>
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
using AutoMapper;
|
||||
using Data.Models.Accounts;
|
||||
|
||||
public class AccountAccessInviteDto
|
||||
{
|
||||
public string Id { get; set; } = null!;
|
||||
public string Account { get; set; } = null!;
|
||||
public bool IsAllowWrite { get; set; }
|
||||
}
|
||||
|
||||
public class AccountAccessInviteDtoProfile: Profile
|
||||
{
|
||||
public AccountAccessInviteDtoProfile()
|
||||
{
|
||||
CreateMap<AccountAccessInvite, AccountAccessInviteDto>()
|
||||
.ForMember(x => x.Account, o => o.MapFrom(x => x.Account!.Name))
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/// <see cref="MyOffice.Data.Models.Accounts.AccountAccountCategory"/>
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
using AutoMapper;
|
||||
using MyOffice.Data.Models.Accounts;
|
||||
|
||||
public class AccountAccountCategoryDto
|
||||
{
|
||||
public Guid CategoryId { get; set; }
|
||||
public AccountCategoryDto? Category { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class AccountAccountCategoryDtoProfile: Profile
|
||||
{
|
||||
public AccountAccountCategoryDtoProfile()
|
||||
{
|
||||
CreateMap<AccountAccountCategory, AccountAccountCategoryDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace MyOffice.Services.Account.Domain
|
||||
{
|
||||
public class AccountAdd
|
||||
{
|
||||
public string Name { get; set; } = null!;
|
||||
public string CurrencyId { get; set; } = null!;
|
||||
public Guid CategoryId { get; set; }
|
||||
public string Type { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace MyOffice.Services.Account.Domain
|
||||
{
|
||||
public enum AccountAddStatus
|
||||
{
|
||||
success,
|
||||
failure,
|
||||
category_not_found,
|
||||
currency_not_found,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/// <see cref="MyOffice.Data.Models.Accounts.AccountCategory"/>
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
using AutoMapper;
|
||||
using MyOffice.Data.Models.Accounts;
|
||||
|
||||
public class AccountCategoryDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
public bool AllowDelete { get; set; }
|
||||
}
|
||||
|
||||
public class AccountCategoryDtoProfile: Profile
|
||||
{
|
||||
public AccountCategoryDtoProfile()
|
||||
{
|
||||
CreateMap<AccountCategory, AccountCategoryDto>()
|
||||
.ForMember(x => x.Id, o => o.MapFrom(x => x.Id))
|
||||
.ForMember(x => x.AllowDelete, o => o.MapFrom(x => !x.Accounts!.Any()))
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace MyOffice.Services.Account.Domain
|
||||
{
|
||||
public enum AccountCategoryRemoveResult
|
||||
{
|
||||
success,
|
||||
failure,
|
||||
not_found,
|
||||
accounts_exists,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/// <see cref="MyOffice.Data.Models.Accounts.Account"/>
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
using AutoMapper;
|
||||
using MyOffice.Core;
|
||||
using MyOffice.Data.Models.Accounts;
|
||||
|
||||
public class AccountDetailedDto: IDataModelDto<AccountDetailed>
|
||||
{
|
||||
public AccountDto Account { get; set; } = null!;
|
||||
public decimal TotalPlus { get; set; }
|
||||
public decimal TotalMinus { get; set; }
|
||||
public decimal Rest => TotalPlus - TotalMinus;
|
||||
}
|
||||
|
||||
public class AccountDetailedDtoProfile: Profile
|
||||
{
|
||||
public AccountDetailedDtoProfile()
|
||||
{
|
||||
CreateMap<AccountDetailed, AccountDetailedDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
using System;
|
||||
using Data.Models.Accounts;
|
||||
using MyOffice.Services.Currency.Domain;
|
||||
using AutoMapper;
|
||||
using MyOffice.Services.Identity;
|
||||
using MyOffice.Core;
|
||||
using MyOffice.Services.Mapper;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
|
||||
public class GenerateMappedDtoAttribute: Attribute
|
||||
{
|
||||
public GenerateMappedDtoAttribute()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[GenerateMappedDto]
|
||||
public class AccountDto : IDataModelDto<Account>
|
||||
{
|
||||
#region Account properties
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public string CurrencyGlobalId { get; set; } = null!;
|
||||
public CurrencyGlobalDto? CurrencyGlobal { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
public Guid OwnerId { get; set; }
|
||||
public UserDto? Owner { get; set; }
|
||||
|
||||
#endregion Account properties
|
||||
|
||||
#region Dto properties
|
||||
|
||||
public Guid CurrencyId { get; set; }
|
||||
public CurrencyDto? Currency { get; set; }
|
||||
public string Type { get; set; } = null!;
|
||||
public List<AccountAccountCategoryDto>? Categories { get; set; }
|
||||
|
||||
public bool HasMotions { get; set; }
|
||||
|
||||
#endregion Dto properties
|
||||
|
||||
#region Permissions
|
||||
|
||||
public List<AccountAccessDto>? AccessRights { get; set; }
|
||||
public bool AllowRead { get; set; }
|
||||
public bool AllowWrite { get; set; }
|
||||
public bool AllowDelete { get; set; }
|
||||
public bool AllowManage { get; set; }
|
||||
|
||||
#endregion Permissions
|
||||
}
|
||||
|
||||
public class AccountDtoProfile : BaseProfile<Account, AccountDto>
|
||||
{
|
||||
public AccountDtoProfile()
|
||||
{
|
||||
Mapping
|
||||
.ForMember(x => x.HasMotions, o => o.MapFrom(x => x.Motions!.Any()))
|
||||
.AfterMap<AccountDtoMappingAction>();
|
||||
}
|
||||
}
|
||||
|
||||
public class AccountDtoMappingAction : BaseMappingAction, IMappingAction<Account, AccountDto>
|
||||
{
|
||||
public AccountDtoMappingAction(IContextProvider contextProvider) : base(contextProvider) { }
|
||||
|
||||
public void Process(Account source, AccountDto destination, ResolutionContext context)
|
||||
{
|
||||
var accessRight = destination.AccessRights?.FirstOrDefault(x => x.UserId == ContextProvider.UserId);
|
||||
destination.AllowRead = accessRight?.IsAllowRead ?? false;
|
||||
destination.AllowWrite = accessRight?.IsAllowWrite ?? false;
|
||||
destination.AllowManage = accessRight?.IsAllowManage ?? false;
|
||||
destination.AllowDelete = (accessRight?.IsOwner ?? false) && !destination.HasMotions;
|
||||
destination.Name = accessRight?.Name ?? destination.Name;
|
||||
destination.Type = accessRight != null ? accessRight.Type.ToString() : destination.Type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
public class AccountEdit
|
||||
{
|
||||
public string Name { get; set; } = null!;
|
||||
public string CurrencyId { get; set; } = null!;
|
||||
public Guid? CategoryId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public string? Type { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
public enum AccountEditStatus
|
||||
{
|
||||
success,
|
||||
failure,
|
||||
not_found,
|
||||
category_not_found,
|
||||
currency_not_found,
|
||||
forbidden,
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace MyOffice.Services.Account.Domain
|
||||
{
|
||||
public enum InviteAcceptStatus
|
||||
{
|
||||
success,
|
||||
invite_not_found,
|
||||
account_not_found,
|
||||
already_accepted,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/// <see cref="MyOffice.Data.Models.Items.ItemCategory"/>
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
using AutoMapper;
|
||||
using MyOffice.Data.Models.Items;
|
||||
|
||||
public class ItemCategoryDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public UserDto User { get; set; } = null!;
|
||||
public string Name { get; set; } = null!;
|
||||
public List<ItemDto> Items { get; set; } = null!;
|
||||
public bool IsInternal { get; set; }
|
||||
}
|
||||
|
||||
public class ItemCategoryDtoProfile: Profile
|
||||
{
|
||||
public ItemCategoryDtoProfile()
|
||||
{
|
||||
CreateMap<ItemCategory, ItemCategoryDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/// <see cref="MyOffice.Data.Models.Items.Item"/>
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
using AutoMapper;
|
||||
using MyOffice.Data.Models.Items;
|
||||
|
||||
public class ItemDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public bool AllowDelete { get; set; }
|
||||
|
||||
public Guid CategoryId { get; set; }
|
||||
public ItemCategoryDto? Category { get; set; }
|
||||
}
|
||||
|
||||
public class ItemDtoProfile: Profile
|
||||
{
|
||||
public ItemDtoProfile()
|
||||
{
|
||||
CreateMap<Item, ItemDto>()
|
||||
.ForMember(x => x.Id, o => o.MapFrom(x => x.ItemGlobalId))
|
||||
.ForMember(x => x.Name, o => o.MapFrom(x => x.ItemGlobal.Name))
|
||||
.ForMember(x => x.AllowDelete, o => o.MapFrom(x => !x.Motions!.Any()))
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
public enum MotionAddStatus
|
||||
{
|
||||
success,
|
||||
failure,
|
||||
account_not_found,
|
||||
forbidden,
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
using AutoMapper;
|
||||
|
||||
public class MotionAddUpdate
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public string Item { get; set; } = null!;
|
||||
public string? ItemId { get; set; } = null!;
|
||||
public string? AccountId { get; set; } = null!;
|
||||
public string? Description { get; set; }
|
||||
public decimal Plus { get; set; }
|
||||
public decimal Minus { get; set; }
|
||||
public decimal AmountBalancing { get; set; }
|
||||
}
|
||||
|
||||
public class MotionAddUpdateProfile: Profile
|
||||
{
|
||||
public MotionAddUpdateProfile()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
public enum MotionDeleteStatus
|
||||
{
|
||||
success,
|
||||
failure,
|
||||
not_found,
|
||||
forbidden,
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
using AutoMapper;
|
||||
using MyOffice.Data.Models.Accounts;
|
||||
|
||||
public class MotionDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public DateTime CreatedOn { get; set; }
|
||||
public DateTime DateTime { get; set; }
|
||||
public Guid AccountId { get; set; }
|
||||
public AccountDto Account { get; set; } = null!;
|
||||
public int ItemId { get; set; }
|
||||
public ItemDto Item { get; set; } = null!;
|
||||
public string? Description { get; set; }
|
||||
public decimal AmountPlus { get; set; }
|
||||
public decimal AmountMinus { get; set; }
|
||||
public DateTime? DeletedOn { get; set; }
|
||||
}
|
||||
|
||||
public class MotionDtoProfile: Profile
|
||||
{
|
||||
public MotionDtoProfile()
|
||||
{
|
||||
CreateMap<Motion, MotionDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
public enum MotionUpdateStatus
|
||||
{
|
||||
success,
|
||||
failure,
|
||||
not_found,
|
||||
forbidden,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/// <see cref="MyOffice.Data.Models.Users.User"/>
|
||||
namespace MyOffice.Services.Account.Domain;
|
||||
|
||||
using AutoMapper;
|
||||
using MyOffice.Data.Models.Users;
|
||||
using MyOffice.Services.Currency.Domain;
|
||||
|
||||
public class UserDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string UserName { get; set; } = null!;
|
||||
public string Email { get; set; } = null!;
|
||||
public string? FirstName { get; set; }
|
||||
public string? LastName { get; set; }
|
||||
public string? FullName { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
|
||||
public string CurrencyId { get; set; } = null!;
|
||||
public CurrencyGlobalDto? Currency { get; set; }
|
||||
}
|
||||
|
||||
public class UserDtoProfile : Profile
|
||||
{
|
||||
public UserDtoProfile()
|
||||
{
|
||||
CreateMap<User, UserDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
namespace MyOffice.Services.Currency;
|
||||
|
||||
using AutoMapper;
|
||||
|
||||
using Core;
|
||||
using Data.Models.Currencies;
|
||||
using Data.Repositories.Currency;
|
||||
using Domain;
|
||||
|
||||
public class CurrencyService
|
||||
{
|
||||
private readonly ICurrencyGlobalRepository _currencyGlobalRepository;
|
||||
private readonly ICurrencyRepository _currencyRepository;
|
||||
private readonly ICurrencyRateRepository _currencyRateRepository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public CurrencyService(
|
||||
ICurrencyGlobalRepository currencyGlobalRepository,
|
||||
ICurrencyRepository currencyRepository,
|
||||
ICurrencyRateRepository currencyRateRepository,
|
||||
IMapper mapper
|
||||
)
|
||||
{
|
||||
_currencyGlobalRepository = currencyGlobalRepository;
|
||||
_currencyRepository = currencyRepository;
|
||||
_currencyRateRepository = currencyRateRepository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<List<CurrencyGlobalDto>> GetGlobalAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _mapper.Map<List<CurrencyGlobalDto>>(await _currencyGlobalRepository.GetAllAsync(cancellationToken));
|
||||
}
|
||||
|
||||
public async Task<List<CurrencyDto>> GetAllAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _mapper.Map<List<CurrencyDto>>(await _currencyRepository.GetAllAsync(userId, cancellationToken));
|
||||
}
|
||||
|
||||
public async Task<List<CurrencyWithRateDto>> GetAllWithRatesAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var list = await _currencyRepository.GetAllAsync(userId, cancellationToken);
|
||||
|
||||
var result = new List<CurrencyWithRateDto>(list.Count);
|
||||
foreach (var currency in list)
|
||||
{
|
||||
var rates = await _currencyRateRepository.GetLastRatesAsync(currency.Id, cancellationToken: cancellationToken);
|
||||
result.Add(new CurrencyWithRateDto
|
||||
{
|
||||
Currency = _mapper.Map<CurrencyDto>(currency),
|
||||
Rate = _mapper.Map<CurrencyRateDto>(rates.FirstOrDefault()),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Exec<CurrencyDto, CurrencyAddStatus>> CurrencyAddAsync(
|
||||
Guid userId,
|
||||
CurrencyDto input,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (input == null)
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
|
||||
var result = new Exec<CurrencyDto, CurrencyAddStatus>(CurrencyAddStatus.success);
|
||||
|
||||
var exists = await _currencyRepository.GetByGlobalCurrencyAsync(userId, input.CurrencyGlobalId, cancellationToken);
|
||||
if (exists != null)
|
||||
{
|
||||
return result.Set(_mapper.Map<CurrencyDto>(exists), CurrencyAddStatus.exists);
|
||||
}
|
||||
|
||||
var currency = new Currency
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
CurrencyGlobalId = input.CurrencyGlobalId,
|
||||
Name = input.Name,
|
||||
ShortName = input.ShortName,
|
||||
};
|
||||
|
||||
if (!await _currencyRepository.AddAsync(currency, cancellationToken))
|
||||
{
|
||||
return result.Set(CurrencyAddStatus.failed);
|
||||
}
|
||||
|
||||
return result.Set(_mapper.Map<CurrencyDto>(currency));
|
||||
}
|
||||
|
||||
public async Task<Exec<CurrencyDto, CurrencyEditStatus>> CurrencyUpdateAsync(
|
||||
Guid userId,
|
||||
Guid currencyId,
|
||||
CurrencyEdit currency,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (currency == null)
|
||||
throw new ArgumentNullException(nameof(currency));
|
||||
|
||||
var result = new Exec<CurrencyDto, CurrencyEditStatus>(CurrencyEditStatus.success);
|
||||
|
||||
var exists = await _currencyRepository.GetAsync(userId, currencyId, cancellationToken);
|
||||
if (exists == null)
|
||||
{
|
||||
return result.Set(CurrencyEditStatus.not_found);
|
||||
}
|
||||
|
||||
exists.Name = currency.Name;
|
||||
exists.ShortName = currency.ShortName;
|
||||
exists.IsPrimary = currency.IsPrimary;
|
||||
|
||||
if (!await _currencyRepository.UpdateAsync(exists, cancellationToken))
|
||||
{
|
||||
return result.Set(CurrencyEditStatus.failed);
|
||||
}
|
||||
|
||||
if (exists.IsPrimary)
|
||||
{
|
||||
var primaries = await _currencyRepository.GetPrimariesAsync(userId, cancellationToken);
|
||||
foreach (var primary in primaries)
|
||||
{
|
||||
if (primary.Id != exists.Id)
|
||||
{
|
||||
primary.IsPrimary = false;
|
||||
await _currencyRepository.UpdateAsync(primary, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.Set(_mapper.Map<CurrencyDto>(exists));
|
||||
}
|
||||
|
||||
public async Task<Exec<CurrencyRateDto, CurrencyAddRateStatus>> CurrencyRateAddAsync(
|
||||
Guid userId,
|
||||
Guid currencyId,
|
||||
CurrencyRateDto input,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (input == null)
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
|
||||
var result = new Exec<CurrencyRateDto, CurrencyAddRateStatus>(CurrencyAddRateStatus.success);
|
||||
|
||||
var currency = await _currencyRepository.GetAsync(userId, currencyId, cancellationToken);
|
||||
if (currency == null)
|
||||
{
|
||||
return result.Set(CurrencyAddRateStatus.not_found);
|
||||
}
|
||||
|
||||
var currencyRate = new CurrencyRate
|
||||
{
|
||||
CurrencyId = currency.Id,
|
||||
DateTime = input.DateTime.Date,
|
||||
Rate = input.Rate,
|
||||
Quantity = input.Quantity,
|
||||
};
|
||||
|
||||
var rates = await _currencyRateRepository.GetAtDateAsync(currencyRate.CurrencyId, currencyRate.DateTime, cancellationToken);
|
||||
var rate = rates.Find(x => x.Rate == currencyRate.Rate);
|
||||
|
||||
if (rate == null)
|
||||
{
|
||||
if (!await _currencyRateRepository.AddRateAsync(currencyRate, cancellationToken))
|
||||
{
|
||||
return result.Set(CurrencyAddRateStatus.failed);
|
||||
}
|
||||
|
||||
// TODO: more logic to fix current rate
|
||||
if (currencyRate.DateTime.Date == DateTime.UtcNow.Date || !currency.CurrentRateId.HasValue)
|
||||
{
|
||||
currency.CurrencyGlobal = null;
|
||||
currency.CurrentRateId = currencyRate.Id;
|
||||
await _currencyRepository.UpdateAsync(currency, cancellationToken);
|
||||
}
|
||||
|
||||
rate = currencyRate;
|
||||
}
|
||||
|
||||
return result.Set(_mapper.Map<CurrencyRateDto>(rate));
|
||||
}
|
||||
|
||||
public async Task<Exec<CurrencyDto, GeneralExecStatus>> RemoveAsync(
|
||||
Guid userId,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new Exec<CurrencyDto, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var currency = await _currencyRepository.GetAsync(userId, id, cancellationToken);
|
||||
if (currency == null)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
if (!await _currencyRepository.RemoveAsync(currency, cancellationToken))
|
||||
{
|
||||
return result.Set(GeneralExecStatus.failure);
|
||||
}
|
||||
|
||||
return result.Set(_mapper.Map<CurrencyDto>(currency));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MyOffice.Services.Currency.Domain;
|
||||
|
||||
public enum CurrencyAddRateStatus
|
||||
{
|
||||
not_found,
|
||||
success,
|
||||
failed,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace MyOffice.Services.Currency.Domain
|
||||
{
|
||||
public enum CurrencyAddStatus
|
||||
{
|
||||
success,
|
||||
failed,
|
||||
exists,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/// <see cref="MyOffice.Data.Models.Currencies.Currency"/>
|
||||
namespace MyOffice.Services.Currency.Domain;
|
||||
|
||||
using AutoMapper;
|
||||
using MyOffice.Data.Models.Currencies;
|
||||
using MyOffice.Services.Account.Domain;
|
||||
|
||||
public class CurrencyDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string CurrencyGlobalId { get; set; } = null!;
|
||||
public CurrencyGlobalDto? CurrencyGlobal { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public UserDto? User { get; set; }
|
||||
|
||||
public string Name { get; set; } = null!;
|
||||
public string ShortName { get; set; } = null!;
|
||||
public List<CurrencyRateDto>? Rates { get; set; }
|
||||
|
||||
public int? CurrentRateId { get; set; }
|
||||
public CurrencyRateDto? CurrentRate { get; set; }
|
||||
|
||||
public bool IsPrimary { get; set; }
|
||||
}
|
||||
|
||||
public class CurrencyDtoProfile: Profile
|
||||
{
|
||||
public CurrencyDtoProfile()
|
||||
{
|
||||
CreateMap<Currency, CurrencyDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MyOffice.Services.Currency.Domain;
|
||||
|
||||
public class CurrencyEdit
|
||||
{
|
||||
public string Name { get; set; } = null!;
|
||||
public string ShortName { get; set; } = null!;
|
||||
public bool IsPrimary { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MyOffice.Services.Currency.Domain;
|
||||
|
||||
public enum CurrencyEditStatus
|
||||
{
|
||||
success,
|
||||
not_found,
|
||||
failed,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/// <see cref="MyOffice.Data.Models.Currencies.CurrencyGlobal"/>
|
||||
namespace MyOffice.Services.Currency.Domain;
|
||||
|
||||
using AutoMapper;
|
||||
using MyOffice.Data.Models.Currencies;
|
||||
|
||||
public class CurrencyGlobalDto
|
||||
{
|
||||
public string Id { get; set; } = null!;
|
||||
public string Name { get; set; } = null!;
|
||||
public string Symbol { get; set; } = null!;
|
||||
public int DefaultQuantity { get; set; }
|
||||
}
|
||||
|
||||
public class CurrencyGlobalDtoProfile: Profile
|
||||
{
|
||||
public CurrencyGlobalDtoProfile()
|
||||
{
|
||||
CreateMap<CurrencyGlobal, CurrencyGlobalDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/// <see cref="MyOffice.Data.Models.Currencies.CurrencyRate"/>
|
||||
namespace MyOffice.Services.Currency.Domain;
|
||||
|
||||
using AutoMapper;
|
||||
using MyOffice.Data.Models.Currencies;
|
||||
|
||||
public class CurrencyRateDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public Guid CurrencyId { get; set; }
|
||||
public CurrencyDto? Currency { get; set; }
|
||||
public DateTime DateTime { get; set; }
|
||||
public int Quantity { get; set; }
|
||||
public decimal Rate { get; set; }
|
||||
public List<CurrencyDto>? Currencies { get; set; }
|
||||
}
|
||||
|
||||
public class CurrencyRateDtoProfile : Profile
|
||||
{
|
||||
public CurrencyRateDtoProfile()
|
||||
{
|
||||
CreateMap<CurrencyRate, CurrencyRateDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace MyOffice.Services.Currency.Domain;
|
||||
|
||||
using MyOffice.Data.Models.Currencies;
|
||||
|
||||
public class CurrencyWithRateDto
|
||||
{
|
||||
public CurrencyDto Currency { get; set; } = null!;
|
||||
public CurrencyRateDto? Rate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
namespace MyOffice.Services.Dashboard;
|
||||
|
||||
using Core.Extensions;
|
||||
using Data.Repositories.Account;
|
||||
using Data.Repositories.Currency;
|
||||
using MyOffice.Data.Models.Accounts;
|
||||
using MyOffice.Services.Dashboard.Domain;
|
||||
|
||||
public class DashboardService
|
||||
{
|
||||
private readonly IAccountRepository _accountRepository;
|
||||
private readonly ICurrencyRateRepository _currencyRateRepository;
|
||||
|
||||
public DashboardService(
|
||||
IAccountRepository accountRepository,
|
||||
ICurrencyRateRepository currencyRateRepository
|
||||
)
|
||||
{
|
||||
_accountRepository = accountRepository;
|
||||
_currencyRateRepository = currencyRateRepository;
|
||||
}
|
||||
|
||||
public async Task<DashboardData> GetDashboardRestDataAsync(
|
||||
Guid userId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var rests = await _accountRepository.GetRestAtDateAsync(userId, DateTime.UtcNow, cancellationToken);
|
||||
|
||||
return new DashboardData
|
||||
{
|
||||
Balance = rests
|
||||
.Where(x => (x.Type == AccountAccessTypeEnum.balance || x.Type == AccountAccessTypeEnum.credit) && x.CurrencyRate.HasValue)
|
||||
.Sum(x => x.Balance * (x.CurrencyRate * x.CurrencyQuantity)),
|
||||
|
||||
BalanceDebit = rests
|
||||
.Where(x => x.Type == AccountAccessTypeEnum.balance && x.CurrencyRate.HasValue)
|
||||
.Sum(x => x.Balance * (x.CurrencyRate * x.CurrencyQuantity)),
|
||||
|
||||
BalanceCredit = rests
|
||||
.Where(x => x.Type == AccountAccessTypeEnum.credit && x.CurrencyRate.HasValue)
|
||||
.Sum(x => x.Balance * (x.CurrencyRate * x.CurrencyQuantity)),
|
||||
|
||||
BalanceRests = rests
|
||||
.Where(x => x.Type == AccountAccessTypeEnum.balance)
|
||||
.Where(x => x.CurrencyRate.HasValue && x.Balance.HasValue && x.Balance != 0)
|
||||
.Select(x => new DashboardRestData
|
||||
{
|
||||
Id = x.Id,
|
||||
Name = x.Name,
|
||||
Balance = x.Balance,
|
||||
CurrencyName = x.CurrencyName!,
|
||||
CurrencyShortName = x.CurrencyShortName!,
|
||||
CurrencyRate = x.CurrencyRate,
|
||||
CurrencyQuantity = x.CurrencyQuantity,
|
||||
})
|
||||
.OrderByDescending(x => x.BalanceAtRate)
|
||||
.ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DashboardIncomeData> GetDashboardIncomeDataAsync(
|
||||
Guid userId,
|
||||
DateTime from,
|
||||
DateTime to,
|
||||
Guid? category,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var data = category.HasValue
|
||||
? await _accountRepository.GetIncomeByCategoryAsync(userId, category.Value, from.ToUtc(), to.ToUtc(), cancellationToken)
|
||||
: await _accountRepository.GetIncomeByCategoriesAsync(userId, from.ToUtc(), to.ToUtc(), cancellationToken);
|
||||
|
||||
data = data
|
||||
.Where(x => x.Amount != 0)
|
||||
.ToList();
|
||||
|
||||
var currencies = data
|
||||
.Select(x => x.CurrencyId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var rates = (await _currencyRateRepository.GetLastRatesAsync(userId, currencies, to.ToUtc(), cancellationToken))
|
||||
.ToDictionary(x => x.Key, x => x.Value.Rate * x.Value.Quantity);
|
||||
|
||||
return BuildIncomeData(data, rates);
|
||||
}
|
||||
|
||||
public async Task<DashboardIncomeData> GetDashboardOutcomeDataAsync(
|
||||
Guid userId,
|
||||
DateTime from,
|
||||
DateTime to,
|
||||
Guid? category,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var data = category.HasValue
|
||||
? await _accountRepository.GetOutcomeByCategoryAsync(userId, category.Value, from.ToUtc(), to.ToUtc(), cancellationToken)
|
||||
: await _accountRepository.GetOutcomeByCategoriesAsync(userId, from.ToUtc(), to.ToUtc(), cancellationToken);
|
||||
|
||||
data = data
|
||||
.Where(x => x.Amount != 0)
|
||||
.ToList();
|
||||
|
||||
var currencies = data
|
||||
.Select(x => x.CurrencyId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var rates = (await _currencyRateRepository.GetLastRatesAsync(userId, currencies, to.ToUtc(), cancellationToken))
|
||||
.ToDictionary(x => x.Key, x => x.Value.Rate * x.Value.Quantity);
|
||||
|
||||
return BuildIncomeData(data, rates);
|
||||
}
|
||||
|
||||
private static DashboardIncomeData BuildIncomeData(
|
||||
List<MotionTotalSimple> data,
|
||||
Dictionary<string, decimal> rates
|
||||
)
|
||||
{
|
||||
return new DashboardIncomeData
|
||||
{
|
||||
Data = data.Select(x => new
|
||||
{
|
||||
Id = x.Id.ToShort(),
|
||||
Name = x.Name,
|
||||
ValueRaw = x.Amount,
|
||||
Value = x.Amount * rates.FirstOrDefault(r => r.Key == x.CurrencyId).Value,
|
||||
})
|
||||
.GroupBy(x => new { x.Id, x.Name })
|
||||
.Select(x => new DashboardIncomeDataItem
|
||||
{
|
||||
Id = x.Key.Id,
|
||||
Name = x.Key.Name,
|
||||
Value = x.Sum(s => s.Value),
|
||||
ValueRaw = x.Sum(s => s.ValueRaw),
|
||||
})
|
||||
.OrderBy(x => x.Name)
|
||||
.ToList(),
|
||||
|
||||
Details = data.Select(x => new
|
||||
{
|
||||
Id = x.CurrencyId,
|
||||
Name = x.CurrencyName,
|
||||
ValueRaw = x.Amount,
|
||||
Value = x.Amount * rates.FirstOrDefault(r => r.Key == x.CurrencyId).Value,
|
||||
})
|
||||
.GroupBy(x => new { x.Id, x.Name })
|
||||
.Select(x => new DashboardIncomeDataItem
|
||||
{
|
||||
Id = x.Key.Id,
|
||||
Name = x.Key.Name,
|
||||
Value = x.Sum(s => s.Value),
|
||||
ValueRaw = x.Sum(s => s.ValueRaw),
|
||||
})
|
||||
.OrderBy(x => x.Name)
|
||||
.ToList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace MyOffice.Services.Dashboard.Domain;
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class DashboardData
|
||||
{
|
||||
public decimal IncomeLast { get; set; }
|
||||
public decimal IncomePrevious { get; set; }
|
||||
public decimal OutcomeLast { get; set; }
|
||||
public decimal OutcomePrevious { get; set; }
|
||||
public decimal? Balance { get; set; }
|
||||
public decimal? BalanceDebit { get; set; }
|
||||
public decimal? BalanceCredit { get; set; }
|
||||
public List<DashboardRestData> BalanceRests { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class DashboardRestData
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
public string CurrencyName { get; set; } = null!;
|
||||
public string CurrencyShortName { get; set; } = null!;
|
||||
public decimal? Balance { get; set; }
|
||||
public decimal? CurrencyRate { get; set; }
|
||||
public decimal? CurrencyQuantity { get; set; }
|
||||
|
||||
public decimal? BalanceAtRate => Balance * (CurrencyRate * CurrencyQuantity);
|
||||
}
|
||||
|
||||
public class DashboardIncomeData
|
||||
{
|
||||
public List<DashboardIncomeDataItem> Data { get; set; } = null!;
|
||||
public List<DashboardIncomeDataItem> Details { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class DashboardIncomeDataItem
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string Currency { get; set; } = null!;
|
||||
public string Name { get; set; } = null!;
|
||||
public decimal Value { get; set; }
|
||||
public decimal ValueRaw { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace MyOffice.Services.Identity;
|
||||
|
||||
using Data.Models.Users;
|
||||
|
||||
public interface IContextProvider
|
||||
{
|
||||
User User { get; }
|
||||
Guid UserId { get; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace MyOffice.Services.Item.Domain
|
||||
{
|
||||
public enum ItemCategoryRemoveResult
|
||||
{
|
||||
success,
|
||||
failure,
|
||||
not_found,
|
||||
accounts_exists,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace MyOffice.Services.Item.Domain
|
||||
{
|
||||
public enum ItemGetOrAddResult
|
||||
{
|
||||
success,
|
||||
failure,
|
||||
not_found,
|
||||
accounts_exists,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
namespace MyOffice.Services.Item;
|
||||
|
||||
using AutoMapper;
|
||||
using Domain;
|
||||
using MyOffice.Core;
|
||||
using MyOffice.Data.Models.Items;
|
||||
using MyOffice.Data.Repositories.Item;
|
||||
using MyOffice.Services.Account.Domain;
|
||||
|
||||
public class ItemService
|
||||
{
|
||||
private readonly IItemCategoryRepository _itemCategoryRepository;
|
||||
private readonly IItemRepository _itemRepository;
|
||||
private readonly IItemGlobalRepository _itemGlobalRepository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public ItemService(
|
||||
IItemCategoryRepository itemCategoryRepository,
|
||||
IItemRepository itemRepository,
|
||||
IItemGlobalRepository itemGlobalRepository,
|
||||
IMapper mapper
|
||||
)
|
||||
{
|
||||
_itemCategoryRepository = itemCategoryRepository;
|
||||
_itemRepository = itemRepository;
|
||||
_itemGlobalRepository = itemGlobalRepository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<List<ItemCategoryDto>> GetAllCategoriesAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _itemCategoryRepository.GetAllAsync(userId, cancellationToken);
|
||||
if (result.All(x => x.Id != userId))
|
||||
{
|
||||
var category = new ItemCategory
|
||||
{
|
||||
Id = userId,
|
||||
UserId = userId,
|
||||
Name = "UnCategorized",
|
||||
};
|
||||
await _itemCategoryRepository.AddAsync(category, cancellationToken);
|
||||
|
||||
result.Add((await _itemCategoryRepository.GetAsync(userId, userId, cancellationToken))!);
|
||||
}
|
||||
return _mapper.Map<List<ItemCategoryDto>>(result);
|
||||
}
|
||||
|
||||
public async Task<Exec<ItemCategoryDto, GeneralExecStatus>> CategoryAddAsync(
|
||||
Guid userId,
|
||||
ItemCategoryDto input,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (input == null)
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
|
||||
var result = new Exec<ItemCategoryDto, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var category = new ItemCategory
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Name = input.Name,
|
||||
IsInternal = input.IsInternal,
|
||||
};
|
||||
|
||||
if (!await _itemCategoryRepository.AddAsync(category, cancellationToken))
|
||||
{
|
||||
return result.Set(GeneralExecStatus.failure);
|
||||
}
|
||||
|
||||
return result.Set(_mapper.Map<ItemCategoryDto>(category));
|
||||
}
|
||||
|
||||
public async Task<Exec<ItemCategoryDto, GeneralExecStatus>> CategoryUpdateAsync(
|
||||
Guid userId,
|
||||
Guid id,
|
||||
ItemCategoryDto category,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (category == null)
|
||||
throw new ArgumentNullException(nameof(category));
|
||||
|
||||
var result = new Exec<ItemCategoryDto, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var exists = await _itemCategoryRepository.GetAsync(userId, id, cancellationToken);
|
||||
if (exists == null)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
exists.Name = category.Name;
|
||||
exists.IsInternal = category.IsInternal;
|
||||
|
||||
if (!await _itemCategoryRepository.UpdateAsync(exists, cancellationToken))
|
||||
{
|
||||
return result.Set(GeneralExecStatus.failure);
|
||||
}
|
||||
|
||||
return result.Set(_mapper.Map<ItemCategoryDto>(exists));
|
||||
}
|
||||
|
||||
public async Task<Exec<ItemCategoryDto, ItemCategoryRemoveResult>> CategoryRemoveAsync(
|
||||
Guid userId,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new Exec<ItemCategoryDto, ItemCategoryRemoveResult>(ItemCategoryRemoveResult.success);
|
||||
|
||||
var exists = await _itemCategoryRepository.GetAsync(userId, id, cancellationToken);
|
||||
if (exists == null)
|
||||
{
|
||||
return result.Set(ItemCategoryRemoveResult.not_found);
|
||||
}
|
||||
|
||||
if (exists.Items.Any())
|
||||
{
|
||||
return result.Set(ItemCategoryRemoveResult.accounts_exists);
|
||||
}
|
||||
if (!await _itemCategoryRepository.RemoveAsync(exists, cancellationToken))
|
||||
{
|
||||
return result.Set(ItemCategoryRemoveResult.failure);
|
||||
}
|
||||
|
||||
return result.Set(_mapper.Map<ItemCategoryDto>(exists));
|
||||
}
|
||||
|
||||
public async Task<List<ItemDto>> GetAllAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _mapper.Map<List<ItemDto>>(await _itemRepository.GetAllAsync(userId, cancellationToken));
|
||||
}
|
||||
|
||||
public async Task<List<ItemDto>> GetByCategoryAsync(
|
||||
Guid userId,
|
||||
Guid categoryId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _mapper.Map<List<ItemDto>>(await _itemRepository.GetByCategoryAsync(userId, categoryId, cancellationToken));
|
||||
}
|
||||
|
||||
public async Task<List<ItemDto>> GetByUncategorizedAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var itemGlobals = await _itemGlobalRepository.GetAvailableToUserAsync(userId, cancellationToken);
|
||||
foreach (var itemGlobal in itemGlobals)
|
||||
{
|
||||
await GetOrCreateAsync(userId, itemGlobal, cancellationToken);
|
||||
}
|
||||
|
||||
return await GetByCategoryAsync(userId, userId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Exec<ItemDto, GeneralExecStatus>> UpdateAsync(
|
||||
Guid userId,
|
||||
Guid motionId,
|
||||
Guid categoryId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new Exec<ItemDto, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var item = await _itemRepository.GetByGlobalAsync(userId, motionId, cancellationToken);
|
||||
if (item == null)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
item.CategoryId = categoryId;
|
||||
await _itemRepository.UpdateAsync(item, cancellationToken);
|
||||
|
||||
return result.Set(_mapper.Map<ItemDto>(item));
|
||||
}
|
||||
|
||||
public async Task<Exec<Item, ItemGetOrAddResult>> GetOrCreateAsync(
|
||||
Guid userId,
|
||||
string name,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (name == null)
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
|
||||
var result = new Exec<Item, ItemGetOrAddResult>(ItemGetOrAddResult.success);
|
||||
|
||||
var itemGlobal = await _itemGlobalRepository.GetByNameAsync(name, cancellationToken);
|
||||
if (itemGlobal == null)
|
||||
{
|
||||
itemGlobal = new ItemGlobal
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = name.Trim(),
|
||||
};
|
||||
|
||||
if (!await _itemGlobalRepository.AddAsync(itemGlobal, cancellationToken))
|
||||
{
|
||||
return result.Set(ItemGetOrAddResult.failure);
|
||||
}
|
||||
|
||||
return await GetOrCreateAsync(userId, itemGlobal, cancellationToken);
|
||||
}
|
||||
|
||||
return await GetOrCreateAsync(userId, itemGlobal, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Exec<Item, ItemGetOrAddResult>> GetOrCreateAsync(
|
||||
Guid userId,
|
||||
ItemGlobal itemGlobal,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (itemGlobal == null)
|
||||
throw new ArgumentNullException(nameof(itemGlobal));
|
||||
|
||||
var result = new Exec<Item, ItemGetOrAddResult>(ItemGetOrAddResult.success);
|
||||
|
||||
var item = await _itemRepository.GetByGlobalAsync(userId, itemGlobal.Id, cancellationToken);
|
||||
if (item == null)
|
||||
{
|
||||
item = new Item
|
||||
{
|
||||
CategoryId = userId,
|
||||
ItemGlobalId = itemGlobal.Id,
|
||||
};
|
||||
await _itemRepository.AddAsync(item, cancellationToken);
|
||||
}
|
||||
|
||||
item.ItemGlobal = itemGlobal;
|
||||
return result.Set(item);
|
||||
}
|
||||
|
||||
public async Task<List<ItemDto>> FindItemsAsync(
|
||||
Guid userId,
|
||||
string term,
|
||||
int limit = 15,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (term == null)
|
||||
throw new ArgumentNullException(nameof(term));
|
||||
|
||||
return _mapper.Map<List<ItemDto>>(await _itemRepository.FindAsync(userId, term, limit, cancellationToken));
|
||||
}
|
||||
|
||||
public async Task<GeneralExecStatus> UpdateItemsCategoryAsync(
|
||||
Guid userId,
|
||||
Guid categoryId,
|
||||
List<Guid> items,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var category = await _itemCategoryRepository.GetAsync(userId, categoryId, cancellationToken);
|
||||
if (category == null)
|
||||
{
|
||||
return GeneralExecStatus.not_found;
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var dbItem = await _itemRepository.GetByGlobalAsync(userId, item, cancellationToken);
|
||||
if (dbItem != null)
|
||||
{
|
||||
dbItem.CategoryId = category.Id;
|
||||
await _itemRepository.UpdateAsync(dbItem, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
return GeneralExecStatus.success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using AutoMapper;
|
||||
using MyOffice.Core;
|
||||
|
||||
namespace MyOffice.Services;
|
||||
|
||||
public static class AutomapperExtensions
|
||||
{
|
||||
public static List<TTo> ToDto<TTo>(this IEnumerable<IDataModel> list, IMapper mapper)
|
||||
{
|
||||
return mapper.Map<List<TTo>>(list);
|
||||
}
|
||||
|
||||
public static TTo ToDto<TTo>(this IDataModel item, IMapper mapper)
|
||||
{
|
||||
return mapper.Map<TTo>(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using MyOffice.Services.Identity;
|
||||
|
||||
namespace MyOffice.Services.Mapper;
|
||||
|
||||
public class BaseMappingAction
|
||||
{
|
||||
protected readonly IContextProvider ContextProvider;
|
||||
public BaseMappingAction(IContextProvider contextProvider)
|
||||
{
|
||||
ContextProvider = contextProvider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using AutoMapper;
|
||||
|
||||
namespace MyOffice.Services.Mapper;
|
||||
|
||||
public class BaseProfile<TSource, TDestination> : Profile
|
||||
{
|
||||
protected IMappingExpression<TSource, TDestination> Mapping;
|
||||
|
||||
public BaseProfile()
|
||||
{
|
||||
Mapping = CreateMap<TSource, TDestination>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace MyOffice.Services.Mapper;
|
||||
|
||||
using AutoMapper;
|
||||
using Identity;
|
||||
|
||||
public class UserIdResolver : IValueResolver<object, object, Guid>
|
||||
{
|
||||
private readonly IContextProvider _contextProvider;
|
||||
|
||||
public UserIdResolver(
|
||||
IContextProvider contextProvider
|
||||
)
|
||||
{
|
||||
_contextProvider = contextProvider;
|
||||
}
|
||||
|
||||
public Guid Resolve(object source, object destination, Guid member, ResolutionContext context)
|
||||
{
|
||||
return _contextProvider.UserId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="16.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MyOffice.Core\MyOffice.Core.csproj" />
|
||||
<ProjectReference Include="..\MyOffice.Data.Repositories\MyOffice.Data.Repositories.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,80 @@
|
||||
namespace MyOffice.Services.Notifications;
|
||||
|
||||
using MyOffice.Core;
|
||||
using MyOffice.Data.Models.Notifications;
|
||||
using MyOffice.Data.Models.Users;
|
||||
using MyOffice.Data.Models.Verifications;
|
||||
using MyOffice.Data.Repositories.Item;
|
||||
using MyOffice.Services.Verifications;
|
||||
|
||||
public class EmailNotificationService
|
||||
{
|
||||
private readonly VerificationService _verificationService;
|
||||
|
||||
public EmailNotificationService(
|
||||
VerificationService verificationService
|
||||
)
|
||||
{
|
||||
_verificationService = verificationService;
|
||||
}
|
||||
|
||||
public void PasswordResetEmail(User user)
|
||||
{
|
||||
var code = _verificationService.Add(
|
||||
user,
|
||||
user.Email,
|
||||
VerificationCodeTemplateEnum.password_restore,
|
||||
VerificationCodeTypeEnum.email
|
||||
);
|
||||
|
||||
var subject = "";
|
||||
var body = "";
|
||||
}
|
||||
}
|
||||
|
||||
public class TemplateSevice
|
||||
{
|
||||
private readonly IEmailTemplateRepository _emailTemplateRepository;
|
||||
|
||||
public TemplateSevice(
|
||||
IEmailTemplateRepository emailTemplateRepository
|
||||
)
|
||||
{
|
||||
_emailTemplateRepository = emailTemplateRepository;
|
||||
}
|
||||
|
||||
public Exec<EmailTemplateFormated, GeneralExecStatus> GetTemplate(
|
||||
EmailTemplateEnum template,
|
||||
Dictionary<string, string> tokens
|
||||
)
|
||||
{
|
||||
var result = new Exec<EmailTemplateFormated, GeneralExecStatus>(GeneralExecStatus.success);
|
||||
|
||||
var emailTemplate = _emailTemplateRepository.Get(template);
|
||||
if (emailTemplate == null)
|
||||
{
|
||||
return result.Set(GeneralExecStatus.not_found);
|
||||
}
|
||||
|
||||
result.Set(new EmailTemplateFormated
|
||||
{
|
||||
Subject = emailTemplate.Subject,
|
||||
Body = emailTemplate.Template,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class EmailTemplateFormated
|
||||
{
|
||||
public string Sender { get; set; }
|
||||
public string SenderName { get; set; }
|
||||
public string Subject { get; set; }
|
||||
public string Body { get; set; }
|
||||
}
|
||||
|
||||
public interface IEmailSender
|
||||
{
|
||||
//Exec<bool, GeneralExecStatus> Send(string from, string[] to, string subject, string body);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MyOffice.Services.Users.Domain;
|
||||
|
||||
public enum AddUserExternalStatusEnum
|
||||
{
|
||||
success,
|
||||
externalid_used,
|
||||
user_not_valid
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
namespace MyOffice.Services.Users;
|
||||
|
||||
using Domain;
|
||||
using Core;
|
||||
using Data.Models.Users;
|
||||
using Data.Repositories.Users;
|
||||
|
||||
public class UserService
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserExternalRepository _userExternalRepository;
|
||||
private readonly SemaphoreSlim _addExternalLock = new(1, 1);
|
||||
|
||||
public UserService(
|
||||
IUserRepository userRepository,
|
||||
IUserExternalRepository userExternalRepository
|
||||
)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_userExternalRepository = userExternalRepository;
|
||||
}
|
||||
|
||||
public async Task<User?> Get(Guid id)
|
||||
{
|
||||
return await _userRepository.GetUserAsync(id);
|
||||
}
|
||||
|
||||
public async Task<List<UserExternal>> GetUserExternals(Guid userId)
|
||||
{
|
||||
return await _userExternalRepository.GetUserExternalsByUserIdAsync(userId);
|
||||
}
|
||||
|
||||
public async Task<UserExternal?> GetUserExternal(Guid userId, string provider)
|
||||
{
|
||||
return await _userExternalRepository.GetByUserIdAsync(userId, provider);
|
||||
}
|
||||
|
||||
public async Task<Exec<UserExternal>> RemoveUserExternal(
|
||||
Guid userId,
|
||||
string provider,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new Exec<UserExternal>();
|
||||
|
||||
var userExternal = await _userExternalRepository.GetByUserIdAsync(userId, provider);
|
||||
if (userExternal == null) return result.Set(GeneralExecStatus.not_found);
|
||||
|
||||
await _userExternalRepository.RemoveUserExternalAsync(userExternal, cancellationToken);
|
||||
|
||||
return result.Set(userExternal);
|
||||
}
|
||||
|
||||
public async Task<Exec<UserExternal, AddUserExternalStatusEnum>> AddUserExternalAsync(
|
||||
Guid userId,
|
||||
string provider,
|
||||
string externalId,
|
||||
string email,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var result = new Exec<UserExternal, AddUserExternalStatusEnum>(AddUserExternalStatusEnum.success);
|
||||
|
||||
var externalLogin = await _userExternalRepository.GetByUserIdAsync(userId, provider);
|
||||
|
||||
if (externalLogin != null) return result.Set(externalLogin);
|
||||
|
||||
await _addExternalLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
externalLogin = await _userExternalRepository.GetByUserIdAsync(userId, provider);
|
||||
if (externalLogin != null) return result.Set(externalLogin);
|
||||
|
||||
externalLogin = await _userExternalRepository.GetByExternalIdAsync(externalId, provider);
|
||||
if (externalLogin != null) return result.Set(AddUserExternalStatusEnum.externalid_used);
|
||||
|
||||
var user = await _userRepository.GetByUserUserNameAsync(email);
|
||||
if (user?.Id != userId) return result.Set(AddUserExternalStatusEnum.user_not_valid);
|
||||
|
||||
externalLogin = new UserExternal
|
||||
{
|
||||
UserId = userId,
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
Provider = provider.ToLower(),
|
||||
ExternalId = externalId,
|
||||
Email = email
|
||||
};
|
||||
await _userExternalRepository.AddUserExternalAsync(externalLogin, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_addExternalLock.Release();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MyOffice.Services.Validators
|
||||
{
|
||||
public static class ContextValidator
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace MyOffice.Services.Verifications;
|
||||
|
||||
using MyOffice.Core.Helpers;
|
||||
using MyOffice.Data.Models.Users;
|
||||
using MyOffice.Data.Models.Verifications;
|
||||
using MyOffice.Data.Repositories.Item;
|
||||
|
||||
public class VerificationService
|
||||
{
|
||||
private readonly TimeSpan _defaultExpires = TimeSpan.FromDays(1);
|
||||
|
||||
private readonly IVerificationCodeRepository _verificationCodeRepository;
|
||||
|
||||
public VerificationService(
|
||||
IVerificationCodeRepository verificationCodeRepository
|
||||
)
|
||||
{
|
||||
_verificationCodeRepository = verificationCodeRepository;
|
||||
}
|
||||
|
||||
public VerificationCode Add(
|
||||
User user,
|
||||
string destination,
|
||||
VerificationCodeTemplateEnum template,
|
||||
VerificationCodeTypeEnum type,
|
||||
DateTime? expiresOn = null,
|
||||
string? metadata = null
|
||||
)
|
||||
{
|
||||
if (user == null)
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
if (destination == null)
|
||||
throw new ArgumentNullException(nameof(destination));
|
||||
|
||||
var verificationCode = new VerificationCode
|
||||
{
|
||||
UserId = user.Id,
|
||||
Code = RandomizationHelper.Generate(40),
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
ExpiresOn = expiresOn ?? DateTime.UtcNow.Add(_defaultExpires),
|
||||
Template = template.ToString(),
|
||||
DestinationType = type.ToString(),
|
||||
Destination = destination,
|
||||
Metadata = metadata,
|
||||
};
|
||||
|
||||
_verificationCodeRepository.Add(verificationCode);
|
||||
|
||||
return verificationCode;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user