Publish from private repository

This commit is contained in:
Gitea Actions
2026-08-01 11:48:55 +00:00
commit 814c39f5ce
617 changed files with 65073 additions and 0 deletions
+18
View File
@@ -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,
}
}
+264
View File
@@ -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;
}
}
+13
View File
@@ -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
}
+96
View File
@@ -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;
}
}