Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 17 additions & 31 deletions src/MergeDay.Api/Features/Invoices/CreateNewInvoiceEndpoint.cs
Original file line number Diff line number Diff line change
@@ -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<CreateNewInvoiceRequestLine> lines);
public record CreateNewInvoiceRequestLine(decimal Quantity, decimal UnitPrice, string Unit);

[EndpointGroup("Invoices")]
public sealed class Endpoint : IEndpoint
{
public void MapEndpoint(IEndpointRouteBuilder app)
Expand All @@ -27,42 +24,31 @@ public void MapEndpoint(IEndpointRouteBuilder app)
public static async Task<IResult> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<CreateNewInvoiceFromTogglRequest, IResult>("/invoices", Handler)
.WithName("Create and submit new invoice")
.WithSummary("Creates a new invoice in Fakturoid.")
.RequireAuthorization(AppPolicy.UserOrAdmin);
}
}

public static async Task<IResult> 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));
}
}
70 changes: 70 additions & 0 deletions src/MergeDay.Api/Features/Toggl/GetProjectsTimeSummaryEndpoint.cs
Original file line number Diff line number Diff line change
@@ -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<IEnumerable<GetProjectsTimeSummaryResponse>>(
"/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<IResult> 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);
}

}