diff --git a/.dockerignore b/.dockerignore
index 29a98aa..ce19690 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -33,6 +33,7 @@ README.md
**/*Tests.cs
**/*TestsRelated.cs
**/TestsConfig/**
+**/HttpClientTestBase.cs
**/bin/*
**/obj/*
diff --git a/Api/Api.csproj b/Api/Api.csproj
index f1f7922..1041a1a 100644
--- a/Api/Api.csproj
+++ b/Api/Api.csproj
@@ -13,6 +13,7 @@
+
all
@@ -23,6 +24,7 @@
+
all
diff --git a/Api/Features/Tracking/TrackingControllerTests.cs b/Api/Features/Tracking/TrackingControllerTests.cs
new file mode 100644
index 0000000..eeb7d61
--- /dev/null
+++ b/Api/Features/Tracking/TrackingControllerTests.cs
@@ -0,0 +1,41 @@
+using Api.Features.Tracking.CreateWorkEntry;
+using Application.Commands;
+using Application.TestsConfig;
+using Core.Entities;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Xunit;
+
+namespace Api.Features.Tracking;
+
+[IntegrationTest]
+public class TrackingControllerTests : HttpClientTestBase
+{
+ public TrackingControllerTests(WebApplicationFactory factory) : base(factory)
+ {
+ }
+
+ [Fact]
+ public async Task CreateWorkEntryAsync_ShouldThrowValidationErrorIfAtLeastOneOfRequiredFieldsIsEmpty()
+ {
+ var createWorkEntryRequest = new CreateWorkEntryRequest
+ {
+ Title = "",
+ StartTime = new DateTime(2025, 11, 24, 9, 0, 0),
+ EndTime = new DateTime(2025, 11, 24, 10, 0, 0),
+ TaskId = "#2231",
+ Description = "Task description",
+ };
+
+ var response = await HttpClient.PostAsJsonAsync("/api/time/tracking/work-entries", createWorkEntryRequest);
+
+ Assert.NotNull(response);
+ Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
+
+ var validationProblemDetails = await response.Content.ReadFromJsonAsync();
+
+ Assert.NotNull(validationProblemDetails);
+ Assert.Equal("Fill in all the fields", validationProblemDetails.Detail);
+ Assert.Equal("Validation error", validationProblemDetails.Title);
+ }
+}
diff --git a/Api/HttpClientTestBase.cs b/Api/HttpClientTestBase.cs
new file mode 100644
index 0000000..cc57217
--- /dev/null
+++ b/Api/HttpClientTestBase.cs
@@ -0,0 +1,84 @@
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using Api;
+using Application;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+public class HttpClientTestBase : IClassFixture>, IAsyncLifetime
+{
+ protected const long EMPLOYEE_ID = 1;
+ protected const long TENANT_ID = 777;
+
+ protected HttpClient HttpClient = null!;
+ private WebApplicationFactory _factory = null!;
+
+ public HttpClientTestBase(WebApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ public async Task InitializeAsync()
+ {
+ _factory = _factory.WithWebHostBuilder(builder =>
+ {
+ builder.ConfigureTestServices(services =>
+ {
+ // Replacing the authentication service with a fake
+ services
+ .AddAuthentication("Test")
+ .AddScheme("Test", options => { });
+
+ // Add fake mockClaimsProbider
+ var mockClaimsProvider = new Mock();
+ mockClaimsProvider
+ .Setup(x => x.EmployeeId)
+ .Returns(EMPLOYEE_ID);
+ mockClaimsProvider
+ .Setup(x => x.TenantId)
+ .Returns(TENANT_ID);
+
+ services.AddScoped(_ => mockClaimsProvider.Object);
+ });
+ });
+
+ HttpClient = _factory.CreateClient();
+ }
+
+ public async Task DisposeAsync()
+ {
+ HttpClient.Dispose();
+ _factory.Dispose();
+ }
+}
+
+public class FakeAuthHandler : AuthenticationHandler
+{
+ public FakeAuthHandler(
+ IOptionsMonitor options,
+ ILoggerFactory logger,
+ UrlEncoder encoder
+ ) : base(options, logger, encoder)
+ {
+ }
+
+ protected override Task HandleAuthenticateAsync()
+ {
+ var claims = new[]
+ {
+ // Adding permissions claims to bypass permissions
+ new Claim("permissions", UserClaimsProvider.CanManagePersonalTimeTracker),
+ new Claim("permissions", UserClaimsProvider.AUTO_TESTS_ONLY_IsWorkEntriesHardDeleteAllowed)
+ };
+
+ var identity = new ClaimsIdentity(claims, "Test");
+ var principal = new ClaimsPrincipal(identity);
+ var ticket = new AuthenticationTicket(principal, "Test");
+
+ return Task.FromResult(AuthenticateResult.Success(ticket));
+ }
+}
diff --git a/Api/Program.cs b/Api/Program.cs
index cb6cb84..0c657ad 100644
--- a/Api/Program.cs
+++ b/Api/Program.cs
@@ -1,4 +1,6 @@
using Application;
+using Hellang.Middleware.ProblemDetails;
+using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.EntityFrameworkCore;
using TourmalineCore.AspNetCore.JwtAuthentication.Core;
@@ -34,12 +36,46 @@ public static void Main(string[] args)
builder.Services.AddApplication(configuration);
+ builder.Services.AddProblemDetails(options =>
+ {
+ options.IncludeExceptionDetails = (ctx, ex) => builder.Environment.IsDevelopment();
+
+ options.Map(ex =>
+ {
+ return new ProblemDetails
+ {
+ Title = "Invalid time range",
+ Status = StatusCodes.Status400BadRequest,
+ Detail = ex.Message,
+ };
+ });
+ });
+
+ builder.Services.AddControllers()
+ .ConfigureApiBehaviorOptions(options =>
+ {
+ options.InvalidModelStateResponseFactory = context =>
+ {
+ var problemDetails = new ValidationProblemDetails(context.ModelState)
+ {
+ Title = "Validation error",
+ Status = StatusCodes.Status400BadRequest,
+ Detail = "Fill in all the fields",
+ Instance = context.HttpContext.Request.Path
+ };
+
+ throw new ProblemDetailsException(problemDetails);
+ };
+ });
+
var authenticationOptions = configuration.GetSection(nameof(AuthenticationOptions)).Get();
builder.Services.Configure(configuration.GetSection(nameof(AuthenticationOptions)));
builder.Services.AddJwtAuthentication(authenticationOptions).WithUserClaimsProvider(UserClaimsProvider.PermissionClaimType);
var app = builder.Build();
+ app.UseProblemDetails();
+
app.MapOpenApi("api/swagger/openapi/v1.json");
app.UseSwaggerUI(options =>
@@ -48,14 +84,18 @@ public static void Main(string[] args)
options.RoutePrefix = "api/swagger";
});
- using (var serviceScope = app.Services.CreateScope())
- {
- var context = serviceScope.ServiceProvider.GetRequiredService();
- context.Database.Migrate();
- }
+ MigrateDatabase(app.Services);
app.MapControllers();
app.Run();
}
+
+ private static void MigrateDatabase(IServiceProvider serviceProvider)
+ {
+ using var serviceScope = serviceProvider.CreateScope();
+
+ var context = serviceScope.ServiceProvider.GetRequiredService();
+ context.Database.Migrate();
+ }
}
diff --git a/Application/AppDbContext.cs b/Application/AppDbContext.cs
index c22d60a..51f3477 100644
--- a/Application/AppDbContext.cs
+++ b/Application/AppDbContext.cs
@@ -25,22 +25,28 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder
.Entity()
- .Property(p => p.Duration)
+ .Property(entry => entry.Duration)
.HasComputedColumnSql("end_time - start_time", stored: true);
modelBuilder
.Entity()
- .Property(e => e.StartTime)
+ .Property(entry => entry.StartTime)
.HasColumnType("timestamp without time zone");
modelBuilder
.Entity()
- .Property(e => e.EndTime)
+ .Property(entry => entry.EndTime)
.HasColumnType("timestamp without time zone");
modelBuilder
.Entity()
- .ToTable(b => b.HasCheckConstraint("ck_work_entries_type_not_zero", "\"type\" <> 0"));
+ .ToTable(table => table.HasCheckConstraint("ck_work_entries_type_not_zero", "\"type\" <> 0"));
+
+ modelBuilder
+ .Entity()
+ .ToTable(table => table.HasCheckConstraint(
+ "ck_work_entries_end_time_is_greater_than_start_time",
+ "\"end_time\" > \"start_time\""));
base.OnModelCreating(modelBuilder);
}
diff --git a/Application/Commands/CreateWorkEntryCommand.cs b/Application/Commands/CreateWorkEntryCommand.cs
index 723d7c7..5568104 100644
--- a/Application/Commands/CreateWorkEntryCommand.cs
+++ b/Application/Commands/CreateWorkEntryCommand.cs
@@ -1,4 +1,6 @@
using Core.Entities;
+using Microsoft.EntityFrameworkCore;
+using Npgsql;
namespace Application.Commands;
@@ -33,24 +35,35 @@ IClaimsProvider claimsProvider
public async Task ExecuteAsync(CreateWorkEntryCommandParams createWorkEntryCommandParams)
{
- var workEntry = new WorkEntry
+ try
{
- TenantId = _claimsProvider.TenantId,
- EmployeeId = _claimsProvider.EmployeeId,
- Title = createWorkEntryCommandParams.Title,
- StartTime = createWorkEntryCommandParams.StartTime,
- EndTime = createWorkEntryCommandParams.EndTime,
- TaskId = createWorkEntryCommandParams.TaskId,
- Description = createWorkEntryCommandParams.Description,
- Type = createWorkEntryCommandParams.Type,
- };
-
- await _context
- .WorkEntries
- .AddAsync(workEntry);
-
- await _context.SaveChangesAsync();
-
- return workEntry.Id;
+ var workEntry = new WorkEntry
+ {
+ TenantId = _claimsProvider.TenantId,
+ EmployeeId = _claimsProvider.EmployeeId,
+ Title = createWorkEntryCommandParams.Title,
+ StartTime = createWorkEntryCommandParams.StartTime,
+ EndTime = createWorkEntryCommandParams.EndTime,
+ TaskId = createWorkEntryCommandParams.TaskId,
+ Description = createWorkEntryCommandParams.Description,
+ Type = createWorkEntryCommandParams.Type,
+ };
+
+ await _context
+ .WorkEntries
+ .AddAsync(workEntry);
+
+ await _context.SaveChangesAsync();
+
+ return workEntry.Id;
+ }
+ catch (DbUpdateException ex) when (ex.InnerException is PostgresException pgEx &&
+ pgEx.ConstraintName == "ck_work_entries_end_time_is_greater_than_start_time")
+ {
+ throw new InvalidTimeRangeException(
+ "End time must be greater than start time",
+ ex
+ );
+ }
}
}
diff --git a/Application/Commands/CreateWorkEntryCommandTests.cs b/Application/Commands/CreateWorkEntryCommandTests.cs
index 1e063ca..8f85f17 100644
--- a/Application/Commands/CreateWorkEntryCommandTests.cs
+++ b/Application/Commands/CreateWorkEntryCommandTests.cs
@@ -68,4 +68,31 @@ public async Task CreateWorkEntryAsync_ShouldThrowErrorIfTypeIsUnspecified()
Assert.Contains("ck_work_entries_type_not_zero", ex.InnerException?.Message);
}
+
+ [Fact]
+ public async Task CreateWorkEntryAsync_ShouldThrowInvalidTimeRangeExceptionIfStartTimeIsGreaterEndTime()
+ {
+ var context = CreateTenantDbContext();
+
+ var mockClaimsProvider = GetMockClaimsProvider();
+
+ var createWorkEntryCommand = new CreateWorkEntryCommand(context, mockClaimsProvider);
+
+ var createWorkEntryCommandParams = new CreateWorkEntryCommandParams
+ {
+ Title = "Task 1",
+ StartTime = new DateTime(2025, 11, 24, 11, 0, 0),
+ EndTime = new DateTime(2025, 11, 24, 10, 0, 0),
+ TaskId = "#2231",
+ Description = "Task description",
+ Type = EventType.Task
+ };
+
+ InvalidTimeRangeException ex = await Assert.ThrowsAsync(
+ async () => await createWorkEntryCommand.ExecuteAsync(createWorkEntryCommandParams)
+ );
+
+ Assert.Contains("ck_work_entries_end_time_is_greater_than_start_time", ex.InnerException?.InnerException?.Message);
+ Assert.Equal("End time must be greater than start time", ex.Message);
+ }
}
diff --git a/Application/Commands/UpdateWorkEntryCommand.cs b/Application/Commands/UpdateWorkEntryCommand.cs
index d8f7e8f..5339e97 100644
--- a/Application/Commands/UpdateWorkEntryCommand.cs
+++ b/Application/Commands/UpdateWorkEntryCommand.cs
@@ -1,5 +1,6 @@
using Core.Entities;
using Microsoft.EntityFrameworkCore;
+using Npgsql;
namespace Application.Commands;
@@ -36,17 +37,27 @@ IClaimsProvider claimsProvider
public async Task ExecuteAsync(UpdateWorkEntryCommandParams updateWorkEntryCommandParams)
{
- await _context
- .QueryableWithinTenant()
- .Where(x => x.EmployeeId == _claimsProvider.EmployeeId)
- .Where(x => x.Id == updateWorkEntryCommandParams.Id)
- .ExecuteUpdateAsync(setters => setters
- .SetProperty(x => x.Title, updateWorkEntryCommandParams.Title)
- .SetProperty(x => x.StartTime, updateWorkEntryCommandParams.StartTime)
- .SetProperty(x => x.EndTime, updateWorkEntryCommandParams.EndTime)
- .SetProperty(x => x.TaskId, updateWorkEntryCommandParams.TaskId)
- .SetProperty(x => x.Description, updateWorkEntryCommandParams.Description)
- .SetProperty(x => x.Type, updateWorkEntryCommandParams.Type)
+ try
+ {
+ await _context
+ .QueryableWithinTenant()
+ .Where(x => x.EmployeeId == _claimsProvider.EmployeeId)
+ .Where(x => x.Id == updateWorkEntryCommandParams.Id)
+ .ExecuteUpdateAsync(setters => setters
+ .SetProperty(x => x.Title, updateWorkEntryCommandParams.Title)
+ .SetProperty(x => x.StartTime, updateWorkEntryCommandParams.StartTime)
+ .SetProperty(x => x.EndTime, updateWorkEntryCommandParams.EndTime)
+ .SetProperty(x => x.TaskId, updateWorkEntryCommandParams.TaskId)
+ .SetProperty(x => x.Description, updateWorkEntryCommandParams.Description)
+ .SetProperty(x => x.Type, updateWorkEntryCommandParams.Type)
+ );
+ }
+ catch (PostgresException pgEx) when (pgEx.ConstraintName == "ck_work_entries_end_time_is_greater_than_start_time")
+ {
+ throw new InvalidTimeRangeException(
+ "End time must be greater than start time",
+ pgEx
);
+ }
}
}
diff --git a/Application/Commands/UpdateWorkEntryCommandTests.cs b/Application/Commands/UpdateWorkEntryCommandTests.cs
index 5b96a3f..a29c955 100644
--- a/Application/Commands/UpdateWorkEntryCommandTests.cs
+++ b/Application/Commands/UpdateWorkEntryCommandTests.cs
@@ -51,4 +51,43 @@ public async Task UpdateWorkEntryAsync_ShouldUpdateWorkEntryDataInDb()
Assert.Equal(updateWorkEntryCommandParams.Description, updatedWorkEntry.Description);
Assert.Equal(updateWorkEntryCommandParams.EndTime - updateWorkEntryCommandParams.StartTime, updatedWorkEntry.Duration);
}
+
+ [Fact]
+ public async Task UpdateWorkEntryAsync_ShouldThrowInvalidTimeRangeExceptionIfStartTimeIsGreaterEndTime()
+ {
+ var context = CreateTenantDbContext();
+
+ var mockClaimsProvider = GetMockClaimsProvider();
+
+ var updateWorkEntryCommand = new UpdateWorkEntryCommand(context, mockClaimsProvider);
+
+ var workEntry = await SaveEntityAsync(context, new WorkEntry
+ {
+ EmployeeId = EMPLOYEE_ID,
+ Title = "Task 1",
+ StartTime = new DateTime(2025, 11, 24, 9, 0, 0),
+ EndTime = new DateTime(2025, 11, 24, 10, 0, 0),
+ TaskId = "#2231",
+ Description = "Task description",
+ Type = EventType.Task
+ });
+
+ var updateWorkEntryCommandParams = new UpdateWorkEntryCommandParams
+ {
+ Id = workEntry.Id,
+ Title = "Task 2",
+ StartTime = new DateTime(2025, 11, 25, 12, 0, 0),
+ EndTime = new DateTime(2025, 11, 25, 11, 0, 0),
+ TaskId = "#22",
+ Description = "Task description",
+ Type = EventType.Task
+ };
+
+ InvalidTimeRangeException ex = await Assert.ThrowsAsync(
+ async () => await updateWorkEntryCommand.ExecuteAsync(updateWorkEntryCommandParams)
+ );
+
+ Assert.Contains("ck_work_entries_end_time_is_greater_than_start_time", ex.InnerException?.Message);
+ Assert.Equal("End time must be greater than start time", ex.Message);
+ }
}
diff --git a/Application/Exceptions/InvalidTimeRangeException.cs b/Application/Exceptions/InvalidTimeRangeException.cs
new file mode 100644
index 0000000..f97eee3
--- /dev/null
+++ b/Application/Exceptions/InvalidTimeRangeException.cs
@@ -0,0 +1,6 @@
+public class InvalidTimeRangeException : Exception
+{
+ public InvalidTimeRangeException() { }
+ public InvalidTimeRangeException(string message) : base(message) { }
+ public InvalidTimeRangeException(string message, Exception inner) : base(message, inner) { }
+}
diff --git a/Application/Migrations/20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries.Designer.cs b/Application/Migrations/20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries.Designer.cs
new file mode 100644
index 0000000..f04ee64
--- /dev/null
+++ b/Application/Migrations/20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries.Designer.cs
@@ -0,0 +1,99 @@
+//
+using System;
+using Application;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Application.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries")]
+ partial class AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.7")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Core.Entities.WorkEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property("Duration")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("interval")
+ .HasColumnName("duration")
+ .HasComputedColumnSql("end_time - start_time", true);
+
+ b.Property("EmployeeId")
+ .HasColumnType("bigint")
+ .HasColumnName("employee_id");
+
+ b.Property("EndTime")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("end_time");
+
+ b.Property("IsDeleted")
+ .HasColumnType("boolean")
+ .HasColumnName("is_deleted");
+
+ b.Property("StartTime")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("start_time");
+
+ b.Property("TaskId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("task_id");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasColumnName("tenant_id");
+
+ b.Property("TimeZoneId")
+ .HasColumnType("text")
+ .HasColumnName("time_zone_id");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("type");
+
+ b.HasKey("Id")
+ .HasName("pk_work_entries");
+
+ b.ToTable("work_entries", null, t =>
+ {
+ t.HasCheckConstraint("ck_work_entries_end_time_is_greater_than_start_time", "\"end_time\" > \"start_time\"");
+
+ t.HasCheckConstraint("ck_work_entries_type_not_zero", "\"type\" <> 0");
+ });
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Application/Migrations/20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries.cs b/Application/Migrations/20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries.cs
new file mode 100644
index 0000000..9b7511c
--- /dev/null
+++ b/Application/Migrations/20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries.cs
@@ -0,0 +1,27 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Application.Migrations
+{
+ ///
+ public partial class AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddCheckConstraint(
+ name: "ck_work_entries_end_time_is_greater_than_start_time",
+ table: "work_entries",
+ sql: "\"end_time\" > \"start_time\"");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropCheckConstraint(
+ name: "ck_work_entries_end_time_is_greater_than_start_time",
+ table: "work_entries");
+ }
+ }
+}
diff --git a/Application/Migrations/AppDbContextModelSnapshot.cs b/Application/Migrations/AppDbContextModelSnapshot.cs
index 185b764..2d9a4d5 100644
--- a/Application/Migrations/AppDbContextModelSnapshot.cs
+++ b/Application/Migrations/AppDbContextModelSnapshot.cs
@@ -85,6 +85,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.ToTable("work_entries", null, t =>
{
+ t.HasCheckConstraint("ck_work_entries_end_time_is_greater_than_start_time", "\"end_time\" > \"start_time\"");
+
t.HasCheckConstraint("ck_work_entries_type_not_zero", "\"type\" <> 0");
});
});
diff --git a/README.md b/README.md
index 6d28b70..37ca56c 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
# inner-circle-time-api
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
This repo contains Inner Circle Time API.