-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into dependabot/nuget/DrifterApps.Seeds.Applicati…
…on-0.1.74
- Loading branch information
Showing
43 changed files
with
802 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
src/Api/Holefeeder.Application/Features/Statistics/Queries/GetSummary.Handler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
src/Api/Holefeeder.Application/Features/Statistics/Queries/GetSummary.Request.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
src/Api/Holefeeder.Application/Features/Statistics/Queries/GetSummary.Validator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
src/Api/Holefeeder.Application/Features/Statistics/Queries/GetSummary.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
3 changes: 3 additions & 0 deletions
3
src/Api/Holefeeder.Application/Features/Statistics/Queries/SummaryDto.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
6 changes: 6 additions & 0 deletions
6
src/Api/Holefeeder.Application/Features/Statistics/Queries/SummaryValue.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
src/Api/Holefeeder.FunctionalTests/Features/Statistics/ScenarioGetSummary.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.