feat(api,web): retrieve and display transactions
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using FiscalOS.API.Accounts.Add;
|
||||
|
||||
namespace FiscalOS.API.Accounts;
|
||||
|
||||
internal static class AccountsExtensions
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
using FiscalOS.API.Auth.Login;
|
||||
using FiscalOS.API.Auth.Logout;
|
||||
using FiscalOS.API.Auth.Refresh;
|
||||
|
||||
namespace FiscalOS.API.Auth;
|
||||
|
||||
internal static class AuthExtensions
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
namespace FiscalOS.API.Auth.Logout;
|
||||
|
||||
internal static class Endpoint
|
||||
{
|
||||
private const string Route = "/logout";
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace FiscalOS.API.Common;
|
||||
|
||||
internal sealed record PagedQuery
|
||||
{
|
||||
public int PageNumber { get; init; }
|
||||
public int PageSize { get; init; }
|
||||
|
||||
public static async ValueTask<PagedQuery> BindAsync(HttpContext context)
|
||||
{
|
||||
var pageNumber = int.TryParse(context.Request.Query["pageNumber"], out var pn) ? pn : 1;
|
||||
var pageSize = int.TryParse(context.Request.Query["pageSize"], out var ps) ? ps : 1000;
|
||||
|
||||
return new PagedQuery
|
||||
{
|
||||
PageNumber = pageNumber,
|
||||
PageSize = pageSize
|
||||
};
|
||||
}
|
||||
|
||||
public Dictionary<string, string[]> Validate()
|
||||
{
|
||||
var validationResults = new Dictionary<string, string[]>();
|
||||
|
||||
if (PageNumber <= 0)
|
||||
{
|
||||
validationResults[nameof(PageNumber)] = ["PageNumber must be greater than 0."];
|
||||
}
|
||||
|
||||
if (PageSize <= 0 || PageSize > 1000)
|
||||
{
|
||||
validationResults[nameof(PageSize)] = ["PageSize must be between 1 and 1000."];
|
||||
}
|
||||
|
||||
return validationResults;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace FiscalOS.API.Common;
|
||||
|
||||
internal sealed record PagedResponse<T>
|
||||
{
|
||||
public int PageNumber { get; init; }
|
||||
public int PageSize { get; init; }
|
||||
public int TotalItems { get; init; }
|
||||
public int TotalPages { get; init; }
|
||||
public T[] Items { get; init; } = [];
|
||||
|
||||
[JsonConstructor]
|
||||
private PagedResponse()
|
||||
{
|
||||
}
|
||||
|
||||
public static PagedResponse<T> From(
|
||||
int pageNumber,
|
||||
int pageSize,
|
||||
int totalItems,
|
||||
T[] items
|
||||
)
|
||||
{
|
||||
var totalPages = (int)Math.Ceiling((double)totalItems / pageSize);
|
||||
|
||||
return new PagedResponse<T>
|
||||
{
|
||||
PageNumber = pageNumber,
|
||||
PageSize = pageSize,
|
||||
TotalItems = totalItems,
|
||||
TotalPages = totalPages,
|
||||
Items = items
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
using FiscalOS.API.Institutions.Connect;
|
||||
using FiscalOS.API.Institutions.Get;
|
||||
using FiscalOS.API.Institutions.GetAvailable;
|
||||
using FiscalOS.API.Institutions.Link;
|
||||
|
||||
namespace FiscalOS.API.Institutions;
|
||||
|
||||
internal static class InstitutionsExtensions
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace FiscalOS.API.Transactions.Get;
|
||||
|
||||
internal static class Endpoint
|
||||
{
|
||||
private const string Route = "/";
|
||||
|
||||
public static RouteHandlerBuilder MapGetEndpoint(this RouteGroupBuilder groupBuilder)
|
||||
{
|
||||
return groupBuilder.MapGet(Route, HandleAsync);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleAsync(
|
||||
HttpContext httpContext,
|
||||
PagedQuery pagedQuery,
|
||||
[FromServices] AppDbContext appDbContext,
|
||||
CancellationToken ct
|
||||
)
|
||||
{
|
||||
var pagedQueryValidationResults = pagedQuery.Validate();
|
||||
|
||||
if (pagedQueryValidationResults.Count is not 0)
|
||||
{
|
||||
return Results.ValidationProblem(pagedQueryValidationResults);
|
||||
}
|
||||
|
||||
var userId = httpContext.GetUserId();
|
||||
var query = appDbContext.Transactions
|
||||
.Where(t => t.UserId == userId);
|
||||
|
||||
var transactions = await query
|
||||
.Skip((pagedQuery.PageNumber - 1) * pagedQuery.PageSize)
|
||||
.Take(pagedQuery.PageSize)
|
||||
// TODO: Migrate Date to DateTime
|
||||
// to avoid in-memory sorting
|
||||
.AsAsyncEnumerable()
|
||||
.OrderByDescending(t => t.Date)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var count = await query
|
||||
.Where(t => t.UserId == userId)
|
||||
.CountAsync(ct);
|
||||
|
||||
var transactionDtos = transactions.Select(TransactionDto.From).ToArray();
|
||||
var pagedResponse = PagedResponse<TransactionDto>.From(
|
||||
pagedQuery.PageNumber,
|
||||
pagedQuery.PageSize,
|
||||
count,
|
||||
transactionDtos
|
||||
);
|
||||
|
||||
return Results.Ok(pagedResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Transaction = FiscalOS.Core.Transactions.Transaction;
|
||||
|
||||
namespace FiscalOS.API.Transactions;
|
||||
|
||||
internal sealed record TransactionDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string MerchantName { get; init; } = string.Empty;
|
||||
public decimal Amount { get; init; }
|
||||
public DateTimeOffset Date { get; init; }
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
[JsonConstructor]
|
||||
private TransactionDto()
|
||||
{
|
||||
}
|
||||
|
||||
public static TransactionDto From(Transaction transaction)
|
||||
{
|
||||
return new TransactionDto
|
||||
{
|
||||
Id = transaction.Id,
|
||||
MerchantName = transaction.MerchantName,
|
||||
Amount = transaction.Amount,
|
||||
Date = transaction.Date,
|
||||
Description = transaction.Description
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
using FiscalOS.API.Transactions.FireWebhook;
|
||||
using FiscalOS.API.Transactions.Get;
|
||||
using FiscalOS.API.Transactions.Webhook;
|
||||
|
||||
namespace FiscalOS.API.Transactions;
|
||||
|
||||
internal static class TransactionsExtensions
|
||||
@@ -9,13 +13,14 @@ internal static class TransactionsExtensions
|
||||
var transactionsGroup = app.MapGroup(RouteGroupPrefix)
|
||||
.RequireAuthorization();
|
||||
|
||||
transactionsGroup.MapGetEndpoint();
|
||||
transactionsGroup.MapWebhookEndpoint().AllowAnonymous();
|
||||
|
||||
if (app.Environment.IsProduction() is false)
|
||||
{
|
||||
transactionsGroup.MapFireWebhookEndpoint();
|
||||
}
|
||||
|
||||
transactionsGroup.MapWebhookEndpoint().AllowAnonymous();
|
||||
|
||||
return transactionsGroup;
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,11 @@ global using System.Security.Claims;
|
||||
global using System.Text.Json.Serialization;
|
||||
|
||||
global using FiscalOS.API.Accounts;
|
||||
global using FiscalOS.API.Accounts.Add;
|
||||
global using FiscalOS.API.Auth;
|
||||
global using FiscalOS.API.Auth.Login;
|
||||
global using FiscalOS.API.Auth.Refresh;
|
||||
global using FiscalOS.API.Common;
|
||||
global using FiscalOS.API.Http;
|
||||
global using FiscalOS.API.Institutions;
|
||||
global using FiscalOS.API.Institutions.Connect;
|
||||
global using FiscalOS.API.Institutions.Get;
|
||||
global using FiscalOS.API.Institutions.GetAvailable;
|
||||
global using FiscalOS.API.Institutions.Link;
|
||||
global using FiscalOS.API.Transactions;
|
||||
global using FiscalOS.API.Transactions.FireWebhook;
|
||||
global using FiscalOS.API.Transactions.Webhook;
|
||||
global using FiscalOS.Core.Authentication;
|
||||
global using FiscalOS.Core.Identity;
|
||||
global using FiscalOS.Core.Queuing;
|
||||
|
||||
Reference in New Issue
Block a user