Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/nuget/DrifterApps.Seeds.Applicati…
Browse files Browse the repository at this point in the history
…on-0.1.74
  • Loading branch information
patmoreau authored Jan 9, 2024
2 parents 6fa458f + 594e595 commit 1b89a2f
Show file tree
Hide file tree
Showing 43 changed files with 802 additions and 35 deletions.
2 changes: 1 addition & 1 deletion src/Api/Holefeeder.Api/Holefeeder.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<PackageReference Include="AspNetCore.HealthChecks.MySql" Version="8.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="8.0.0" />
<PackageReference Include="Carter" Version="8.0.0" />
<PackageReference Include="DrifterApps.Seeds.Infrastructure" Version="0.1.73" />
<PackageReference Include="DrifterApps.Seeds.Infrastructure" Version="0.1.74" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="MicroElements.Swashbuckle.FluentValidation" Version="6.0.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Licensed to the.NET Foundation under one or more agreements.
// The.NET Foundation licenses this file to you under the MIT license.

using DrifterApps.Seeds.Application;

using Holefeeder.Application.Context;
using Holefeeder.Domain.Features.Categories;

using Microsoft.EntityFrameworkCore;

namespace Holefeeder.Application.Features.Statistics.Queries;

public partial class GetSummary
{
internal class Handler(IUserContext userContext, BudgetingContext context) : IRequestHandler<Request, SummaryDto>
{
public async Task<SummaryDto> Handle(Request request, CancellationToken cancellationToken)
{
var query = from category in context.Categories
join transaction in context.Transactions on category.Id equals transaction.CategoryId
where category.UserId == userContext.Id && !category.System
group transaction by new
{
category.Type,
transaction.Date.Year,
transaction.Date.Month
}
into groupedTransactions
select new
{
groupedTransactions.Key.Type,
groupedTransactions.Key.Year,
groupedTransactions.Key.Month,
TotalAmount = groupedTransactions.Sum(t => t.Amount)
}
into summarizedTransactions
group summarizedTransactions by new
{
summarizedTransactions.Type,
summarizedTransactions.Year
}
into groupedSummaries
select new
{
groupedSummaries.Key.Type,
groupedSummaries.Key.Year,
TotalAmountByYear = groupedSummaries.Sum(s => s.TotalAmount),
MonthlyTotals = groupedSummaries.Select(s => new { s.Month, TotalAmountByMonth = s.TotalAmount })
};

var results = await query.ToListAsync(cancellationToken);

var gains = results
.Where(x => x.Type == CategoryType.Gain)
.GroupBy(x => x.Type,
arg => new YearStatisticsDto(arg.Year, arg.TotalAmountByYear,
arg.MonthlyTotals.Select(x => new MonthStatisticsDto(x.Month, x.TotalAmountByMonth)).ToList()))
.ToList();
var expenses = results
.Where(x => x.Type == CategoryType.Expense).GroupBy(x => x.Type,
arg => new YearStatisticsDto(arg.Year, arg.TotalAmountByYear,
arg.MonthlyTotals.Select(x => new MonthStatisticsDto(x.Month, x.TotalAmountByMonth)).ToList()))
.ToList();

return new SummaryDto(
new SummaryValue(Month(gains, request.AsOf.AddMonths(-1)), Month(expenses, request.AsOf.AddMonths(-1))),
new SummaryValue(Month(gains, request.AsOf), Month(expenses, request.AsOf)),
new SummaryValue(Average(gains), Average(expenses)));
}

private static decimal Month(IList<IGrouping<CategoryType, YearStatisticsDto>>? dto, DateOnly asOf) =>
dto == null
? 0m
: dto.SelectMany(x =>
x.Where(y => y.Year == asOf.Year).SelectMany(y => y.Months.Where(z => z.Month == asOf.Month)))
.Sum(x => x.Total);

private static decimal Average(IList<IGrouping<CategoryType, YearStatisticsDto>>? dto) =>
dto == null
? 0m
: Math.Round(dto.Sum(x => x.Sum(y => y.Total)) / dto.Sum(x => x.Sum(y => y.Months.Count())), 2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Licensed to the.NET Foundation under one or more agreements.
// The.NET Foundation licenses this file to you under the MIT license.

using System.Reflection;

using Microsoft.AspNetCore.Http;

namespace Holefeeder.Application.Features.Statistics.Queries;

public partial class GetSummary
{
internal record Request(DateOnly AsOf) : IRequest<SummaryDto>
{
public static ValueTask<Request?> BindAsync(HttpContext context, ParameterInfo parameter)
{
const string asOfKey = "as-of";

var hasAsOfDate = DateOnly.TryParse(context.Request.Query[asOfKey], out var asOf);

Request result = new(hasAsOfDate ? asOf : DateOnly.FromDateTime(DateTime.Today));

return ValueTask.FromResult<Request?>(result);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the.NET Foundation under one or more agreements.
// The.NET Foundation licenses this file to you under the MIT license.

namespace Holefeeder.Application.Features.Statistics.Queries;

public partial class GetSummary
{
internal class Validator : AbstractValidator<Request>
{
public Validator()
{
RuleFor(command => command.AsOf).NotEmpty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace Holefeeder.Application.Features.Statistics.Queries;

public partial class GetSummary : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app) =>
app.MapGet("api/v2/summary/statistics",
async (Request request, IMediator mediator, CancellationToken cancellationToken) =>
{
var results = await mediator.Send(request, cancellationToken);
return Results.Ok(results);
})
.Produces<SummaryDto>()
.Produces(StatusCodes.Status401Unauthorized)
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesValidationProblem(StatusCodes.Status422UnprocessableEntity)
.WithTags(nameof(Statistics))
.WithName(nameof(GetSummary))
.RequireAuthorization();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Holefeeder.Application;

public record class SummaryDto(SummaryValue Last, SummaryValue Current, SummaryValue Average);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Holefeeder.Application;

public record class SummaryValue(decimal Gains, decimal Expenses)
{
public static SummaryValue Empty { get; set; } = new(0, 0);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<ItemGroup>
<PackageReference Include="Carter" Version="8.0.0" />
<PackageReference Include="DrifterApps.Seeds.Application" Version="0.1.74" />
<PackageReference Include="DrifterApps.Seeds.Application.Mediatr" Version="0.1.73" />
<PackageReference Include="DrifterApps.Seeds.Application.Mediatr" Version="0.1.74" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.7" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.8.0" />
<PackageReference Include="MediatR" Version="12.2.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using Holefeeder.Application;
using Holefeeder.Domain.Features.Accounts;
using Holefeeder.Domain.Features.Categories;
using Holefeeder.FunctionalTests.Drivers;
using Holefeeder.FunctionalTests.Infrastructure;
using Holefeeder.FunctionalTests.StepDefinitions;
using Holefeeder.Tests.Common.Builders.Accounts;
using Holefeeder.Tests.Common.Builders.Categories;
using Holefeeder.Tests.Common.Builders.Transactions;

namespace Holefeeder.FunctionalTests.Features.Statistics;

[ComponentTest]
[Collection("Api collection")]
public class ScenarioGetSummary(ApiApplicationDriver applicationDriver, ITestOutputHelper testOutputHelper) : HolefeederScenario(applicationDriver, testOutputHelper)
{
private readonly Guid _userId = UserStepDefinition.HolefeederUserId;

private readonly Dictionary<string, Category> _categories = [];
private readonly Dictionary<string, Account> _accounts = [];

[Fact]
public Task WhenGettingSummaryStatistics() =>
ScenarioFor("A user sends was to know his statistics", runner =>
runner.Given("the user is authorized", GivenUserIsAuthorized)
.And("a 'purchase' transaction was made on the 'credit card' account in February 2023", () => CreateTransaction("purchase", "credit card", new DateOnly(2023, 2, 1), 500.5m))
.And("a 'food and drink' transaction was made on the 'credit card' account in December 2022", () => CreateTransaction("food and drink", "credit card", new DateOnly(2022, 12, 1), 100.1m))
.And("a 'food and drink' transaction was made on the 'credit card' account in January 2023", () => CreateTransaction("food and drink", "credit card", new DateOnly(2023, 1, 1), 200.2m))
.And("a 'food and drink' transaction was made on the 'credit card' account in February 2023", () => CreateTransaction("food and drink", "credit card", new DateOnly(2023, 2, 1), 300.3m))
.And("a second 'food and drink' transaction was made on the 'checking' account in February 2023", () => CreateTransaction("food and drink", "checking", new DateOnly(2023, 2, 10), 400.4m))
.And("an 'income' gain was made to the 'checking' account in December 2022", () => CreateGain("income", "checking", new DateOnly(2022, 12, 1), 1000.1m))
.And("an 'income' gain was made to the 'checking' account in January 2023", () => CreateGain("income", "checking", new DateOnly(2023, 1, 1), 2000.2m))
.And("an 'income' gain was made to the 'checking' account in February 2023", () => CreateGain("income", "checking", new DateOnly(2023, 2, 1), 3000.3m))
.When("user gets their Febuary summary statistics", () => HttpClientDriver.SendGetRequestAsync(ApiResources.GetSummary, new DateOnly(2023, 2, 1)))
.Then("the summary should match the expected", ValidateResponse));

private Task ValidateResponse()
{
var expectedSummary = new SummaryDto(
new SummaryValue(2000.2m, 200.2m),
new SummaryValue(3000.3m, 1201.2m),
new SummaryValue(2000.2m, 500.5m));

var results = HttpClientDriver.DeserializeContent<SummaryDto>();
results.Should()
.NotBeNull()
.And.BeEquivalentTo(expectedSummary);

return Task.CompletedTask;
}

private async Task CreateTransaction(string categoryName, string accountName, DateOnly date, decimal amount)
{
if (!_categories.TryGetValue(categoryName, out var category))
{
category = await CategoryBuilder.GivenACategory().OfType(CategoryType.Expense).WithName(categoryName).ForUser(_userId).SavedInDbAsync(DatabaseDriver);
_categories.Add(categoryName, category);
}

if (!_accounts.TryGetValue(accountName, out var account))
{
account = await AccountBuilder.GivenAnActiveAccount().WithName(accountName).ForUser(_userId).SavedInDbAsync(DatabaseDriver);
_accounts.Add(accountName, account);
}

await TransactionBuilder
.GivenATransaction()
.ForAccount(account)
.ForCategory(category)
.OnDate(date)
.OfAmount(amount)
.SavedInDbAsync(DatabaseDriver);
}

private async Task CreateGain(string categoryName, string accountName, DateOnly date, decimal amount)
{
if (!_categories.TryGetValue(categoryName, out var category))
{
category = await CategoryBuilder.GivenACategory().OfType(CategoryType.Gain).WithName(categoryName).ForUser(_userId).SavedInDbAsync(DatabaseDriver);
_categories.Add(categoryName, category);
}

if (!_accounts.TryGetValue(accountName, out var account))
{
account = await AccountBuilder.GivenAnActiveAccount().WithName(accountName).ForUser(_userId).SavedInDbAsync(DatabaseDriver);
_accounts.Add(accountName, account);
}

await TransactionBuilder
.GivenATransaction()
.ForAccount(account)
.ForCategory(category)
.OnDate(date)
.OfAmount(amount)
.SavedInDbAsync(DatabaseDriver);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="Respawn" Version="6.1.0" />
<PackageReference Include="WireMock.Net" Version="1.5.46" />
<PackageReference Include="xunit" Version="2.6.4" />
<PackageReference Include="xunit" Version="2.6.5" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public static class ApiResources
public static readonly ApiResource GetTransactions = ApiResource.DefineApi("api/v2/transactions", HttpMethod.Get);

public static readonly ApiResource GetForAllCategories = ApiResource.DefineApi("api/v2/categories/statistics", HttpMethod.Get);
public static readonly ApiResource GetSummary = ApiResource.DefineApi("api/v2/summary/statistics?as-of={0}", HttpMethod.Get);

public static readonly ApiResource GetTagsWithCount = ApiResource.DefineApi("api/v2/tags", HttpMethod.Get);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Nito.AsyncEx.Coordination" Version="5.1.2" />
<PackageReference Include="Respawn" Version="6.1.0" />
<PackageReference Include="xunit" Version="2.6.4" />
<PackageReference Include="xunit" Version="2.6.5" />
<PackageReference Include="xunit.abstractions" Version="2.0.3" />
</ItemGroup>

Expand Down
4 changes: 2 additions & 2 deletions src/Api/Holefeeder.UnitTests/Holefeeder.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
<ItemGroup>
<PackageReference Include="DrifterApps.Seeds.Testing" Version="0.1.74" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="FluentAssertions.Analyzers" Version="0.28.0">
<PackageReference Include="FluentAssertions.Analyzers" Version="0.29.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="JunitXml.TestLogger" Version="3.0.134" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="xunit" Version="2.6.4" />
<PackageReference Include="xunit" Version="2.6.5" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
Expand Down
16 changes: 15 additions & 1 deletion src/Web/Holefeeder.Web/ClientApp/mocks/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -592,5 +592,19 @@
}
]
}
]
],
"summary": {
"last": {
"gains": 1000.1,
"expenses": 100.1
},
"current": {
"gains": 2000.2,
"expenses": 200.2
},
"average": {
"gains": 3000.3,
"expenses": 300.3
}
}
}
1 change: 1 addition & 0 deletions src/Web/Holefeeder.Web/ClientApp/mocks/routes.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"/gateway/api/v2/store-items?filter=code:eq:settings": "/store-items",
"/gateway/api/v2/categories/statistics": "/statistics",
"/gateway/api/v2/summary/*": "/summary",
"/gateway/api/v2/cashflows/*": "/$1",
"/gateway/api/v2/*": "/$1"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { AccountAdapter, accountType } from '@app/core/adapters';
import { formatErrors, mapToPagingInfo } from '../utils/api.utils';
import { StateService } from './state.service';

const apiRoute = 'api/v2/accounts';
const apiRoute = 'accounts';

interface AccountState {
accounts: Account[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { catchError, map, switchMap } from 'rxjs/operators';
import { CashflowDetailAdapter } from '../adapters';
import { StateService } from './state.service';

const apiRoute = 'api/v2/cashflows';
const apiRoute = 'cashflows';

interface CashflowState {
cashflows: CashflowDetail[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { AuthFeature } from '@app/core/store/auth/auth.feature';
import { filterTrue, tapTrace } from '@app/shared/helpers';
import { LoggerService } from '@app/core/logger';

const apiRoute = 'api/v2/store-items';
const apiRoute = 'store-items';

interface SettingsState {
period: DateInterval;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { catchError } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { formatErrors } from '@app/core/utils/api.utils';

const apiRoute = 'api/v2/categories/statistics';
const apiRoute = 'categories/statistics';

@Injectable({ providedIn: 'root' })
export class StatisticsService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Observable, of, tap } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators';
import { transactionDetailType, TransactionDetailAdapter } from '../adapters';

const apiRoute = 'api/v2/transactions';
const apiRoute = 'transactions';

type idType = { id: string };

Expand Down
Loading

0 comments on commit 1b89a2f

Please sign in to comment.