Skip to content

Commit

Permalink
refactor: ownership model (#109)
Browse files Browse the repository at this point in the history
* stash
* blyat
* easy peasy drop your local database squeezy
  • Loading branch information
kwinkel authored Jun 5, 2024
1 parent c17ba2a commit 4ed24dd
Show file tree
Hide file tree
Showing 56 changed files with 843 additions and 1,812 deletions.
3 changes: 0 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
version: '3.1'


networks:
core:

Expand Down
6 changes: 6 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "8.0.104",
"rollForward": "latestMinor"
}
}
8 changes: 4 additions & 4 deletions src/Api/Api.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>ELifeRPG.Core.Api</AssemblyName>
Expand All @@ -13,9 +13,9 @@

<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.0.0-rc9.13" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Api/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Debug"
}
},
"ConnectionStrings": {
Expand Down
6 changes: 3 additions & 3 deletions src/Application/Application.csproj
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>ELifeRPG.Application</RootNamespace>
<AssemblyName>ELifeRPG.Application</AssemblyName>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.11" />
<PackageReference Include="MediatR" Version="12.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.5" />
<PackageReference Include="MediatR" Version="12.2.0" />
</ItemGroup>

<ItemGroup>
Expand Down
16 changes: 4 additions & 12 deletions src/Application/Banking/Commands/DepositMoneyCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ public class DepositMoneyCommand : IRequest<DepositMoneyCommandResult>
{
public Guid BankAccountId { get; init; }

public Guid CharacterId { get; init; }

public decimal Amount { get; init; }
}

Expand All @@ -40,24 +38,18 @@ public DepositMoneyCommandHandler(IDatabaseContext databaseContext)
public async Task<DepositMoneyCommandResult> Handle(DepositMoneyCommand request, CancellationToken cancellationToken)
{
var bankAccount = await _databaseContext.BankAccounts
.Include(x => x.OwningCharacter)
.Include(x => x.Owner.Character)
.Include(x => x.Owner.Company)
.Include(x => x.BankCondition)
.Include(x => x.Bookings!.Take(0))
.SingleOrDefaultAsync(x => x.Id == request.BankAccountId, cancellationToken);

if (bankAccount is null)
{
throw new ELifeEntityNotFoundException();
}

var character = await _databaseContext.Characters
.SingleOrDefaultAsync(x => x.Id == request.CharacterId, cancellationToken);

if (character is null)
{
throw new ELifeEntityNotFoundException();
}

var (transaction, booking) = bankAccount.DepositMoney(character, request.Amount);
var (transaction, booking) = bankAccount.DepositMoney(request.Amount);

await _databaseContext.SaveChangesAsync(cancellationToken);

Expand Down
2 changes: 1 addition & 1 deletion src/Application/Banking/Commands/MakeTransactionCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public async Task<MakeTransactionCommandResult> Handle(MakeTransactionCommand re
var sourceBankAccount = selectedBankAccounts.First(x => x.Id == request.SourceBankAccountId);
var targetBankAccount = selectedBankAccounts.First(x => x.Id == request.TargetBankAccountId);

var transaction = sourceBankAccount.TransferMoneyTo(targetBankAccount, character, request.Amount);
var transaction = sourceBankAccount.TransferMoneyTo(targetBankAccount, request.Amount, character);
await _databaseContext.SaveChangesAsync(cancellationToken);

return new MakeTransactionCommandResult(transaction);
Expand Down
6 changes: 4 additions & 2 deletions src/Application/Banking/Commands/OpenBankAccountCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public async Task<OpenBankAccountCommandResult> Handle(OpenBankAccountCommand re
private async Task<BankAccount> OpenAccountForCharacter(Bank bank, Guid characterId, CancellationToken cancellationToken)
{
var character = await _databaseContext.Characters
.Include(x => x.Person)
.AsNoTracking()
.SingleOrDefaultAsync(x => x.Id == characterId, cancellationToken);

Expand All @@ -87,7 +88,7 @@ private async Task<BankAccount> OpenAccountForCharacter(Bank bank, Guid characte
throw new ELifeEntityNotFoundException();
}

var bankAccount = bank.OpenAccount(character);
var bankAccount = bank.OpenAccount(character.Person!);
await _databaseContext.SaveChangesAsync(cancellationToken);

return bankAccount;
Expand All @@ -96,6 +97,7 @@ private async Task<BankAccount> OpenAccountForCharacter(Bank bank, Guid characte
private async Task<BankAccount> OpenAccountForCompany(Bank bank, CompanyId companyId, CancellationToken cancellationToken)
{
var company = await _databaseContext.Companies
.Include(x => x.Person)
.AsNoTracking()
.SingleOrDefaultAsync(x => x.Id == companyId.Value, cancellationToken);

Expand All @@ -104,7 +106,7 @@ private async Task<BankAccount> OpenAccountForCompany(Bank bank, CompanyId compa
throw new ELifeEntityNotFoundException();
}

var bankAccount = bank.OpenAccount(company);
var bankAccount = bank.OpenAccount(company.Person!);
await _databaseContext.SaveChangesAsync(cancellationToken);

return bankAccount;
Expand Down
3 changes: 2 additions & 1 deletion src/Application/Banking/Commands/WithdrawMoneyCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ public WithdrawMoneyCommandHandler(IDatabaseContext databaseContext)
public async Task<WithdrawMoneyCommandResult> Handle(WithdrawMoneyCommand request, CancellationToken cancellationToken)
{
var bankAccount = await _databaseContext.BankAccounts
.Include(x => x.OwningCharacter)
.Include(x => x.Owner.Character)
.Include(x => x.BankCondition)
.Include(x => x.Bookings!.Take(0))
.SingleOrDefaultAsync(x => x.Id == request.BankAccountId, cancellationToken);

if (bankAccount is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public Task Handle(BankAccountTransactionExecutedEvent notification, Cancellatio
BankAccountTransactionExecutedEvent(
_logger,
notification.Transaction.BankAccount.Id,
notification.ExecutingCharacter.Id,
notification.Character?.Id ?? default,
notification.Transaction.Source!.Id,
notification.Transaction.Amount,
notification.Transaction.Fees,
Expand Down
2 changes: 1 addition & 1 deletion src/Application/Banking/Queries/BankAccountsQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public async Task<BankAccountsQueryResult> Handle(BankAccountsQuery request, Can

if (request.CharacterId is not null)
{
query = query.Where(x => x.OwningCharacter!.Id == request.CharacterId);
query = query.Where(x => x.Owner.Character!.Id == request.CharacterId);
}

var result = await query.AsNoTracking().ToListAsync(cancellationToken);
Expand Down
4 changes: 2 additions & 2 deletions src/Application/Characters/CharacterLogManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using ELifeRPG.Domain.Characters;
using ELifeRPG.Domain.Characters.Events;
using ELifeRPG.Domain.Characters.Sessions;
using MediatR;
using Microsoft.Extensions.Logging;
Expand All @@ -19,7 +19,7 @@ public Task Handle(CharacterCreatedEvent notification, CancellationToken cancell
_logger.LogInformation("Character {CharacterId} has been created", notification.Character.Id.ToString());
return Task.CompletedTask;
}

public Task Handle(CharacterSessionCreatedEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("Session for character {CharacterId} has been started", notification.Character.Id.ToString());
Expand Down
12 changes: 3 additions & 9 deletions src/Domain/Banking/Bank.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using ELifeRPG.Domain.Common;
using ELifeRPG.Domain.Companies;
using ELifeRPG.Domain.Countries;
using ELifeRPG.Domain.Persons;

namespace ELifeRPG.Domain.Banking;

Expand Down Expand Up @@ -35,16 +36,9 @@ public Bank(Country country, int? number = null)

public IReadOnlyCollection<BankAccount>? Accounts => _accounts;

public BankAccount OpenAccount(Character owningCharacter)
public BankAccount OpenAccount(Person person)
{
var account = new BankAccount(this, owningCharacter);
HandleNewAccount(account);
return account;
}

public BankAccount OpenAccount(Company owningCompany)
{
var account = new BankAccount(this, owningCompany);
var account = new BankAccount(this, person);
HandleNewAccount(account);
return account;
}
Expand Down
50 changes: 19 additions & 31 deletions src/Domain/Banking/BankAccount.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using ELifeRPG.Domain.Banking.Events;
using System.Buffers;
using ELifeRPG.Domain.Banking.Events;
using ELifeRPG.Domain.Characters;
using ELifeRPG.Domain.Common;
using ELifeRPG.Domain.Common.Exceptions;
using ELifeRPG.Domain.Companies;
using ELifeRPG.Domain.Persons;

namespace ELifeRPG.Domain.Banking;

Expand All @@ -18,18 +20,11 @@ internal BankAccount()
{
}

public BankAccount(Bank bank, Character owningCharacter)
public BankAccount(Bank bank, Person owner)
: this(bank)
{
OwningCharacter = owningCharacter;
Type = BankAccountType.Personal;
}

public BankAccount(Bank bank, Company owningCompany)
: this(bank)
{
OwningCompany = owningCompany;
Type = BankAccountType.Corporate;
Owner = owner;
Type = owner.Character is null && owner.Company is not null ? BankAccountType.Corporate : BankAccountType.Personal;
}

private BankAccount(Bank bank)
Expand All @@ -53,42 +48,40 @@ private BankAccount(Bank bank)
public Bank? Bank { get; init; }

public BankCondition? BankCondition { get; init; }

public Character? OwningCharacter { get; init; }

public Company? OwningCompany { get; init; }


public Person Owner { get; init; } = null!;

public ICollection<BankAccountBooking>? Bookings { get; init; }

public List<DomainEvent> DomainEvents { get; } = new();

public bool Can(Character character, BankAccountCapabilities capability)
public bool Can(Person person, BankAccountCapabilities capability)
{
if (Type == BankAccountType.Personal)
{
return character.Id == OwningCharacter!.Id;
return person.Character!.Id == Owner.Character!.Id;
}

var companyMembership = OwningCompany?.Memberships!.SingleOrDefault(x => x.Character.Id == character.Id);
var companyMembership = Owner.Company!.Memberships!.SingleOrDefault(x => x.Character.Id == person.Id);
return companyMembership is not null && MapFromCompanyPosition(companyMembership.Position.Permissions).Contains(capability);
}

/// <summary>
/// Tries to execute a transaction to the destination bank-account.
/// </summary>
/// <param name="targetAccount">The destination bank-account.</param>
/// <param name="character">The executing character.</param>
/// <param name="amount">The amount to be transferred, without fees.</param>
/// <param name="amount">The amount to be transferred, without fees.</param>
/// <param name="character">The executing person.</param>
/// <returns>The transaction including fees.</returns>
/// <exception cref="ELifeInvalidOperationException">Throws if the character is not allowed to execute the transaction.</exception>
public BankAccountTransaction TransferMoneyTo(BankAccount targetAccount, Character character, decimal amount)
public BankAccountTransaction TransferMoneyTo(BankAccount targetAccount, decimal amount, Character? character)
{
if (Bookings is null || targetAccount.Bookings is null)
{
throw new InvalidOperationException();
}

if (!Can(character, BankAccountCapabilities.CommitTransactions))
if (character is not null && !Can(character.Person!, BankAccountCapabilities.CommitTransactions))
{
throw new ELifeInvalidOperationException();
}
Expand Down Expand Up @@ -127,7 +120,7 @@ public BankAccountTransaction WithdrawMoney(Character character, decimal amount)
throw new InvalidOperationException();
}

if (!Can(character, BankAccountCapabilities.CommitTransactions))
if (!Can(character.Person!, BankAccountCapabilities.CommitTransactions))
{
throw new ELifeInvalidOperationException();
}
Expand All @@ -148,24 +141,19 @@ public BankAccountTransaction WithdrawMoney(Character character, decimal amount)
return transaction;
}

public (BankAccountTransaction Transaction, BankAccountBooking Booking) DepositMoney(Character character, decimal amount)
public (BankAccountTransaction Transaction, BankAccountBooking Booking) DepositMoney(decimal amount)
{
if (Bookings is null)
{
throw new InvalidOperationException();
}

if (!Can(character, BankAccountCapabilities.CommitTransactions))
{
throw new ELifeInvalidOperationException();
}

var transaction = new BankAccountTransaction(this, BankAccountTransactionType.CashDeposit, amount);
var booking = new BankAccountBooking(this, transaction);
Bookings.Add(booking);
Balance += booking.Amount;

DomainEvents.Add(new BankAccountTransactionExecutedEvent(transaction, character));
DomainEvents.Add(new BankAccountTransactionExecutedEvent(transaction));

return (transaction, booking);
}
Expand Down
4 changes: 1 addition & 3 deletions src/Domain/Banking/BankAccountNumber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ namespace ELifeRPG.Domain.Banking;
/// </summary>
public class BankAccountNumber
{
private static readonly Random Random = new();

public BankAccountNumber(Bank bank)
{
var randomNumber = Random.NextInt64(1000000000, 99999999999);
var randomNumber = Random.Shared.NextInt64(1000000000, 99999999999);
var checkNumber = BuildCheckNumber(bank.Country.Code, bank.Number, randomNumber);

Value = BuildValue(bank.Country.Code, checkNumber, bank.Number, randomNumber);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ namespace ELifeRPG.Domain.Banking.Events;

public class BankAccountTransactionExecutedEvent : DomainEvent
{
public BankAccountTransactionExecutedEvent(BankAccountTransaction transaction, Character executingCharacter)
public BankAccountTransactionExecutedEvent(BankAccountTransaction transaction, Character? character = null)
{
Transaction = transaction;
ExecutingCharacter = executingCharacter;
Character = character;
}

public BankAccountTransaction Transaction { get; }

public Character ExecutingCharacter { get; }
public Character? Character { get; }
}
Loading

0 comments on commit 4ed24dd

Please sign in to comment.