diff --git a/src/MergeDay.Api/Features/Invoices/CreateNewInvoiceEndpoint.cs b/src/MergeDay.Api/Features/Invoices/CreateNewInvoiceEndpoint.cs index 44f1393..7d9183a 100644 --- a/src/MergeDay.Api/Features/Invoices/CreateNewInvoiceEndpoint.cs +++ b/src/MergeDay.Api/Features/Invoices/CreateNewInvoiceEndpoint.cs @@ -1,18 +1,15 @@ using MergeDay.Api.Common; using MergeDay.Api.Endpoints; using MergeDay.Api.Features.Fakturoid.Connector.Request; -using MergeDay.Api.Infrastructure.Persistence; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using static MergeDay.Api.Features.Fakturoid.CreateFakturoidInvoiceEndpoint; namespace MergeDay.Api.Features.Invoices; public static class CreateNewInvoiceEndpoint { - public record CreateNewInvoiceRequest(DateTime From, DateTime to); + public record CreateNewInvoiceRequest(IEnumerable lines); + public record CreateNewInvoiceRequestLine(decimal Quantity, decimal UnitPrice, string Unit); - [EndpointGroup("Invoices")] public sealed class Endpoint : IEndpoint { public void MapEndpoint(IEndpointRouteBuilder app) @@ -27,42 +24,31 @@ public void MapEndpoint(IEndpointRouteBuilder app) public static async Task Handler( [FromBody] CreateNewInvoiceRequest req, [FromServices] FakturoidService fakturoidService, - [FromServices] TogglService togglService, - [FromServices] MergeDayDbContext dbContext) + ILoggerFactory loggerFactory) { - var timeEntries = await togglService.GetTimeEntriesAsync(req.From, req.to); - timeEntries = timeEntries - .Where(t => t.ProjectId != null) - .Where(t => t.Description != null) - .Where(t => t.Stop.HasValue) - .ToList(); - var groupedEntries = timeEntries - .GroupBy(te => te.ProjectId) - .ToList(); - var projectIds = groupedEntries - .Select(g => g.Key) - .Where(id => id != null) - .ToList(); - var projects = await dbContext.TogglProjects - .Where(p => projectIds.Contains(p.TogglId)) - .ToListAsync(); + var logger = loggerFactory.CreateLogger(nameof(CreateNewInvoiceEndpoint)); var dto = new FakturoidInvoiceCreateDto { SubjectId = 27826666, Currency = "CZK", PaymentMethod = "bank", - Lines = groupedEntries.Select(g => new FakturoidInvoiceLineDto() + Lines = req.lines.Select(l => new FakturoidInvoiceLineDto() { - Name = projects.First(p => p.TogglId == g.Key).Name, - UnitPrice = 100, // TODO: Add real price from user profile - Quantity = (decimal)g.Sum(t => (t.Stop!.Value - t.Start).TotalHours) - }).ToList(), + Name = l.Unit, + UnitPrice = l.UnitPrice, + Quantity = l.Quantity + }).ToList() }; - var invoice = await fakturoidService.CreateInvoiceAsync(dto); + var createdInvoice = await fakturoidService.CreateInvoiceAsync(dto); + if (createdInvoice == null) + { + logger.LogError("Failed to create invoice in Fakturoid."); + return Results.StatusCode(500); + } - return Results.Created(invoice.Html_Url, - new CreateFakturoidInvoiceResponse(invoice.Id, invoice.Number, invoice.Pdf_Url)); + logger.LogInformation("Created invoice {InvoiceId} in Fakturoid.", createdInvoice.Id); + return Results.Ok(createdInvoice); } } diff --git a/src/MergeDay.Api/Features/Invoices/CreateNewInvoiceFromTogglEndpoint.cs b/src/MergeDay.Api/Features/Invoices/CreateNewInvoiceFromTogglEndpoint.cs new file mode 100644 index 0000000..04d6b07 --- /dev/null +++ b/src/MergeDay.Api/Features/Invoices/CreateNewInvoiceFromTogglEndpoint.cs @@ -0,0 +1,76 @@ +using MergeDay.Api.Common; +using MergeDay.Api.Endpoints; +using MergeDay.Api.Features.Fakturoid.Connector.Request; +using MergeDay.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using static MergeDay.Api.Features.Fakturoid.CreateFakturoidInvoiceEndpoint; + +namespace MergeDay.Api.Features.Invoices; + +public static class CreateNewInvoiceFromTogglEndpoint +{ + public record CreateNewInvoiceFromTogglRequest(DateTime From, DateTime to, decimal pricePerHour); + + [EndpointGroup("Invoices")] + public sealed class Endpoint : IEndpoint + { + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapStandardPost("/invoices", Handler) + .WithName("Create and submit new invoice") + .WithSummary("Creates a new invoice in Fakturoid.") + .RequireAuthorization(AppPolicy.UserOrAdmin); + } + } + + public static async Task Handler( + [FromBody] CreateNewInvoiceFromTogglRequest req, + [FromServices] FakturoidService fakturoidService, + [FromServices] TogglService togglService, + [FromServices] MergeDayDbContext dbContext, + ILoggerFactory loggerFactory) + { + var logger = loggerFactory.CreateLogger(nameof(CreateNewInvoiceFromTogglEndpoint)); + + var timeEntries = await togglService.GetTimeEntriesAsync(req.From, req.to); + timeEntries = timeEntries + .Where(t => t.ProjectId != null) + .Where(t => t.Description != null) + .Where(t => t.Stop.HasValue) + .ToList(); + var groupedEntries = timeEntries + .GroupBy(te => te.ProjectId) + .ToList(); + var projectIds = groupedEntries + .Select(g => g.Key) + .Where(id => id != null) + .ToList(); + var projects = await dbContext.TogglProjects + .Where(p => projectIds.Contains(p.TogglId)) + .ToListAsync(); + + if (projects.Count != projectIds.Count) + { + logger.LogWarning("Some projects from time entries were not found in the database."); + } + + var dto = new FakturoidInvoiceCreateDto + { + SubjectId = 27826666, + Currency = "CZK", + PaymentMethod = "bank", + Lines = groupedEntries.Select(g => new FakturoidInvoiceLineDto() + { + Name = projects.FirstOrDefault(p => p.TogglId == g.Key)?.Name ?? string.Empty, + UnitPrice = req.pricePerHour, + Quantity = (decimal)g.Sum(t => (t.Stop!.Value - t.Start).TotalHours) + }).ToList(), + }; + + var invoice = await fakturoidService.CreateInvoiceAsync(dto); + + return Results.Created(invoice.Html_Url, + new CreateFakturoidInvoiceResponse(invoice.Id, invoice.Number, invoice.Pdf_Url)); + } +} diff --git a/src/MergeDay.Api/Features/Toggl/GetProjectsTimeSummaryEndpoint.cs b/src/MergeDay.Api/Features/Toggl/GetProjectsTimeSummaryEndpoint.cs new file mode 100644 index 0000000..d88b69b --- /dev/null +++ b/src/MergeDay.Api/Features/Toggl/GetProjectsTimeSummaryEndpoint.cs @@ -0,0 +1,70 @@ +using MergeDay.Api.Common; +using MergeDay.Api.Endpoints; +using MergeDay.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace MergeDay.Api.Features.Toggl; + +public static class GetProjectsTimeSummaryEndpoint +{ + public record GetProjectsTimeSummaryResponse( + string ProjectName, + decimal TotalHours + ); + + [EndpointGroup("Toggl")] + public sealed class Endpoint : IEndpoint + { + public void MapEndpoint(IEndpointRouteBuilder app) + { + app.MapStandardGet>( + "/time-entries/summary/by-project", Handler) + .WithName("Get time summary grouped by project") + .WithSummary("Retrieve total tracked time grouped by project within a date range.") + .RequireAuthorization(AppPolicy.UserOrAdmin); + } + } + + public static async Task Handler( + [FromQuery] DateTime start, + [FromQuery] DateTime end, + ILoggerFactory loggerFactory, + TogglService togglService, + [FromServices] MergeDayDbContext dbContext) + { + var logger = loggerFactory.CreateLogger(nameof(Endpoint)); + + var timeEntries = await togglService.GetTimeEntriesAsync(start, end); + + if (timeEntries == null || timeEntries.Count == 0) + { + logger.LogInformation("No time entries found for the requested range."); + return Results.NotFound("No time entries found."); + } + + var groupedEntries = timeEntries + .GroupBy(te => te.ProjectId) + .ToList(); + var projectIds = groupedEntries + .Select(g => g.Key) + .Where(id => id != null) + .ToList(); + var projects = await dbContext.TogglProjects + .Where(p => projectIds.Contains(p.TogglId)) + .ToListAsync(); + + if (projects.Count != projectIds.Count) + { + logger.LogWarning("Some projects from time entries were not found in the database."); + } + + var grouped = groupedEntries.Select(g => new GetProjectsTimeSummaryResponse( + ProjectName: projects.FirstOrDefault(p => p.TogglId == g.Key)?.Name ?? string.Empty, + TotalHours: (decimal)g.Sum(t => (t.Stop!.Value - t.Start).TotalHours) + )).ToList(); + + return Results.Ok(grouped); + } + +}