Finishline #90

Merged
khwezi merged 3 commits from finishline into main 2026-09-24 12:01:37 +02:00
40 changed files with 11004 additions and 394 deletions
Showing only changes of commit 020bdee36f - Show all commits
@@ -16,6 +16,30 @@ namespace PostFundManagement.Application.Extensions;
public static class Services
{
public static IServiceCollection AddLoggedCachedServiceScoped<TInterface, TImplementation, TCacheDecorator>(this IServiceCollection services)
where TInterface : class
where TImplementation : class, TInterface
where TCacheDecorator : class, TInterface
{
services.AddScoped<TImplementation>();
services.AddScoped<TCacheDecorator>();
services.AddScoped(sp =>
{
var proxyGenerator = sp.GetRequiredService<ProxyGenerator>();
var loggingInterceptor = sp.GetRequiredService<LoggingInterceptor>();
var actual = sp.GetRequiredService<TImplementation>();
var cached = ActivatorUtilities.CreateInstance<TCacheDecorator>(sp, actual);
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(cached,
loggingInterceptor.ToInterceptor());
});
return services;
}
public static IServiceCollection AddLoggedAuditedCachedServiceScoped<TInterface, TImplementation, TCacheDecorator>(this IServiceCollection services)
where TInterface : class
where TImplementation : class, TInterface
@@ -56,11 +80,8 @@ public static class Services
services.AddLoggedAuditedCachedServiceScoped<IGrantsService, GrantsService, GrantsCacheService>();
services.AddLoggedAuditedCachedServiceScoped<IProjectService, ProjectService, ProjectCacheService>();
services.AddScoped<IIdentityService, IdentityService>();
services.Decorate<IIdentityService, IdentityCacheService>();
services.AddScoped<IToolService, ToolService>();
services.Decorate<IToolService, ToolCacheService>();
services.AddLoggedCachedServiceScoped<IIdentityService, IdentityService, IdentityCacheService>();
services.AddLoggedCachedServiceScoped<IToolService, ToolService, ToolCacheService>();
services.AddStackExchangeRedisCache(options =>
{
@@ -25,8 +25,12 @@ public sealed class ActiveUserFilter(IIdentityService identityService) : IEndpoi
var path = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrWhiteSpace(path) && !path!.EndsWith("integration/sync", StringComparison.InvariantCultureIgnoreCase))
if ((await identityService.UserIsAuthorisedAsync(userId, context.HttpContext.RequestAborted)).IsFailed)
{
var userAuthResult = await identityService.UserIsAuthorisedAsync(userId, context.HttpContext.RequestAborted);
if (userAuthResult.IsFailed)
return Results.Json(new { message = "Account approval is pending or inactive." }, statusCode: StatusCodes.Status403Forbidden);
}
context.HttpContext.Items[UserIdItemKey] = userId;
context.HttpContext.Items[EmailtemKey] = email;
@@ -64,8 +64,8 @@ public sealed class LoggingInterceptor(ILogger<LoggingInterceptor> logger) : IAs
var errorsString = string.Join("; ", errorMessages);
logger.LogError("Method {MethodName} failed. Errors: {Errors}. Parameters: {@Parameters}",
method.Name, errorsString, parameters);
logger.LogError("Method {MethodName} failed. Errors: [{ErrorType}] {Errors}. Parameters: {@Parameters}",
method.Name, result.Reasons?.FirstOrDefault()?.GetType().Name.Replace("Error", string.Empty), errorsString, parameters);
}
catch (Exception ex)
{
@@ -15,7 +15,7 @@ public sealed class AuditService(IDbContextFactory<ApplicationDbContext> context
{
EntityId = entityId,
Action = action,
Changes = new Domain.Models.AuditChanges { Logs = changes },
Changes = changes,
EntityName = entityName,
PerformedBy = performedBy,
Timestamp = DateTime.UtcNow,
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<ActivityConfiguration, Activity>]
public class Activity : Models.Activity, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -1,8 +1,8 @@
using PostFundManagement.Domain.Models;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<AuditLogConfiguration, AuditLog>]
public sealed class AuditLog : Models.AuditLog;
public sealed class AuditLogConfiguration : IEntityTypeConfiguration<AuditLog>
@@ -16,9 +16,7 @@ public sealed class AuditLogConfiguration : IEntityTypeConfiguration<AuditLog>
builder.Property(f => f.EntityName).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.EntityId).IsRequired();
builder.Property(f => f.Action).IsRequired().HasMaxLength(ShortLabelLength);
builder.Property(f => f.Changes).IsRequired(false).HasColumnType("jsonb")
.HasConversion(model => JsonSerializer.Serialize(model, JsonSerializerOptions.Default),
provider => JsonSerializer.Deserialize<AuditChanges>(provider, JsonSerializerOptions.Default));
builder.Property(f => f.Changes).IsRequired(false);
builder.Property(f => f.PerformedBy).IsRequired();
builder.Property(f => f.Timestamp).IsRequired().HasDefaultValueSql("now()");
}
@@ -2,6 +2,7 @@ using PostFundManagement.Domain.Abstractions;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<AwardConfiguration, Award>]
public class Award : Models.Award, ITenantEntity
{
public virtual User? Creator { get; set; }
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<BeneficiaryConfiguration, Beneficiary>]
public class Beneficiary : Models.Beneficiary, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<BudgetConfiguration, Budget>]
public class Budget : Models.Budget, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<ChangeRequestConfiguration, ChangeRequest>]
public class ChangeRequest : Models.ChangeRequest, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<ComplianceItemConfiguration, ComplianceItem>]
public class ComplianceItem : Models.ComplianceItem, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -2,6 +2,7 @@ using PostFundManagement.Domain.Abstractions;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<ContractConfiguration, Contract>]
public class Contract : Models.Contract, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -2,6 +2,7 @@ using PostFundManagement.Domain.Abstractions;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<DisbursementConfiguration, Disbursement>]
public class Disbursement : Models.Disbursement, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -2,6 +2,7 @@ using PostFundManagement.Domain.Abstractions;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<EvidenceConfiguration, Evidence>]
public class Evidence : Models.Evidence, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<IndicatorConfiguration, Indicator>]
public class Indicator : Models.Indicator, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<InstrumentConfiguration, Instrument>]
public class Instrument : Models.Instrument, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -2,6 +2,7 @@ using PostFundManagement.Domain.Abstractions;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<InviteConfiguration, Invite>]
public class Invite : Models.Invite, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<IssueConfiguration, Issue>]
public class Issue : Models.Issue, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<MilestoneConfiguration, Milestone>]
public class Milestone : Models.Milestone, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<NotificationConfiguration, Notification>]
public class Notification : Models.Notification, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -2,6 +2,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<OrganisationConfiguration, Organisation>]
public class Organisation : Models.Organisation
{
public virtual User? Creator { get; set; }
+12 -1
View File
@@ -1,9 +1,13 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
public class Outcome : Models.Outcome
[EntityTypeConfiguration<OutcomeConfiguration, Outcome>]
public class Outcome : Models.Outcome, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
public virtual Beneficiary? Beneficiary { get; set; }
public virtual User? Creator { get; set; }
@@ -16,12 +20,19 @@ public sealed class OutcomeConfiguration : IEntityTypeConfiguration<Outcome>
builder.ToTable(nameof(Outcome).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.BeneficiaryId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength);
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Beneficiary)
.WithMany()
.HasForeignKey(fk => fk.BeneficiaryId)
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<PortfolioConfiguration, Portfolio>]
public class Portfolio : Models.Portfolio, ITenantEntity
{
public virtual User? Creator { get; set; }
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<ProgrammeConfiguration, Programme>]
public class Programme : Models.Programme, ITenantEntity
{
public virtual User? Creator { get; set; }
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<ProjectConfiguration, Project>]
public class Project : Models.Project, ITenantEntity
{
public virtual User? Creator { get; set; }
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<RiskConfiguration, Risk>]
public class Risk : Models.Risk, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<RuleConfiguration, Rule>]
public class Rule : Models.Rule, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -3,6 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<SiteVisitConfiguration, SiteVisit>]
public class SiteVisit : Models.SiteVisit, ITenantEntity
{
public virtual Tenant? Tenant { get; set; }
@@ -2,6 +2,7 @@
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<TenantConfiguration, Tenant>]
public class Tenant : Models.Tenant
{
public virtual User? Creator { get; set; }
@@ -1,5 +1,6 @@
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<TenantOrganisationConfiguration, TenantOrganisation>]
public class TenantOrganisation : Models.TenantOrganisation
{
public virtual Tenant? Tenant { get; set; }
@@ -16,8 +17,6 @@ public sealed class TenantOrganisationConfiguration : IEntityTypeConfiguration<T
builder.ToTable(nameof(TenantOrganisation).Pluralize());
builder.HasKey(pk => pk.Id);
builder.HasIndex(to => new { to.TenantId, to.OrganisationId }).IsUnique();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.TenantId).IsRequired();
@@ -1,5 +1,6 @@
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<TenantUserConfiguration, TenantUser>]
public class TenantUser : Models.TenantUser
{
public virtual Tenant? Tenant { get; set; }
@@ -17,9 +18,7 @@ public sealed class TenantUserConfiguration : IEntityTypeConfiguration<TenantUse
{
builder.ToTable(nameof(TenantUser).Pluralize());
builder.HasKey(pk => pk.Id);
builder.HasIndex(tu => new { tu.TenantId, tu.UserId }).IsUnique();
builder.HasKey(pk => pk.Id);
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UserId).IsRequired();
@@ -2,6 +2,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<UserConfiguration, User>]
public sealed class User : Models.User;
public sealed class UserConfiguration : IEntityTypeConfiguration<User>
+1 -2
View File
@@ -12,8 +12,7 @@ public class AuditLog
public string? Action { get; set; }
[NotMapped]
public AuditChanges? Changes { get; set; }
public string[]? Changes { get; set; }
public Guid PerformedBy { get; set; }
@@ -4,6 +4,8 @@ public class Outcome
{
public long Id { get; set; }
public long TenantId { get; set; }
public long BeneficiaryId { get; set; }
public Guid CreatedBy { get; set; }
@@ -73,8 +73,6 @@ public sealed class ApplicationDbContext(DbContextOptions<ApplicationDbContext>
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (typeof(ITenantEntity).IsAssignableFrom(entityType.ClrType))
@@ -89,8 +87,11 @@ public sealed class ApplicationDbContext(DbContextOptions<ApplicationDbContext>
modelBuilder.Entity<Organisation>().HasQueryFilter(o => BypassFilter ||
Set<TenantOrganisation>().Any(to => to.TenantId == CurrentTenantId && to.OrganisationId == o.Id));
modelBuilder.Entity<User>().HasQueryFilter(o => BypassFilter ||
Set<TenantUser>().Any(to => to.TenantId == CurrentTenantId && to.UserId == o.Id));
modelBuilder.Entity<TenantOrganisation>()
.HasQueryFilter(to => BypassFilter || to.TenantId == CurrentTenantId);
modelBuilder.Entity<TenantUser>()
.HasQueryFilter(to => BypassFilter || to.TenantId == CurrentTenantId);
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
@@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PostFundManagement.Infrastructure.Database.Migrations
{
/// <inheritdoc />
public partial class AddedTenantIdToOutcomes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "TenantId",
table: "Outcomes",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.CreateIndex(
name: "IX_Outcomes_TenantId",
table: "Outcomes",
column: "TenantId");
migrationBuilder.AddForeignKey(
name: "FK_Outcomes_Tenants_TenantId",
table: "Outcomes",
column: "TenantId",
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Outcomes_Tenants_TenantId",
table: "Outcomes");
migrationBuilder.DropIndex(
name: "IX_Outcomes_TenantId",
table: "Outcomes");
migrationBuilder.DropColumn(
name: "TenantId",
table: "Outcomes");
}
}
}