From a5689dd8fc4533013ca7af8f78bdee8f7195bad6 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 15:06:08 +0200 Subject: [PATCH 01/50] Implemented models --- PostFundManagement.Domain/Class1.cs | 6 -- .../Configuration/Instrument.cs | 22 +++++++ .../Configuration/Portfolio.cs | 27 ++++++++ .../Configuration/Programme.cs | 43 +++++++++++++ .../Configuration/User.cs | 14 ++++ .../Entities/Instrument.cs | 9 +++ .../Entities/Portfolio.cs | 11 ++++ .../Entities/Programme.cs | 13 ++++ PostFundManagement.Domain/Entities/User.cs | 4 ++ PostFundManagement.Domain/Enums.cs | 64 +++++++++++++++++++ PostFundManagement.Domain/Models/AuditLog.cs | 18 ++++++ PostFundManagement.Domain/Models/Award.cs | 26 ++++++++ .../Models/Beneficiary.cs | 24 +++++++ PostFundManagement.Domain/Models/Budget.cs | 20 ++++++ .../Models/ChangeRequest.cs | 26 ++++++++ .../Models/ComplianceItem.cs | 22 +++++++ PostFundManagement.Domain/Models/Contract.cs | 24 +++++++ .../Models/Disbursement.cs | 22 +++++++ PostFundManagement.Domain/Models/Evidence.cs | 26 ++++++++ PostFundManagement.Domain/Models/Indicator.cs | 22 +++++++ .../Models/Instrument.cs | 18 ++++++ PostFundManagement.Domain/Models/Issue.cs | 22 +++++++ PostFundManagement.Domain/Models/Milestone.cs | 22 +++++++ .../Models/Organisation.cs | 20 ++++++ PostFundManagement.Domain/Models/Outcome.cs | 16 +++++ PostFundManagement.Domain/Models/Portfolio.cs | 16 +++++ PostFundManagement.Domain/Models/Programme.cs | 24 +++++++ PostFundManagement.Domain/Models/Project.cs | 26 ++++++++ PostFundManagement.Domain/Models/Risk.cs | 20 ++++++ PostFundManagement.Domain/Models/Rule.cs | 22 +++++++ PostFundManagement.Domain/Models/SiteVisit.cs | 20 ++++++ PostFundManagement.Domain/Models/User.cs | 12 ++++ .../PostFundManagement.Domain.csproj | 26 ++++++++ PostFundManagement.Infrastructure/Class1.cs | 6 -- .../Database/ApplicationDbContext.cs | 12 ++++ .../PostFundManagement.Infrastructure.csproj | 8 +++ PostFundManagement.code-workspace | 8 +++ 37 files changed, 729 insertions(+), 12 deletions(-) delete mode 100644 PostFundManagement.Domain/Class1.cs create mode 100644 PostFundManagement.Domain/Configuration/Instrument.cs create mode 100644 PostFundManagement.Domain/Configuration/Portfolio.cs create mode 100644 PostFundManagement.Domain/Configuration/Programme.cs create mode 100644 PostFundManagement.Domain/Configuration/User.cs create mode 100644 PostFundManagement.Domain/Entities/Instrument.cs create mode 100644 PostFundManagement.Domain/Entities/Portfolio.cs create mode 100644 PostFundManagement.Domain/Entities/Programme.cs create mode 100644 PostFundManagement.Domain/Entities/User.cs create mode 100644 PostFundManagement.Domain/Enums.cs create mode 100644 PostFundManagement.Domain/Models/AuditLog.cs create mode 100644 PostFundManagement.Domain/Models/Award.cs create mode 100644 PostFundManagement.Domain/Models/Beneficiary.cs create mode 100644 PostFundManagement.Domain/Models/Budget.cs create mode 100644 PostFundManagement.Domain/Models/ChangeRequest.cs create mode 100644 PostFundManagement.Domain/Models/ComplianceItem.cs create mode 100644 PostFundManagement.Domain/Models/Contract.cs create mode 100644 PostFundManagement.Domain/Models/Disbursement.cs create mode 100644 PostFundManagement.Domain/Models/Evidence.cs create mode 100644 PostFundManagement.Domain/Models/Indicator.cs create mode 100644 PostFundManagement.Domain/Models/Instrument.cs create mode 100644 PostFundManagement.Domain/Models/Issue.cs create mode 100644 PostFundManagement.Domain/Models/Milestone.cs create mode 100644 PostFundManagement.Domain/Models/Organisation.cs create mode 100644 PostFundManagement.Domain/Models/Outcome.cs create mode 100644 PostFundManagement.Domain/Models/Portfolio.cs create mode 100644 PostFundManagement.Domain/Models/Programme.cs create mode 100644 PostFundManagement.Domain/Models/Project.cs create mode 100644 PostFundManagement.Domain/Models/Risk.cs create mode 100644 PostFundManagement.Domain/Models/Rule.cs create mode 100644 PostFundManagement.Domain/Models/SiteVisit.cs create mode 100644 PostFundManagement.Domain/Models/User.cs delete mode 100644 PostFundManagement.Infrastructure/Class1.cs create mode 100644 PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs create mode 100644 PostFundManagement.code-workspace diff --git a/PostFundManagement.Domain/Class1.cs b/PostFundManagement.Domain/Class1.cs deleted file mode 100644 index 48ddb5b..0000000 --- a/PostFundManagement.Domain/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -ο»Ώnamespace PostFundManagement.Domain; - -public class Class1 -{ - -} diff --git a/PostFundManagement.Domain/Configuration/Instrument.cs b/PostFundManagement.Domain/Configuration/Instrument.cs new file mode 100644 index 0000000..759baf4 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Instrument.cs @@ -0,0 +1,22 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Instrument : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Instrument).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.Code).IsRequired().HasMaxLength(50); + builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Description).IsRequired(false).HasMaxLength(1024); + builder.Property(f => f.Status).IsRequired().HasDefaultValue(ActivityStatus.Inactive); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(f => f.CreatedBy) + .OnDelete(DeleteBehavior.NoAction); + } +} diff --git a/PostFundManagement.Domain/Configuration/Portfolio.cs b/PostFundManagement.Domain/Configuration/Portfolio.cs new file mode 100644 index 0000000..8fe363a --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Portfolio.cs @@ -0,0 +1,27 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Portfolio : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Portfolio).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.OwnedBy).IsRequired(false); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Description).IsRequired(false).HasMaxLength(1024); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(f => f.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Owner) + .WithMany() + .HasForeignKey(f => f.OwnedBy) + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/Programme.cs b/PostFundManagement.Domain/Configuration/Programme.cs new file mode 100644 index 0000000..b69aec6 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Programme.cs @@ -0,0 +1,43 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Programme : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Programme).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.PortfolioId).IsRequired(); + builder.Property(f => f.InstrumentId).IsRequired(); + builder.Property(f => f.OwnedBy).IsRequired(false); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.UpdatedBy).IsRequired(false); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Status).IsRequired(false).HasDefaultValue(ApprovalStatus.Pending); + + builder.HasOne(f => f.Portfolio) + .WithMany(f => f.Programmes) + .HasForeignKey(f => f.PortfolioId) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Instrument) + .WithMany(f => f.Programmes) + .HasForeignKey(f => f.InstrumentId) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(f => f.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Owner) + .WithMany() + .HasForeignKey(f => f.OwnedBy) + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/User.cs b/PostFundManagement.Domain/Configuration/User.cs new file mode 100644 index 0000000..2a67850 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/User.cs @@ -0,0 +1,14 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class User : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.User).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.Email).IsRequired().HasMaxLength(256); + builder.Property(f => f.Status).IsRequired().HasDefaultValue(ActivityStatus.Inactive); + builder.Property(f => f.LastLoginAt).IsRequired(false); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Instrument.cs b/PostFundManagement.Domain/Entities/Instrument.cs new file mode 100644 index 0000000..b3be8e0 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Instrument.cs @@ -0,0 +1,9 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Instrument : Models.Instrument +{ + public virtual User? Creator { get; set; } + + public virtual ICollection Programmes { get; set; } = []; +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Portfolio.cs b/PostFundManagement.Domain/Entities/Portfolio.cs new file mode 100644 index 0000000..48b30e6 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Portfolio.cs @@ -0,0 +1,11 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Portfolio : Models.Portfolio +{ + public virtual User? Creator { get; set; } + + public virtual User? Owner { get; set; } + + public virtual ICollection Programmes { get; set; } = []; +} diff --git a/PostFundManagement.Domain/Entities/Programme.cs b/PostFundManagement.Domain/Entities/Programme.cs new file mode 100644 index 0000000..7f3d4d2 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Programme.cs @@ -0,0 +1,13 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Programme : Models.Programme +{ + public virtual User? Creator { get; set; } + + public virtual User? Owner { get; set; } + + public virtual Portfolio? Portfolio { get; set; } + + public virtual Instrument? Instrument { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/User.cs b/PostFundManagement.Domain/Entities/User.cs new file mode 100644 index 0000000..723864a --- /dev/null +++ b/PostFundManagement.Domain/Entities/User.cs @@ -0,0 +1,4 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public sealed class User : Models.User; \ No newline at end of file diff --git a/PostFundManagement.Domain/Enums.cs b/PostFundManagement.Domain/Enums.cs new file mode 100644 index 0000000..a1041ac --- /dev/null +++ b/PostFundManagement.Domain/Enums.cs @@ -0,0 +1,64 @@ +namespace PostFundManagement.Domain; + +public enum Priority : int +{ + Low = 1, + Medium = 2, + High = 3, + Critical = 4 +} + +public enum UnitOfMeasure : int +{ + Count = 1, + Percentage = 2, + Currency = 3, + Area = 4, + Weight = 5, + Volume = 6, + DurationDays = 7, + DurationMonths = 8, + Ratio = 9 +} +public enum ContractStatus : int +{ + Draft = 1, + PendingApproval = 2, + Active = 3, + Expired = 4, + Terminated = 5, + Amended = 6 +} + +public enum RiskLikelihood : int +{ + Rare = 1, + Unlikely = 2, + Possible = 3, + Likely = 4, + AlmostCertain = 5 +} + +public enum RiskImpact : int +{ + Negligible = 1, + Minor = 2, + Moderate = 3, + Major = 4, + Critical = 5 +} + +public enum ApprovalStatus : int +{ + Pending = 1, + Review = 2, + Approved = 3, + Rejected = 4 +} + +public enum ActivityStatus : int +{ + Active = 1, + Inactive = 2, + Disabled = 3 +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/AuditLog.cs b/PostFundManagement.Domain/Models/AuditLog.cs new file mode 100644 index 0000000..91ed184 --- /dev/null +++ b/PostFundManagement.Domain/Models/AuditLog.cs @@ -0,0 +1,18 @@ +namespace PostFundManagement.Domain.Models; + +public class AuditLog +{ + public long Id { get; set; } + + public string? EntityName { get; set; } + + public long EntityId { get; set; } + + public string? Action { get; set; } + + public string[]? Changes { get; set; } + + public Guid PerformedBy { get; set; } + + public DateTime Timestamp { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Award.cs b/PostFundManagement.Domain/Models/Award.cs new file mode 100644 index 0000000..1a97f93 --- /dev/null +++ b/PostFundManagement.Domain/Models/Award.cs @@ -0,0 +1,26 @@ +namespace PostFundManagement.Domain.Models; + +public class Award +{ + public long Id { get; set; } + + public long OrganisationId { get; set; } + + public long ProgrammeId { get; set; } + + public long? ProjectId { get; set; } + + public Guid CreatedBy { get; set; } + + public Guid? UpdatedBy { get; set; } + + public Guid? ApprovedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime UpdatedAt { get; set; } + + public ApprovalStatus Status { get; set; } + + public decimal Amount { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Beneficiary.cs b/PostFundManagement.Domain/Models/Beneficiary.cs new file mode 100644 index 0000000..b7aefb7 --- /dev/null +++ b/PostFundManagement.Domain/Models/Beneficiary.cs @@ -0,0 +1,24 @@ +namespace PostFundManagement.Domain.Models; + +public class Beneficiary +{ + public long Id { get; set; } + + public long ProjectId { get; set; } + + public Guid UserId { get; set; } + + public Guid CreatedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public string? Name { get; set; } + + public string? Category { get; set; } + + public int TargetCount { get; set; } + + public int ReachedCount { get; set; } + + public string? Location { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Budget.cs b/PostFundManagement.Domain/Models/Budget.cs new file mode 100644 index 0000000..c4b6346 --- /dev/null +++ b/PostFundManagement.Domain/Models/Budget.cs @@ -0,0 +1,20 @@ +namespace PostFundManagement.Domain.Models; + +public class Budget +{ + public long Id { get; set; } + + public long ProjectId { get; set; } + + public Guid CreatedBy { get; set; } + + public Guid? ApprovedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public string? Name { get; set; } + + public decimal Amount { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/ChangeRequest.cs b/PostFundManagement.Domain/Models/ChangeRequest.cs new file mode 100644 index 0000000..65f6358 --- /dev/null +++ b/PostFundManagement.Domain/Models/ChangeRequest.cs @@ -0,0 +1,26 @@ +namespace PostFundManagement.Domain.Models; + +public class ChangeRequest +{ + public long Id { get; set; } + + public long ProjectId { get; set; } + + public string? Title { get; set; } + + public string? Justification { get; set; } + + public decimal? RevisedBudget { get; set; } + + public DateTime? RevisedEndedAt { get; set; } + + public ApprovalStatus Status { get; set; } + + public Guid CreatedBy { get; set; } + + public Guid? ApprovedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/ComplianceItem.cs b/PostFundManagement.Domain/Models/ComplianceItem.cs new file mode 100644 index 0000000..4eccdb3 --- /dev/null +++ b/PostFundManagement.Domain/Models/ComplianceItem.cs @@ -0,0 +1,22 @@ +namespace PostFundManagement.Domain.Models; + +public class ComplianceItem +{ + public long Id { get; set; } + + public long ProjectId { get; set; } + + public string? Title { get; set; } + + public string? Requirements { get; set; } + + public ApprovalStatus Status { get; set; } + + public Guid CreatedBy { get; set; } + + public Guid? VerifiedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? VerifiedAt { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Contract.cs b/PostFundManagement.Domain/Models/Contract.cs new file mode 100644 index 0000000..2e0b197 --- /dev/null +++ b/PostFundManagement.Domain/Models/Contract.cs @@ -0,0 +1,24 @@ +namespace PostFundManagement.Domain.Models; + +public class Contract +{ + public long Id { get; set; } + + public long AwardId { get; set; } + + public string? ExternalSignatureId { get; set; } + + public string? SignedDocumentUrl { get; set; } + + public DateTime? EffectiveAt { get; set; } + + public DateTime? ExpiresAt { get; set; } + + public decimal TotalValue { get; set; } + + public ContractStatus Status { get; set; } + + public Guid CreatedBy { get; set; } + + public DateTime CreatedAt { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Disbursement.cs b/PostFundManagement.Domain/Models/Disbursement.cs new file mode 100644 index 0000000..e06e7e1 --- /dev/null +++ b/PostFundManagement.Domain/Models/Disbursement.cs @@ -0,0 +1,22 @@ +namespace PostFundManagement.Domain.Models; + +public class Disbursement +{ + public long Id { get; set; } + + public long ProjectId { get; set; } + + public long? MilestoneId { get; set; } + + public Guid CreatedBy { get; set; } + + public Guid? UpdatedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public decimal Amount { get; set; } + + public ApprovalStatus Status { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Evidence.cs b/PostFundManagement.Domain/Models/Evidence.cs new file mode 100644 index 0000000..14c86a9 --- /dev/null +++ b/PostFundManagement.Domain/Models/Evidence.cs @@ -0,0 +1,26 @@ +namespace PostFundManagement.Domain.Models; + +public class Evidence +{ + public long Id { get; set; } + + public long ProjectId { get; set; } + + public long? MilestoneId { get; set; } + + public long? IndicatorId { get; set; } + + public Guid CreatedBy { get; set; } + + public Guid? UpdatedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public int Version { get; set; } + + public string? DocumentUrl { get; set; } + + public ApprovalStatus Status { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Indicator.cs b/PostFundManagement.Domain/Models/Indicator.cs new file mode 100644 index 0000000..1787d4f --- /dev/null +++ b/PostFundManagement.Domain/Models/Indicator.cs @@ -0,0 +1,22 @@ +namespace PostFundManagement.Domain.Models; + +public class Indicator +{ + public long Id { get; set; } + + public long ProjectId { get; set; } + + public Guid CreatedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public string? Name { get; set; } + + public UnitOfMeasure? UnitOfMeasure { get; set; } + + public decimal BaselineAmount { get; set; } + + public decimal TargetAmount { get; set; } + + public decimal ActualAmount { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Instrument.cs b/PostFundManagement.Domain/Models/Instrument.cs new file mode 100644 index 0000000..1ddbfd3 --- /dev/null +++ b/PostFundManagement.Domain/Models/Instrument.cs @@ -0,0 +1,18 @@ +namespace PostFundManagement.Domain.Models; + +public class Instrument +{ + public long Id { get; set; } + + public string? Code { get; set; } + + public string? Name { get; set; } + + public string? Description { get; set; } + + public ActivityStatus Status { get; set; } + + public Guid CreatedBy { get; set; } + + public DateTime CreatedAt { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Issue.cs b/PostFundManagement.Domain/Models/Issue.cs new file mode 100644 index 0000000..e9ad515 --- /dev/null +++ b/PostFundManagement.Domain/Models/Issue.cs @@ -0,0 +1,22 @@ +namespace PostFundManagement.Domain.Models; + +public class Issue +{ + public long Id { get; set; } + + public long ProjectId { get; set; } + + public string? Title { get; set; } + + public string? Description { get; set; } + + public Priority Priority { get; set; } + + public ActivityStatus Status { get; set; } + + public Guid CreatedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? ResolvedAt { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Milestone.cs b/PostFundManagement.Domain/Models/Milestone.cs new file mode 100644 index 0000000..1da57b7 --- /dev/null +++ b/PostFundManagement.Domain/Models/Milestone.cs @@ -0,0 +1,22 @@ +namespace PostFundManagement.Domain.Models; + +public class Milestone +{ + public long Id { get; set; } + + public long ProjectId { get; set; } + + public Guid CreatedBy { get; set; } + + public Guid? UpdatedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public DateTime DueAt { get; set; } + + public string? Name { get; set; } + + public ApprovalStatus Status { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Organisation.cs b/PostFundManagement.Domain/Models/Organisation.cs new file mode 100644 index 0000000..eeb71c1 --- /dev/null +++ b/PostFundManagement.Domain/Models/Organisation.cs @@ -0,0 +1,20 @@ +namespace PostFundManagement.Domain.Models; + +public class Organisation +{ + public long Id { get; set; } + + public Guid CreatedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public string? RegistrationNo { get; set; } + + public string? Name { get; set; } + + public string? Email { get; set; } + + public int Type { get; set; } + + public ActivityStatus Status { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Outcome.cs b/PostFundManagement.Domain/Models/Outcome.cs new file mode 100644 index 0000000..768b91d --- /dev/null +++ b/PostFundManagement.Domain/Models/Outcome.cs @@ -0,0 +1,16 @@ +namespace PostFundManagement.Domain.Models; + +public class Outcome +{ + public long Id { get; set; } + + public long BeneficiaryId { get; set; } + + public Guid CreatedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public string? Name { get; set; } + + public string? Description { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Portfolio.cs b/PostFundManagement.Domain/Models/Portfolio.cs new file mode 100644 index 0000000..47d8420 --- /dev/null +++ b/PostFundManagement.Domain/Models/Portfolio.cs @@ -0,0 +1,16 @@ +namespace PostFundManagement.Domain.Models; + +public class Portfolio +{ + public long Id { get; set; } + + public Guid OwnedBy { get; set; } + + public Guid CreatedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public string? Name { get; set; } + + public string? Description { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Programme.cs b/PostFundManagement.Domain/Models/Programme.cs new file mode 100644 index 0000000..d04372d --- /dev/null +++ b/PostFundManagement.Domain/Models/Programme.cs @@ -0,0 +1,24 @@ +namespace PostFundManagement.Domain.Models; + +public class Programme +{ + public long Id { get; set; } + + public long PortfolioId { get; set; } + + public long InstrumentId { get; set; } + + public Guid OwnedBy { get; set; } + + public Guid CreatedBy { get; set; } + + public Guid? UpdatedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public string? Name { get; set; } + + public ApprovalStatus Status { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Project.cs b/PostFundManagement.Domain/Models/Project.cs new file mode 100644 index 0000000..4a09337 --- /dev/null +++ b/PostFundManagement.Domain/Models/Project.cs @@ -0,0 +1,26 @@ +namespace PostFundManagement.Domain.Models; + +public class Project +{ + public long Id { get; set; } + + public long ProgrammeId { get; set; } + + public Guid CreatedBy { get; set; } + + public Guid? UpdatedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public DateTime? StartedAt { get; set; } + + public DateTime? EndedAt { get; set; } + + public string? Name { get; set; } + + public string? Description { get; set; } + + public ApprovalStatus Status { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Risk.cs b/PostFundManagement.Domain/Models/Risk.cs new file mode 100644 index 0000000..e22b458 --- /dev/null +++ b/PostFundManagement.Domain/Models/Risk.cs @@ -0,0 +1,20 @@ +namespace PostFundManagement.Domain.Models; + +public class Risk +{ + public long Id { get; set; } + + public long ProjectId { get; set; } + + public Guid CreatedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public RiskLikelihood Likelihood { get; set; } + + public RiskImpact Impact { get; set; } + + public string? Name { get; set; } + + public string? Description { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Rule.cs b/PostFundManagement.Domain/Models/Rule.cs new file mode 100644 index 0000000..ba15a16 --- /dev/null +++ b/PostFundManagement.Domain/Models/Rule.cs @@ -0,0 +1,22 @@ +namespace PostFundManagement.Domain.Models; + +public class Rule +{ + public long Id { get; set; } + + public long ProgrammeId { get; set; } + + public Guid CreatedBy { get; set; } + + public Guid? UpdatedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public string? Name { get; set; } + + public int Version { get; set; } + + public ActivityStatus Status { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/SiteVisit.cs b/PostFundManagement.Domain/Models/SiteVisit.cs new file mode 100644 index 0000000..1f8e0fe --- /dev/null +++ b/PostFundManagement.Domain/Models/SiteVisit.cs @@ -0,0 +1,20 @@ +namespace PostFundManagement.Domain.Models; + +public class SiteVisit +{ + public long Id { get; set; } + + public long ProjectId { get; set; } + + public DateTime VisitedAt { get; set; } + + public string? InspectorName { get; set; } + + public string? Findings { get; set; } + + public ApprovalStatus VerificationStatus { get; set; } + + public Guid CreatedBy { get; set; } + + public DateTime CreatedAt { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/User.cs b/PostFundManagement.Domain/Models/User.cs new file mode 100644 index 0000000..15cb816 --- /dev/null +++ b/PostFundManagement.Domain/Models/User.cs @@ -0,0 +1,12 @@ +namespace PostFundManagement.Domain.Models; + +public class User +{ + public Guid Id { get; set; } + + public DateTime LastLoginAt { get; set; } + + public string? Email { get; set; } + + public ActivityStatus Status { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/PostFundManagement.Domain.csproj b/PostFundManagement.Domain/PostFundManagement.Domain.csproj index b760144..48eb5a5 100644 --- a/PostFundManagement.Domain/PostFundManagement.Domain.csproj +++ b/PostFundManagement.Domain/PostFundManagement.Domain.csproj @@ -6,4 +6,30 @@ enable + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/PostFundManagement.Infrastructure/Class1.cs b/PostFundManagement.Infrastructure/Class1.cs deleted file mode 100644 index 804abcc..0000000 --- a/PostFundManagement.Infrastructure/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -ο»Ώnamespace PostFundManagement.Infrastructure; - -public class Class1 -{ - -} diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs new file mode 100644 index 0000000..a80be6b --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -0,0 +1,12 @@ +using PostFundManagement.Domain.Entities; + +namespace PostFundManagement.Infrastructure.Database; + +public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Portfolios => Set(); + + public DbSet Instruments => Set(); + + public DbSet Users => Set(); +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj index b760144..694b6af 100644 --- a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj +++ b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj @@ -6,4 +6,12 @@ enable + + + + + + + + diff --git a/PostFundManagement.code-workspace b/PostFundManagement.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/PostFundManagement.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file From dc574ee971749a3f6126cf6467bbc0011fa4297c Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 15:23:25 +0200 Subject: [PATCH 02/50] Added rules entity Rerfactored relationships --- .../Configuration/Instrument.cs | 3 +- .../Configuration/Portfolio.cs | 5 ++- .../Configuration/Programme.cs | 9 +++-- .../Configuration/Rule.cs | 37 +++++++++++++++++++ .../Entities/Programme.cs | 2 + PostFundManagement.Domain/Entities/Rule.cs | 11 ++++++ .../Database/ApplicationDbContext.cs | 4 ++ 7 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 PostFundManagement.Domain/Configuration/Rule.cs create mode 100644 PostFundManagement.Domain/Entities/Rule.cs diff --git a/PostFundManagement.Domain/Configuration/Instrument.cs b/PostFundManagement.Domain/Configuration/Instrument.cs index 759baf4..27c38bd 100644 --- a/PostFundManagement.Domain/Configuration/Instrument.cs +++ b/PostFundManagement.Domain/Configuration/Instrument.cs @@ -16,7 +16,8 @@ public sealed class Instrument : IEntityTypeConfiguration builder.HasOne(f => f.Creator) .WithMany() - .HasForeignKey(f => f.CreatedBy) + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() .OnDelete(DeleteBehavior.NoAction); } } diff --git a/PostFundManagement.Domain/Configuration/Portfolio.cs b/PostFundManagement.Domain/Configuration/Portfolio.cs index 8fe363a..35f1e19 100644 --- a/PostFundManagement.Domain/Configuration/Portfolio.cs +++ b/PostFundManagement.Domain/Configuration/Portfolio.cs @@ -15,13 +15,14 @@ public sealed class Portfolio : IEntityTypeConfiguration builder.HasOne(f => f.Creator) .WithMany() - .HasForeignKey(f => f.CreatedBy) + .HasForeignKey(fk => fk.CreatedBy) .IsRequired() .OnDelete(DeleteBehavior.NoAction); builder.HasOne(f => f.Owner) .WithMany() - .HasForeignKey(f => f.OwnedBy) + .HasForeignKey(fk => fk.OwnedBy) + .IsRequired(false) .OnDelete(DeleteBehavior.NoAction); } } \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/Programme.cs b/PostFundManagement.Domain/Configuration/Programme.cs index b69aec6..6555564 100644 --- a/PostFundManagement.Domain/Configuration/Programme.cs +++ b/PostFundManagement.Domain/Configuration/Programme.cs @@ -19,25 +19,26 @@ public sealed class Programme : IEntityTypeConfiguration builder.HasOne(f => f.Portfolio) .WithMany(f => f.Programmes) - .HasForeignKey(f => f.PortfolioId) + .HasForeignKey(fk => fk.PortfolioId) .IsRequired() .OnDelete(DeleteBehavior.NoAction); builder.HasOne(f => f.Instrument) .WithMany(f => f.Programmes) - .HasForeignKey(f => f.InstrumentId) + .HasForeignKey(fk => fk.InstrumentId) .IsRequired() .OnDelete(DeleteBehavior.NoAction); builder.HasOne(f => f.Creator) .WithMany() - .HasForeignKey(f => f.CreatedBy) + .HasForeignKey(fk => fk.CreatedBy) .IsRequired() .OnDelete(DeleteBehavior.NoAction); builder.HasOne(f => f.Owner) .WithMany() - .HasForeignKey(f => f.OwnedBy) + .IsRequired(false) + .HasForeignKey(fk => fk.OwnedBy) .OnDelete(DeleteBehavior.NoAction); } } \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/Rule.cs b/PostFundManagement.Domain/Configuration/Rule.cs new file mode 100644 index 0000000..129a3f7 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Rule.cs @@ -0,0 +1,37 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Rule : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Rule).Pluralize()); + + builder.HasKey(fk => fk.Id); + builder.Property(f => f.ProgrammeId).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.UpdatedBy).IsRequired(false); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Version).IsRequired().HasDefaultValue(1); + builder.Property(f => f.Status).IsRequired().HasDefaultValue(ActivityStatus.Inactive); + + builder.HasOne(f => f.Programme) + .WithMany(f => f.Rules) + .HasForeignKey(fk => fk.ProgrammeId) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Updator) + .WithMany() + .HasForeignKey(fk => fk.UpdatedBy) + .IsRequired(false) + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Programme.cs b/PostFundManagement.Domain/Entities/Programme.cs index 7f3d4d2..096b712 100644 --- a/PostFundManagement.Domain/Entities/Programme.cs +++ b/PostFundManagement.Domain/Entities/Programme.cs @@ -10,4 +10,6 @@ public class Programme : Models.Programme public virtual Portfolio? Portfolio { get; set; } public virtual Instrument? Instrument { get; set; } + + public virtual ICollection Rules { get; set; } = []; } \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Rule.cs b/PostFundManagement.Domain/Entities/Rule.cs new file mode 100644 index 0000000..f33401b --- /dev/null +++ b/PostFundManagement.Domain/Entities/Rule.cs @@ -0,0 +1,11 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Rule : Models.Rule +{ + public virtual User? Creator { get; set; } + + public virtual User? Updator { get; set; } + + public virtual Programme? Programme { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index a80be6b..cbe8b49 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,10 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Rules => Set(); + + public DbSet Programmes => Set(); + public DbSet Portfolios => Set(); public DbSet Instruments => Set(); From 3e79150590c7b81425edc974c60d00d3f8053675 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 15:39:20 +0200 Subject: [PATCH 03/50] Added organisation entity --- .../Configuration/Organisation.cs | 24 ++++++++++ .../Entities/Organisation.cs | 7 +++ PostFundManagement.Domain/Enums.cs | 44 +++++++++++++++++++ .../Models/Organisation.cs | 2 +- .../Database/ApplicationDbContext.cs | 2 + 5 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 PostFundManagement.Domain/Configuration/Organisation.cs create mode 100644 PostFundManagement.Domain/Entities/Organisation.cs diff --git a/PostFundManagement.Domain/Configuration/Organisation.cs b/PostFundManagement.Domain/Configuration/Organisation.cs new file mode 100644 index 0000000..cc7c668 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Organisation.cs @@ -0,0 +1,24 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Organisation : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Organisation).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.RegistrationNo).IsRequired().HasMaxLength(256); + builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Email).IsRequired().HasMaxLength(256); + builder.Property(f => f.Type).IsRequired().HasDefaultValue(OrganisationType.PrivateLimitedCompany); + builder.Property(f => f.Status).IsRequired().HasDefaultValue(ActivityStatus.Active); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Organisation.cs b/PostFundManagement.Domain/Entities/Organisation.cs new file mode 100644 index 0000000..ca75f86 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Organisation.cs @@ -0,0 +1,7 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Organisation : Models.Organisation +{ + public virtual User? Creator { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Enums.cs b/PostFundManagement.Domain/Enums.cs index a1041ac..e79ffc9 100644 --- a/PostFundManagement.Domain/Enums.cs +++ b/PostFundManagement.Domain/Enums.cs @@ -1,5 +1,49 @@ namespace PostFundManagement.Domain; +public enum OrganisationType : int +{ + // Non-Profit & Civil Society + NonGovernmentalOrganisation = 1, // NGO / NPO + CivilSocietyOrganisation = 2, // CSO / Community-based group + Trust = 3, // Charitable Trust / Foundation + FaithBasedOrganisation = 4, // FBO + + // Commercial & Private Sector + PrivateLimitedCompany = 5, // Pty Ltd / Ltd + PublicLimitedCompany = 6, // PLC + SoleProprietorship = 7, // Individual / Sole Trader + Partnership = 8, // General / Limited Partnership + SocialEnterprise = 9, // Revenue-generating impact entity + + // Public Sector & Government + NationalGovernmentMinistry = 10, // Department / Ministry + ProvincialGovernmentEntity = 11, // State / Provincial authority + LocalGovernmentMunicipality = 12,// District / City Council + StateOwnedEnterprise = 13, // Parastatal / SOE + PublicAgency = 14, // Statutory body / Regulator + + // Academia & Research + PublicUniversity = 15, + PrivateUniversity = 16, + ResearchInstitute = 17, // Science / Policy think tank + TechnicalVocationalCollege = 18, // TVET / Vocational school + + // Cooperative & Financial + Cooperative = 19, // Producer / Worker co-op + MicrofinanceInstitution = 20, // MFI / Financial Intermediary + CommercialBank = 21, + DevelopmentFinanceInstitution = 22, // DFI / Multilateral Bank + + // International & Multilateral + UnitedNationsAgency = 23, // UN / UNDP / UNICEF + IntergovernmentalOrganisation = 24, // IGO (e.g., AU, SADC, EU) + InternationalNGO = 25, // INGO + + // Consortium & Special Vehicles + Consortium = 26, // Unincorporated joint venture + SpecialPurposeVehicle = 27 // SPV / Joint Venture Entity +} + public enum Priority : int { Low = 1, diff --git a/PostFundManagement.Domain/Models/Organisation.cs b/PostFundManagement.Domain/Models/Organisation.cs index eeb71c1..b3e1821 100644 --- a/PostFundManagement.Domain/Models/Organisation.cs +++ b/PostFundManagement.Domain/Models/Organisation.cs @@ -14,7 +14,7 @@ public class Organisation public string? Email { get; set; } - public int Type { get; set; } + public OrganisationType Type { get; set; } public ActivityStatus Status { get; set; } } \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index cbe8b49..6ac5a97 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Organisations => Set(); + public DbSet Rules => Set(); public DbSet Programmes => Set(); From d0accc5d416501ad7e196d40836ec7d7262a0dca Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 15:51:17 +0200 Subject: [PATCH 04/50] Added project entity --- .../Configuration/Project.cs | 39 +++++++++++++++++++ .../Entities/Programme.cs | 2 + PostFundManagement.Domain/Entities/Project.cs | 11 ++++++ .../Database/ApplicationDbContext.cs | 2 + 4 files changed, 54 insertions(+) create mode 100644 PostFundManagement.Domain/Configuration/Project.cs create mode 100644 PostFundManagement.Domain/Entities/Project.cs diff --git a/PostFundManagement.Domain/Configuration/Project.cs b/PostFundManagement.Domain/Configuration/Project.cs new file mode 100644 index 0000000..d0e7254 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Project.cs @@ -0,0 +1,39 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Project : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Project).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.ProgrammeId).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.UpdatedBy).IsRequired(false); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.StartedAt).IsRequired(false); + builder.Property(f => f.EndedAt).IsRequired(false); + builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Description).IsRequired().HasMaxLength(1024); + builder.Property(f => f.StartedAt).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + + builder.HasOne(f => f.Programme) + .WithMany(f => f.Projects) + .HasForeignKey(fk => fk.ProgrammeId) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Updator) + .WithMany() + .HasForeignKey(fk => fk.UpdatedBy) + .IsRequired(false) + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Programme.cs b/PostFundManagement.Domain/Entities/Programme.cs index 096b712..0f7c44b 100644 --- a/PostFundManagement.Domain/Entities/Programme.cs +++ b/PostFundManagement.Domain/Entities/Programme.cs @@ -12,4 +12,6 @@ public class Programme : Models.Programme public virtual Instrument? Instrument { get; set; } public virtual ICollection Rules { get; set; } = []; + + public virtual ICollection Projects { get; set; } = []; } \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Project.cs b/PostFundManagement.Domain/Entities/Project.cs new file mode 100644 index 0000000..1440add --- /dev/null +++ b/PostFundManagement.Domain/Entities/Project.cs @@ -0,0 +1,11 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Project : Models.Project +{ + public virtual User? Creator { get; set; } + + public virtual User? Updator { get; set; } + + public virtual Programme? Programme { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index 6ac5a97..9142b07 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Projects => Set(); + public DbSet Organisations => Set(); public DbSet Rules => Set(); From 1d5c7ca6fabedfd825e44c52dfde92735231ef55 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 16:06:50 +0200 Subject: [PATCH 05/50] Added award entity --- .../Configuration/Award.cs | 57 +++++++++++++++++++ PostFundManagement.Domain/Entities/Award.cs | 17 ++++++ .../Entities/Programme.cs | 2 + PostFundManagement.Domain/Entities/Project.cs | 2 + .../Database/ApplicationDbContext.cs | 2 + 5 files changed, 80 insertions(+) create mode 100644 PostFundManagement.Domain/Configuration/Award.cs create mode 100644 PostFundManagement.Domain/Entities/Award.cs diff --git a/PostFundManagement.Domain/Configuration/Award.cs b/PostFundManagement.Domain/Configuration/Award.cs new file mode 100644 index 0000000..de9ff68 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Award.cs @@ -0,0 +1,57 @@ +namespace PostFundManagement.Domain.Configuration; + +public class Award : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Award).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.OrganisationId).IsRequired(); + builder.Property(f => f.ProgrammeId).IsRequired(); + builder.Property(f => f.ProjectId).IsRequired(false); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.UpdatedBy).IsRequired(false); + builder.Property(f => f.ApprovedBy).IsRequired(false); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.Status).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); + + builder.HasOne(f => f.Organisation) + .WithMany() + .HasForeignKey(fk => fk.OrganisationId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Programme) + .WithMany(f => f.Awards) + .HasForeignKey(fk => fk.ProgrammeId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Project) + .WithMany(f => f.Awards) + .HasForeignKey(fk => fk.ProjectId) + .IsRequired(false) + .OnDelete(DeleteBehavior.SetNull); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Updator) + .WithMany() + .HasForeignKey(fk => fk.UpdatedBy) + .IsRequired(false) + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Approver) + .WithMany() + .HasForeignKey(fk => fk.ApprovedBy) + .IsRequired(false) + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Award.cs b/PostFundManagement.Domain/Entities/Award.cs new file mode 100644 index 0000000..f29083e --- /dev/null +++ b/PostFundManagement.Domain/Entities/Award.cs @@ -0,0 +1,17 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Award : Models.Award +{ + public virtual User? Creator { get; set; } + + public virtual User? Updator { get; set; } + + public virtual User? Approver { get; set; } + + public virtual Organisation? Organisation { get; set; } + + public virtual Programme? Programme { get; set; } + + public virtual Project? Project { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Programme.cs b/PostFundManagement.Domain/Entities/Programme.cs index 0f7c44b..fc76353 100644 --- a/PostFundManagement.Domain/Entities/Programme.cs +++ b/PostFundManagement.Domain/Entities/Programme.cs @@ -14,4 +14,6 @@ public class Programme : Models.Programme public virtual ICollection Rules { get; set; } = []; public virtual ICollection Projects { get; set; } = []; + + public virtual ICollection Awards { get; set; } = []; } \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Project.cs b/PostFundManagement.Domain/Entities/Project.cs index 1440add..982fb00 100644 --- a/PostFundManagement.Domain/Entities/Project.cs +++ b/PostFundManagement.Domain/Entities/Project.cs @@ -8,4 +8,6 @@ public class Project : Models.Project public virtual User? Updator { get; set; } public virtual Programme? Programme { get; set; } + + public virtual ICollection Awards { get; set; } = []; } \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index 9142b07..beaa673 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Awards => Set(); + public DbSet Projects => Set(); public DbSet Organisations => Set(); From 772e85a56ca4b4fdb27f156e8080db28b2845c78 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 16:23:44 +0200 Subject: [PATCH 06/50] Added contract entity --- .../Configuration/Contract.cs | 34 +++++++++++++++++++ PostFundManagement.Domain/Entities/Award.cs | 2 ++ .../Entities/Contract.cs | 8 +++++ .../Database/ApplicationDbContext.cs | 2 ++ 4 files changed, 46 insertions(+) create mode 100644 PostFundManagement.Domain/Configuration/Contract.cs create mode 100644 PostFundManagement.Domain/Entities/Contract.cs diff --git a/PostFundManagement.Domain/Configuration/Contract.cs b/PostFundManagement.Domain/Configuration/Contract.cs new file mode 100644 index 0000000..c29ce9f --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Contract.cs @@ -0,0 +1,34 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Contract : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Contract).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.HasIndex(f => f.AwardId).IsUnique(); + + builder.Property(f => f.AwardId).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.ExternalSignatureId).IsRequired(false); + builder.Property(f => f.SignedDocumentUrl).IsRequired(false); + builder.Property(f => f.EffectiveAt).IsRequired(false); + builder.Property(f => f.ExpiresAt).IsRequired(false); + builder.Property(f => f.TotalValue).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.Status).IsRequired().HasDefaultValue(ContractStatus.Draft); + + builder.HasOne(f => f.Award) + .WithOne(f => f.Contract) + .HasForeignKey(fk => fk.AwardId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Award.cs b/PostFundManagement.Domain/Entities/Award.cs index f29083e..66617e2 100644 --- a/PostFundManagement.Domain/Entities/Award.cs +++ b/PostFundManagement.Domain/Entities/Award.cs @@ -14,4 +14,6 @@ public class Award : Models.Award public virtual Programme? Programme { get; set; } public virtual Project? Project { get; set; } + + public virtual Contract? Contract { get; set; } } \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Contract.cs b/PostFundManagement.Domain/Entities/Contract.cs new file mode 100644 index 0000000..414af83 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Contract.cs @@ -0,0 +1,8 @@ +namespace PostFundManagement.Domain.Entities; + +public class Contract : Models.Contract +{ + public virtual Award? Award { get; set; } + + public virtual User? Creator { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index beaa673..c1bf098 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Contracts => Set(); + public DbSet Awards => Set(); public DbSet Projects => Set(); From 092ae14271c57b68a9d9bba417aee0265cd438b7 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 16:37:37 +0200 Subject: [PATCH 07/50] Added budget entity --- .../Configuration/Budget.cs | 36 +++++++++++++++++++ PostFundManagement.Domain/Entities/Budget.cs | 10 ++++++ .../Database/ApplicationDbContext.cs | 2 ++ 3 files changed, 48 insertions(+) create mode 100644 PostFundManagement.Domain/Configuration/Budget.cs create mode 100644 PostFundManagement.Domain/Entities/Budget.cs diff --git a/PostFundManagement.Domain/Configuration/Budget.cs b/PostFundManagement.Domain/Configuration/Budget.cs new file mode 100644 index 0000000..8600b9d --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Budget.cs @@ -0,0 +1,36 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Budget : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Budget).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.ProjectId).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.ApprovedBy).IsRequired(false); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); + + builder.HasOne(f => f.Project) + .WithMany() + .HasForeignKey(fk => fk.ProjectId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Approver) + .WithMany() + .HasForeignKey(fk => fk.ApprovedBy) + .IsRequired(false) + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Budget.cs b/PostFundManagement.Domain/Entities/Budget.cs new file mode 100644 index 0000000..44ef49a --- /dev/null +++ b/PostFundManagement.Domain/Entities/Budget.cs @@ -0,0 +1,10 @@ +namespace PostFundManagement.Domain.Entities; + +public class Budget : Models.Budget +{ + public virtual Project? Project { get; set; } + + public virtual User? Creator { get; set; } + + public virtual User? Approver { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index c1bf098..c58725b 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Budgets => Set(); + public DbSet Contracts => Set(); public DbSet Awards => Set(); From c99885a866f55da7b68bba9e14d502ead2eaa8ac Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 16:38:18 +0200 Subject: [PATCH 08/50] Completed budget entity configuration --- PostFundManagement.Domain/Entities/Budget.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/PostFundManagement.Domain/Entities/Budget.cs b/PostFundManagement.Domain/Entities/Budget.cs index 44ef49a..888463f 100644 --- a/PostFundManagement.Domain/Entities/Budget.cs +++ b/PostFundManagement.Domain/Entities/Budget.cs @@ -1,5 +1,6 @@ namespace PostFundManagement.Domain.Entities; +[EntityTypeConfiguration] public class Budget : Models.Budget { public virtual Project? Project { get; set; } From a5dfc3a3b39d13b2b411313627cfcc239a675c61 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 16:46:55 +0200 Subject: [PATCH 09/50] Added milestone entity --- .../Configuration/Milestone.cs | 37 +++++++++++++++++++ .../Entities/Milestone.cs | 11 ++++++ .../Database/ApplicationDbContext.cs | 2 + 3 files changed, 50 insertions(+) create mode 100644 PostFundManagement.Domain/Configuration/Milestone.cs create mode 100644 PostFundManagement.Domain/Entities/Milestone.cs diff --git a/PostFundManagement.Domain/Configuration/Milestone.cs b/PostFundManagement.Domain/Configuration/Milestone.cs new file mode 100644 index 0000000..3ce043e --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Milestone.cs @@ -0,0 +1,37 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Milestone : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Milestone).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.ProjectId).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.UpdatedBy).IsRequired(false); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.DueAt).IsRequired(); + builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Status).HasDefaultValue(ApprovalStatus.Pending); + + builder.HasOne(f => f.Project) + .WithMany() + .HasForeignKey(fk => fk.ProjectId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Updator) + .WithMany() + .HasForeignKey(fk => fk.UpdatedBy) + .IsRequired(false) + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Milestone.cs b/PostFundManagement.Domain/Entities/Milestone.cs new file mode 100644 index 0000000..8aa288c --- /dev/null +++ b/PostFundManagement.Domain/Entities/Milestone.cs @@ -0,0 +1,11 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Milestone : Models.Milestone +{ + public virtual Project? Project { get; set; } + + public virtual User? Creator { get; set; } + + public virtual User? Updator { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index c58725b..0108a67 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Milestones => Set(); + public DbSet Budgets => Set(); public DbSet Contracts => Set(); From c0ece569ad0cca2762a0586f0af3ddbb8bd04c06 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 16:58:15 +0200 Subject: [PATCH 10/50] Added disbursement entity --- .../Configuration/Disbursement.cs | 43 +++++++++++++++++++ .../Entities/Contract.cs | 1 + .../Entities/Disbursement.cs | 13 ++++++ PostFundManagement.Domain/Enums.cs | 3 +- .../Database/ApplicationDbContext.cs | 2 + 5 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 PostFundManagement.Domain/Configuration/Disbursement.cs create mode 100644 PostFundManagement.Domain/Entities/Disbursement.cs diff --git a/PostFundManagement.Domain/Configuration/Disbursement.cs b/PostFundManagement.Domain/Configuration/Disbursement.cs new file mode 100644 index 0000000..3733dbc --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Disbursement.cs @@ -0,0 +1,43 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Disbursement : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Disbursement).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.ProjectId).IsRequired(); + builder.Property(f => f.MilestoneId).IsRequired(false); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.UpdatedBy).IsRequired(false); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.Status).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + + builder.HasOne(f => f.Project) + .WithMany() + .HasForeignKey(fk => fk.ProjectId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Milestone) + .WithMany() + .HasForeignKey(fk => fk.MilestoneId) + .IsRequired(false) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Updator) + .WithMany() + .HasForeignKey(fk => fk.UpdatedBy) + .IsRequired(false) + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Contract.cs b/PostFundManagement.Domain/Entities/Contract.cs index 414af83..dfa9550 100644 --- a/PostFundManagement.Domain/Entities/Contract.cs +++ b/PostFundManagement.Domain/Entities/Contract.cs @@ -1,5 +1,6 @@ namespace PostFundManagement.Domain.Entities; +[EntityTypeConfiguration] public class Contract : Models.Contract { public virtual Award? Award { get; set; } diff --git a/PostFundManagement.Domain/Entities/Disbursement.cs b/PostFundManagement.Domain/Entities/Disbursement.cs new file mode 100644 index 0000000..44f2530 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Disbursement.cs @@ -0,0 +1,13 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Disbursement : Models.Disbursement +{ + public virtual Project? Project { get; set; } + + public virtual Milestone? Milestone { get; set; } + + public virtual User? Creator { get; set; } + + public virtual User? Updator { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Enums.cs b/PostFundManagement.Domain/Enums.cs index e79ffc9..770d556 100644 --- a/PostFundManagement.Domain/Enums.cs +++ b/PostFundManagement.Domain/Enums.cs @@ -97,7 +97,8 @@ public enum ApprovalStatus : int Pending = 1, Review = 2, Approved = 3, - Rejected = 4 + Rejected = 4, + Removed = 5 } public enum ActivityStatus : int diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index 0108a67..09eb329 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Disbursements => Set(); + public DbSet Milestones => Set(); public DbSet Budgets => Set(); From 4879452e1e131ddffd760bc5815e91d13b95d559 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 17:05:21 +0200 Subject: [PATCH 11/50] Added indicator entity --- .../Configuration/Indicator.cs | 31 +++++++++++++++++++ .../Entities/Indicator.cs | 9 ++++++ .../Database/ApplicationDbContext.cs | 2 ++ 3 files changed, 42 insertions(+) create mode 100644 PostFundManagement.Domain/Configuration/Indicator.cs create mode 100644 PostFundManagement.Domain/Entities/Indicator.cs diff --git a/PostFundManagement.Domain/Configuration/Indicator.cs b/PostFundManagement.Domain/Configuration/Indicator.cs new file mode 100644 index 0000000..cc4a367 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Indicator.cs @@ -0,0 +1,31 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Indicator : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Indicator).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.ProjectId).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.UnitOfMeasure).IsRequired().HasDefaultValue(UnitOfMeasure.Percentage); + builder.Property(f => f.BaselineAmount).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.TargetAmount).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.ActualAmount).IsRequired().HasPrecision(18, 2); + + builder.HasOne(f => f.Project) + .WithMany() + .HasForeignKey(fk => fk.ProjectId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Indicator.cs b/PostFundManagement.Domain/Entities/Indicator.cs new file mode 100644 index 0000000..bc34d6b --- /dev/null +++ b/PostFundManagement.Domain/Entities/Indicator.cs @@ -0,0 +1,9 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Indicator : Models.Indicator +{ + public virtual Project? Project { get; set; } + + public virtual User? Creator { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index 09eb329..ff4bd37 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Indicators => Set(); + public DbSet Disbursements => Set(); public DbSet Milestones => Set(); From 628fe7ea1afb6b2856b4a84547e106ad67812c94 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 17:12:26 +0200 Subject: [PATCH 12/50] Added risk entity --- .../Configuration/Risk.cs | 30 +++++++++++++++++++ PostFundManagement.Domain/Entities/Risk.cs | 9 ++++++ .../Database/ApplicationDbContext.cs | 2 ++ 3 files changed, 41 insertions(+) create mode 100644 PostFundManagement.Domain/Configuration/Risk.cs create mode 100644 PostFundManagement.Domain/Entities/Risk.cs diff --git a/PostFundManagement.Domain/Configuration/Risk.cs b/PostFundManagement.Domain/Configuration/Risk.cs new file mode 100644 index 0000000..7f04fd1 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Risk.cs @@ -0,0 +1,30 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Risk : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Risk).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.ProjectId).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.Likelihood).IsRequired().HasDefaultValue(RiskLikelihood.Possible); + builder.Property(f => f.Impact).IsRequired().HasDefaultValue(RiskImpact.Negligible); + builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Description).IsRequired().HasMaxLength(1024); + + builder.HasOne(f => f.Project) + .WithMany() + .HasForeignKey(fk => fk.ProjectId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Risk.cs b/PostFundManagement.Domain/Entities/Risk.cs new file mode 100644 index 0000000..edcaaaa --- /dev/null +++ b/PostFundManagement.Domain/Entities/Risk.cs @@ -0,0 +1,9 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Risk : Models.Risk +{ + public virtual Project? Project { get; set; } + + public virtual User? Creator { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index ff4bd37..871c81c 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Risks => Set(); + public DbSet Indicators => Set(); public DbSet Disbursements => Set(); From 1e4a94730aa15e7154eedb4cd6b6b6018d3889be Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 17:21:36 +0200 Subject: [PATCH 13/50] Added evidence entity --- .../Configuration/Evidence.cs | 45 +++++++++++++++++++ .../Entities/Evidence.cs | 15 +++++++ .../Database/ApplicationDbContext.cs | 2 + 3 files changed, 62 insertions(+) create mode 100644 PostFundManagement.Domain/Configuration/Evidence.cs create mode 100644 PostFundManagement.Domain/Entities/Evidence.cs diff --git a/PostFundManagement.Domain/Configuration/Evidence.cs b/PostFundManagement.Domain/Configuration/Evidence.cs new file mode 100644 index 0000000..0ab833f --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Evidence.cs @@ -0,0 +1,45 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Evidence : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Evidence).Pluralize()); + + builder.HasKey(f => f.Id); + builder.Property(f => f.ProjectId).IsRequired(); + builder.Property(f => f.MilestoneId).IsRequired(); + builder.Property(f => f.IndicatorId).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.UpdatedBy).IsRequired(); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.Version).IsRequired().HasDefaultValue(1); + builder.Property(f => f.DocumentUrl).IsRequired(); + builder.Property(f => f.Status).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + + builder.HasOne(f => f.Project) + .WithMany() + .HasForeignKey(fk => fk.ProjectId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Milestone) + .WithMany() + .HasForeignKey(fk => fk.MilestoneId) + .IsRequired(false) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Updator) + .WithMany() + .HasForeignKey(fk => fk.UpdatedBy) + .IsRequired(false) + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Evidence.cs b/PostFundManagement.Domain/Entities/Evidence.cs new file mode 100644 index 0000000..4e54706 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Evidence.cs @@ -0,0 +1,15 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Evidence : Models.Evidence +{ + public virtual Project? Project { get; set; } + + public virtual Milestone? Milestone { get; set; } + + public virtual Indicator? Indicator { get; set; } + + public virtual User? Creator { get; set; } + + public virtual User? Updator { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index 871c81c..04d4df2 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Evidences => Set(); + public DbSet Risks => Set(); public DbSet Indicators => Set(); From ba10780ab3625e1db57dd799a9ba72f678b0b82d Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 17:30:50 +0200 Subject: [PATCH 14/50] Added beneficiary entity --- .../Configuration/Beneficiary.cs | 38 +++++++++++++++++++ .../Entities/Beneficiary.cs | 11 ++++++ .../Database/ApplicationDbContext.cs | 2 + 3 files changed, 51 insertions(+) create mode 100644 PostFundManagement.Domain/Configuration/Beneficiary.cs create mode 100644 PostFundManagement.Domain/Entities/Beneficiary.cs diff --git a/PostFundManagement.Domain/Configuration/Beneficiary.cs b/PostFundManagement.Domain/Configuration/Beneficiary.cs new file mode 100644 index 0000000..f4da593 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Beneficiary.cs @@ -0,0 +1,38 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Beneficiary : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Beneficiary).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.ProjectId).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.UserId).IsRequired(); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Category).IsRequired().HasMaxLength(50); + builder.Property(f => f.TargetCount).IsRequired(); + builder.Property(f => f.ReachedCount).IsRequired(); + builder.Property(f => f.Location).IsRequired(); + + builder.HasOne(f => f.Project) + .WithMany() + .HasForeignKey(fk => fk.ProjectId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.User) + .WithMany() + .HasForeignKey(fk => fk.UserId) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Beneficiary.cs b/PostFundManagement.Domain/Entities/Beneficiary.cs new file mode 100644 index 0000000..fec8473 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Beneficiary.cs @@ -0,0 +1,11 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Beneficiary : Models.Beneficiary +{ + public virtual Project? Project { get; set; } + + public virtual User? Creator { get; set; } + + public virtual User? User { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index 04d4df2..9e6279f 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Beneficiaries => Set(); + public DbSet Evidences => Set(); public DbSet Risks => Set(); From 2e4dbd5f7d84e4725327ac4a06cb953bcf97d240 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 17:38:26 +0200 Subject: [PATCH 15/50] Added outcome entity --- .../Configuration/Outcome.cs | 28 +++++++++++++++++++ PostFundManagement.Domain/Entities/Outcome.cs | 9 ++++++ .../Database/ApplicationDbContext.cs | 2 ++ 3 files changed, 39 insertions(+) create mode 100644 PostFundManagement.Domain/Configuration/Outcome.cs create mode 100644 PostFundManagement.Domain/Entities/Outcome.cs diff --git a/PostFundManagement.Domain/Configuration/Outcome.cs b/PostFundManagement.Domain/Configuration/Outcome.cs new file mode 100644 index 0000000..da9e17d --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Outcome.cs @@ -0,0 +1,28 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Outcome : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Outcome).Pluralize()); + + builder.HasKey(pk => pk.Id); + 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(256); + builder.Property(f => f.Description).IsRequired().HasMaxLength(1024); + + builder.HasOne(f => f.Beneficiary) + .WithMany() + .HasForeignKey(fk => fk.BeneficiaryId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Outcome.cs b/PostFundManagement.Domain/Entities/Outcome.cs new file mode 100644 index 0000000..4b5ce29 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Outcome.cs @@ -0,0 +1,9 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Outcome : Models.Outcome +{ + public virtual Beneficiary? Beneficiary { get; set; } + + public virtual User? Creator { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index 9e6279f..c0223a7 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Outcomes => Set(); + public DbSet Beneficiaries => Set(); public DbSet Evidences => Set(); From 1d5f4ad167916aaf209cbe0c1930c953bf4c0449 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 17:47:17 +0200 Subject: [PATCH 16/50] Added issue entity --- .../Configuration/Issue.cs | 31 +++++++++++++++++++ PostFundManagement.Domain/Entities/Issue.cs | 9 ++++++ .../Database/ApplicationDbContext.cs | 2 ++ 3 files changed, 42 insertions(+) create mode 100644 PostFundManagement.Domain/Configuration/Issue.cs create mode 100644 PostFundManagement.Domain/Entities/Issue.cs diff --git a/PostFundManagement.Domain/Configuration/Issue.cs b/PostFundManagement.Domain/Configuration/Issue.cs new file mode 100644 index 0000000..a60ba31 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Issue.cs @@ -0,0 +1,31 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class Issue : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Issue).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.ProjectId).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.ResolvedAt).IsRequired(false); + builder.Property(f => f.Title).IsRequired().HasMaxLength(256); + builder.Property(f => f.Description).IsRequired().HasMaxLength(1024); + builder.Property(f => f.Priority).IsRequired().HasDefaultValue(Priority.Low); + builder.Property(f => f.Status).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + + builder.HasOne(f => f.Project) + .WithMany() + .HasForeignKey(fk => fk.ProjectId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Issue.cs b/PostFundManagement.Domain/Entities/Issue.cs new file mode 100644 index 0000000..91a63b8 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Issue.cs @@ -0,0 +1,9 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Issue : Models.Issue +{ + public virtual Project? Project { get; set; } + + public virtual User? Creator { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index c0223a7..19791f3 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Issues => Set(); + public DbSet Outcomes => Set(); public DbSet Beneficiaries => Set(); From 8f2276f461dccc99d8e41f11d94c667bfe9fa30e Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 17:57:56 +0200 Subject: [PATCH 17/50] Added complianceitem entity --- .../Configuration/ComplianceItem.cs | 37 +++++++++++++++++++ .../Entities/ComplianceItem.cs | 11 ++++++ .../Models/ComplianceItem.cs | 2 +- .../Database/ApplicationDbContext.cs | 2 + 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 PostFundManagement.Domain/Configuration/ComplianceItem.cs create mode 100644 PostFundManagement.Domain/Entities/ComplianceItem.cs diff --git a/PostFundManagement.Domain/Configuration/ComplianceItem.cs b/PostFundManagement.Domain/Configuration/ComplianceItem.cs new file mode 100644 index 0000000..383a364 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/ComplianceItem.cs @@ -0,0 +1,37 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class ComplianceItem : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.ComplianceItem).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.ProjectId).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.VerifiedBy).IsRequired(false); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.Title).IsRequired().HasMaxLength(256); + builder.Property(f => f.Requirements).IsRequired().HasMaxLength(2048); + builder.Property(f => f.Status).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + + builder.HasOne(f => f.Project) + .WithMany() + .HasForeignKey(fk => fk.ProjectId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Verifier) + .WithMany() + .HasForeignKey(fk => fk.VerifiedBy) + .IsRequired(false) + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/ComplianceItem.cs b/PostFundManagement.Domain/Entities/ComplianceItem.cs new file mode 100644 index 0000000..e3b19a2 --- /dev/null +++ b/PostFundManagement.Domain/Entities/ComplianceItem.cs @@ -0,0 +1,11 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class ComplianceItem : Models.ComplianceItem +{ + public virtual Project? Project { get; set; } + + public virtual User? Creator { get; set; } + + public virtual User? Verifier { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/ComplianceItem.cs b/PostFundManagement.Domain/Models/ComplianceItem.cs index 4eccdb3..f16036b 100644 --- a/PostFundManagement.Domain/Models/ComplianceItem.cs +++ b/PostFundManagement.Domain/Models/ComplianceItem.cs @@ -18,5 +18,5 @@ public class ComplianceItem public DateTime CreatedAt { get; set; } - public DateTime? VerifiedAt { get; set; } + public DateTime? UpdatedAt { get; set; } } \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index 19791f3..f0f526a 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet ComplianceItems => Set(); + public DbSet Issues => Set(); public DbSet Outcomes => Set(); From 3efde47b41a3ef28e85508eaea82d7181d3d8d0d Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 18:26:20 +0200 Subject: [PATCH 18/50] Added changerequest entity --- .../Configuration/ChangeRequest.cs | 39 +++++++++++++++++++ .../Entities/ChangeRequest.cs | 11 ++++++ .../Database/ApplicationDbContext.cs | 2 + 3 files changed, 52 insertions(+) create mode 100644 PostFundManagement.Domain/Configuration/ChangeRequest.cs create mode 100644 PostFundManagement.Domain/Entities/ChangeRequest.cs diff --git a/PostFundManagement.Domain/Configuration/ChangeRequest.cs b/PostFundManagement.Domain/Configuration/ChangeRequest.cs new file mode 100644 index 0000000..02c21fc --- /dev/null +++ b/PostFundManagement.Domain/Configuration/ChangeRequest.cs @@ -0,0 +1,39 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class ChangeRequest : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.ChangeRequest).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.ProjectId).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.ApprovedBy).IsRequired(false); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.Title).IsRequired().HasMaxLength(256); + builder.Property(f => f.Justification).IsRequired().HasMaxLength(1024); + builder.Property(f => f.RevisedBudget).IsRequired(false).HasPrecision(18, 2); + builder.Property(f => f.RevisedEndedAt).IsRequired(false); + builder.Property(f => f.Status).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + + builder.HasOne(f => f.Project) + .WithMany() + .HasForeignKey(fk => fk.ProjectId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Approver) + .WithMany() + .HasForeignKey(fk => fk.ApprovedBy) + .IsRequired(false) + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/ChangeRequest.cs b/PostFundManagement.Domain/Entities/ChangeRequest.cs new file mode 100644 index 0000000..cf4252b --- /dev/null +++ b/PostFundManagement.Domain/Entities/ChangeRequest.cs @@ -0,0 +1,11 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class ChangeRequest : Models.ChangeRequest +{ + public virtual Project? Project { get; set; } + + public virtual User? Creator { get; set; } + + public virtual User? Approver { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index f0f526a..2ef75a6 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet ChangeRequests => Set(); + public DbSet ComplianceItems => Set(); public DbSet Issues => Set(); From 868bb6c582c4a17bdecfc597e7b8b2a87f806735 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 18:35:39 +0200 Subject: [PATCH 19/50] Added sitevisit entity --- .../Configuration/SiteVisit.cs | 30 +++++++++++++++++++ .../Entities/SiteVisit.cs | 9 ++++++ PostFundManagement.Domain/Models/SiteVisit.cs | 2 +- .../Database/ApplicationDbContext.cs | 2 ++ 4 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 PostFundManagement.Domain/Configuration/SiteVisit.cs create mode 100644 PostFundManagement.Domain/Entities/SiteVisit.cs diff --git a/PostFundManagement.Domain/Configuration/SiteVisit.cs b/PostFundManagement.Domain/Configuration/SiteVisit.cs new file mode 100644 index 0000000..578473b --- /dev/null +++ b/PostFundManagement.Domain/Configuration/SiteVisit.cs @@ -0,0 +1,30 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class SiteVisit : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.SiteVisit).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.ProjectId).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.VisitedAt).IsRequired(false); + builder.Property(f => f.InspectorName).IsRequired(); + builder.Property(f => f.Findings).IsRequired(false).HasMaxLength(4096); + builder.Property(f => f.Status).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + + builder.HasOne(f => f.Project) + .WithMany() + .HasForeignKey(fk => fk.ProjectId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/SiteVisit.cs b/PostFundManagement.Domain/Entities/SiteVisit.cs new file mode 100644 index 0000000..0981a35 --- /dev/null +++ b/PostFundManagement.Domain/Entities/SiteVisit.cs @@ -0,0 +1,9 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class SiteVisit : Models.SiteVisit +{ + public virtual Project? Project { get; set; } + + public virtual User? Creator { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/SiteVisit.cs b/PostFundManagement.Domain/Models/SiteVisit.cs index 1f8e0fe..9a94b7a 100644 --- a/PostFundManagement.Domain/Models/SiteVisit.cs +++ b/PostFundManagement.Domain/Models/SiteVisit.cs @@ -12,7 +12,7 @@ public class SiteVisit public string? Findings { get; set; } - public ApprovalStatus VerificationStatus { get; set; } + public ApprovalStatus Status { get; set; } public Guid CreatedBy { get; set; } diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index 2ef75a6..ade1572 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet SiteVisits => Set(); + public DbSet ChangeRequests => Set(); public DbSet ComplianceItems => Set(); From 732426899309e954ff4fd9399edc1dd06c46de70 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 18:52:17 +0200 Subject: [PATCH 20/50] Added auditlog entity --- .../Configuration/AuditLog.cs | 23 +++++++++++++++++++ .../Entities/AuditLog.cs | 7 ++++++ .../Database/ApplicationDbContext.cs | 2 ++ 3 files changed, 32 insertions(+) create mode 100644 PostFundManagement.Domain/Configuration/AuditLog.cs create mode 100644 PostFundManagement.Domain/Entities/AuditLog.cs diff --git a/PostFundManagement.Domain/Configuration/AuditLog.cs b/PostFundManagement.Domain/Configuration/AuditLog.cs new file mode 100644 index 0000000..bc19c51 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/AuditLog.cs @@ -0,0 +1,23 @@ +namespace PostFundManagement.Domain.Configuration; + +public sealed class AuditLog : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.AuditLog).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.EntityName).IsRequired().HasMaxLength(128); + builder.Property(f => f.EntityId).IsRequired(); + builder.Property(f => f.Action).IsRequired().HasMaxLength(64); + builder.Property(f => f.Changes).IsRequired(false).HasColumnType("jsonb"); + builder.Property(f => f.PerformedBy).IsRequired(); + builder.Property(f => f.Timestamp).IsRequired().HasDefaultValueSql("now()"); + + builder.HasOne(f => f.Performer) + .WithMany() + .HasForeignKey(fk => fk.PerformedBy) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/AuditLog.cs b/PostFundManagement.Domain/Entities/AuditLog.cs new file mode 100644 index 0000000..087ffeb --- /dev/null +++ b/PostFundManagement.Domain/Entities/AuditLog.cs @@ -0,0 +1,7 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class AuditLog : Models.AuditLog +{ + public virtual User? Performer { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index ade1572..044a49f 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,8 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet AuditLogs => Set(); + public DbSet SiteVisits => Set(); public DbSet ChangeRequests => Set(); From df7ba866f5adfe7342ee1a92f80763b112829665 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 19:18:17 +0200 Subject: [PATCH 21/50] Added dbcontext factory --- .../Database/ApplicationDbContextFactory.cs | 22 +++++++++++++++++++ .../IInfrastructure.cs | 3 +++ .../PostFundManagement.Infrastructure.csproj | 13 +++++++++++ 3 files changed, 38 insertions(+) create mode 100644 PostFundManagement.Infrastructure/Database/ApplicationDbContextFactory.cs create mode 100644 PostFundManagement.Infrastructure/IInfrastructure.cs diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContextFactory.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContextFactory.cs new file mode 100644 index 0000000..f3f27d7 --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContextFactory.cs @@ -0,0 +1,22 @@ +namespace PostFundManagement.Infrastructure.Database; + +public sealed class ApplicationDbContextFactory : IDesignTimeDbContextFactory +{ + public ApplicationDbContext CreateDbContext(string[] args) + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddUserSecrets(optional: true) + .AddEnvironmentVariables() + .Build(); + + var optionsBuilder = new DbContextOptionsBuilder(); + + var connectionString = configuration.GetConnectionString("PfmDatabase") + ?? throw new InvalidOperationException("Connection string 'PfmDatabase' was not found in configuration."); + + optionsBuilder.UseNpgsql(connectionString, npgsqlOptions => npgsqlOptions.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)); + + return new ApplicationDbContext(optionsBuilder.Options); + } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/IInfrastructure.cs b/PostFundManagement.Infrastructure/IInfrastructure.cs new file mode 100644 index 0000000..6795a51 --- /dev/null +++ b/PostFundManagement.Infrastructure/IInfrastructure.cs @@ -0,0 +1,3 @@ +namespace PostFundManagement.Infrastructure; + +public interface IInfrastructure; \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj index 694b6af..4895080 100644 --- a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj +++ b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj @@ -4,10 +4,23 @@ net10.0 enable enable + c4131f8d-ff78-432e-86d2-f6e131bd4cd3 + + + + + + + + + + + + From d4bb0cf7a809f75bd09dcc42a59d3133a4081603 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 15 Aug 2026 20:30:51 +0200 Subject: [PATCH 22/50] Migrated database changes --- .../Configuration/Award.cs | 2 +- .../Configuration/ChangeRequest.cs | 2 +- .../Configuration/ComplianceItem.cs | 2 +- .../Configuration/Contract.cs | 2 +- .../Configuration/Disbursement.cs | 2 +- .../Configuration/Evidence.cs | 2 +- .../Configuration/Indicator.cs | 2 +- .../Configuration/Instrument.cs | 2 +- .../Configuration/Issue.cs | 4 +- .../Configuration/Milestone.cs | 2 +- .../Configuration/Organisation.cs | 4 +- .../Configuration/Portfolio.cs | 4 +- .../Configuration/Programme.cs | 6 +- .../Configuration/Project.cs | 2 +- .../Configuration/Risk.cs | 4 +- .../Configuration/Rule.cs | 2 +- .../Configuration/SiteVisit.cs | 4 +- .../Configuration/User.cs | 2 +- PostFundManagement.Domain/Models/Award.cs | 2 +- PostFundManagement.Domain/Models/User.cs | 2 +- .../20260815175753_Init.Designer.cs | 1624 +++++++++++++++++ .../Migrations/20260815175753_Init.cs | 1108 +++++++++++ .../ApplicationDbContextModelSnapshot.cs | 1621 ++++++++++++++++ .../PostFundManagement.Infrastructure.csproj | 15 +- 24 files changed, 4394 insertions(+), 28 deletions(-) create mode 100644 PostFundManagement.Infrastructure/Database/Migrations/20260815175753_Init.Designer.cs create mode 100644 PostFundManagement.Infrastructure/Database/Migrations/20260815175753_Init.cs create mode 100644 PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs diff --git a/PostFundManagement.Domain/Configuration/Award.cs b/PostFundManagement.Domain/Configuration/Award.cs index de9ff68..1c63692 100644 --- a/PostFundManagement.Domain/Configuration/Award.cs +++ b/PostFundManagement.Domain/Configuration/Award.cs @@ -15,7 +15,7 @@ public class Award : IEntityTypeConfiguration builder.Property(f => f.ApprovedBy).IsRequired(false); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.UpdatedAt).IsRequired(false); - builder.Property(f => f.Status).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); builder.HasOne(f => f.Organisation) diff --git a/PostFundManagement.Domain/Configuration/ChangeRequest.cs b/PostFundManagement.Domain/Configuration/ChangeRequest.cs index 02c21fc..84e6c14 100644 --- a/PostFundManagement.Domain/Configuration/ChangeRequest.cs +++ b/PostFundManagement.Domain/Configuration/ChangeRequest.cs @@ -16,7 +16,7 @@ public sealed class ChangeRequest : IEntityTypeConfiguration f.Justification).IsRequired().HasMaxLength(1024); builder.Property(f => f.RevisedBudget).IsRequired(false).HasPrecision(18, 2); builder.Property(f => f.RevisedEndedAt).IsRequired(false); - builder.Property(f => f.Status).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Project) .WithMany() diff --git a/PostFundManagement.Domain/Configuration/ComplianceItem.cs b/PostFundManagement.Domain/Configuration/ComplianceItem.cs index 383a364..4fec8da 100644 --- a/PostFundManagement.Domain/Configuration/ComplianceItem.cs +++ b/PostFundManagement.Domain/Configuration/ComplianceItem.cs @@ -14,7 +14,7 @@ public sealed class ComplianceItem : IEntityTypeConfiguration f.UpdatedAt).IsRequired(false); builder.Property(f => f.Title).IsRequired().HasMaxLength(256); builder.Property(f => f.Requirements).IsRequired().HasMaxLength(2048); - builder.Property(f => f.Status).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Project) .WithMany() diff --git a/PostFundManagement.Domain/Configuration/Contract.cs b/PostFundManagement.Domain/Configuration/Contract.cs index c29ce9f..7b359b4 100644 --- a/PostFundManagement.Domain/Configuration/Contract.cs +++ b/PostFundManagement.Domain/Configuration/Contract.cs @@ -17,7 +17,7 @@ public sealed class Contract : IEntityTypeConfiguration builder.Property(f => f.EffectiveAt).IsRequired(false); builder.Property(f => f.ExpiresAt).IsRequired(false); builder.Property(f => f.TotalValue).IsRequired().HasPrecision(18, 2); - builder.Property(f => f.Status).IsRequired().HasDefaultValue(ContractStatus.Draft); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Award) .WithOne(f => f.Contract) diff --git a/PostFundManagement.Domain/Configuration/Disbursement.cs b/PostFundManagement.Domain/Configuration/Disbursement.cs index 3733dbc..4e58f8e 100644 --- a/PostFundManagement.Domain/Configuration/Disbursement.cs +++ b/PostFundManagement.Domain/Configuration/Disbursement.cs @@ -14,7 +14,7 @@ public sealed class Disbursement : IEntityTypeConfiguration f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); - builder.Property(f => f.Status).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Project) .WithMany() diff --git a/PostFundManagement.Domain/Configuration/Evidence.cs b/PostFundManagement.Domain/Configuration/Evidence.cs index 0ab833f..a2aad3a 100644 --- a/PostFundManagement.Domain/Configuration/Evidence.cs +++ b/PostFundManagement.Domain/Configuration/Evidence.cs @@ -16,7 +16,7 @@ public sealed class Evidence : IEntityTypeConfiguration builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.Version).IsRequired().HasDefaultValue(1); builder.Property(f => f.DocumentUrl).IsRequired(); - builder.Property(f => f.Status).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Project) .WithMany() diff --git a/PostFundManagement.Domain/Configuration/Indicator.cs b/PostFundManagement.Domain/Configuration/Indicator.cs index cc4a367..140bc75 100644 --- a/PostFundManagement.Domain/Configuration/Indicator.cs +++ b/PostFundManagement.Domain/Configuration/Indicator.cs @@ -11,7 +11,7 @@ public sealed class Indicator : IEntityTypeConfiguration builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.Name).IsRequired().HasMaxLength(256); - builder.Property(f => f.UnitOfMeasure).IsRequired().HasDefaultValue(UnitOfMeasure.Percentage); + builder.Property(f => f.UnitOfMeasure).IsRequired().HasConversion().HasDefaultValueSql("2"); builder.Property(f => f.BaselineAmount).IsRequired().HasPrecision(18, 2); builder.Property(f => f.TargetAmount).IsRequired().HasPrecision(18, 2); builder.Property(f => f.ActualAmount).IsRequired().HasPrecision(18, 2); diff --git a/PostFundManagement.Domain/Configuration/Instrument.cs b/PostFundManagement.Domain/Configuration/Instrument.cs index 27c38bd..911dc8b 100644 --- a/PostFundManagement.Domain/Configuration/Instrument.cs +++ b/PostFundManagement.Domain/Configuration/Instrument.cs @@ -10,7 +10,7 @@ public sealed class Instrument : IEntityTypeConfiguration builder.Property(f => f.Code).IsRequired().HasMaxLength(50); builder.Property(f => f.Name).IsRequired().HasMaxLength(256); builder.Property(f => f.Description).IsRequired(false).HasMaxLength(1024); - builder.Property(f => f.Status).IsRequired().HasDefaultValue(ActivityStatus.Inactive); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("2"); builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); diff --git a/PostFundManagement.Domain/Configuration/Issue.cs b/PostFundManagement.Domain/Configuration/Issue.cs index a60ba31..2a61024 100644 --- a/PostFundManagement.Domain/Configuration/Issue.cs +++ b/PostFundManagement.Domain/Configuration/Issue.cs @@ -13,8 +13,8 @@ public sealed class Issue : IEntityTypeConfiguration builder.Property(f => f.ResolvedAt).IsRequired(false); builder.Property(f => f.Title).IsRequired().HasMaxLength(256); builder.Property(f => f.Description).IsRequired().HasMaxLength(1024); - builder.Property(f => f.Priority).IsRequired().HasDefaultValue(Priority.Low); - builder.Property(f => f.Status).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + builder.Property(f => f.Priority).IsRequired().HasConversion().HasDefaultValueSql("1"); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Project) .WithMany() diff --git a/PostFundManagement.Domain/Configuration/Milestone.cs b/PostFundManagement.Domain/Configuration/Milestone.cs index 3ce043e..4f19795 100644 --- a/PostFundManagement.Domain/Configuration/Milestone.cs +++ b/PostFundManagement.Domain/Configuration/Milestone.cs @@ -14,7 +14,7 @@ public sealed class Milestone : IEntityTypeConfiguration builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.DueAt).IsRequired(); builder.Property(f => f.Name).IsRequired().HasMaxLength(256); - builder.Property(f => f.Status).HasDefaultValue(ApprovalStatus.Pending); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Project) .WithMany() diff --git a/PostFundManagement.Domain/Configuration/Organisation.cs b/PostFundManagement.Domain/Configuration/Organisation.cs index cc7c668..7d57cb7 100644 --- a/PostFundManagement.Domain/Configuration/Organisation.cs +++ b/PostFundManagement.Domain/Configuration/Organisation.cs @@ -12,8 +12,8 @@ public sealed class Organisation : IEntityTypeConfiguration f.RegistrationNo).IsRequired().HasMaxLength(256); builder.Property(f => f.Name).IsRequired().HasMaxLength(256); builder.Property(f => f.Email).IsRequired().HasMaxLength(256); - builder.Property(f => f.Type).IsRequired().HasDefaultValue(OrganisationType.PrivateLimitedCompany); - builder.Property(f => f.Status).IsRequired().HasDefaultValue(ActivityStatus.Active); + builder.Property(f => f.Type).IsRequired().HasConversion().HasDefaultValueSql("5"); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Creator) .WithMany() diff --git a/PostFundManagement.Domain/Configuration/Portfolio.cs b/PostFundManagement.Domain/Configuration/Portfolio.cs index 35f1e19..f6f6a19 100644 --- a/PostFundManagement.Domain/Configuration/Portfolio.cs +++ b/PostFundManagement.Domain/Configuration/Portfolio.cs @@ -8,7 +8,7 @@ public sealed class Portfolio : IEntityTypeConfiguration builder.HasKey(pk => pk.Id); builder.Property(f => f.CreatedBy).IsRequired(); - builder.Property(f => f.OwnedBy).IsRequired(false); + builder.Property(f => f.OwnedBy).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.Name).IsRequired().HasMaxLength(256); builder.Property(f => f.Description).IsRequired(false).HasMaxLength(1024); @@ -22,7 +22,7 @@ public sealed class Portfolio : IEntityTypeConfiguration builder.HasOne(f => f.Owner) .WithMany() .HasForeignKey(fk => fk.OwnedBy) - .IsRequired(false) + .IsRequired() .OnDelete(DeleteBehavior.NoAction); } } \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/Programme.cs b/PostFundManagement.Domain/Configuration/Programme.cs index 6555564..70049ff 100644 --- a/PostFundManagement.Domain/Configuration/Programme.cs +++ b/PostFundManagement.Domain/Configuration/Programme.cs @@ -9,13 +9,13 @@ public sealed class Programme : IEntityTypeConfiguration builder.HasKey(pk => pk.Id); builder.Property(f => f.PortfolioId).IsRequired(); builder.Property(f => f.InstrumentId).IsRequired(); - builder.Property(f => f.OwnedBy).IsRequired(false); + builder.Property(f => f.OwnedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.UpdatedBy).IsRequired(false); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.Name).IsRequired().HasMaxLength(256); - builder.Property(f => f.Status).IsRequired(false).HasDefaultValue(ApprovalStatus.Pending); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Portfolio) .WithMany(f => f.Programmes) @@ -37,7 +37,7 @@ public sealed class Programme : IEntityTypeConfiguration builder.HasOne(f => f.Owner) .WithMany() - .IsRequired(false) + .IsRequired() .HasForeignKey(fk => fk.OwnedBy) .OnDelete(DeleteBehavior.NoAction); } diff --git a/PostFundManagement.Domain/Configuration/Project.cs b/PostFundManagement.Domain/Configuration/Project.cs index d0e7254..11aed2e 100644 --- a/PostFundManagement.Domain/Configuration/Project.cs +++ b/PostFundManagement.Domain/Configuration/Project.cs @@ -16,7 +16,7 @@ public sealed class Project : IEntityTypeConfiguration builder.Property(f => f.EndedAt).IsRequired(false); builder.Property(f => f.Name).IsRequired().HasMaxLength(256); builder.Property(f => f.Description).IsRequired().HasMaxLength(1024); - builder.Property(f => f.StartedAt).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Programme) .WithMany(f => f.Projects) diff --git a/PostFundManagement.Domain/Configuration/Risk.cs b/PostFundManagement.Domain/Configuration/Risk.cs index 7f04fd1..6d82e19 100644 --- a/PostFundManagement.Domain/Configuration/Risk.cs +++ b/PostFundManagement.Domain/Configuration/Risk.cs @@ -10,8 +10,8 @@ public sealed class Risk : IEntityTypeConfiguration builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); - builder.Property(f => f.Likelihood).IsRequired().HasDefaultValue(RiskLikelihood.Possible); - builder.Property(f => f.Impact).IsRequired().HasDefaultValue(RiskImpact.Negligible); + builder.Property(f => f.Likelihood).IsRequired().HasConversion().HasDefaultValueSql("3"); + builder.Property(f => f.Impact).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.Property(f => f.Name).IsRequired().HasMaxLength(256); builder.Property(f => f.Description).IsRequired().HasMaxLength(1024); diff --git a/PostFundManagement.Domain/Configuration/Rule.cs b/PostFundManagement.Domain/Configuration/Rule.cs index 129a3f7..ad11bb5 100644 --- a/PostFundManagement.Domain/Configuration/Rule.cs +++ b/PostFundManagement.Domain/Configuration/Rule.cs @@ -14,7 +14,7 @@ public sealed class Rule : IEntityTypeConfiguration builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.Name).IsRequired().HasMaxLength(256); builder.Property(f => f.Version).IsRequired().HasDefaultValue(1); - builder.Property(f => f.Status).IsRequired().HasDefaultValue(ActivityStatus.Inactive); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("2"); builder.HasOne(f => f.Programme) .WithMany(f => f.Rules) diff --git a/PostFundManagement.Domain/Configuration/SiteVisit.cs b/PostFundManagement.Domain/Configuration/SiteVisit.cs index 578473b..88ed258 100644 --- a/PostFundManagement.Domain/Configuration/SiteVisit.cs +++ b/PostFundManagement.Domain/Configuration/SiteVisit.cs @@ -10,10 +10,10 @@ public sealed class SiteVisit : IEntityTypeConfiguration builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); - builder.Property(f => f.VisitedAt).IsRequired(false); + builder.Property(f => f.VisitedAt).IsRequired(); builder.Property(f => f.InspectorName).IsRequired(); builder.Property(f => f.Findings).IsRequired(false).HasMaxLength(4096); - builder.Property(f => f.Status).IsRequired().HasDefaultValue(ApprovalStatus.Pending); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Project) .WithMany() diff --git a/PostFundManagement.Domain/Configuration/User.cs b/PostFundManagement.Domain/Configuration/User.cs index 2a67850..89c1a7e 100644 --- a/PostFundManagement.Domain/Configuration/User.cs +++ b/PostFundManagement.Domain/Configuration/User.cs @@ -8,7 +8,7 @@ public sealed class User : IEntityTypeConfiguration builder.HasKey(pk => pk.Id); builder.Property(f => f.Email).IsRequired().HasMaxLength(256); - builder.Property(f => f.Status).IsRequired().HasDefaultValue(ActivityStatus.Inactive); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("2"); builder.Property(f => f.LastLoginAt).IsRequired(false); } } \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Award.cs b/PostFundManagement.Domain/Models/Award.cs index 1a97f93..cbb27e0 100644 --- a/PostFundManagement.Domain/Models/Award.cs +++ b/PostFundManagement.Domain/Models/Award.cs @@ -18,7 +18,7 @@ public class Award public DateTime CreatedAt { get; set; } - public DateTime UpdatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } public ApprovalStatus Status { get; set; } diff --git a/PostFundManagement.Domain/Models/User.cs b/PostFundManagement.Domain/Models/User.cs index 15cb816..32aa308 100644 --- a/PostFundManagement.Domain/Models/User.cs +++ b/PostFundManagement.Domain/Models/User.cs @@ -4,7 +4,7 @@ public class User { public Guid Id { get; set; } - public DateTime LastLoginAt { get; set; } + public DateTime? LastLoginAt { get; set; } public string? Email { get; set; } diff --git a/PostFundManagement.Infrastructure/Database/Migrations/20260815175753_Init.Designer.cs b/PostFundManagement.Infrastructure/Database/Migrations/20260815175753_Init.Designer.cs new file mode 100644 index 0000000..9d913e8 --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Migrations/20260815175753_Init.Designer.cs @@ -0,0 +1,1624 @@ +ο»Ώ// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PostFundManagement.Infrastructure.Database; + +#nullable disable + +namespace PostFundManagement.Infrastructure.Database.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260815175753_Init")] + partial class Init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.PrimitiveCollection("Changes") + .HasColumnType("jsonb"); + + b.Property("EntityId") + .HasColumnType("bigint"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("PerformedBy") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("PerformedBy"); + + b.ToTable("AuditLogs", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("OrganisationId"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Awards", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Beneficiary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Location") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("ReachedCount") + .HasColumnType("integer"); + + b.Property("TargetCount") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UserId"); + + b.ToTable("Beneficiaries", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Budget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Budgets", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ChangeRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Justification") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("RevisedBudget") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("RevisedEndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("ChangeRequests", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ComplianceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Requirements") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerifiedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("VerifiedBy"); + + b.ToTable("ComplianceItems", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Contract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AwardId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("EffectiveAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalSignatureId") + .HasColumnType("text"); + + b.Property("SignedDocumentUrl") + .HasColumnType("text"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TotalValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("AwardId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.ToTable("Contracts", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Disbursement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("MilestoneId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("MilestoneId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Disbursements", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Evidence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("IndicatorId") + .HasColumnType("bigint"); + + b.Property("MilestoneId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("MilestoneId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Evidences", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Indicator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActualAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BaselineAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("TargetAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UnitOfMeasure") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Indicators", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Instrument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Instruments", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Issue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Issues", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Milestone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DueAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Milestones", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Organisation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RegistrationNo") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("5"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Organisations", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Outcome", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BeneficiaryId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("BeneficiaryId"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Outcomes", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("OwnedBy"); + + b.ToTable("Portfolios", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnedBy") + .HasColumnType("uuid"); + + b.Property("PortfolioId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("InstrumentId"); + + b.HasIndex("OwnedBy"); + + b.HasIndex("PortfolioId"); + + b.ToTable("Programmes", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Risk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Impact") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Likelihood") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("3"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Risks", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Rule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Rules", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.SiteVisit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Findings") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("InspectorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("VisitedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("SiteVisits", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.HasKey("Id"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.AuditLog", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Performer") + .WithMany() + .HasForeignKey("PerformedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Performer"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Awards") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany("Awards") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Organisation"); + + b.Navigation("Programme"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Beneficiary", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Budget", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ChangeRequest", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ComplianceItem", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Verifier") + .WithMany() + .HasForeignKey("VerifiedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Verifier"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Contract", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Award", "Award") + .WithOne("Contract") + .HasForeignKey("PostFundManagement.Domain.Entities.Contract", "AwardId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Award"); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Disbursement", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Milestone", "Milestone") + .WithMany() + .HasForeignKey("MilestoneId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Milestone"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Evidence", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Indicator", "Indicator") + .WithMany() + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Milestone", "Milestone") + .WithMany() + .HasForeignKey("MilestoneId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Indicator"); + + b.Navigation("Milestone"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Indicator", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Instrument", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Issue", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Milestone", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Organisation", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Outcome", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Beneficiary", "Beneficiary") + .WithMany() + .HasForeignKey("BeneficiaryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Beneficiary"); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Instrument", "Instrument") + .WithMany("Programmes") + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Portfolio", "Portfolio") + .WithMany("Programmes") + .HasForeignKey("PortfolioId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Instrument"); + + b.Navigation("Owner"); + + b.Navigation("Portfolio"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Projects") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Programme"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Risk", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Rule", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Rules") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Programme"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.SiteVisit", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.Navigation("Contract"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Instrument", b => + { + b.Navigation("Programmes"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.Navigation("Programmes"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.Navigation("Awards"); + + b.Navigation("Projects"); + + b.Navigation("Rules"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.Navigation("Awards"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PostFundManagement.Infrastructure/Database/Migrations/20260815175753_Init.cs b/PostFundManagement.Infrastructure/Database/Migrations/20260815175753_Init.cs new file mode 100644 index 0000000..48c046b --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Migrations/20260815175753_Init.cs @@ -0,0 +1,1108 @@ +ο»Ώusing System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace PostFundManagement.Infrastructure.Database.Migrations +{ + /// + public partial class Init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + LastLoginAt = table.Column(type: "timestamp with time zone", nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "2") + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AuditLogs", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + EntityName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + EntityId = table.Column(type: "bigint", nullable: false), + Action = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Changes = table.Column(type: "jsonb", nullable: true), + PerformedBy = table.Column(type: "uuid", nullable: false), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("PK_AuditLogs", x => x.Id); + table.ForeignKey( + name: "FK_AuditLogs_Users_PerformedBy", + column: x => x.PerformedBy, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Instruments", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Code = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Description = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "2"), + CreatedBy = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("PK_Instruments", x => x.Id); + table.ForeignKey( + name: "FK_Instruments_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Organisations", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedBy = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + RegistrationNo = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Type = table.Column(type: "integer", nullable: false, defaultValueSql: "5"), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "1") + }, + constraints: table => + { + table.PrimaryKey("PK_Organisations", x => x.Id); + table.ForeignKey( + name: "FK_Organisations_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Portfolios", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + OwnedBy = table.Column(type: "uuid", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Description = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Portfolios", x => x.Id); + table.ForeignKey( + name: "FK_Portfolios_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Portfolios_Users_OwnedBy", + column: x => x.OwnedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Programmes", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PortfolioId = table.Column(type: "bigint", nullable: false), + InstrumentId = table.Column(type: "bigint", nullable: false), + OwnedBy = table.Column(type: "uuid", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "1") + }, + constraints: table => + { + table.PrimaryKey("PK_Programmes", x => x.Id); + table.ForeignKey( + name: "FK_Programmes_Instruments_InstrumentId", + column: x => x.InstrumentId, + principalTable: "Instruments", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Programmes_Portfolios_PortfolioId", + column: x => x.PortfolioId, + principalTable: "Portfolios", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Programmes_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Programmes_Users_OwnedBy", + column: x => x.OwnedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Projects", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProgrammeId = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + StartedAt = table.Column(type: "timestamp with time zone", nullable: true), + EndedAt = table.Column(type: "timestamp with time zone", nullable: true), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Description = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "1") + }, + constraints: table => + { + table.PrimaryKey("PK_Projects", x => x.Id); + table.ForeignKey( + name: "FK_Projects_Programmes_ProgrammeId", + column: x => x.ProgrammeId, + principalTable: "Programmes", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Projects_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Projects_Users_UpdatedBy", + column: x => x.UpdatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Rules", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProgrammeId = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Version = table.Column(type: "integer", nullable: false, defaultValue: 1), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "2") + }, + constraints: table => + { + table.PrimaryKey("PK_Rules", x => x.Id); + table.ForeignKey( + name: "FK_Rules_Programmes_ProgrammeId", + column: x => x.ProgrammeId, + principalTable: "Programmes", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Rules_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Rules_Users_UpdatedBy", + column: x => x.UpdatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Awards", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + OrganisationId = table.Column(type: "bigint", nullable: false), + ProgrammeId = table.Column(type: "bigint", nullable: false), + ProjectId = table.Column(type: "bigint", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: true), + ApprovedBy = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "1"), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Awards", x => x.Id); + table.ForeignKey( + name: "FK_Awards_Organisations_OrganisationId", + column: x => x.OrganisationId, + principalTable: "Organisations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Awards_Programmes_ProgrammeId", + column: x => x.ProgrammeId, + principalTable: "Programmes", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Awards_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Awards_Users_ApprovedBy", + column: x => x.ApprovedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Awards_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Awards_Users_UpdatedBy", + column: x => x.UpdatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Beneficiaries", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectId = table.Column(type: "bigint", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Category = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + TargetCount = table.Column(type: "integer", nullable: false), + ReachedCount = table.Column(type: "integer", nullable: false), + Location = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Beneficiaries", x => x.Id); + table.ForeignKey( + name: "FK_Beneficiaries_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Beneficiaries_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Beneficiaries_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Budgets", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectId = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + ApprovedBy = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Budgets", x => x.Id); + table.ForeignKey( + name: "FK_Budgets_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Budgets_Users_ApprovedBy", + column: x => x.ApprovedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Budgets_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ChangeRequests", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectId = table.Column(type: "bigint", nullable: false), + Title = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Justification = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + RevisedBudget = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), + RevisedEndedAt = table.Column(type: "timestamp with time zone", nullable: true), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "1"), + CreatedBy = table.Column(type: "uuid", nullable: false), + ApprovedBy = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ChangeRequests", x => x.Id); + table.ForeignKey( + name: "FK_ChangeRequests_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ChangeRequests_Users_ApprovedBy", + column: x => x.ApprovedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_ChangeRequests_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ComplianceItems", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectId = table.Column(type: "bigint", nullable: false), + Title = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Requirements = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "1"), + CreatedBy = table.Column(type: "uuid", nullable: false), + VerifiedBy = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ComplianceItems", x => x.Id); + table.ForeignKey( + name: "FK_ComplianceItems_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ComplianceItems_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_ComplianceItems_Users_VerifiedBy", + column: x => x.VerifiedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Indicators", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectId = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + UnitOfMeasure = table.Column(type: "integer", nullable: false, defaultValueSql: "2"), + BaselineAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + TargetAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + ActualAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Indicators", x => x.Id); + table.ForeignKey( + name: "FK_Indicators_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Indicators_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Issues", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectId = table.Column(type: "bigint", nullable: false), + Title = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Description = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + Priority = table.Column(type: "integer", nullable: false, defaultValueSql: "1"), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "1"), + CreatedBy = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + ResolvedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Issues", x => x.Id); + table.ForeignKey( + name: "FK_Issues_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Issues_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Milestones", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectId = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + DueAt = table.Column(type: "timestamp with time zone", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "1") + }, + constraints: table => + { + table.PrimaryKey("PK_Milestones", x => x.Id); + table.ForeignKey( + name: "FK_Milestones_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Milestones_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Milestones_Users_UpdatedBy", + column: x => x.UpdatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Risks", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectId = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Likelihood = table.Column(type: "integer", nullable: false, defaultValueSql: "3"), + Impact = table.Column(type: "integer", nullable: false, defaultValueSql: "1"), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Description = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Risks", x => x.Id); + table.ForeignKey( + name: "FK_Risks_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Risks_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "SiteVisits", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectId = table.Column(type: "bigint", nullable: false), + VisitedAt = table.Column(type: "timestamp with time zone", nullable: false), + InspectorName = table.Column(type: "text", nullable: false), + Findings = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "1"), + CreatedBy = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("PK_SiteVisits", x => x.Id); + table.ForeignKey( + name: "FK_SiteVisits_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SiteVisits_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Contracts", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AwardId = table.Column(type: "bigint", nullable: false), + ExternalSignatureId = table.Column(type: "text", nullable: true), + SignedDocumentUrl = table.Column(type: "text", nullable: true), + EffectiveAt = table.Column(type: "timestamp with time zone", nullable: true), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true), + TotalValue = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "1"), + CreatedBy = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("PK_Contracts", x => x.Id); + table.ForeignKey( + name: "FK_Contracts_Awards_AwardId", + column: x => x.AwardId, + principalTable: "Awards", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Contracts_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Outcomes", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + BeneficiaryId = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Description = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Outcomes", x => x.Id); + table.ForeignKey( + name: "FK_Outcomes_Beneficiaries_BeneficiaryId", + column: x => x.BeneficiaryId, + principalTable: "Beneficiaries", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Outcomes_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Disbursements", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectId = table.Column(type: "bigint", nullable: false), + MilestoneId = table.Column(type: "bigint", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "1") + }, + constraints: table => + { + table.PrimaryKey("PK_Disbursements", x => x.Id); + table.ForeignKey( + name: "FK_Disbursements_Milestones_MilestoneId", + column: x => x.MilestoneId, + principalTable: "Milestones", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Disbursements_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Disbursements_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Disbursements_Users_UpdatedBy", + column: x => x.UpdatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Evidences", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectId = table.Column(type: "bigint", nullable: false), + MilestoneId = table.Column(type: "bigint", nullable: false), + IndicatorId = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Version = table.Column(type: "integer", nullable: false, defaultValue: 1), + DocumentUrl = table.Column(type: "text", nullable: false), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "1") + }, + constraints: table => + { + table.PrimaryKey("PK_Evidences", x => x.Id); + table.ForeignKey( + name: "FK_Evidences_Indicators_IndicatorId", + column: x => x.IndicatorId, + principalTable: "Indicators", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Evidences_Milestones_MilestoneId", + column: x => x.MilestoneId, + principalTable: "Milestones", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Evidences_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Evidences_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Evidences_Users_UpdatedBy", + column: x => x.UpdatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_AuditLogs_PerformedBy", + table: "AuditLogs", + column: "PerformedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Awards_ApprovedBy", + table: "Awards", + column: "ApprovedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Awards_CreatedBy", + table: "Awards", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Awards_OrganisationId", + table: "Awards", + column: "OrganisationId"); + + migrationBuilder.CreateIndex( + name: "IX_Awards_ProgrammeId", + table: "Awards", + column: "ProgrammeId"); + + migrationBuilder.CreateIndex( + name: "IX_Awards_ProjectId", + table: "Awards", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_Awards_UpdatedBy", + table: "Awards", + column: "UpdatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Beneficiaries_CreatedBy", + table: "Beneficiaries", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Beneficiaries_ProjectId", + table: "Beneficiaries", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_Beneficiaries_UserId", + table: "Beneficiaries", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Budgets_ApprovedBy", + table: "Budgets", + column: "ApprovedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Budgets_CreatedBy", + table: "Budgets", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Budgets_ProjectId", + table: "Budgets", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_ChangeRequests_ApprovedBy", + table: "ChangeRequests", + column: "ApprovedBy"); + + migrationBuilder.CreateIndex( + name: "IX_ChangeRequests_CreatedBy", + table: "ChangeRequests", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_ChangeRequests_ProjectId", + table: "ChangeRequests", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_ComplianceItems_CreatedBy", + table: "ComplianceItems", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_ComplianceItems_ProjectId", + table: "ComplianceItems", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_ComplianceItems_VerifiedBy", + table: "ComplianceItems", + column: "VerifiedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Contracts_AwardId", + table: "Contracts", + column: "AwardId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Contracts_CreatedBy", + table: "Contracts", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Disbursements_CreatedBy", + table: "Disbursements", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Disbursements_MilestoneId", + table: "Disbursements", + column: "MilestoneId"); + + migrationBuilder.CreateIndex( + name: "IX_Disbursements_ProjectId", + table: "Disbursements", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_Disbursements_UpdatedBy", + table: "Disbursements", + column: "UpdatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Evidences_CreatedBy", + table: "Evidences", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Evidences_IndicatorId", + table: "Evidences", + column: "IndicatorId"); + + migrationBuilder.CreateIndex( + name: "IX_Evidences_MilestoneId", + table: "Evidences", + column: "MilestoneId"); + + migrationBuilder.CreateIndex( + name: "IX_Evidences_ProjectId", + table: "Evidences", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_Evidences_UpdatedBy", + table: "Evidences", + column: "UpdatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Indicators_CreatedBy", + table: "Indicators", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Indicators_ProjectId", + table: "Indicators", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_Instruments_CreatedBy", + table: "Instruments", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Issues_CreatedBy", + table: "Issues", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Issues_ProjectId", + table: "Issues", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_Milestones_CreatedBy", + table: "Milestones", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Milestones_ProjectId", + table: "Milestones", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_Milestones_UpdatedBy", + table: "Milestones", + column: "UpdatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Organisations_CreatedBy", + table: "Organisations", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Outcomes_BeneficiaryId", + table: "Outcomes", + column: "BeneficiaryId"); + + migrationBuilder.CreateIndex( + name: "IX_Outcomes_CreatedBy", + table: "Outcomes", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Portfolios_CreatedBy", + table: "Portfolios", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Portfolios_OwnedBy", + table: "Portfolios", + column: "OwnedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Programmes_CreatedBy", + table: "Programmes", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Programmes_InstrumentId", + table: "Programmes", + column: "InstrumentId"); + + migrationBuilder.CreateIndex( + name: "IX_Programmes_OwnedBy", + table: "Programmes", + column: "OwnedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Programmes_PortfolioId", + table: "Programmes", + column: "PortfolioId"); + + migrationBuilder.CreateIndex( + name: "IX_Projects_CreatedBy", + table: "Projects", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Projects_ProgrammeId", + table: "Projects", + column: "ProgrammeId"); + + migrationBuilder.CreateIndex( + name: "IX_Projects_UpdatedBy", + table: "Projects", + column: "UpdatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Risks_CreatedBy", + table: "Risks", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Risks_ProjectId", + table: "Risks", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_Rules_CreatedBy", + table: "Rules", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Rules_ProgrammeId", + table: "Rules", + column: "ProgrammeId"); + + migrationBuilder.CreateIndex( + name: "IX_Rules_UpdatedBy", + table: "Rules", + column: "UpdatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_SiteVisits_CreatedBy", + table: "SiteVisits", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_SiteVisits_ProjectId", + table: "SiteVisits", + column: "ProjectId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AuditLogs"); + + migrationBuilder.DropTable( + name: "Budgets"); + + migrationBuilder.DropTable( + name: "ChangeRequests"); + + migrationBuilder.DropTable( + name: "ComplianceItems"); + + migrationBuilder.DropTable( + name: "Contracts"); + + migrationBuilder.DropTable( + name: "Disbursements"); + + migrationBuilder.DropTable( + name: "Evidences"); + + migrationBuilder.DropTable( + name: "Issues"); + + migrationBuilder.DropTable( + name: "Outcomes"); + + migrationBuilder.DropTable( + name: "Risks"); + + migrationBuilder.DropTable( + name: "Rules"); + + migrationBuilder.DropTable( + name: "SiteVisits"); + + migrationBuilder.DropTable( + name: "Awards"); + + migrationBuilder.DropTable( + name: "Indicators"); + + migrationBuilder.DropTable( + name: "Milestones"); + + migrationBuilder.DropTable( + name: "Beneficiaries"); + + migrationBuilder.DropTable( + name: "Organisations"); + + migrationBuilder.DropTable( + name: "Projects"); + + migrationBuilder.DropTable( + name: "Programmes"); + + migrationBuilder.DropTable( + name: "Instruments"); + + migrationBuilder.DropTable( + name: "Portfolios"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs b/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..03bd61f --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,1621 @@ +ο»Ώ// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PostFundManagement.Infrastructure.Database; + +#nullable disable + +namespace PostFundManagement.Infrastructure.Database.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.PrimitiveCollection("Changes") + .HasColumnType("jsonb"); + + b.Property("EntityId") + .HasColumnType("bigint"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("PerformedBy") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("PerformedBy"); + + b.ToTable("AuditLogs", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("OrganisationId"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Awards", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Beneficiary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Location") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("ReachedCount") + .HasColumnType("integer"); + + b.Property("TargetCount") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UserId"); + + b.ToTable("Beneficiaries", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Budget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Budgets", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ChangeRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Justification") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("RevisedBudget") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("RevisedEndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("ChangeRequests", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ComplianceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Requirements") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerifiedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("VerifiedBy"); + + b.ToTable("ComplianceItems", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Contract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AwardId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("EffectiveAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalSignatureId") + .HasColumnType("text"); + + b.Property("SignedDocumentUrl") + .HasColumnType("text"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TotalValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("AwardId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.ToTable("Contracts", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Disbursement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("MilestoneId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("MilestoneId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Disbursements", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Evidence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("IndicatorId") + .HasColumnType("bigint"); + + b.Property("MilestoneId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("MilestoneId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Evidences", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Indicator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActualAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BaselineAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("TargetAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UnitOfMeasure") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Indicators", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Instrument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Instruments", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Issue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Issues", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Milestone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DueAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Milestones", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Organisation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RegistrationNo") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("5"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Organisations", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Outcome", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BeneficiaryId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("BeneficiaryId"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Outcomes", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("OwnedBy"); + + b.ToTable("Portfolios", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnedBy") + .HasColumnType("uuid"); + + b.Property("PortfolioId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("InstrumentId"); + + b.HasIndex("OwnedBy"); + + b.HasIndex("PortfolioId"); + + b.ToTable("Programmes", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Risk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Impact") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Likelihood") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("3"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Risks", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Rule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Rules", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.SiteVisit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Findings") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("InspectorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("VisitedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("SiteVisits", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.HasKey("Id"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.AuditLog", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Performer") + .WithMany() + .HasForeignKey("PerformedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Performer"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Awards") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany("Awards") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Organisation"); + + b.Navigation("Programme"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Beneficiary", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Budget", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ChangeRequest", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ComplianceItem", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Verifier") + .WithMany() + .HasForeignKey("VerifiedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Verifier"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Contract", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Award", "Award") + .WithOne("Contract") + .HasForeignKey("PostFundManagement.Domain.Entities.Contract", "AwardId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Award"); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Disbursement", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Milestone", "Milestone") + .WithMany() + .HasForeignKey("MilestoneId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Milestone"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Evidence", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Indicator", "Indicator") + .WithMany() + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Milestone", "Milestone") + .WithMany() + .HasForeignKey("MilestoneId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Indicator"); + + b.Navigation("Milestone"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Indicator", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Instrument", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Issue", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Milestone", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Organisation", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Outcome", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Beneficiary", "Beneficiary") + .WithMany() + .HasForeignKey("BeneficiaryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Beneficiary"); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Instrument", "Instrument") + .WithMany("Programmes") + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Portfolio", "Portfolio") + .WithMany("Programmes") + .HasForeignKey("PortfolioId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Instrument"); + + b.Navigation("Owner"); + + b.Navigation("Portfolio"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Projects") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Programme"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Risk", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Rule", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Rules") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Programme"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.SiteVisit", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.Navigation("Contract"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Instrument", b => + { + b.Navigation("Programmes"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.Navigation("Programmes"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.Navigation("Awards"); + + b.Navigation("Projects"); + + b.Navigation("Rules"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.Navigation("Awards"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj index 4895080..b0d01a3 100644 --- a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj +++ b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj @@ -9,8 +9,21 @@ - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + From f7b30f89eaf0d78f6ca31a9ab0df948c80ebd338 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 16 Aug 2026 08:27:20 +0200 Subject: [PATCH 23/50] Added dbml file --- pfm.dbml | 349 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100644 pfm.dbml diff --git a/pfm.dbml b/pfm.dbml new file mode 100644 index 0000000..7df0882 --- /dev/null +++ b/pfm.dbml @@ -0,0 +1,349 @@ +Table "AuditLogs" { + "Id" int8 [not null] + "EntityName" varchar(128) [not null] + "EntityId" int8 [not null] + "Action" varchar(64) [not null] + "Changes" jsonb + "PerformedBy" uuid [not null] + "Timestamp" timestamptz [not null] +} + +Table "Awards" { + "Id" int8 [not null] + "OrganisationId" int8 [not null] + "ProgrammeId" int8 [not null] + "ProjectId" int8 + "CreatedBy" uuid [not null] + "UpdatedBy" uuid + "ApprovedBy" uuid + "CreatedAt" timestamptz [not null] + "UpdatedAt" timestamptz + "Status" int4 [not null] + "Amount" numeric(18,2) [not null] +} + +Table "Beneficiaries" { + "Id" int8 [not null] + "AwardId" int8 [not null] + "CreatedBy" uuid [not null] + "CreatedAt" timestamptz [not null] + "Name" varchar(256) [not null] + "Category" varchar(128) [not null] + "TargetCount" int4 [not null] + "ReachedCount" int4 [not null] + "Location" varchar(256) [not null] +} + +Table "ChangeRequests" { + "Id" int8 [not null] + "ProjectId" int8 [not null] + "Title" varchar(256) [not null] + "Justification" text [not null] + "RevisedBudget" numeric(18,2) + "RevisedEndDate" timestamptz + "Status" int4 [not null] + "CreatedBy" uuid [not null] + "ApprovedBy" uuid + "CreatedAt" timestamptz [not null] + "UpdatedAt" timestamptz +} + +Table "ComplianceItems" { + "Id" int8 [not null] + "ProjectId" int8 [not null] + "Title" varchar(256) [not null] + "Requirements" text [not null] + "Status" int4 [not null] + "CreatedBy" uuid [not null] + "VerifiedBy" uuid + "CreatedAt" timestamptz [not null] + "UpdatedBy" uuid +} + +Table "Contracts" { + "Id" int8 [not null] + "AwardId" int8 [not null] + "ExternalSignatureId" varchar(256) + "SignedDocumentUrl" varchar(2048) + "EffectiveAt" timestamptz + "ExpiresAt" timestamptz + "TotalValue" numeric(18,2) [not null] + "Status" int4 [not null] + "CreatedBy" uuid [not null] + "CreatedAt" timestamptz [not null] +} + +Table "Disbursements" { + "Id" int8 [not null] + "ProjectId" int8 [not null] + "CreatedBy" uuid [not null] + "ApprovedBy" uuid + "CreatedOn" timestamptz [not null] + "UpdatedBy" uuid + "Amount" numeric(18,2) [not null] +} + +Table "Evidences" { + "Id" int8 [not null] + "ProjectId" int8 [not null] + "MilestoneId" int8 + "IndicatorId" int8 + "CreatedBy" uuid [not null] + "UpdatedBy" uuid + "CreatedAt" timestamptz [not null] + "UpdatedAt" timestamptz + "Version" int4 [not null] + "DocumentUrl" text [not null] + "Status" int4 [not null] +} + +Table "Indicators" { + "Id" int8 [not null] + "ProjectId" int8 [not null] + "CreatedBy" uuid [not null] + "CreatedAt" timestamptz [not null] + "Name" varchar(256) [not null] + "UnitOfMeasure" varchar(64) [not null] + "BaselineAmount" numeric(18,2) [not null] + "TargetAmount" numeric(18,2) [not null] + "ActualAmount" numeric(18,2) [not null] +} + +Table "Instruments" { + "Id" int8 [not null] + "Code" varchar(32) [not null] + "Name" varchar(256) [not null] + "Description" text + "Status" int4 [not null] + "CreatedBy" uuid [not null] + "CreatedAt" timestamptz [not null] +} + +Table "Issues" { + "Id" int8 [not null] + "ProjectId" int8 [not null] + "Title" varchar(256) [not null] + "Description" varchar(1024) [not null] + "Priority" int4 [not null] + "Status" int4 [not null] + "CreatedBy" uuid [not null] + "CreatedAt" timestamptz [not null] + "ResolvedAt" timestamptz +} + +Table "Milestones" { + "Id" int8 [not null] + "ProjectId" int8 [not null] + "CreatedBy" uuid [not null] + "UpdatedBy" uuid + "CreatedAt" timestamptz [not null] + "UpdatedAt" timestamptz + "DueDate" timestamptz [not null] + "Name" varchar(256) [not null] + "Status" int4 [not null] +} + +Table "Organisations" { + "Id" int8 [not null] + "CreatedBy" uuid [not null] + "CreatedAt" timestamptz [not null] + "RegistrationNo" varchar(128) [not null] + "Name" varchar(256) [not null] + "Email" varchar(256) [not null] + "Type" int4 [not null] + "Status" int4 [not null] +} + +Table "Outcomes" { + "Id" int8 [not null] + "BeneficiaryId" int8 [not null] + "CreatedBy" uuid [not null] + "CreatedAt" timestamptz [not null] + "Name" varchar(256) [not null] + "Description" text [not null] +} + +Table "Portfolios" { + "Id" int8 [not null] + "CreatedBy" uuid [not null] + "OwnedBy" uuid + "CreatedAt" timestamptz [not null] + "Name" varchar(256) [not null] + "Description" varchar(1024) +} + +Table "Programmes" { + "Id" int8 [not null] + "PortfolioId" int8 [not null] + "InstrumentId" int8 [not null] + "OwnedBy" uuid [not null] + "CreatedBy" uuid [not null] + "UpdatedBy" uuid + "CreatedAt" timestamptz [not null] + "UpdatedAt" timestamptz + "Name" varchar(256) [not null] + "Status" int4 [not null] +} + +Table "Projects" { + "Id" int8 [not null] + "ProgrammeId" int8 [not null] + "CreatedBy" uuid [not null] + "UpdatedBy" uuid + "CreatedAt" timestamptz [not null] + "UpdatedAt" timestamptz + "Name" varchar(256) [not null] + "Description" varchar(1024) + "Status" int4 [not null] +} + +Table "Risks" { + "Id" int8 [not null] + "ProjectId" int8 [not null] + "CreatedBy" uuid [not null] + "CreatedAt" timestamptz [not null] + "Likelihood" int4 [not null] + "Impact" int4 [not null] + "Name" varchar(256) [not null] + "Description" varchar(1024) [not null] +} + +Table "Rules" { + "Id" int8 [not null] + "ProgrammeId" int8 [not null] + "CreatedBy" uuid [not null] + "ApprovedBy" uuid + "CreatedAt" timestamptz [not null] + "UpdatedBy" uuid + "Name" varchar(256) [not null] + "Notes" text [not null] + "Status" int4 [not null] +} + +Table "SiteVisits" { + "Id" int8 [not null] + "ProjectId" int8 [not null] + "VisitedAt" timestamptz [not null] + "InspectorName" varchar(256) [not null] + "Findings" text [not null] + "Status" int4 [not null] + "CreatedBy" uuid [not null] + "CreatedAt" timestamptz [not null] +} + +Table "Users" { + "Id" uuid [not null] + "Firstnames" text [not null] + "Surname" text [not null] + "Status" int4 [not null] +} + +Ref "FK_AuditLogs_Users_PerformedBy":"Users"."Id" < "AuditLogs"."PerformedBy" [delete: restrict] + +Ref "FK_Awards_Organisations_OrganisationId":"Organisations"."Id" < "Awards"."OrganisationId" [delete: restrict] + +Ref "FK_Awards_Programmes_ProgrammeId":"Programmes"."Id" < "Awards"."ProgrammeId" [delete: restrict] + +Ref "FK_Awards_Projects_ProjectId":"Projects"."Id" < "Awards"."ProjectId" [delete: set null] + +Ref "FK_Awards_Users_ApprovedBy":"Users"."Id" < "Awards"."ApprovedBy" + +Ref "FK_Awards_Users_CreatedBy":"Users"."Id" < "Awards"."CreatedBy" + +Ref "FK_Awards_Users_UpdatedBy":"Users"."Id" < "Awards"."UpdatedBy" + +Ref "FK_Beneficiaries_Awards_AwardId":"Awards"."Id" < "Beneficiaries"."AwardId" [delete: restrict] + +Ref "FK_Beneficiaries_Users_CreatedBy":"Users"."Id" < "Beneficiaries"."CreatedBy" + +Ref "FK_ChangeRequests_Projects_ProjectId":"Projects"."Id" < "ChangeRequests"."ProjectId" [delete: restrict] + +Ref "FK_ChangeRequests_Users_ApprovedBy":"Users"."Id" < "ChangeRequests"."ApprovedBy" + +Ref "FK_ChangeRequests_Users_CreatedBy":"Users"."Id" < "ChangeRequests"."CreatedBy" + +Ref "FK_ComplianceItems_Projects_ProjectId":"Projects"."Id" < "ComplianceItems"."ProjectId" [delete: restrict] + +Ref "FK_ComplianceItems_Users_CreatedBy":"Users"."Id" < "ComplianceItems"."CreatedBy" + +Ref "FK_ComplianceItems_Users_VerifiedBy":"Users"."Id" < "ComplianceItems"."VerifiedBy" + +Ref "FK_Contracts_Awards_AwardId":"Awards"."Id" < "Contracts"."AwardId" [delete: restrict] + +Ref "FK_Contracts_Users_CreatedBy":"Users"."Id" < "Contracts"."CreatedBy" + +Ref "FK_Disbursements_Projects_ProjectId":"Projects"."Id" < "Disbursements"."ProjectId" [delete: restrict] + +Ref "FK_Disbursements_Users_ApprovedBy":"Users"."Id" < "Disbursements"."ApprovedBy" + +Ref "FK_Disbursements_Users_CreatedBy":"Users"."Id" < "Disbursements"."CreatedBy" + +Ref "FK_Disbursements_Users_UpdatedBy":"Users"."Id" < "Disbursements"."UpdatedBy" + +Ref "FK_Evidences_Indicators_IndicatorId":"Indicators"."Id" < "Evidences"."IndicatorId" + +Ref "FK_Evidences_Milestones_MilestoneId":"Milestones"."Id" < "Evidences"."MilestoneId" + +Ref "FK_Evidences_Projects_ProjectId":"Projects"."Id" < "Evidences"."ProjectId" [delete: restrict] + +Ref "FK_Evidences_Users_CreatedBy":"Users"."Id" < "Evidences"."CreatedBy" + +Ref "FK_Evidences_Users_UpdatedBy":"Users"."Id" < "Evidences"."UpdatedBy" + +Ref "FK_Indicators_Projects_ProjectId":"Projects"."Id" < "Indicators"."ProjectId" [delete: restrict] + +Ref "FK_Indicators_Users_CreatedBy":"Users"."Id" < "Indicators"."CreatedBy" + +Ref "FK_Instruments_Users_CreatedBy":"Users"."Id" < "Instruments"."CreatedBy" + +Ref "FK_Issues_Projects_ProjectId":"Projects"."Id" < "Issues"."ProjectId" [delete: restrict] + +Ref "FK_Issues_Users_CreatedBy":"Users"."Id" < "Issues"."CreatedBy" + +Ref "FK_Milestones_Projects_ProjectId":"Projects"."Id" < "Milestones"."ProjectId" [delete: restrict] + +Ref "FK_Milestones_Users_CreatedBy":"Users"."Id" < "Milestones"."CreatedBy" + +Ref "FK_Milestones_Users_UpdatedBy":"Users"."Id" < "Milestones"."UpdatedBy" + +Ref "FK_Organisations_Users_CreatedBy":"Users"."Id" < "Organisations"."CreatedBy" + +Ref "FK_Outcomes_Beneficiaries_BeneficiaryId":"Beneficiaries"."Id" < "Outcomes"."BeneficiaryId" [delete: restrict] + +Ref "FK_Outcomes_Users_CreatedBy":"Users"."Id" < "Outcomes"."CreatedBy" + +Ref "FK_Portfolios_Users_CreatedBy":"Users"."Id" < "Portfolios"."CreatedBy" + +Ref "FK_Portfolios_Users_OwnedBy":"Users"."Id" < "Portfolios"."OwnedBy" + +Ref "FK_Programmes_Instruments_InstrumentId":"Instruments"."Id" < "Programmes"."InstrumentId" [delete: restrict] + +Ref "FK_Programmes_Portfolios_PortfolioId":"Portfolios"."Id" < "Programmes"."PortfolioId" [delete: restrict] + +Ref "FK_Programmes_Users_CreatedBy":"Users"."Id" < "Programmes"."CreatedBy" + +Ref "FK_Programmes_Users_OwnedBy":"Users"."Id" < "Programmes"."OwnedBy" [delete: restrict] + +Ref "FK_Programmes_Users_UpdatedBy":"Users"."Id" < "Programmes"."UpdatedBy" + +Ref "FK_Projects_Programmes_ProgrammeId":"Programmes"."Id" < "Projects"."ProgrammeId" [delete: restrict] + +Ref "FK_Projects_Users_CreatedBy":"Users"."Id" < "Projects"."CreatedBy" + +Ref "FK_Projects_Users_UpdatedBy":"Users"."Id" < "Projects"."UpdatedBy" + +Ref "FK_Risks_Projects_ProjectId":"Projects"."Id" < "Risks"."ProjectId" [delete: restrict] + +Ref "FK_Risks_Users_CreatedBy":"Users"."Id" < "Risks"."CreatedBy" + +Ref "FK_Rules_Programmes_ProgrammeId":"Programmes"."Id" < "Rules"."ProgrammeId" [delete: restrict] + +Ref "FK_Rules_Users_ApprovedBy":"Users"."Id" < "Rules"."ApprovedBy" + +Ref "FK_Rules_Users_CreatedBy":"Users"."Id" < "Rules"."CreatedBy" + +Ref "FK_Rules_Users_UpdatedBy":"Users"."Id" < "Rules"."UpdatedBy" + +Ref "FK_SiteVisits_Projects_ProjectId":"Projects"."Id" < "SiteVisits"."ProjectId" [delete: restrict] + +Ref "FK_SiteVisits_Users_CreatedBy":"Users"."Id" < "SiteVisits"."CreatedBy" From 109fa0602e2356e37af0ec00b332d7dda5abb38d Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 16 Aug 2026 10:08:33 +0200 Subject: [PATCH 24/50] Added entity-model mappers Added and applied field size constants --- .../Configuration/AuditLog.cs | 6 +- .../Configuration/Beneficiary.cs | 6 +- .../Configuration/Budget.cs | 4 +- .../Configuration/ChangeRequest.cs | 6 +- .../Configuration/ComplianceItem.cs | 6 +- .../Configuration/Indicator.cs | 4 +- .../Configuration/Instrument.cs | 8 +- .../Configuration/Issue.cs | 6 +- .../Configuration/Milestone.cs | 4 +- .../Configuration/Organisation.cs | 8 +- .../Configuration/Outcome.cs | 6 +- .../Configuration/Portfolio.cs | 6 +- .../Configuration/Programme.cs | 4 +- .../Configuration/Project.cs | 6 +- .../Configuration/Risk.cs | 6 +- .../Configuration/Rule.cs | 4 +- .../Configuration/SiteVisit.cs | 4 +- .../Configuration/User.cs | 4 +- .../Extensions/Constants.cs | 14 + .../Extensions/Mappers.cs | 269 ++++++++++++++++++ 20 files changed, 350 insertions(+), 31 deletions(-) create mode 100644 PostFundManagement.Domain/Extensions/Constants.cs create mode 100644 PostFundManagement.Domain/Extensions/Mappers.cs diff --git a/PostFundManagement.Domain/Configuration/AuditLog.cs b/PostFundManagement.Domain/Configuration/AuditLog.cs index bc19c51..c0bd320 100644 --- a/PostFundManagement.Domain/Configuration/AuditLog.cs +++ b/PostFundManagement.Domain/Configuration/AuditLog.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class AuditLog : IEntityTypeConfiguration @@ -7,9 +9,9 @@ public sealed class AuditLog : IEntityTypeConfiguration builder.ToTable(nameof(Entities.AuditLog).Pluralize()); builder.HasKey(pk => pk.Id); - builder.Property(f => f.EntityName).IsRequired().HasMaxLength(128); + builder.Property(f => f.EntityName).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.EntityId).IsRequired(); - builder.Property(f => f.Action).IsRequired().HasMaxLength(64); + builder.Property(f => f.Action).IsRequired().HasMaxLength(ShortLabelLength); builder.Property(f => f.Changes).IsRequired(false).HasColumnType("jsonb"); builder.Property(f => f.PerformedBy).IsRequired(); builder.Property(f => f.Timestamp).IsRequired().HasDefaultValueSql("now()"); diff --git a/PostFundManagement.Domain/Configuration/Beneficiary.cs b/PostFundManagement.Domain/Configuration/Beneficiary.cs index f4da593..7763db5 100644 --- a/PostFundManagement.Domain/Configuration/Beneficiary.cs +++ b/PostFundManagement.Domain/Configuration/Beneficiary.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class Beneficiary : IEntityTypeConfiguration @@ -11,8 +13,8 @@ public sealed class Beneficiary : IEntityTypeConfiguration builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.UserId).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); - builder.Property(f => f.Name).IsRequired().HasMaxLength(256); - builder.Property(f => f.Category).IsRequired().HasMaxLength(50); + builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.Category).IsRequired().HasMaxLength(ShortLabelLength); builder.Property(f => f.TargetCount).IsRequired(); builder.Property(f => f.ReachedCount).IsRequired(); builder.Property(f => f.Location).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Budget.cs b/PostFundManagement.Domain/Configuration/Budget.cs index 8600b9d..dd8606f 100644 --- a/PostFundManagement.Domain/Configuration/Budget.cs +++ b/PostFundManagement.Domain/Configuration/Budget.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class Budget : IEntityTypeConfiguration @@ -12,7 +14,7 @@ public sealed class Budget : IEntityTypeConfiguration builder.Property(f => f.ApprovedBy).IsRequired(false); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.UpdatedAt).IsRequired(false); - builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); builder.HasOne(f => f.Project) diff --git a/PostFundManagement.Domain/Configuration/ChangeRequest.cs b/PostFundManagement.Domain/Configuration/ChangeRequest.cs index 84e6c14..6767dac 100644 --- a/PostFundManagement.Domain/Configuration/ChangeRequest.cs +++ b/PostFundManagement.Domain/Configuration/ChangeRequest.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class ChangeRequest : IEntityTypeConfiguration @@ -12,8 +14,8 @@ public sealed class ChangeRequest : IEntityTypeConfiguration f.ApprovedBy).IsRequired(false); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.UpdatedAt).IsRequired(false); - builder.Property(f => f.Title).IsRequired().HasMaxLength(256); - builder.Property(f => f.Justification).IsRequired().HasMaxLength(1024); + builder.Property(f => f.Title).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.Justification).IsRequired().HasMaxLength(TextLength); builder.Property(f => f.RevisedBudget).IsRequired(false).HasPrecision(18, 2); builder.Property(f => f.RevisedEndedAt).IsRequired(false); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); diff --git a/PostFundManagement.Domain/Configuration/ComplianceItem.cs b/PostFundManagement.Domain/Configuration/ComplianceItem.cs index 4fec8da..ca7c6da 100644 --- a/PostFundManagement.Domain/Configuration/ComplianceItem.cs +++ b/PostFundManagement.Domain/Configuration/ComplianceItem.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class ComplianceItem : IEntityTypeConfiguration @@ -12,8 +14,8 @@ public sealed class ComplianceItem : IEntityTypeConfiguration f.VerifiedBy).IsRequired(false); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.UpdatedAt).IsRequired(false); - builder.Property(f => f.Title).IsRequired().HasMaxLength(256); - builder.Property(f => f.Requirements).IsRequired().HasMaxLength(2048); + builder.Property(f => f.Title).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.Requirements).IsRequired().HasMaxLength(MediumTextLength); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Project) diff --git a/PostFundManagement.Domain/Configuration/Indicator.cs b/PostFundManagement.Domain/Configuration/Indicator.cs index 140bc75..5a0cde7 100644 --- a/PostFundManagement.Domain/Configuration/Indicator.cs +++ b/PostFundManagement.Domain/Configuration/Indicator.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class Indicator : IEntityTypeConfiguration @@ -10,7 +12,7 @@ public sealed class Indicator : IEntityTypeConfiguration builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); - builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.UnitOfMeasure).IsRequired().HasConversion().HasDefaultValueSql("2"); builder.Property(f => f.BaselineAmount).IsRequired().HasPrecision(18, 2); builder.Property(f => f.TargetAmount).IsRequired().HasPrecision(18, 2); diff --git a/PostFundManagement.Domain/Configuration/Instrument.cs b/PostFundManagement.Domain/Configuration/Instrument.cs index 911dc8b..28618ec 100644 --- a/PostFundManagement.Domain/Configuration/Instrument.cs +++ b/PostFundManagement.Domain/Configuration/Instrument.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class Instrument : IEntityTypeConfiguration @@ -7,9 +9,9 @@ public sealed class Instrument : IEntityTypeConfiguration builder.ToTable(nameof(Entities.Instrument).Pluralize()); builder.HasKey(pk => pk.Id); - builder.Property(f => f.Code).IsRequired().HasMaxLength(50); - builder.Property(f => f.Name).IsRequired().HasMaxLength(256); - builder.Property(f => f.Description).IsRequired(false).HasMaxLength(1024); + builder.Property(f => f.Code).IsRequired().HasMaxLength(ShortLabelLength); + builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.Description).IsRequired(false).HasMaxLength(TextLength); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("2"); builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); diff --git a/PostFundManagement.Domain/Configuration/Issue.cs b/PostFundManagement.Domain/Configuration/Issue.cs index 2a61024..3e42e2d 100644 --- a/PostFundManagement.Domain/Configuration/Issue.cs +++ b/PostFundManagement.Domain/Configuration/Issue.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class Issue : IEntityTypeConfiguration @@ -11,8 +13,8 @@ public sealed class Issue : IEntityTypeConfiguration builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.ResolvedAt).IsRequired(false); - builder.Property(f => f.Title).IsRequired().HasMaxLength(256); - builder.Property(f => f.Description).IsRequired().HasMaxLength(1024); + builder.Property(f => f.Title).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength); builder.Property(f => f.Priority).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); diff --git a/PostFundManagement.Domain/Configuration/Milestone.cs b/PostFundManagement.Domain/Configuration/Milestone.cs index 4f19795..5f16428 100644 --- a/PostFundManagement.Domain/Configuration/Milestone.cs +++ b/PostFundManagement.Domain/Configuration/Milestone.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class Milestone : IEntityTypeConfiguration @@ -13,7 +15,7 @@ public sealed class Milestone : IEntityTypeConfiguration builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.DueAt).IsRequired(); - builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Project) diff --git a/PostFundManagement.Domain/Configuration/Organisation.cs b/PostFundManagement.Domain/Configuration/Organisation.cs index 7d57cb7..050c24f 100644 --- a/PostFundManagement.Domain/Configuration/Organisation.cs +++ b/PostFundManagement.Domain/Configuration/Organisation.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class Organisation : IEntityTypeConfiguration @@ -9,9 +11,9 @@ public sealed class Organisation : IEntityTypeConfiguration pk.Id); builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); - builder.Property(f => f.RegistrationNo).IsRequired().HasMaxLength(256); - builder.Property(f => f.Name).IsRequired().HasMaxLength(256); - builder.Property(f => f.Email).IsRequired().HasMaxLength(256); + builder.Property(f => f.RegistrationNo).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.Email).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Type).IsRequired().HasConversion().HasDefaultValueSql("5"); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); diff --git a/PostFundManagement.Domain/Configuration/Outcome.cs b/PostFundManagement.Domain/Configuration/Outcome.cs index da9e17d..bd1c1a4 100644 --- a/PostFundManagement.Domain/Configuration/Outcome.cs +++ b/PostFundManagement.Domain/Configuration/Outcome.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class Outcome : IEntityTypeConfiguration @@ -10,8 +12,8 @@ public sealed class Outcome : IEntityTypeConfiguration 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(256); - builder.Property(f => f.Description).IsRequired().HasMaxLength(1024); + builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength); builder.HasOne(f => f.Beneficiary) .WithMany() diff --git a/PostFundManagement.Domain/Configuration/Portfolio.cs b/PostFundManagement.Domain/Configuration/Portfolio.cs index f6f6a19..60e5627 100644 --- a/PostFundManagement.Domain/Configuration/Portfolio.cs +++ b/PostFundManagement.Domain/Configuration/Portfolio.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class Portfolio : IEntityTypeConfiguration @@ -10,8 +12,8 @@ public sealed class Portfolio : IEntityTypeConfiguration builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.OwnedBy).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); - builder.Property(f => f.Name).IsRequired().HasMaxLength(256); - builder.Property(f => f.Description).IsRequired(false).HasMaxLength(1024); + builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.Description).IsRequired(false).HasMaxLength(TextLength); builder.HasOne(f => f.Creator) .WithMany() diff --git a/PostFundManagement.Domain/Configuration/Programme.cs b/PostFundManagement.Domain/Configuration/Programme.cs index 70049ff..8a2f211 100644 --- a/PostFundManagement.Domain/Configuration/Programme.cs +++ b/PostFundManagement.Domain/Configuration/Programme.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class Programme : IEntityTypeConfiguration @@ -14,7 +16,7 @@ public sealed class Programme : IEntityTypeConfiguration builder.Property(f => f.UpdatedBy).IsRequired(false); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.UpdatedAt).IsRequired(false); - builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Portfolio) diff --git a/PostFundManagement.Domain/Configuration/Project.cs b/PostFundManagement.Domain/Configuration/Project.cs index 11aed2e..3ea0a79 100644 --- a/PostFundManagement.Domain/Configuration/Project.cs +++ b/PostFundManagement.Domain/Configuration/Project.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class Project : IEntityTypeConfiguration @@ -14,8 +16,8 @@ public sealed class Project : IEntityTypeConfiguration builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.StartedAt).IsRequired(false); builder.Property(f => f.EndedAt).IsRequired(false); - builder.Property(f => f.Name).IsRequired().HasMaxLength(256); - builder.Property(f => f.Description).IsRequired().HasMaxLength(1024); + builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Programme) diff --git a/PostFundManagement.Domain/Configuration/Risk.cs b/PostFundManagement.Domain/Configuration/Risk.cs index 6d82e19..9f7584d 100644 --- a/PostFundManagement.Domain/Configuration/Risk.cs +++ b/PostFundManagement.Domain/Configuration/Risk.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class Risk : IEntityTypeConfiguration @@ -12,8 +14,8 @@ public sealed class Risk : IEntityTypeConfiguration builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.Likelihood).IsRequired().HasConversion().HasDefaultValueSql("3"); builder.Property(f => f.Impact).IsRequired().HasConversion().HasDefaultValueSql("1"); - builder.Property(f => f.Name).IsRequired().HasMaxLength(256); - builder.Property(f => f.Description).IsRequired().HasMaxLength(1024); + builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength); builder.HasOne(f => f.Project) .WithMany() diff --git a/PostFundManagement.Domain/Configuration/Rule.cs b/PostFundManagement.Domain/Configuration/Rule.cs index ad11bb5..c602ef2 100644 --- a/PostFundManagement.Domain/Configuration/Rule.cs +++ b/PostFundManagement.Domain/Configuration/Rule.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class Rule : IEntityTypeConfiguration @@ -12,7 +14,7 @@ public sealed class Rule : IEntityTypeConfiguration builder.Property(f => f.UpdatedBy).IsRequired(false); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.UpdatedAt).IsRequired(false); - builder.Property(f => f.Name).IsRequired().HasMaxLength(256); + builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Version).IsRequired().HasDefaultValue(1); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("2"); diff --git a/PostFundManagement.Domain/Configuration/SiteVisit.cs b/PostFundManagement.Domain/Configuration/SiteVisit.cs index 88ed258..b395ed8 100644 --- a/PostFundManagement.Domain/Configuration/SiteVisit.cs +++ b/PostFundManagement.Domain/Configuration/SiteVisit.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class SiteVisit : IEntityTypeConfiguration @@ -12,7 +14,7 @@ public sealed class SiteVisit : IEntityTypeConfiguration builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.VisitedAt).IsRequired(); builder.Property(f => f.InspectorName).IsRequired(); - builder.Property(f => f.Findings).IsRequired(false).HasMaxLength(4096); + builder.Property(f => f.Findings).IsRequired(false).HasMaxLength(LargeTextLength); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Project) diff --git a/PostFundManagement.Domain/Configuration/User.cs b/PostFundManagement.Domain/Configuration/User.cs index 89c1a7e..0562e43 100644 --- a/PostFundManagement.Domain/Configuration/User.cs +++ b/PostFundManagement.Domain/Configuration/User.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Domain.Extensions.Constants; + namespace PostFundManagement.Domain.Configuration; public sealed class User : IEntityTypeConfiguration @@ -7,7 +9,7 @@ public sealed class User : IEntityTypeConfiguration builder.ToTable(nameof(Entities.User).Pluralize()); builder.HasKey(pk => pk.Id); - builder.Property(f => f.Email).IsRequired().HasMaxLength(256); + builder.Property(f => f.Email).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("2"); builder.Property(f => f.LastLoginAt).IsRequired(false); } diff --git a/PostFundManagement.Domain/Extensions/Constants.cs b/PostFundManagement.Domain/Extensions/Constants.cs new file mode 100644 index 0000000..6d7291a --- /dev/null +++ b/PostFundManagement.Domain/Extensions/Constants.cs @@ -0,0 +1,14 @@ +namespace PostFundManagement.Domain.Extensions; + +public static class Constants +{ + public const int LabelLength = 256; + + public const int ShortLabelLength = 100; + + public const int TextLength = 1024; + + public const int MediumTextLength = 2048; + + public const int LargeTextLength = 4096; +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Extensions/Mappers.cs b/PostFundManagement.Domain/Extensions/Mappers.cs new file mode 100644 index 0000000..fc720d9 --- /dev/null +++ b/PostFundManagement.Domain/Extensions/Mappers.cs @@ -0,0 +1,269 @@ +using PostFundManagement.Domain.Models; + +namespace PostFundManagement.Domain.Extensions; + +public static class Mappers +{ + public static AuditLog Map(this Entities.AuditLog entity) => new() + { + Id = entity.Id, + Action = entity.Action, + Changes = entity.Changes, + EntityId = entity.EntityId, + EntityName = entity.EntityName, + PerformedBy = entity.PerformedBy, + Timestamp = entity.Timestamp + }; + + public static Award Map(this Entities.Award entity) => new() + { + Id = entity.Id, + Amount = entity.Amount, + ApprovedBy = entity.ApprovedBy, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + OrganisationId = entity.OrganisationId, + ProgrammeId = entity.ProgrammeId, + ProjectId = entity.ProjectId, + Status = entity.Status, + UpdatedAt = entity.UpdatedAt, + UpdatedBy = entity.UpdatedBy + }; + + public static Beneficiary Map(this Entities.Beneficiary entity) => new() + { + Id = entity.Id, + Category = entity.Category, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Location = entity.Location, + Name = entity.Name, + ProjectId = entity.ProjectId, + ReachedCount = entity.ReachedCount, + TargetCount = entity.TargetCount, + UserId = entity.UserId + }; + + public static Budget Map(this Entities.Budget entity) => new() + { + Id = entity.Id, + Amount = entity.Amount, + ApprovedBy = entity.ApprovedBy, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Name = entity.Name, + ProjectId = entity.ProjectId, + UpdatedAt = entity.UpdatedAt + }; + + public static ComplianceItem Map(this Entities.ComplianceItem entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + ProjectId = entity.ProjectId, + Requirements = entity.Requirements, + Status = entity.Status, + Title = entity.Title, + UpdatedAt = entity.UpdatedAt, + VerifiedBy = entity.VerifiedBy + }; + + public static Contract Map(this Entities.Contract entity) => new() + { + Id = entity.Id, + AwardId = entity.AwardId, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + EffectiveAt = entity.EffectiveAt, + ExpiresAt = entity.ExpiresAt, + ExternalSignatureId = entity.ExternalSignatureId, + SignedDocumentUrl = entity.SignedDocumentUrl, + Status = entity.Status, + TotalValue = entity.TotalValue + }; + + public static Disbursement Map(this Entities.Disbursement entity) => new() + { + Id = entity.Id, + Amount = entity.Amount, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + MilestoneId = entity.MilestoneId, + ProjectId = entity.ProjectId, + Status = entity.Status, + UpdatedAt = entity.UpdatedAt, + UpdatedBy = entity.UpdatedBy + }; + + public static Evidence Map(this Entities.Evidence entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + DocumentUrl = entity.DocumentUrl, + IndicatorId = entity.IndicatorId, + MilestoneId = entity.MilestoneId, + ProjectId = entity.ProjectId, + Status = entity.Status, + UpdatedAt = entity.UpdatedAt, + UpdatedBy = entity.UpdatedBy, + Version = entity.Version + }; + + public static Indicator Map(this Entities.Indicator entity) => new() + { + Id = entity.Id, + ActualAmount = entity.ActualAmount, + BaselineAmount = entity.BaselineAmount, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Name = entity.Name, + ProjectId = entity.ProjectId, + TargetAmount = entity.TargetAmount, + UnitOfMeasure = entity.UnitOfMeasure + }; + + public static Instrument Map(this Entities.Instrument entity) => new() + { + Id = entity.Id, + Code = entity.Code, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Description = entity.Description, + Name = entity.Name, + Status = entity.Status + }; + + public static Issue Map(this Entities.Issue entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Description = entity.Description, + Priority = entity.Priority, + ProjectId = entity.ProjectId, + ResolvedAt = entity.ResolvedAt, + Status = entity.Status, + Title = entity.Title + }; + + public static Milestone Map(this Entities.Milestone entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + DueAt = entity.DueAt, + Name = entity.Name, + ProjectId = entity.ProjectId, + Status = entity.Status, + UpdatedAt = entity.UpdatedAt, + UpdatedBy = entity.UpdatedBy + }; + + public static Organisation Map(this Entities.Organisation entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Email = entity.Email, + Name = entity.Name, + RegistrationNo = entity.RegistrationNo, + Status = entity.Status, + Type = entity.Type + }; + + public static Outcome Map(this Entities.Outcome entity) => new() + { + Id = entity.Id, + BeneficiaryId = entity.BeneficiaryId, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Description = entity.Description, + Name = entity.Name + }; + + public static Portfolio Map(this Entities.Portfolio entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Description = entity.Description, + Name = entity.Name, + OwnedBy = entity.OwnedBy + }; + + public static Programme Map(this Entities.Programme entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + InstrumentId = entity.InstrumentId, + Name = entity.Name, + OwnedBy = entity.OwnedBy, + PortfolioId = entity.PortfolioId, + Status = entity.Status, + UpdatedAt = entity.UpdatedAt, + UpdatedBy = entity.UpdatedBy + }; + + public static Project Map(this Entities.Project entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Description = entity.Description, + EndedAt = entity.EndedAt, + Name = entity.Name, + ProgrammeId = entity.ProgrammeId, + StartedAt = entity.StartedAt, + Status = entity.Status, + UpdatedAt = entity.UpdatedAt, + UpdatedBy = entity.UpdatedBy + }; + + public static Risk Map(this Entities.Risk entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Description = entity.Description, + Impact = entity.Impact, + Likelihood = entity.Likelihood, + Name = entity.Name, + ProjectId = entity.ProjectId + }; + + public static Rule Map(this Entities.Rule entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Name = entity.Name, + ProgrammeId = entity.ProgrammeId, + Status = entity.Status, + UpdatedAt = entity.UpdatedAt, + UpdatedBy = entity.UpdatedBy, + Version = entity.Version + }; + + public static SiteVisit Map(this Entities.SiteVisit entity) => new() + { + Id = entity.Id, + ProjectId = entity.ProjectId, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Findings = entity.Findings, + InspectorName = entity.InspectorName, + Status = entity.Status, + VisitedAt = entity.VisitedAt + }; + + public static User Map(this Entities.User entity) => new() + { + Id = entity.Id, + Email = entity.Email, + LastLoginAt = entity.LastLoginAt, + Status = entity.Status + }; +} \ No newline at end of file From b7ac97fe6e1730ad500b40e5714392dea476e223 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 16 Aug 2026 10:16:21 +0200 Subject: [PATCH 25/50] Updated Microsoft.OpenApi version to latest safe and stabel release --- PostFundManagement.Api/PostFundManagement.Api.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/PostFundManagement.Api/PostFundManagement.Api.csproj b/PostFundManagement.Api/PostFundManagement.Api.csproj index 3f01fc2..5e5f843 100644 --- a/PostFundManagement.Api/PostFundManagement.Api.csproj +++ b/PostFundManagement.Api/PostFundManagement.Api.csproj @@ -8,6 +8,7 @@ + From 9540ebef9d3a8a9b825e0177c14e3fbab05016cf Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 16 Aug 2026 10:19:15 +0200 Subject: [PATCH 26/50] Updated aspNet OpenApi library --- PostFundManagement.Api/PostFundManagement.Api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PostFundManagement.Api/PostFundManagement.Api.csproj b/PostFundManagement.Api/PostFundManagement.Api.csproj index 5e5f843..958507a 100644 --- a/PostFundManagement.Api/PostFundManagement.Api.csproj +++ b/PostFundManagement.Api/PostFundManagement.Api.csproj @@ -7,7 +7,7 @@ - + From a9f4b45d2089b18b0da2cdb49d928fd7a7b2e5a4 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 16 Aug 2026 10:21:55 +0200 Subject: [PATCH 27/50] Updated field size changes to database --- ...6082045_StandardisedFieldSizes.Designer.cs | 1624 +++++++++++++++++ .../20260816082045_StandardisedFieldSizes.cs | 98 + .../ApplicationDbContextModelSnapshot.cs | 16 +- 3 files changed, 1730 insertions(+), 8 deletions(-) create mode 100644 PostFundManagement.Infrastructure/Database/Migrations/20260816082045_StandardisedFieldSizes.Designer.cs create mode 100644 PostFundManagement.Infrastructure/Database/Migrations/20260816082045_StandardisedFieldSizes.cs diff --git a/PostFundManagement.Infrastructure/Database/Migrations/20260816082045_StandardisedFieldSizes.Designer.cs b/PostFundManagement.Infrastructure/Database/Migrations/20260816082045_StandardisedFieldSizes.Designer.cs new file mode 100644 index 0000000..e801bea --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Migrations/20260816082045_StandardisedFieldSizes.Designer.cs @@ -0,0 +1,1624 @@ +ο»Ώ// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PostFundManagement.Infrastructure.Database; + +#nullable disable + +namespace PostFundManagement.Infrastructure.Database.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260816082045_StandardisedFieldSizes")] + partial class StandardisedFieldSizes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.PrimitiveCollection("Changes") + .HasColumnType("jsonb"); + + b.Property("EntityId") + .HasColumnType("bigint"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PerformedBy") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("PerformedBy"); + + b.ToTable("AuditLogs", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("OrganisationId"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Awards", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Beneficiary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Location") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("ReachedCount") + .HasColumnType("integer"); + + b.Property("TargetCount") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UserId"); + + b.ToTable("Beneficiaries", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Budget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Budgets", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ChangeRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Justification") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("RevisedBudget") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("RevisedEndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("ChangeRequests", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ComplianceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Requirements") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerifiedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("VerifiedBy"); + + b.ToTable("ComplianceItems", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Contract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AwardId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("EffectiveAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalSignatureId") + .HasColumnType("text"); + + b.Property("SignedDocumentUrl") + .HasColumnType("text"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TotalValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("AwardId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.ToTable("Contracts", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Disbursement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("MilestoneId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("MilestoneId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Disbursements", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Evidence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("IndicatorId") + .HasColumnType("bigint"); + + b.Property("MilestoneId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("MilestoneId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Evidences", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Indicator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActualAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BaselineAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("TargetAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UnitOfMeasure") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Indicators", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Instrument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Instruments", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Issue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Issues", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Milestone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DueAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Milestones", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Organisation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RegistrationNo") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("5"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Organisations", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Outcome", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BeneficiaryId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("BeneficiaryId"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Outcomes", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("OwnedBy"); + + b.ToTable("Portfolios", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnedBy") + .HasColumnType("uuid"); + + b.Property("PortfolioId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("InstrumentId"); + + b.HasIndex("OwnedBy"); + + b.HasIndex("PortfolioId"); + + b.ToTable("Programmes", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Risk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Impact") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Likelihood") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("3"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Risks", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Rule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Rules", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.SiteVisit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Findings") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("InspectorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("VisitedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("SiteVisits", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.HasKey("Id"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.AuditLog", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Performer") + .WithMany() + .HasForeignKey("PerformedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Performer"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Awards") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany("Awards") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Organisation"); + + b.Navigation("Programme"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Beneficiary", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Budget", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ChangeRequest", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ComplianceItem", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Verifier") + .WithMany() + .HasForeignKey("VerifiedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Verifier"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Contract", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Award", "Award") + .WithOne("Contract") + .HasForeignKey("PostFundManagement.Domain.Entities.Contract", "AwardId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Award"); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Disbursement", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Milestone", "Milestone") + .WithMany() + .HasForeignKey("MilestoneId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Milestone"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Evidence", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Indicator", "Indicator") + .WithMany() + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Milestone", "Milestone") + .WithMany() + .HasForeignKey("MilestoneId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Indicator"); + + b.Navigation("Milestone"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Indicator", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Instrument", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Issue", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Milestone", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Organisation", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Outcome", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Beneficiary", "Beneficiary") + .WithMany() + .HasForeignKey("BeneficiaryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Beneficiary"); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Instrument", "Instrument") + .WithMany("Programmes") + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Portfolio", "Portfolio") + .WithMany("Programmes") + .HasForeignKey("PortfolioId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Instrument"); + + b.Navigation("Owner"); + + b.Navigation("Portfolio"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Projects") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Programme"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Risk", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Rule", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Rules") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Programme"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.SiteVisit", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.Navigation("Contract"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Instrument", b => + { + b.Navigation("Programmes"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.Navigation("Programmes"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.Navigation("Awards"); + + b.Navigation("Projects"); + + b.Navigation("Rules"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.Navigation("Awards"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PostFundManagement.Infrastructure/Database/Migrations/20260816082045_StandardisedFieldSizes.cs b/PostFundManagement.Infrastructure/Database/Migrations/20260816082045_StandardisedFieldSizes.cs new file mode 100644 index 0000000..5c038da --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Migrations/20260816082045_StandardisedFieldSizes.cs @@ -0,0 +1,98 @@ +ο»Ώusing Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PostFundManagement.Infrastructure.Database.Migrations +{ + /// + public partial class StandardisedFieldSizes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Code", + table: "Instruments", + type: "character varying(100)", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(50)", + oldMaxLength: 50); + + migrationBuilder.AlterColumn( + name: "Category", + table: "Beneficiaries", + type: "character varying(100)", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(50)", + oldMaxLength: 50); + + migrationBuilder.AlterColumn( + name: "EntityName", + table: "AuditLogs", + type: "character varying(256)", + maxLength: 256, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128); + + migrationBuilder.AlterColumn( + name: "Action", + table: "AuditLogs", + type: "character varying(100)", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Code", + table: "Instruments", + type: "character varying(50)", + maxLength: 50, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100); + + migrationBuilder.AlterColumn( + name: "Category", + table: "Beneficiaries", + type: "character varying(50)", + maxLength: 50, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100); + + migrationBuilder.AlterColumn( + name: "EntityName", + table: "AuditLogs", + type: "character varying(128)", + maxLength: 128, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256); + + migrationBuilder.AlterColumn( + name: "Action", + table: "AuditLogs", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100); + } + } +} diff --git a/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs b/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs index 03bd61f..b90e527 100644 --- a/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs @@ -32,8 +32,8 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.Property("Action") .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.PrimitiveCollection("Changes") .HasColumnType("jsonb"); @@ -43,8 +43,8 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.Property("EntityName") .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.Property("PerformedBy") .HasColumnType("uuid"); @@ -131,8 +131,8 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.Property("Category") .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.Property("CreatedAt") .ValueGeneratedOnAdd() @@ -548,8 +548,8 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.Property("Code") .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.Property("CreatedAt") .ValueGeneratedOnAdd() From def73c3fa89d656974e64fe8d4f81e3448933fd4 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 16 Aug 2026 10:33:12 +0200 Subject: [PATCH 28/50] Added instrastructure extensions Implemented database service registration --- .../Database/ApplicationDbContextFactory.cs | 6 ++++-- .../Extensions/Constants.cs | 6 ++++++ .../Extensions/Postgres.cs | 17 +++++++++++++++++ .../PostFundManagement.Infrastructure.csproj | 3 ++- 4 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 PostFundManagement.Infrastructure/Extensions/Constants.cs create mode 100644 PostFundManagement.Infrastructure/Extensions/Postgres.cs diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContextFactory.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContextFactory.cs index f3f27d7..a11f435 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContextFactory.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContextFactory.cs @@ -1,3 +1,5 @@ +using static PostFundManagement.Infrastructure.Extensions.Constants; + namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContextFactory : IDesignTimeDbContextFactory @@ -12,8 +14,8 @@ public sealed class ApplicationDbContextFactory : IDesignTimeDbContextFactory(); - var connectionString = configuration.GetConnectionString("PfmDatabase") - ?? throw new InvalidOperationException("Connection string 'PfmDatabase' was not found in configuration."); + var connectionString = configuration.GetConnectionString(DatabaseConfigName) + ?? throw new InvalidOperationException($"Connection string '{DatabaseConfigName}' was not found in configuration."); optionsBuilder.UseNpgsql(connectionString, npgsqlOptions => npgsqlOptions.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)); diff --git a/PostFundManagement.Infrastructure/Extensions/Constants.cs b/PostFundManagement.Infrastructure/Extensions/Constants.cs new file mode 100644 index 0000000..66ac514 --- /dev/null +++ b/PostFundManagement.Infrastructure/Extensions/Constants.cs @@ -0,0 +1,6 @@ +namespace PostFundManagement.Infrastructure.Extensions; + +public static class Constants +{ + public const string DatabaseConfigName = "PfmDatabase"; +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Extensions/Postgres.cs b/PostFundManagement.Infrastructure/Extensions/Postgres.cs new file mode 100644 index 0000000..2e943c1 --- /dev/null +++ b/PostFundManagement.Infrastructure/Extensions/Postgres.cs @@ -0,0 +1,17 @@ +using PostFundManagement.Infrastructure.Database; +using static PostFundManagement.Infrastructure.Extensions.Constants; + +namespace PostFundManagement.Infrastructure.Extensions; + +public static class Postgres +{ + public static IServiceCollection AddApplicationDbContext(this IServiceCollection services, IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString(DatabaseConfigName) + ?? throw new InvalidOperationException($"Connection string '{DatabaseConfigName}' was not found in configuration."); + + services.AddPooledDbContextFactory(builder => builder.UseNpgsql(connectionString)); + + return services; + } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj index b0d01a3..7a4e7e4 100644 --- a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj +++ b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj @@ -27,13 +27,14 @@ - + + From 8d77e99c75fc9fc71572e79a04e4b4c2d767a2eb Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 16 Aug 2026 14:21:34 +0200 Subject: [PATCH 29/50] Implemented token fetch --- .gitignore | 2 ++ .../PostFundManagement.Api.csproj | 4 +++- .../PostFundManagement.Api.http | 6 ------ PostFundManagement.Api/Program.cs | 9 ++++++++- .../appsettings.Development.json | 8 -------- PostFundManagement.Api/appsettings.json | 4 ++++ PostFundManagement.Api/http/api.http | 16 ++++++++++++++++ PostFundManagement.Api/http/token.http | 6 ++++++ .../Configuration/MachineIdentity.cs | 12 ++++++++++++ 9 files changed, 51 insertions(+), 16 deletions(-) delete mode 100644 PostFundManagement.Api/PostFundManagement.Api.http delete mode 100644 PostFundManagement.Api/appsettings.Development.json create mode 100644 PostFundManagement.Api/http/api.http create mode 100644 PostFundManagement.Api/http/token.http create mode 100644 PostFundManagement.Infrastructure/Configuration/MachineIdentity.cs diff --git a/.gitignore b/.gitignore index 6a182b5..9ead147 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ *.userosscache *.sln.docstates *.env +*.env.json # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -55,6 +56,7 @@ bld/ # Visual Studio 2015/2017 cache/options directory .vs/ +.vscode/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ diff --git a/PostFundManagement.Api/PostFundManagement.Api.csproj b/PostFundManagement.Api/PostFundManagement.Api.csproj index 958507a..a7e664e 100644 --- a/PostFundManagement.Api/PostFundManagement.Api.csproj +++ b/PostFundManagement.Api/PostFundManagement.Api.csproj @@ -1,14 +1,16 @@ - +ο»Ώ net10.0 enable enable + 59a05094-4ac2-472d-af04-3407e909432d + diff --git a/PostFundManagement.Api/PostFundManagement.Api.http b/PostFundManagement.Api/PostFundManagement.Api.http deleted file mode 100644 index 201cc3a..0000000 --- a/PostFundManagement.Api/PostFundManagement.Api.http +++ /dev/null @@ -1,6 +0,0 @@ -@PostFundManagement.Api_HostAddress = http://localhost:5112 - -GET {{PostFundManagement.Api_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/PostFundManagement.Api/Program.cs b/PostFundManagement.Api/Program.cs index ee9d65d..60e8031 100644 --- a/PostFundManagement.Api/Program.cs +++ b/PostFundManagement.Api/Program.cs @@ -1,3 +1,5 @@ +using Scalar.AspNetCore; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. @@ -9,6 +11,11 @@ var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { + app.MapScalarApiReference(options => + { + options.WithTitle("Post-fund Management API") + .WithTheme(ScalarTheme.BluePlanet); + }); app.MapOpenApi(); } @@ -21,7 +28,7 @@ var summaries = new[] app.MapGet("/weatherforecast", () => { - var forecast = Enumerable.Range(1, 5).Select(index => + var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), diff --git a/PostFundManagement.Api/appsettings.Development.json b/PostFundManagement.Api/appsettings.Development.json deleted file mode 100644 index 0c208ae..0000000 --- a/PostFundManagement.Api/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/PostFundManagement.Api/appsettings.json b/PostFundManagement.Api/appsettings.json index 10f68b8..a2002e3 100644 --- a/PostFundManagement.Api/appsettings.json +++ b/PostFundManagement.Api/appsettings.json @@ -1,4 +1,8 @@ { + "MachineIdentity": { + "Authority": "https://sts.security.khongisa.co.za", + "Audience": "pfm-api-dev" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/PostFundManagement.Api/http/api.http b/PostFundManagement.Api/http/api.http new file mode 100644 index 0000000..ca524aa --- /dev/null +++ b/PostFundManagement.Api/http/api.http @@ -0,0 +1,16 @@ +@hostname = api.example.com +@port = 8080 +@host = {{hostname}}:{{port}} +@contentType = application/json + +### +# @prompt username +# @prompt refCode Your reference code display on webpage +# @prompt otp Your one-time password in your mailbox +POST https://{{host}}/verify-otp/{{refCode}} HTTP/1.1 +Content-Type: {{contentType}} + +{ + "username": "{{username}}", + "otp": "{{otp}}" +} \ No newline at end of file diff --git a/PostFundManagement.Api/http/token.http b/PostFundManagement.Api/http/token.http new file mode 100644 index 0000000..8ecb8bf --- /dev/null +++ b/PostFundManagement.Api/http/token.http @@ -0,0 +1,6 @@ +### Token Request +POST https://{{authority}}/connect/token +Content-Type: application/x-www-form-urlencoded +Accept-Encoding: identity + +grant_type={{grantType}}&client_id={{clientId}}&client_secret={{clientSecret}}&scope={{scope}} diff --git a/PostFundManagement.Infrastructure/Configuration/MachineIdentity.cs b/PostFundManagement.Infrastructure/Configuration/MachineIdentity.cs new file mode 100644 index 0000000..2434107 --- /dev/null +++ b/PostFundManagement.Infrastructure/Configuration/MachineIdentity.cs @@ -0,0 +1,12 @@ +namespace PostFundManagement.Infrastructure.Configuration; + +public sealed class MachineIdentity +{ + public string? Authority { get; set; } + + public string? Audience { get; set; } + + public string? ClientId { get; set; } + + public string? ClientSecret { get; set; } +} \ No newline at end of file From b8789b0e2e882e98e36d5d5e1987015299821ddf Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 16 Aug 2026 14:24:44 +0200 Subject: [PATCH 30/50] Added git ignore to not include the bin folder on projects --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9ead147..d7bd59d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ obj/x64/ [Rr]eleases/x86/ bin/x86/ obj/x86/ +bin/ [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ From c0375229b69b215d4dc1858aa4d47cd3534a238c Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 16 Aug 2026 16:09:51 +0200 Subject: [PATCH 31/50] Added packages --- .../Abstractions/IEndpoint.cs | 6 + .../PostFundManagement.Domain.csproj | 213 ++++++++++++++++-- 2 files changed, 201 insertions(+), 18 deletions(-) create mode 100644 PostFundManagement.Domain/Abstractions/IEndpoint.cs diff --git a/PostFundManagement.Domain/Abstractions/IEndpoint.cs b/PostFundManagement.Domain/Abstractions/IEndpoint.cs new file mode 100644 index 0000000..8ed7056 --- /dev/null +++ b/PostFundManagement.Domain/Abstractions/IEndpoint.cs @@ -0,0 +1,6 @@ +namespace PostFundManagement.Domain.Abstractions; + +public interface IEndpoint +{ + void Map(IEndpointRouteBuilder builder); +} \ No newline at end of file diff --git a/PostFundManagement.Domain/PostFundManagement.Domain.csproj b/PostFundManagement.Domain/PostFundManagement.Domain.csproj index 48eb5a5..0d7ce7a 100644 --- a/PostFundManagement.Domain/PostFundManagement.Domain.csproj +++ b/PostFundManagement.Domain/PostFundManagement.Domain.csproj @@ -6,30 +6,207 @@ enable + - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From f3c405fca27e638b3e80691522272be95cfc5302 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 16 Aug 2026 16:12:42 +0200 Subject: [PATCH 32/50] Fixed metrix ambiguity --- PostFundManagement.Domain/PostFundManagement.Domain.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/PostFundManagement.Domain/PostFundManagement.Domain.csproj b/PostFundManagement.Domain/PostFundManagement.Domain.csproj index 0d7ce7a..ab9ebe2 100644 --- a/PostFundManagement.Domain/PostFundManagement.Domain.csproj +++ b/PostFundManagement.Domain/PostFundManagement.Domain.csproj @@ -201,8 +201,6 @@ - - From 5ab977e33591688839d084880739c4dccb358d15 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 20 Aug 2026 13:37:24 +0200 Subject: [PATCH 33/50] Added Invite, Notification and Activity --- .../Configuration/Activity.cs | 56 + .../Configuration/Award.cs | 2 +- .../Configuration/Evidence.cs | 16 +- .../Configuration/Instrument.cs | 9 +- .../Configuration/Invite.cs | 51 + .../Configuration/Issue.cs | 8 + .../Configuration/Milestone.cs | 1 + .../Configuration/Notification.cs | 36 + .../Configuration/Programme.cs | 7 - .../Configuration/Project.cs | 4 +- .../Entities/Activity.cs | 17 + .../Entities/Evidence.cs | 2 + .../Entities/Instrument.cs | 2 +- PostFundManagement.Domain/Entities/Invite.cs | 15 + PostFundManagement.Domain/Entities/Issue.cs | 2 + .../Entities/Notification.cs | 9 + .../Entities/Programme.cs | 2 +- PostFundManagement.Domain/Enums.cs | 79 + .../Extensions/Mappers.cs | 68 +- PostFundManagement.Domain/Models/Activity.cs | 32 + PostFundManagement.Domain/Models/Evidence.cs | 8 +- .../Models/Instrument.cs | 2 + PostFundManagement.Domain/Models/Invite.cs | 24 + PostFundManagement.Domain/Models/Issue.cs | 4 + PostFundManagement.Domain/Models/Milestone.cs | 2 + .../Models/Notification.cs | 28 + PostFundManagement.Domain/Models/Programme.cs | 2 - PostFundManagement.Domain/Models/Project.cs | 4 + .../Database/ApplicationDbContext.cs | 6 + ...3615_AddedActivityNotification.Designer.cs | 1966 +++++++++++++++++ ...0260820113615_AddedActivityNotification.cs | 455 ++++ .../ApplicationDbContextModelSnapshot.cs | 384 +++- pfm.dbml | 349 --- 33 files changed, 3259 insertions(+), 393 deletions(-) create mode 100644 PostFundManagement.Domain/Configuration/Activity.cs create mode 100644 PostFundManagement.Domain/Configuration/Invite.cs create mode 100644 PostFundManagement.Domain/Configuration/Notification.cs create mode 100644 PostFundManagement.Domain/Entities/Activity.cs create mode 100644 PostFundManagement.Domain/Entities/Invite.cs create mode 100644 PostFundManagement.Domain/Entities/Notification.cs create mode 100644 PostFundManagement.Domain/Models/Activity.cs create mode 100644 PostFundManagement.Domain/Models/Invite.cs create mode 100644 PostFundManagement.Domain/Models/Notification.cs create mode 100644 PostFundManagement.Infrastructure/Database/Migrations/20260820113615_AddedActivityNotification.Designer.cs create mode 100644 PostFundManagement.Infrastructure/Database/Migrations/20260820113615_AddedActivityNotification.cs delete mode 100644 pfm.dbml diff --git a/PostFundManagement.Domain/Configuration/Activity.cs b/PostFundManagement.Domain/Configuration/Activity.cs new file mode 100644 index 0000000..63a5194 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Activity.cs @@ -0,0 +1,56 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration; + +public sealed class Activity : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Activity).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.ProjectId).IsRequired(); + builder.Property(f => f.MilestoneId).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.UpdatedBy).IsRequired(false); + builder.Property(f => f.ApprovedBy).IsRequired(false); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.PerformedAt).IsRequired(false); + builder.Property(f => f.Category).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.Status).IsRequired().HasDefaultValueSql("1"); + builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.Title).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength); + + builder.HasOne(f => f.Project) + .WithMany() + .HasForeignKey(fk => fk.ProjectId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Milestone) + .WithMany() + .HasForeignKey(fk => fk.MilestoneId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Updator) + .WithMany() + .HasForeignKey(fk => fk.UpdatedBy) + .IsRequired(false) + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Approver) + .WithMany() + .HasForeignKey(fk => fk.ApprovedBy) + .IsRequired(false) + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/Award.cs b/PostFundManagement.Domain/Configuration/Award.cs index 1c63692..0c68765 100644 --- a/PostFundManagement.Domain/Configuration/Award.cs +++ b/PostFundManagement.Domain/Configuration/Award.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Configuration; -public class Award : IEntityTypeConfiguration +public sealed class Award : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/PostFundManagement.Domain/Configuration/Evidence.cs b/PostFundManagement.Domain/Configuration/Evidence.cs index a2aad3a..fc2ff03 100644 --- a/PostFundManagement.Domain/Configuration/Evidence.cs +++ b/PostFundManagement.Domain/Configuration/Evidence.cs @@ -10,6 +10,7 @@ public sealed class Evidence : IEntityTypeConfiguration builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.MilestoneId).IsRequired(); builder.Property(f => f.IndicatorId).IsRequired(); + builder.Property(f => f.ActivityId).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.UpdatedBy).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); @@ -17,6 +18,7 @@ public sealed class Evidence : IEntityTypeConfiguration builder.Property(f => f.Version).IsRequired().HasDefaultValue(1); builder.Property(f => f.DocumentUrl).IsRequired(); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); + builder.Property(f => f.Type).IsRequired().HasConversion().HasDefaultValueSql("9"); builder.HasOne(f => f.Project) .WithMany() @@ -27,7 +29,19 @@ public sealed class Evidence : IEntityTypeConfiguration builder.HasOne(f => f.Milestone) .WithMany() .HasForeignKey(fk => fk.MilestoneId) - .IsRequired(false) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Indicator) + .WithMany() + .HasForeignKey(fk => fk.IndicatorId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Activity) + .WithMany(f => f.Evidences) + .HasForeignKey(fk => fk.ActivityId) + .IsRequired() .OnDelete(DeleteBehavior.Restrict); builder.HasOne(f => f.Creator) diff --git a/PostFundManagement.Domain/Configuration/Instrument.cs b/PostFundManagement.Domain/Configuration/Instrument.cs index 28618ec..298f536 100644 --- a/PostFundManagement.Domain/Configuration/Instrument.cs +++ b/PostFundManagement.Domain/Configuration/Instrument.cs @@ -9,13 +9,20 @@ public sealed class Instrument : IEntityTypeConfiguration builder.ToTable(nameof(Entities.Instrument).Pluralize()); builder.HasKey(pk => pk.Id); + builder.Property(f => f.ProgrammeId).IsRequired(); builder.Property(f => f.Code).IsRequired().HasMaxLength(ShortLabelLength); builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Description).IsRequired(false).HasMaxLength(TextLength); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("2"); - builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.HasOne(f => f.Programme) + .WithMany(f => f.Instruments) + .HasForeignKey(fk => fk.ProgrammeId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + builder.HasOne(f => f.Creator) .WithMany() .HasForeignKey(fk => fk.CreatedBy) diff --git a/PostFundManagement.Domain/Configuration/Invite.cs b/PostFundManagement.Domain/Configuration/Invite.cs new file mode 100644 index 0000000..e76959b --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Invite.cs @@ -0,0 +1,51 @@ + +namespace PostFundManagement.Domain.Configuration; + +public sealed class Invite : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Invite).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.OrganisationId).IsRequired(); + builder.Property(f => f.InvitedOrganisationId).IsRequired(); + builder.Property(f => f.AwardId).IsRequired(); + builder.Property(f => f.ContractId).IsRequired(); + builder.Property(f => f.Recipient).IsRequired(); + builder.Property(f => f.CreatedBy).IsRequired(); + builder.Property(f => f.UpdatedBy).IsRequired(false); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + + builder.HasOne(f => f.Organisation) + .WithMany() + .HasForeignKey(fk => fk.OrganisationId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.InvitedOrganisation) + .WithMany() + .HasForeignKey(fk => fk.InvitedOrganisationId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Award) + .WithMany() + .HasForeignKey(fk => fk.AwardId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Contract) + .WithMany() + .HasForeignKey(fk => fk.ContractId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.RecipientUser) + .WithMany() + .HasForeignKey(fk => fk.Recipient) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/Issue.cs b/PostFundManagement.Domain/Configuration/Issue.cs index 3e42e2d..fd3f4fa 100644 --- a/PostFundManagement.Domain/Configuration/Issue.cs +++ b/PostFundManagement.Domain/Configuration/Issue.cs @@ -13,10 +13,12 @@ public sealed class Issue : IEntityTypeConfiguration builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.ResolvedAt).IsRequired(false); + builder.Property(f => f.AssignedTo).IsRequired(false); builder.Property(f => f.Title).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength); builder.Property(f => f.Priority).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); + builder.Property(f => f.Resolution).IsRequired(); builder.HasOne(f => f.Project) .WithMany() @@ -29,5 +31,11 @@ public sealed class Issue : IEntityTypeConfiguration .HasForeignKey(fk => fk.CreatedBy) .IsRequired() .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Assignee) + .WithMany() + .HasForeignKey(fk => fk.AssignedTo) + .IsRequired() + .OnDelete(DeleteBehavior.SetNull); } } \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/Milestone.cs b/PostFundManagement.Domain/Configuration/Milestone.cs index 5f16428..e38e4ce 100644 --- a/PostFundManagement.Domain/Configuration/Milestone.cs +++ b/PostFundManagement.Domain/Configuration/Milestone.cs @@ -17,6 +17,7 @@ public sealed class Milestone : IEntityTypeConfiguration builder.Property(f => f.DueAt).IsRequired(); builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); + builder.Property(f => f.Priority).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Project) .WithMany() diff --git a/PostFundManagement.Domain/Configuration/Notification.cs b/PostFundManagement.Domain/Configuration/Notification.cs new file mode 100644 index 0000000..c2fd488 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Notification.cs @@ -0,0 +1,36 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration; + +public sealed class Notification : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Entities.Notification).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.SentAt).IsRequired(false); + builder.Property(f => f.Recipient).IsRequired(); + builder.Property(f => f.OrganisationId).IsRequired(); + builder.Property(f => f.Platform).IsRequired().HasConversion().HasDefaultValueSql("1"); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); + builder.Property(f => f.Subject).IsRequired(false).HasMaxLength(LabelLength); + builder.Property(f => f.Message).IsRequired().HasMaxLength(LargeTextLength); + builder.Property(f => f.Destination).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.Error).IsRequired(false); + + builder.HasOne(f => f.Organisation) + .WithMany() + .HasForeignKey(fk => fk.OrganisationId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.RecipientUser) + .WithMany() + .HasForeignKey(fk => fk.Recipient) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/Programme.cs b/PostFundManagement.Domain/Configuration/Programme.cs index 8a2f211..a2b79c6 100644 --- a/PostFundManagement.Domain/Configuration/Programme.cs +++ b/PostFundManagement.Domain/Configuration/Programme.cs @@ -10,7 +10,6 @@ public sealed class Programme : IEntityTypeConfiguration builder.HasKey(pk => pk.Id); builder.Property(f => f.PortfolioId).IsRequired(); - builder.Property(f => f.InstrumentId).IsRequired(); builder.Property(f => f.OwnedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.UpdatedBy).IsRequired(false); @@ -25,12 +24,6 @@ public sealed class Programme : IEntityTypeConfiguration .IsRequired() .OnDelete(DeleteBehavior.NoAction); - builder.HasOne(f => f.Instrument) - .WithMany(f => f.Programmes) - .HasForeignKey(fk => fk.InstrumentId) - .IsRequired() - .OnDelete(DeleteBehavior.NoAction); - builder.HasOne(f => f.Creator) .WithMany() .HasForeignKey(fk => fk.CreatedBy) diff --git a/PostFundManagement.Domain/Configuration/Project.cs b/PostFundManagement.Domain/Configuration/Project.cs index 3ea0a79..e3e833d 100644 --- a/PostFundManagement.Domain/Configuration/Project.cs +++ b/PostFundManagement.Domain/Configuration/Project.cs @@ -18,13 +18,15 @@ public sealed class Project : IEntityTypeConfiguration builder.Property(f => f.EndedAt).IsRequired(false); builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength); + builder.Property(f => f.Sector).IsRequired().HasConversion().HasDefaultValueSql("11"); + builder.Property(f => f.ProjectType).IsRequired().HasConversion().HasDefaultValueSql("10"); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); builder.HasOne(f => f.Programme) .WithMany(f => f.Projects) .HasForeignKey(fk => fk.ProgrammeId) .IsRequired() - .OnDelete(DeleteBehavior.NoAction); + .OnDelete(DeleteBehavior.Restrict); builder.HasOne(f => f.Creator) .WithMany() diff --git a/PostFundManagement.Domain/Entities/Activity.cs b/PostFundManagement.Domain/Entities/Activity.cs new file mode 100644 index 0000000..07ea73b --- /dev/null +++ b/PostFundManagement.Domain/Entities/Activity.cs @@ -0,0 +1,17 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Activity : Models.Activity +{ + public virtual User? Creator { get; set; } + + public virtual User? Updator { get; set; } + + public virtual User? Approver { get; set; } + + public virtual Project? Project { get; set; } + + public virtual Milestone? Milestone { get; set; } + + public virtual ICollection Evidences { get; set; } = []; +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Evidence.cs b/PostFundManagement.Domain/Entities/Evidence.cs index 4e54706..7023532 100644 --- a/PostFundManagement.Domain/Entities/Evidence.cs +++ b/PostFundManagement.Domain/Entities/Evidence.cs @@ -9,6 +9,8 @@ public class Evidence : Models.Evidence public virtual Indicator? Indicator { get; set; } + public virtual Activity? Activity { get; set; } + public virtual User? Creator { get; set; } public virtual User? Updator { get; set; } diff --git a/PostFundManagement.Domain/Entities/Instrument.cs b/PostFundManagement.Domain/Entities/Instrument.cs index b3be8e0..d43f19c 100644 --- a/PostFundManagement.Domain/Entities/Instrument.cs +++ b/PostFundManagement.Domain/Entities/Instrument.cs @@ -5,5 +5,5 @@ public class Instrument : Models.Instrument { public virtual User? Creator { get; set; } - public virtual ICollection Programmes { get; set; } = []; + public virtual Programme? Programme { get; set; } } \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Invite.cs b/PostFundManagement.Domain/Entities/Invite.cs new file mode 100644 index 0000000..a444f3c --- /dev/null +++ b/PostFundManagement.Domain/Entities/Invite.cs @@ -0,0 +1,15 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Invite : Models.Invite +{ + public virtual Organisation? Organisation { get; set; } + + public virtual Organisation? InvitedOrganisation { get; set; } + + public virtual Award? Award { get; set; } + + public virtual Contract? Contract { get; set; } + + public virtual User? RecipientUser { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Issue.cs b/PostFundManagement.Domain/Entities/Issue.cs index 91a63b8..a3cf5ef 100644 --- a/PostFundManagement.Domain/Entities/Issue.cs +++ b/PostFundManagement.Domain/Entities/Issue.cs @@ -6,4 +6,6 @@ public class Issue : Models.Issue public virtual Project? Project { get; set; } public virtual User? Creator { get; set; } + + public virtual User? Assignee { get; set; } } \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Notification.cs b/PostFundManagement.Domain/Entities/Notification.cs new file mode 100644 index 0000000..f14b0fc --- /dev/null +++ b/PostFundManagement.Domain/Entities/Notification.cs @@ -0,0 +1,9 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +public class Notification : Models.Notification +{ + public virtual Organisation? Organisation { get; set; } + + public virtual User? RecipientUser { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Entities/Programme.cs b/PostFundManagement.Domain/Entities/Programme.cs index fc76353..5c98681 100644 --- a/PostFundManagement.Domain/Entities/Programme.cs +++ b/PostFundManagement.Domain/Entities/Programme.cs @@ -9,7 +9,7 @@ public class Programme : Models.Programme public virtual Portfolio? Portfolio { get; set; } - public virtual Instrument? Instrument { get; set; } + public virtual ICollection Instruments { get; set; } = []; public virtual ICollection Rules { get; set; } = []; diff --git a/PostFundManagement.Domain/Enums.cs b/PostFundManagement.Domain/Enums.cs index 770d556..d251779 100644 --- a/PostFundManagement.Domain/Enums.cs +++ b/PostFundManagement.Domain/Enums.cs @@ -1,5 +1,84 @@ namespace PostFundManagement.Domain; +public enum NotificationStatus : int +{ + Pending = 1, + Queued = 2, + Processing = 3, + Sent = 4, + Delivered = 5, + Read = 6, + Failed = 7, + Cancelled = 8, + Bounced = 9 +} + +public enum NotificationPlatform : int +{ + Email = 1, + SMS = 2, + WhatsApp = 3, + PushNotification = 4, + InApp = 5, + Teams = 6, + Slack = 7, + Webhook = 8, + Other = 99 +} + +public enum EvidenceType : int +{ + AttendanceRegister = 1, + BankStatement = 2, + CompletionCertificate = 3, + FinancialInvoiceOrReceipt = 4, + GeotaggedPhoto = 5, + InspectionReport = 6, + MeetingMinutes = 7, + PayrollOrStipendRegister = 8, + ProcurementDocument = 9, + TechnicalSpecificationOrDesign = 10, + VideoRecording = 11, + Other = 99 +} + +public enum ProjectType : int +{ + CapacityBuildingAndTraining = 1, + CapitalExpenditureAndInfrastructure = 2, + CommercializationAndScaling = 3, + FeasibilityAndTechnicalAssistance = 4, + OperationalGrantAndSubsidy = 5, + ProductDevelopmentAndPrototyping = 6, + ResearchAndDevelopment = 7, + SocialImpactAndCommunityDevelopment = 8, + TechnologyTransferAndAdoption = 9, + Innovation = 10, + Other = 99 +} + +public enum IndustrySector : int +{ + AgricultureAndAgroProcessing = 1, + AutomotiveAndTransportEquipment = 2, + ChemicalsAndPharmaceuticals = 3, + ConstructionAndInfrastructure = 4, + CreativeIndustriesAndMedia = 5, + DefenseAndAerospace = 6, + EducationAndSkillsDevelopment = 7, + EnergyAndUtilities = 8, + FinancialServices = 9, + HealthcareAndLifeSciences = 10, + InformationCommunicationTechnology = 11, + ManufacturingAndIndustrial = 12, + MiningAndMetals = 13, + RealEstateAndHousing = 14, + RetailAndConsumerGoods = 15, + TourismAndHospitality = 16, + WaterAndSanitation = 17, + Other = 99 +} + public enum OrganisationType : int { // Non-Profit & Civil Society diff --git a/PostFundManagement.Domain/Extensions/Mappers.cs b/PostFundManagement.Domain/Extensions/Mappers.cs index fc720d9..d9153cd 100644 --- a/PostFundManagement.Domain/Extensions/Mappers.cs +++ b/PostFundManagement.Domain/Extensions/Mappers.cs @@ -4,6 +4,25 @@ namespace PostFundManagement.Domain.Extensions; public static class Mappers { + public static Activity Map(this Entities.Activity entity) => new() + { + Id = entity.Id, + Amount = entity.Amount, + ApprovedBy = entity.ApprovedBy, + Category = entity.Category, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Description = entity.Description, + MilestoneId = entity.MilestoneId, + PerformedAt = entity.PerformedAt, + ProjectId = entity.ProjectId, + Status = entity.Status, + Title = entity.Title, + UpdatedAt = entity.UpdatedAt, + UpdatedBy = entity.UpdatedBy + }; + + public static AuditLog Map(this Entities.AuditLog entity) => new() { Id = entity.Id, @@ -108,7 +127,9 @@ public static class Mappers Status = entity.Status, UpdatedAt = entity.UpdatedAt, UpdatedBy = entity.UpdatedBy, - Version = entity.Version + Version = entity.Version, + ActivityId = entity.ActivityId, + Type = entity.Type }; public static Indicator Map(this Entities.Indicator entity) => new() @@ -132,7 +153,22 @@ public static class Mappers CreatedBy = entity.CreatedBy, Description = entity.Description, Name = entity.Name, - Status = entity.Status + Status = entity.Status, + ProgrammeId = entity.ProgrammeId + }; + + public static Invite Map(this Entities.Invite entity) => new() + { + Id = entity.Id, + AwardId = entity.AwardId, + ContractId = entity.ContractId, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + InvitedOrganisationId = entity.InvitedOrganisationId, + OrganisationId = entity.OrganisationId, + Recipient = entity.Recipient, + UpdatedAt = entity.UpdatedAt, + UpdatedBy = entity.UpdatedBy }; public static Issue Map(this Entities.Issue entity) => new() @@ -145,7 +181,9 @@ public static class Mappers ProjectId = entity.ProjectId, ResolvedAt = entity.ResolvedAt, Status = entity.Status, - Title = entity.Title + Title = entity.Title, + AssignedTo = entity.AssignedTo, + Resolution = entity.Resolution }; public static Milestone Map(this Entities.Milestone entity) => new() @@ -158,7 +196,8 @@ public static class Mappers ProjectId = entity.ProjectId, Status = entity.Status, UpdatedAt = entity.UpdatedAt, - UpdatedBy = entity.UpdatedBy + UpdatedBy = entity.UpdatedBy, + Priority = entity.Priority }; public static Organisation Map(this Entities.Organisation entity) => new() @@ -193,12 +232,27 @@ public static class Mappers OwnedBy = entity.OwnedBy }; + public static Notification Map(this Entities.Notification entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + Destination = entity.Destination, + Error = entity.Error, + Message = entity.Message, + OrganisationId = entity.OrganisationId, + Platform = entity.Platform, + Recipient = entity.Recipient, + SentAt = entity.SentAt, + Status = entity.Status, + Subject = entity.Subject, + UpdatedAt = entity.UpdatedAt + }; + public static Programme Map(this Entities.Programme entity) => new() { Id = entity.Id, CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, - InstrumentId = entity.InstrumentId, Name = entity.Name, OwnedBy = entity.OwnedBy, PortfolioId = entity.PortfolioId, @@ -219,7 +273,9 @@ public static class Mappers StartedAt = entity.StartedAt, Status = entity.Status, UpdatedAt = entity.UpdatedAt, - UpdatedBy = entity.UpdatedBy + UpdatedBy = entity.UpdatedBy, + ProjectType = entity.ProjectType, + Sector = entity.Sector }; public static Risk Map(this Entities.Risk entity) => new() diff --git a/PostFundManagement.Domain/Models/Activity.cs b/PostFundManagement.Domain/Models/Activity.cs new file mode 100644 index 0000000..8235199 --- /dev/null +++ b/PostFundManagement.Domain/Models/Activity.cs @@ -0,0 +1,32 @@ +namespace PostFundManagement.Domain.Models; + +public class Activity +{ + public long Id { get; set; } + + public long? ProjectId { get; set; } + + public long? MilestoneId { get; set; } + + public Guid CreatedBy { get; set; } + + public Guid? UpdatedBy { get; set; } + + public Guid? ApprovedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public string? Category { get; set; } + + public ApprovalStatus Status { get; set; } + + public decimal Amount { get; set; } + + public string? Title { get; set; } + + public string? Description { get; set; } + + public DateTime? PerformedAt { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Evidence.cs b/PostFundManagement.Domain/Models/Evidence.cs index 14c86a9..ac4bd82 100644 --- a/PostFundManagement.Domain/Models/Evidence.cs +++ b/PostFundManagement.Domain/Models/Evidence.cs @@ -6,9 +6,11 @@ public class Evidence public long ProjectId { get; set; } - public long? MilestoneId { get; set; } + public long MilestoneId { get; set; } - public long? IndicatorId { get; set; } + public long IndicatorId { get; set; } + + public long ActivityId { get; set; } public Guid CreatedBy { get; set; } @@ -20,6 +22,8 @@ public class Evidence public int Version { get; set; } + public EvidenceType Type { get; set; } + public string? DocumentUrl { get; set; } public ApprovalStatus Status { get; set; } diff --git a/PostFundManagement.Domain/Models/Instrument.cs b/PostFundManagement.Domain/Models/Instrument.cs index 1ddbfd3..3cd09ab 100644 --- a/PostFundManagement.Domain/Models/Instrument.cs +++ b/PostFundManagement.Domain/Models/Instrument.cs @@ -4,6 +4,8 @@ public class Instrument { public long Id { get; set; } + public long ProgrammeId { get; set; } + public string? Code { get; set; } public string? Name { get; set; } diff --git a/PostFundManagement.Domain/Models/Invite.cs b/PostFundManagement.Domain/Models/Invite.cs new file mode 100644 index 0000000..f78ac27 --- /dev/null +++ b/PostFundManagement.Domain/Models/Invite.cs @@ -0,0 +1,24 @@ +namespace PostFundManagement.Domain.Models; + +public class Invite +{ + public long Id { get; set; } + + public long OrganisationId { get; set; } + + public long InvitedOrganisationId { get; set; } + + public long AwardId { get; set; } + + public long ContractId { get; set; } + + public Guid Recipient { get; set; } + + public Guid CreatedBy { get; set; } + + public Guid? UpdatedBy { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Issue.cs b/PostFundManagement.Domain/Models/Issue.cs index e9ad515..4bdbf45 100644 --- a/PostFundManagement.Domain/Models/Issue.cs +++ b/PostFundManagement.Domain/Models/Issue.cs @@ -18,5 +18,9 @@ public class Issue public DateTime CreatedAt { get; set; } + public Guid? AssignedTo { get; set; } + public DateTime? ResolvedAt { get; set; } + + public string? Resolution { get; set; } } \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Milestone.cs b/PostFundManagement.Domain/Models/Milestone.cs index 1da57b7..c42d74e 100644 --- a/PostFundManagement.Domain/Models/Milestone.cs +++ b/PostFundManagement.Domain/Models/Milestone.cs @@ -19,4 +19,6 @@ public class Milestone public string? Name { get; set; } public ApprovalStatus Status { get; set; } + + public Priority Priority { get; set; } } \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Notification.cs b/PostFundManagement.Domain/Models/Notification.cs new file mode 100644 index 0000000..affa356 --- /dev/null +++ b/PostFundManagement.Domain/Models/Notification.cs @@ -0,0 +1,28 @@ +namespace PostFundManagement.Domain.Models; + +public class Notification +{ + public long Id { get; set; } + + public long OrganisationId { get; set; } + + public Guid Recipient { get; set; } + + public NotificationPlatform Platform { get; set; } + + public NotificationStatus Status { get; set; } + + public string? Subject { get; set; } + + public string? Message { get; set; } + + public string? Destination { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? SentAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public string? Error { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Programme.cs b/PostFundManagement.Domain/Models/Programme.cs index d04372d..e05fd15 100644 --- a/PostFundManagement.Domain/Models/Programme.cs +++ b/PostFundManagement.Domain/Models/Programme.cs @@ -6,8 +6,6 @@ public class Programme public long PortfolioId { get; set; } - public long InstrumentId { get; set; } - public Guid OwnedBy { get; set; } public Guid CreatedBy { get; set; } diff --git a/PostFundManagement.Domain/Models/Project.cs b/PostFundManagement.Domain/Models/Project.cs index 4a09337..795f75a 100644 --- a/PostFundManagement.Domain/Models/Project.cs +++ b/PostFundManagement.Domain/Models/Project.cs @@ -18,6 +18,10 @@ public class Project public DateTime? EndedAt { get; set; } + public IndustrySector Sector { get; set; } + + public ProjectType ProjectType { get; set; } + public string? Name { get; set; } public string? Description { get; set; } diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs index 044a49f..0f4a87b 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -4,6 +4,12 @@ namespace PostFundManagement.Infrastructure.Database; public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Notifications => Set(); + + public DbSet Invites => Set(); + + public DbSet Activities => Set(); + public DbSet AuditLogs => Set(); public DbSet SiteVisits => Set(); diff --git a/PostFundManagement.Infrastructure/Database/Migrations/20260820113615_AddedActivityNotification.Designer.cs b/PostFundManagement.Infrastructure/Database/Migrations/20260820113615_AddedActivityNotification.Designer.cs new file mode 100644 index 0000000..f683f64 --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Migrations/20260820113615_AddedActivityNotification.Designer.cs @@ -0,0 +1,1966 @@ +ο»Ώ// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PostFundManagement.Infrastructure.Database; + +#nullable disable + +namespace PostFundManagement.Infrastructure.Database.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260820113615_AddedActivityNotification")] + partial class AddedActivityNotification + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Activity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("MilestoneId") + .HasColumnType("bigint"); + + b.Property("PerformedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("MilestoneId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Activities", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.PrimitiveCollection("Changes") + .HasColumnType("jsonb"); + + b.Property("EntityId") + .HasColumnType("bigint"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PerformedBy") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("PerformedBy"); + + b.ToTable("AuditLogs", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("OrganisationId"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Awards", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Beneficiary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Location") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("ReachedCount") + .HasColumnType("integer"); + + b.Property("TargetCount") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UserId"); + + b.ToTable("Beneficiaries", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Budget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Budgets", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ChangeRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Justification") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("RevisedBudget") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("RevisedEndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("ChangeRequests", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ComplianceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Requirements") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerifiedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("VerifiedBy"); + + b.ToTable("ComplianceItems", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Contract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AwardId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("EffectiveAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalSignatureId") + .HasColumnType("text"); + + b.Property("SignedDocumentUrl") + .HasColumnType("text"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TotalValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("AwardId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.ToTable("Contracts", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Disbursement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("MilestoneId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("MilestoneId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Disbursements", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Evidence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("IndicatorId") + .HasColumnType("bigint"); + + b.Property("MilestoneId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("9"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.HasKey("Id"); + + b.HasIndex("ActivityId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("MilestoneId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Evidences", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Indicator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActualAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BaselineAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("TargetAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UnitOfMeasure") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Indicators", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Instrument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProgrammeId"); + + b.ToTable("Instruments", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Invite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AwardId") + .HasColumnType("bigint"); + + b.Property("ContractId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("InvitedOrganisationId") + .HasColumnType("bigint"); + + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("Recipient") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AwardId"); + + b.HasIndex("ContractId"); + + b.HasIndex("InvitedOrganisationId"); + + b.HasIndex("OrganisationId"); + + b.HasIndex("Recipient"); + + b.ToTable("Invites", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Issue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AssignedTo") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Resolution") + .IsRequired() + .HasColumnType("text"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("AssignedTo"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Issues", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Milestone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DueAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Milestones", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Destination") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Error") + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("Platform") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Recipient") + .HasColumnType("uuid"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Subject") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganisationId"); + + b.HasIndex("Recipient"); + + b.ToTable("Notifications", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Organisation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RegistrationNo") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("5"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Organisations", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Outcome", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BeneficiaryId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("BeneficiaryId"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Outcomes", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("OwnedBy"); + + b.ToTable("Portfolios", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnedBy") + .HasColumnType("uuid"); + + b.Property("PortfolioId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("OwnedBy"); + + b.HasIndex("PortfolioId"); + + b.ToTable("Programmes", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("ProjectType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("10"); + + b.Property("Sector") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("11"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Risk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Impact") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Likelihood") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("3"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("Risks", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Rule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Rules", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.SiteVisit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Findings") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("InspectorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("VisitedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.ToTable("SiteVisits", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.HasKey("Id"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Activity", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Milestone", "Milestone") + .WithMany() + .HasForeignKey("MilestoneId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Milestone"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.AuditLog", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Performer") + .WithMany() + .HasForeignKey("PerformedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Performer"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Awards") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany("Awards") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Organisation"); + + b.Navigation("Programme"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Beneficiary", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Budget", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ChangeRequest", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ComplianceItem", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Verifier") + .WithMany() + .HasForeignKey("VerifiedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Verifier"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Contract", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Award", "Award") + .WithOne("Contract") + .HasForeignKey("PostFundManagement.Domain.Entities.Contract", "AwardId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Award"); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Disbursement", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Milestone", "Milestone") + .WithMany() + .HasForeignKey("MilestoneId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Milestone"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Evidence", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Activity", "Activity") + .WithMany("Evidences") + .HasForeignKey("ActivityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Indicator", "Indicator") + .WithMany() + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Milestone", "Milestone") + .WithMany() + .HasForeignKey("MilestoneId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Activity"); + + b.Navigation("Creator"); + + b.Navigation("Indicator"); + + b.Navigation("Milestone"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Indicator", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Instrument", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Instruments") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Programme"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Invite", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Award", "Award") + .WithMany() + .HasForeignKey("AwardId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Contract", "Contract") + .WithMany() + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "InvitedOrganisation") + .WithMany() + .HasForeignKey("InvitedOrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "RecipientUser") + .WithMany() + .HasForeignKey("Recipient") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Award"); + + b.Navigation("Contract"); + + b.Navigation("InvitedOrganisation"); + + b.Navigation("Organisation"); + + b.Navigation("RecipientUser"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Issue", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Assignee") + .WithMany() + .HasForeignKey("AssignedTo") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Assignee"); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Milestone", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Notification", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "RecipientUser") + .WithMany() + .HasForeignKey("Recipient") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organisation"); + + b.Navigation("RecipientUser"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Organisation", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Outcome", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Beneficiary", "Beneficiary") + .WithMany() + .HasForeignKey("BeneficiaryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Beneficiary"); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Portfolio", "Portfolio") + .WithMany("Programmes") + .HasForeignKey("PortfolioId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Owner"); + + b.Navigation("Portfolio"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Projects") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Programme"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Risk", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Rule", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Rules") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Programme"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.SiteVisit", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Activity", b => + { + b.Navigation("Evidences"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.Navigation("Contract"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.Navigation("Programmes"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.Navigation("Awards"); + + b.Navigation("Instruments"); + + b.Navigation("Projects"); + + b.Navigation("Rules"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.Navigation("Awards"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PostFundManagement.Infrastructure/Database/Migrations/20260820113615_AddedActivityNotification.cs b/PostFundManagement.Infrastructure/Database/Migrations/20260820113615_AddedActivityNotification.cs new file mode 100644 index 0000000..d7cd6db --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Migrations/20260820113615_AddedActivityNotification.cs @@ -0,0 +1,455 @@ +ο»Ώusing System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace PostFundManagement.Infrastructure.Database.Migrations +{ + /// + public partial class AddedActivityNotification : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Evidences_Indicators_IndicatorId", + table: "Evidences"); + + migrationBuilder.DropForeignKey( + name: "FK_Programmes_Instruments_InstrumentId", + table: "Programmes"); + + migrationBuilder.DropForeignKey( + name: "FK_Projects_Programmes_ProgrammeId", + table: "Projects"); + + migrationBuilder.DropIndex( + name: "IX_Programmes_InstrumentId", + table: "Programmes"); + + migrationBuilder.DropColumn( + name: "InstrumentId", + table: "Programmes"); + + migrationBuilder.AddColumn( + name: "ProjectType", + table: "Projects", + type: "integer", + nullable: false, + defaultValueSql: "10"); + + migrationBuilder.AddColumn( + name: "Sector", + table: "Projects", + type: "integer", + nullable: false, + defaultValueSql: "11"); + + migrationBuilder.AddColumn( + name: "Priority", + table: "Milestones", + type: "integer", + nullable: false, + defaultValueSql: "1"); + + migrationBuilder.AddColumn( + name: "AssignedTo", + table: "Issues", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "Resolution", + table: "Issues", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "ProgrammeId", + table: "Instruments", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "ActivityId", + table: "Evidences", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "Type", + table: "Evidences", + type: "integer", + nullable: false, + defaultValueSql: "9"); + + migrationBuilder.CreateTable( + name: "Activities", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectId = table.Column(type: "bigint", nullable: false), + MilestoneId = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: true), + ApprovedBy = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Category = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "1"), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Title = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Description = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + PerformedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Activities", x => x.Id); + table.ForeignKey( + name: "FK_Activities_Milestones_MilestoneId", + column: x => x.MilestoneId, + principalTable: "Milestones", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Activities_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Activities_Users_ApprovedBy", + column: x => x.ApprovedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Activities_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Activities_Users_UpdatedBy", + column: x => x.UpdatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Invites", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + OrganisationId = table.Column(type: "bigint", nullable: false), + InvitedOrganisationId = table.Column(type: "bigint", nullable: false), + AwardId = table.Column(type: "bigint", nullable: false), + ContractId = table.Column(type: "bigint", nullable: false), + Recipient = table.Column(type: "uuid", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Invites", x => x.Id); + table.ForeignKey( + name: "FK_Invites_Awards_AwardId", + column: x => x.AwardId, + principalTable: "Awards", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Invites_Contracts_ContractId", + column: x => x.ContractId, + principalTable: "Contracts", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Invites_Organisations_InvitedOrganisationId", + column: x => x.InvitedOrganisationId, + principalTable: "Organisations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Invites_Organisations_OrganisationId", + column: x => x.OrganisationId, + principalTable: "Organisations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Invites_Users_Recipient", + column: x => x.Recipient, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Notifications", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + OrganisationId = table.Column(type: "bigint", nullable: false), + Recipient = table.Column(type: "uuid", nullable: false), + Platform = table.Column(type: "integer", nullable: false, defaultValueSql: "1"), + Status = table.Column(type: "integer", nullable: false, defaultValueSql: "1"), + Subject = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Message = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: false), + Destination = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + SentAt = table.Column(type: "timestamp with time zone", nullable: true), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Error = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Notifications", x => x.Id); + table.ForeignKey( + name: "FK_Notifications_Organisations_OrganisationId", + column: x => x.OrganisationId, + principalTable: "Organisations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Notifications_Users_Recipient", + column: x => x.Recipient, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Issues_AssignedTo", + table: "Issues", + column: "AssignedTo"); + + migrationBuilder.CreateIndex( + name: "IX_Instruments_ProgrammeId", + table: "Instruments", + column: "ProgrammeId"); + + migrationBuilder.CreateIndex( + name: "IX_Evidences_ActivityId", + table: "Evidences", + column: "ActivityId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_ApprovedBy", + table: "Activities", + column: "ApprovedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_CreatedBy", + table: "Activities", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_MilestoneId", + table: "Activities", + column: "MilestoneId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_ProjectId", + table: "Activities", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_Activities_UpdatedBy", + table: "Activities", + column: "UpdatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Invites_AwardId", + table: "Invites", + column: "AwardId"); + + migrationBuilder.CreateIndex( + name: "IX_Invites_ContractId", + table: "Invites", + column: "ContractId"); + + migrationBuilder.CreateIndex( + name: "IX_Invites_InvitedOrganisationId", + table: "Invites", + column: "InvitedOrganisationId"); + + migrationBuilder.CreateIndex( + name: "IX_Invites_OrganisationId", + table: "Invites", + column: "OrganisationId"); + + migrationBuilder.CreateIndex( + name: "IX_Invites_Recipient", + table: "Invites", + column: "Recipient"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_OrganisationId", + table: "Notifications", + column: "OrganisationId"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_Recipient", + table: "Notifications", + column: "Recipient"); + + migrationBuilder.AddForeignKey( + name: "FK_Evidences_Activities_ActivityId", + table: "Evidences", + column: "ActivityId", + principalTable: "Activities", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Evidences_Indicators_IndicatorId", + table: "Evidences", + column: "IndicatorId", + principalTable: "Indicators", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Instruments_Programmes_ProgrammeId", + table: "Instruments", + column: "ProgrammeId", + principalTable: "Programmes", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Issues_Users_AssignedTo", + table: "Issues", + column: "AssignedTo", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + + migrationBuilder.AddForeignKey( + name: "FK_Projects_Programmes_ProgrammeId", + table: "Projects", + column: "ProgrammeId", + principalTable: "Programmes", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Evidences_Activities_ActivityId", + table: "Evidences"); + + migrationBuilder.DropForeignKey( + name: "FK_Evidences_Indicators_IndicatorId", + table: "Evidences"); + + migrationBuilder.DropForeignKey( + name: "FK_Instruments_Programmes_ProgrammeId", + table: "Instruments"); + + migrationBuilder.DropForeignKey( + name: "FK_Issues_Users_AssignedTo", + table: "Issues"); + + migrationBuilder.DropForeignKey( + name: "FK_Projects_Programmes_ProgrammeId", + table: "Projects"); + + migrationBuilder.DropTable( + name: "Activities"); + + migrationBuilder.DropTable( + name: "Invites"); + + migrationBuilder.DropTable( + name: "Notifications"); + + migrationBuilder.DropIndex( + name: "IX_Issues_AssignedTo", + table: "Issues"); + + migrationBuilder.DropIndex( + name: "IX_Instruments_ProgrammeId", + table: "Instruments"); + + migrationBuilder.DropIndex( + name: "IX_Evidences_ActivityId", + table: "Evidences"); + + migrationBuilder.DropColumn( + name: "ProjectType", + table: "Projects"); + + migrationBuilder.DropColumn( + name: "Sector", + table: "Projects"); + + migrationBuilder.DropColumn( + name: "Priority", + table: "Milestones"); + + migrationBuilder.DropColumn( + name: "AssignedTo", + table: "Issues"); + + migrationBuilder.DropColumn( + name: "Resolution", + table: "Issues"); + + migrationBuilder.DropColumn( + name: "ProgrammeId", + table: "Instruments"); + + migrationBuilder.DropColumn( + name: "ActivityId", + table: "Evidences"); + + migrationBuilder.DropColumn( + name: "Type", + table: "Evidences"); + + migrationBuilder.AddColumn( + name: "InstrumentId", + table: "Programmes", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.CreateIndex( + name: "IX_Programmes_InstrumentId", + table: "Programmes", + column: "InstrumentId"); + + migrationBuilder.AddForeignKey( + name: "FK_Evidences_Indicators_IndicatorId", + table: "Evidences", + column: "IndicatorId", + principalTable: "Indicators", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Programmes_Instruments_InstrumentId", + table: "Programmes", + column: "InstrumentId", + principalTable: "Instruments", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Projects_Programmes_ProgrammeId", + table: "Projects", + column: "ProgrammeId", + principalTable: "Programmes", + principalColumn: "Id"); + } + } +} diff --git a/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs b/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs index b90e527..a739df6 100644 --- a/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs @@ -22,6 +22,79 @@ namespace PostFundManagement.Infrastructure.Database.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("PostFundManagement.Domain.Entities.Activity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("MilestoneId") + .HasColumnType("bigint"); + + b.Property("PerformedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("MilestoneId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Activities", (string)null); + }); + modelBuilder.Entity("PostFundManagement.Domain.Entities.AuditLog", b => { b.Property("Id") @@ -436,6 +509,9 @@ namespace PostFundManagement.Infrastructure.Database.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("ActivityId") + .HasColumnType("bigint"); + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -462,6 +538,11 @@ namespace PostFundManagement.Infrastructure.Database.Migrations .HasColumnType("integer") .HasDefaultValueSql("1"); + b.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("9"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -475,6 +556,8 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.HasKey("Id"); + b.HasIndex("ActivityId"); + b.HasIndex("CreatedBy"); b.HasIndex("IndicatorId"); @@ -568,6 +651,9 @@ namespace PostFundManagement.Infrastructure.Database.Migrations .HasMaxLength(256) .HasColumnType("character varying(256)"); + b.Property("ProgrammeId") + .HasColumnType("bigint"); + b.Property("Status") .ValueGeneratedOnAdd() .HasColumnType("integer") @@ -577,9 +663,63 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.HasIndex("CreatedBy"); + b.HasIndex("ProgrammeId"); + b.ToTable("Instruments", (string)null); }); + modelBuilder.Entity("PostFundManagement.Domain.Entities.Invite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AwardId") + .HasColumnType("bigint"); + + b.Property("ContractId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("InvitedOrganisationId") + .HasColumnType("bigint"); + + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("Recipient") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AwardId"); + + b.HasIndex("ContractId"); + + b.HasIndex("InvitedOrganisationId"); + + b.HasIndex("OrganisationId"); + + b.HasIndex("Recipient"); + + b.ToTable("Invites", (string)null); + }); + modelBuilder.Entity("PostFundManagement.Domain.Entities.Issue", b => { b.Property("Id") @@ -588,6 +728,9 @@ namespace PostFundManagement.Infrastructure.Database.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AssignedTo") + .HasColumnType("uuid"); + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -609,6 +752,10 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.Property("ProjectId") .HasColumnType("bigint"); + b.Property("Resolution") + .IsRequired() + .HasColumnType("text"); + b.Property("ResolvedAt") .HasColumnType("timestamp with time zone"); @@ -624,6 +771,8 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.HasKey("Id"); + b.HasIndex("AssignedTo"); + b.HasIndex("CreatedBy"); b.HasIndex("ProjectId"); @@ -655,6 +804,11 @@ namespace PostFundManagement.Infrastructure.Database.Migrations .HasMaxLength(256) .HasColumnType("character varying(256)"); + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + b.Property("ProjectId") .HasColumnType("bigint"); @@ -680,6 +834,67 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.ToTable("Milestones", (string)null); }); + modelBuilder.Entity("PostFundManagement.Domain.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Destination") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Error") + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("Platform") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Recipient") + .HasColumnType("uuid"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Subject") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganisationId"); + + b.HasIndex("Recipient"); + + b.ToTable("Notifications", (string)null); + }); + modelBuilder.Entity("PostFundManagement.Domain.Entities.Organisation", b => { b.Property("Id") @@ -819,9 +1034,6 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.Property("CreatedBy") .HasColumnType("uuid"); - b.Property("InstrumentId") - .HasColumnType("bigint"); - b.Property("Name") .IsRequired() .HasMaxLength(256) @@ -848,8 +1060,6 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.HasIndex("CreatedBy"); - b.HasIndex("InstrumentId"); - b.HasIndex("OwnedBy"); b.HasIndex("PortfolioId"); @@ -889,6 +1099,16 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.Property("ProgrammeId") .HasColumnType("bigint"); + b.Property("ProjectType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("10"); + + b.Property("Sector") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("11"); + b.Property("StartedAt") .HasColumnType("timestamp with time zone"); @@ -1081,6 +1301,47 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.ToTable("Users", (string)null); }); + modelBuilder.Entity("PostFundManagement.Domain.Entities.Activity", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Milestone", "Milestone") + .WithMany() + .HasForeignKey("MilestoneId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Milestone"); + + b.Navigation("Project"); + + b.Navigation("Updator"); + }); + modelBuilder.Entity("PostFundManagement.Domain.Entities.AuditLog", b => { b.HasOne("PostFundManagement.Domain.Entities.User", "Performer") @@ -1299,6 +1560,12 @@ namespace PostFundManagement.Infrastructure.Database.Migrations modelBuilder.Entity("PostFundManagement.Domain.Entities.Evidence", b => { + b.HasOne("PostFundManagement.Domain.Entities.Activity", "Activity") + .WithMany("Evidences") + .HasForeignKey("ActivityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") .WithMany() .HasForeignKey("CreatedBy") @@ -1308,13 +1575,14 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.HasOne("PostFundManagement.Domain.Entities.Indicator", "Indicator") .WithMany() .HasForeignKey("IndicatorId") - .OnDelete(DeleteBehavior.Cascade) + .OnDelete(DeleteBehavior.Restrict) .IsRequired(); b.HasOne("PostFundManagement.Domain.Entities.Milestone", "Milestone") .WithMany() .HasForeignKey("MilestoneId") - .OnDelete(DeleteBehavior.Restrict); + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") .WithMany() @@ -1327,6 +1595,8 @@ namespace PostFundManagement.Infrastructure.Database.Migrations .HasForeignKey("UpdatedBy") .OnDelete(DeleteBehavior.NoAction); + b.Navigation("Activity"); + b.Navigation("Creator"); b.Navigation("Indicator"); @@ -1365,11 +1635,68 @@ namespace PostFundManagement.Infrastructure.Database.Migrations .OnDelete(DeleteBehavior.NoAction) .IsRequired(); + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Instruments") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + b.Navigation("Creator"); + + b.Navigation("Programme"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Invite", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Award", "Award") + .WithMany() + .HasForeignKey("AwardId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Contract", "Contract") + .WithMany() + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "InvitedOrganisation") + .WithMany() + .HasForeignKey("InvitedOrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "RecipientUser") + .WithMany() + .HasForeignKey("Recipient") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Award"); + + b.Navigation("Contract"); + + b.Navigation("InvitedOrganisation"); + + b.Navigation("Organisation"); + + b.Navigation("RecipientUser"); }); modelBuilder.Entity("PostFundManagement.Domain.Entities.Issue", b => { + b.HasOne("PostFundManagement.Domain.Entities.User", "Assignee") + .WithMany() + .HasForeignKey("AssignedTo") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") .WithMany() .HasForeignKey("CreatedBy") @@ -1382,6 +1709,8 @@ namespace PostFundManagement.Infrastructure.Database.Migrations .OnDelete(DeleteBehavior.Restrict) .IsRequired(); + b.Navigation("Assignee"); + b.Navigation("Creator"); b.Navigation("Project"); @@ -1413,6 +1742,25 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.Navigation("Updator"); }); + modelBuilder.Entity("PostFundManagement.Domain.Entities.Notification", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "RecipientUser") + .WithMany() + .HasForeignKey("Recipient") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organisation"); + + b.Navigation("RecipientUser"); + }); + modelBuilder.Entity("PostFundManagement.Domain.Entities.Organisation", b => { b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") @@ -1470,12 +1818,6 @@ namespace PostFundManagement.Infrastructure.Database.Migrations .OnDelete(DeleteBehavior.NoAction) .IsRequired(); - b.HasOne("PostFundManagement.Domain.Entities.Instrument", "Instrument") - .WithMany("Programmes") - .HasForeignKey("InstrumentId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - b.HasOne("PostFundManagement.Domain.Entities.User", "Owner") .WithMany() .HasForeignKey("OwnedBy") @@ -1490,8 +1832,6 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.Navigation("Creator"); - b.Navigation("Instrument"); - b.Navigation("Owner"); b.Navigation("Portfolio"); @@ -1508,7 +1848,7 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") .WithMany("Projects") .HasForeignKey("ProgrammeId") - .OnDelete(DeleteBehavior.NoAction) + .OnDelete(DeleteBehavior.Restrict) .IsRequired(); b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") @@ -1587,16 +1927,16 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.Navigation("Project"); }); + modelBuilder.Entity("PostFundManagement.Domain.Entities.Activity", b => + { + b.Navigation("Evidences"); + }); + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => { b.Navigation("Contract"); }); - modelBuilder.Entity("PostFundManagement.Domain.Entities.Instrument", b => - { - b.Navigation("Programmes"); - }); - modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => { b.Navigation("Programmes"); @@ -1606,6 +1946,8 @@ namespace PostFundManagement.Infrastructure.Database.Migrations { b.Navigation("Awards"); + b.Navigation("Instruments"); + b.Navigation("Projects"); b.Navigation("Rules"); diff --git a/pfm.dbml b/pfm.dbml deleted file mode 100644 index 7df0882..0000000 --- a/pfm.dbml +++ /dev/null @@ -1,349 +0,0 @@ -Table "AuditLogs" { - "Id" int8 [not null] - "EntityName" varchar(128) [not null] - "EntityId" int8 [not null] - "Action" varchar(64) [not null] - "Changes" jsonb - "PerformedBy" uuid [not null] - "Timestamp" timestamptz [not null] -} - -Table "Awards" { - "Id" int8 [not null] - "OrganisationId" int8 [not null] - "ProgrammeId" int8 [not null] - "ProjectId" int8 - "CreatedBy" uuid [not null] - "UpdatedBy" uuid - "ApprovedBy" uuid - "CreatedAt" timestamptz [not null] - "UpdatedAt" timestamptz - "Status" int4 [not null] - "Amount" numeric(18,2) [not null] -} - -Table "Beneficiaries" { - "Id" int8 [not null] - "AwardId" int8 [not null] - "CreatedBy" uuid [not null] - "CreatedAt" timestamptz [not null] - "Name" varchar(256) [not null] - "Category" varchar(128) [not null] - "TargetCount" int4 [not null] - "ReachedCount" int4 [not null] - "Location" varchar(256) [not null] -} - -Table "ChangeRequests" { - "Id" int8 [not null] - "ProjectId" int8 [not null] - "Title" varchar(256) [not null] - "Justification" text [not null] - "RevisedBudget" numeric(18,2) - "RevisedEndDate" timestamptz - "Status" int4 [not null] - "CreatedBy" uuid [not null] - "ApprovedBy" uuid - "CreatedAt" timestamptz [not null] - "UpdatedAt" timestamptz -} - -Table "ComplianceItems" { - "Id" int8 [not null] - "ProjectId" int8 [not null] - "Title" varchar(256) [not null] - "Requirements" text [not null] - "Status" int4 [not null] - "CreatedBy" uuid [not null] - "VerifiedBy" uuid - "CreatedAt" timestamptz [not null] - "UpdatedBy" uuid -} - -Table "Contracts" { - "Id" int8 [not null] - "AwardId" int8 [not null] - "ExternalSignatureId" varchar(256) - "SignedDocumentUrl" varchar(2048) - "EffectiveAt" timestamptz - "ExpiresAt" timestamptz - "TotalValue" numeric(18,2) [not null] - "Status" int4 [not null] - "CreatedBy" uuid [not null] - "CreatedAt" timestamptz [not null] -} - -Table "Disbursements" { - "Id" int8 [not null] - "ProjectId" int8 [not null] - "CreatedBy" uuid [not null] - "ApprovedBy" uuid - "CreatedOn" timestamptz [not null] - "UpdatedBy" uuid - "Amount" numeric(18,2) [not null] -} - -Table "Evidences" { - "Id" int8 [not null] - "ProjectId" int8 [not null] - "MilestoneId" int8 - "IndicatorId" int8 - "CreatedBy" uuid [not null] - "UpdatedBy" uuid - "CreatedAt" timestamptz [not null] - "UpdatedAt" timestamptz - "Version" int4 [not null] - "DocumentUrl" text [not null] - "Status" int4 [not null] -} - -Table "Indicators" { - "Id" int8 [not null] - "ProjectId" int8 [not null] - "CreatedBy" uuid [not null] - "CreatedAt" timestamptz [not null] - "Name" varchar(256) [not null] - "UnitOfMeasure" varchar(64) [not null] - "BaselineAmount" numeric(18,2) [not null] - "TargetAmount" numeric(18,2) [not null] - "ActualAmount" numeric(18,2) [not null] -} - -Table "Instruments" { - "Id" int8 [not null] - "Code" varchar(32) [not null] - "Name" varchar(256) [not null] - "Description" text - "Status" int4 [not null] - "CreatedBy" uuid [not null] - "CreatedAt" timestamptz [not null] -} - -Table "Issues" { - "Id" int8 [not null] - "ProjectId" int8 [not null] - "Title" varchar(256) [not null] - "Description" varchar(1024) [not null] - "Priority" int4 [not null] - "Status" int4 [not null] - "CreatedBy" uuid [not null] - "CreatedAt" timestamptz [not null] - "ResolvedAt" timestamptz -} - -Table "Milestones" { - "Id" int8 [not null] - "ProjectId" int8 [not null] - "CreatedBy" uuid [not null] - "UpdatedBy" uuid - "CreatedAt" timestamptz [not null] - "UpdatedAt" timestamptz - "DueDate" timestamptz [not null] - "Name" varchar(256) [not null] - "Status" int4 [not null] -} - -Table "Organisations" { - "Id" int8 [not null] - "CreatedBy" uuid [not null] - "CreatedAt" timestamptz [not null] - "RegistrationNo" varchar(128) [not null] - "Name" varchar(256) [not null] - "Email" varchar(256) [not null] - "Type" int4 [not null] - "Status" int4 [not null] -} - -Table "Outcomes" { - "Id" int8 [not null] - "BeneficiaryId" int8 [not null] - "CreatedBy" uuid [not null] - "CreatedAt" timestamptz [not null] - "Name" varchar(256) [not null] - "Description" text [not null] -} - -Table "Portfolios" { - "Id" int8 [not null] - "CreatedBy" uuid [not null] - "OwnedBy" uuid - "CreatedAt" timestamptz [not null] - "Name" varchar(256) [not null] - "Description" varchar(1024) -} - -Table "Programmes" { - "Id" int8 [not null] - "PortfolioId" int8 [not null] - "InstrumentId" int8 [not null] - "OwnedBy" uuid [not null] - "CreatedBy" uuid [not null] - "UpdatedBy" uuid - "CreatedAt" timestamptz [not null] - "UpdatedAt" timestamptz - "Name" varchar(256) [not null] - "Status" int4 [not null] -} - -Table "Projects" { - "Id" int8 [not null] - "ProgrammeId" int8 [not null] - "CreatedBy" uuid [not null] - "UpdatedBy" uuid - "CreatedAt" timestamptz [not null] - "UpdatedAt" timestamptz - "Name" varchar(256) [not null] - "Description" varchar(1024) - "Status" int4 [not null] -} - -Table "Risks" { - "Id" int8 [not null] - "ProjectId" int8 [not null] - "CreatedBy" uuid [not null] - "CreatedAt" timestamptz [not null] - "Likelihood" int4 [not null] - "Impact" int4 [not null] - "Name" varchar(256) [not null] - "Description" varchar(1024) [not null] -} - -Table "Rules" { - "Id" int8 [not null] - "ProgrammeId" int8 [not null] - "CreatedBy" uuid [not null] - "ApprovedBy" uuid - "CreatedAt" timestamptz [not null] - "UpdatedBy" uuid - "Name" varchar(256) [not null] - "Notes" text [not null] - "Status" int4 [not null] -} - -Table "SiteVisits" { - "Id" int8 [not null] - "ProjectId" int8 [not null] - "VisitedAt" timestamptz [not null] - "InspectorName" varchar(256) [not null] - "Findings" text [not null] - "Status" int4 [not null] - "CreatedBy" uuid [not null] - "CreatedAt" timestamptz [not null] -} - -Table "Users" { - "Id" uuid [not null] - "Firstnames" text [not null] - "Surname" text [not null] - "Status" int4 [not null] -} - -Ref "FK_AuditLogs_Users_PerformedBy":"Users"."Id" < "AuditLogs"."PerformedBy" [delete: restrict] - -Ref "FK_Awards_Organisations_OrganisationId":"Organisations"."Id" < "Awards"."OrganisationId" [delete: restrict] - -Ref "FK_Awards_Programmes_ProgrammeId":"Programmes"."Id" < "Awards"."ProgrammeId" [delete: restrict] - -Ref "FK_Awards_Projects_ProjectId":"Projects"."Id" < "Awards"."ProjectId" [delete: set null] - -Ref "FK_Awards_Users_ApprovedBy":"Users"."Id" < "Awards"."ApprovedBy" - -Ref "FK_Awards_Users_CreatedBy":"Users"."Id" < "Awards"."CreatedBy" - -Ref "FK_Awards_Users_UpdatedBy":"Users"."Id" < "Awards"."UpdatedBy" - -Ref "FK_Beneficiaries_Awards_AwardId":"Awards"."Id" < "Beneficiaries"."AwardId" [delete: restrict] - -Ref "FK_Beneficiaries_Users_CreatedBy":"Users"."Id" < "Beneficiaries"."CreatedBy" - -Ref "FK_ChangeRequests_Projects_ProjectId":"Projects"."Id" < "ChangeRequests"."ProjectId" [delete: restrict] - -Ref "FK_ChangeRequests_Users_ApprovedBy":"Users"."Id" < "ChangeRequests"."ApprovedBy" - -Ref "FK_ChangeRequests_Users_CreatedBy":"Users"."Id" < "ChangeRequests"."CreatedBy" - -Ref "FK_ComplianceItems_Projects_ProjectId":"Projects"."Id" < "ComplianceItems"."ProjectId" [delete: restrict] - -Ref "FK_ComplianceItems_Users_CreatedBy":"Users"."Id" < "ComplianceItems"."CreatedBy" - -Ref "FK_ComplianceItems_Users_VerifiedBy":"Users"."Id" < "ComplianceItems"."VerifiedBy" - -Ref "FK_Contracts_Awards_AwardId":"Awards"."Id" < "Contracts"."AwardId" [delete: restrict] - -Ref "FK_Contracts_Users_CreatedBy":"Users"."Id" < "Contracts"."CreatedBy" - -Ref "FK_Disbursements_Projects_ProjectId":"Projects"."Id" < "Disbursements"."ProjectId" [delete: restrict] - -Ref "FK_Disbursements_Users_ApprovedBy":"Users"."Id" < "Disbursements"."ApprovedBy" - -Ref "FK_Disbursements_Users_CreatedBy":"Users"."Id" < "Disbursements"."CreatedBy" - -Ref "FK_Disbursements_Users_UpdatedBy":"Users"."Id" < "Disbursements"."UpdatedBy" - -Ref "FK_Evidences_Indicators_IndicatorId":"Indicators"."Id" < "Evidences"."IndicatorId" - -Ref "FK_Evidences_Milestones_MilestoneId":"Milestones"."Id" < "Evidences"."MilestoneId" - -Ref "FK_Evidences_Projects_ProjectId":"Projects"."Id" < "Evidences"."ProjectId" [delete: restrict] - -Ref "FK_Evidences_Users_CreatedBy":"Users"."Id" < "Evidences"."CreatedBy" - -Ref "FK_Evidences_Users_UpdatedBy":"Users"."Id" < "Evidences"."UpdatedBy" - -Ref "FK_Indicators_Projects_ProjectId":"Projects"."Id" < "Indicators"."ProjectId" [delete: restrict] - -Ref "FK_Indicators_Users_CreatedBy":"Users"."Id" < "Indicators"."CreatedBy" - -Ref "FK_Instruments_Users_CreatedBy":"Users"."Id" < "Instruments"."CreatedBy" - -Ref "FK_Issues_Projects_ProjectId":"Projects"."Id" < "Issues"."ProjectId" [delete: restrict] - -Ref "FK_Issues_Users_CreatedBy":"Users"."Id" < "Issues"."CreatedBy" - -Ref "FK_Milestones_Projects_ProjectId":"Projects"."Id" < "Milestones"."ProjectId" [delete: restrict] - -Ref "FK_Milestones_Users_CreatedBy":"Users"."Id" < "Milestones"."CreatedBy" - -Ref "FK_Milestones_Users_UpdatedBy":"Users"."Id" < "Milestones"."UpdatedBy" - -Ref "FK_Organisations_Users_CreatedBy":"Users"."Id" < "Organisations"."CreatedBy" - -Ref "FK_Outcomes_Beneficiaries_BeneficiaryId":"Beneficiaries"."Id" < "Outcomes"."BeneficiaryId" [delete: restrict] - -Ref "FK_Outcomes_Users_CreatedBy":"Users"."Id" < "Outcomes"."CreatedBy" - -Ref "FK_Portfolios_Users_CreatedBy":"Users"."Id" < "Portfolios"."CreatedBy" - -Ref "FK_Portfolios_Users_OwnedBy":"Users"."Id" < "Portfolios"."OwnedBy" - -Ref "FK_Programmes_Instruments_InstrumentId":"Instruments"."Id" < "Programmes"."InstrumentId" [delete: restrict] - -Ref "FK_Programmes_Portfolios_PortfolioId":"Portfolios"."Id" < "Programmes"."PortfolioId" [delete: restrict] - -Ref "FK_Programmes_Users_CreatedBy":"Users"."Id" < "Programmes"."CreatedBy" - -Ref "FK_Programmes_Users_OwnedBy":"Users"."Id" < "Programmes"."OwnedBy" [delete: restrict] - -Ref "FK_Programmes_Users_UpdatedBy":"Users"."Id" < "Programmes"."UpdatedBy" - -Ref "FK_Projects_Programmes_ProgrammeId":"Programmes"."Id" < "Projects"."ProgrammeId" [delete: restrict] - -Ref "FK_Projects_Users_CreatedBy":"Users"."Id" < "Projects"."CreatedBy" - -Ref "FK_Projects_Users_UpdatedBy":"Users"."Id" < "Projects"."UpdatedBy" - -Ref "FK_Risks_Projects_ProjectId":"Projects"."Id" < "Risks"."ProjectId" [delete: restrict] - -Ref "FK_Risks_Users_CreatedBy":"Users"."Id" < "Risks"."CreatedBy" - -Ref "FK_Rules_Programmes_ProgrammeId":"Programmes"."Id" < "Rules"."ProgrammeId" [delete: restrict] - -Ref "FK_Rules_Users_ApprovedBy":"Users"."Id" < "Rules"."ApprovedBy" - -Ref "FK_Rules_Users_CreatedBy":"Users"."Id" < "Rules"."CreatedBy" - -Ref "FK_Rules_Users_UpdatedBy":"Users"."Id" < "Rules"."UpdatedBy" - -Ref "FK_SiteVisits_Projects_ProjectId":"Projects"."Id" < "SiteVisits"."ProjectId" [delete: restrict] - -Ref "FK_SiteVisits_Users_CreatedBy":"Users"."Id" < "SiteVisits"."CreatedBy" From 32a80eb84d8b237297594cbe8d0918705480f96e Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 20 Aug 2026 15:15:54 +0200 Subject: [PATCH 34/50] Implemented security functionality Added data protection feature Migrated changes --- PostFundManagement.Api/Extensions/Api.cs | 124 ++++++++++++ .../PostFundManagement.Api.csproj | 15 ++ PostFundManagement.Api/Program.cs | 28 +-- .../Abstractions/EventBase.cs | 12 ++ .../Abstractions/IEvent.cs | 12 ++ .../Abstractions/IJobOrchestrator.cs | 12 ++ .../Api/ApiVersionTargetAttribute.cs | 7 + .../Configuration/SecurityClientSettings.cs | 14 ++ .../Api/Configuration/SecuritySettings.cs | 12 ++ .../Api/Models/TokenErrorResponse.cs | 13 ++ .../Api/Models/TokenRequest.cs | 20 ++ .../Api/Models/TokenResponse.cs | 16 ++ .../OpenApiBearerSecuritySchemeTransformer.cs | 16 ++ PostFundManagement.Domain/Extensions/Api.cs | 177 ++++++++++++++++++ .../Extensions/Mappers.cs | 1 - .../Extensions/Timezones.cs | 27 +++ .../PostFundManagement.Domain.csproj | 1 + .../Sdk/ISecurityConnectApi.cs | 7 + .../Services/TokenService.cs | 66 +++++++ .../Database/DataProtectionDbContext.cs | 15 ++ .../ApplicationDbContextFactory.cs | 4 +- .../DataProtectionDbContextFactory.cs | 20 ++ .../20260820130901_Init.Designer.cs | 48 +++++ .../SecurityMigrations/20260820130901_Init.cs | 41 ++++ .../DataProtectionDbContextModelSnapshot.cs | 45 +++++ .../Extensions/Constants.cs | 6 - .../Extensions/Postgres.cs | 13 +- 27 files changed, 735 insertions(+), 37 deletions(-) create mode 100644 PostFundManagement.Api/Extensions/Api.cs create mode 100644 PostFundManagement.Domain/Abstractions/EventBase.cs create mode 100644 PostFundManagement.Domain/Abstractions/IEvent.cs create mode 100644 PostFundManagement.Domain/Abstractions/IJobOrchestrator.cs create mode 100644 PostFundManagement.Domain/Api/ApiVersionTargetAttribute.cs create mode 100644 PostFundManagement.Domain/Api/Configuration/SecurityClientSettings.cs create mode 100644 PostFundManagement.Domain/Api/Configuration/SecuritySettings.cs create mode 100644 PostFundManagement.Domain/Api/Models/TokenErrorResponse.cs create mode 100644 PostFundManagement.Domain/Api/Models/TokenRequest.cs create mode 100644 PostFundManagement.Domain/Api/Models/TokenResponse.cs create mode 100644 PostFundManagement.Domain/Api/OpenApiBearerSecuritySchemeTransformer.cs create mode 100644 PostFundManagement.Domain/Extensions/Api.cs create mode 100644 PostFundManagement.Domain/Extensions/Timezones.cs create mode 100644 PostFundManagement.Domain/Sdk/ISecurityConnectApi.cs create mode 100644 PostFundManagement.Domain/Services/TokenService.cs create mode 100644 PostFundManagement.Infrastructure/Database/DataProtectionDbContext.cs rename PostFundManagement.Infrastructure/Database/{ => Factories}/ApplicationDbContextFactory.cs (87%) create mode 100644 PostFundManagement.Infrastructure/Database/Factories/DataProtectionDbContextFactory.cs create mode 100644 PostFundManagement.Infrastructure/Database/SecurityMigrations/20260820130901_Init.Designer.cs create mode 100644 PostFundManagement.Infrastructure/Database/SecurityMigrations/20260820130901_Init.cs create mode 100644 PostFundManagement.Infrastructure/Database/SecurityMigrations/DataProtectionDbContextModelSnapshot.cs delete mode 100644 PostFundManagement.Infrastructure/Extensions/Constants.cs diff --git a/PostFundManagement.Api/Extensions/Api.cs b/PostFundManagement.Api/Extensions/Api.cs new file mode 100644 index 0000000..2f3c3f5 --- /dev/null +++ b/PostFundManagement.Api/Extensions/Api.cs @@ -0,0 +1,124 @@ +using PostFundManagement.Domain.Api.Configuration; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Sdk; +using PostFundManagement.Domain.Services; +using PostFundManagement.Infrastructure.Database; + +namespace PostFundManagement.Api.Extensions; + +public static class Api +{ + public static IServiceCollection AddSecurityApiSdk(this IServiceCollection services, IConfiguration configuration) + { + var configSection = configuration.GetSection(nameof(SecurityClientSettings)); + + var authOptions = new SecurityClientSettings(); + configSection.Bind(authOptions); + + services.Configure(configSection); + + if (string.IsNullOrWhiteSpace(authOptions.Authority)) + return services; + + if (!authOptions.Authority.EndsWith("/", StringComparison.Ordinal)) authOptions.Authority += "/"; + + services.AddRefitClient() + .ConfigureHttpClient(config => + { + config.BaseAddress = new Uri(authOptions.Authority); + config.Timeout = TimeSpan.FromSeconds(15); + }) + .AddStandardResilienceHandler(options => + { + options.Retry.MaxRetryAttempts = 3; + options.Retry.Delay = TimeSpan.FromSeconds(1); + options.Retry.BackoffType = Polly.DelayBackoffType.Exponential; + }); + + services.AddScoped(); + + return services; + } + + public static IServiceCollection AddWebSecurity(this IServiceCollection services, IConfiguration configuration) + { + var certString = configuration["DataProtection:Certificate"] ?? configuration["DataProtection__Certificate"]; + var certPassword = configuration["DataProtection:Password"] ?? configuration["DataProtection__Password"]; + + if (string.IsNullOrEmpty(certString)) + throw new InvalidOperationException("Data Protection Certificate configuration is missing."); + + var certificate = X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(certString), certPassword); + + services.AddDataProtection().PersistKeysToDbContext() + .ProtectKeysWithCertificate(certificate) + .SetApplicationName("LiteCharmsApp"); + + services.Configure(options => options.ApplicationDiscriminator = "LiteCharmsApp"); + + services.ConfigureCookieOidcSameSiteSupport(); + + var configSection = configuration.GetSection(nameof(SecuritySettings)); + + var authOptions = new SecuritySettings(); + configSection.Bind(authOptions); + + services.Configure(configSection); + + services.AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => + { + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + options.Cookie.SameSite = SameSiteMode.Lax; + options.Cookie.Name = "LiteCharmsApp.Session"; + }) + .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => + { + options.Authority = authOptions.Authority; + options.ClientId = authOptions.ClientId; + options.ClientSecret = authOptions.ClientSecret; + options.ResponseType = "code"; + + options.SaveTokens = true; + options.GetClaimsFromUserInfoEndpoint = true; + options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always; + options.CorrelationCookie.SameSite = SameSiteMode.None; + + options.NonceCookie.SecurePolicy = CookieSecurePolicy.Always; + options.NonceCookie.SameSite = SameSiteMode.None; + + options.ForwardSignOut = CookieAuthenticationDefaults.AuthenticationScheme; + + options.Scope.Clear(); + options.Scope.Add("openid"); + options.Scope.Add("profile"); + options.Scope.Add("email"); + + options.Events = new OpenIdConnectEvents + { + OnRedirectToIdentityProviderForSignOut = context => + { + var idToken = context.ProtocolMessage.IdTokenHint; + + if (string.IsNullOrEmpty(idToken)) + { + var tokens = context.Properties.GetTokens(); + var idTokenItem = tokens.FirstOrDefault(t => string.Equals(t.Name, "id_token", StringComparison.Ordinal)); + + if (idTokenItem != null) context.ProtocolMessage.IdTokenHint = idTokenItem.Value; + } + + return Task.CompletedTask; + }, + }; + }); + + services.AddCascadingAuthenticationState(); + + return services; + } +} \ No newline at end of file diff --git a/PostFundManagement.Api/PostFundManagement.Api.csproj b/PostFundManagement.Api/PostFundManagement.Api.csproj index a7e664e..f1c628a 100644 --- a/PostFundManagement.Api/PostFundManagement.Api.csproj +++ b/PostFundManagement.Api/PostFundManagement.Api.csproj @@ -13,4 +13,19 @@ + + + + + + + + + + + + + + + diff --git a/PostFundManagement.Api/Program.cs b/PostFundManagement.Api/Program.cs index 60e8031..23c494a 100644 --- a/PostFundManagement.Api/Program.cs +++ b/PostFundManagement.Api/Program.cs @@ -2,13 +2,10 @@ using Scalar.AspNetCore; var builder = WebApplication.CreateBuilder(args); -// Add services to the container. -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); var app = builder.Build(); -// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapScalarApiReference(options => @@ -16,33 +13,10 @@ if (app.Environment.IsDevelopment()) options.WithTitle("Post-fund Management API") .WithTheme(ScalarTheme.BluePlanet); }); + app.MapOpenApi(); } app.UseHttpsRedirection(); -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; - -app.MapGet("/weatherforecast", () => -{ - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; -}) -.WithName("GetWeatherForecast"); - app.Run(); - -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} diff --git a/PostFundManagement.Domain/Abstractions/EventBase.cs b/PostFundManagement.Domain/Abstractions/EventBase.cs new file mode 100644 index 0000000..af940a7 --- /dev/null +++ b/PostFundManagement.Domain/Abstractions/EventBase.cs @@ -0,0 +1,12 @@ +using static PostFundManagement.Domain.Extensions.Timezones; + +namespace PostFundManagement.Domain.Abstractions; + +public abstract class EventBase +{ + public Guid Id { get; set; } = Guid.CreateVersion7(); + + public DateTimeOffset EnqueueAt { get; set; } = (DateTimeOffset)SouthAfricanTimeZone.UtcNow(); + + public string CorrelationId { get; set; } = Guid.CreateVersion7().ToString(); +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Abstractions/IEvent.cs b/PostFundManagement.Domain/Abstractions/IEvent.cs new file mode 100644 index 0000000..09464e8 --- /dev/null +++ b/PostFundManagement.Domain/Abstractions/IEvent.cs @@ -0,0 +1,12 @@ +namespace PostFundManagement.Domain.Abstractions; + +public interface IEvent : INotification +{ + Guid Id { get; set; } + + string Name { get; set; } + + DateTimeOffset EnqueueAt { get; set; } + + string CorrelationId { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Abstractions/IJobOrchestrator.cs b/PostFundManagement.Domain/Abstractions/IJobOrchestrator.cs new file mode 100644 index 0000000..d92a19e --- /dev/null +++ b/PostFundManagement.Domain/Abstractions/IJobOrchestrator.cs @@ -0,0 +1,12 @@ +namespace PostFundManagement.Domain.Abstractions; + +public interface IJobOrchestrator +{ + ValueTask SendAsync(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : IEvent; + + ValueTask ScheduleAsync(TNotification notification, string cronExpression, CancellationToken cancellationToken = default) + where TNotification : IEvent; + + ValueTask InterruptAsync(string eventName, string? correlationId = null, CancellationToken cancellationToken = default); +} diff --git a/PostFundManagement.Domain/Api/ApiVersionTargetAttribute.cs b/PostFundManagement.Domain/Api/ApiVersionTargetAttribute.cs new file mode 100644 index 0000000..2e0d1d1 --- /dev/null +++ b/PostFundManagement.Domain/Api/ApiVersionTargetAttribute.cs @@ -0,0 +1,7 @@ +namespace PostFundManagement.Domain.Api; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public sealed class ApiVersionTargetAttribute(int majorVersion) : Attribute +{ + public int MajorVersion { get; } = majorVersion; +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Api/Configuration/SecurityClientSettings.cs b/PostFundManagement.Domain/Api/Configuration/SecurityClientSettings.cs new file mode 100644 index 0000000..0c383dc --- /dev/null +++ b/PostFundManagement.Domain/Api/Configuration/SecurityClientSettings.cs @@ -0,0 +1,14 @@ +namespace PostFundManagement.Domain.Api.Configuration; + +public sealed class SecurityClientSettings +{ + public string? Authority { get; set; } + + public string? GrantType { get; set; } + + public string? ClientId { get; set; } + + public string? ClientSecret { get; set; } + + public string? Scope { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Api/Configuration/SecuritySettings.cs b/PostFundManagement.Domain/Api/Configuration/SecuritySettings.cs new file mode 100644 index 0000000..349a194 --- /dev/null +++ b/PostFundManagement.Domain/Api/Configuration/SecuritySettings.cs @@ -0,0 +1,12 @@ +namespace PostFundManagement.Domain.Api.Configuration; + +public sealed class SecuritySettings +{ + public string? Authority { get; set; } + + public string? ClientId { get; set; } + + public string? ClientSecret { get; set; } + + public string? Audience { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Api/Models/TokenErrorResponse.cs b/PostFundManagement.Domain/Api/Models/TokenErrorResponse.cs new file mode 100644 index 0000000..00c8e48 --- /dev/null +++ b/PostFundManagement.Domain/Api/Models/TokenErrorResponse.cs @@ -0,0 +1,13 @@ +namespace PostFundManagement.Domain.Api.Models; + +public sealed class TokenErrorResponse +{ + [JsonPropertyName("error")] + public string? Error { get; set; } + + [JsonPropertyName("error_description")] + public string? ErrorDescription { get; set; } + + [JsonPropertyName("error_uri")] + public string? ErrorUri { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Api/Models/TokenRequest.cs b/PostFundManagement.Domain/Api/Models/TokenRequest.cs new file mode 100644 index 0000000..6114592 --- /dev/null +++ b/PostFundManagement.Domain/Api/Models/TokenRequest.cs @@ -0,0 +1,20 @@ +namespace PostFundManagement.Domain.Api.Models; + +public sealed class TokenRequest +{ + [JsonPropertyName("grant_type")] + [AliasAs("grant_type")] + public string? GrantType { get; set; } + + [JsonPropertyName("client_id")] + [AliasAs("client_id")] + public string? ClientId { get; set; } + + [JsonPropertyName("client_secret")] + [AliasAs("client_secret")] + public string? ClientSecret { get; set; } + + [JsonPropertyName("scope")] + [AliasAs("scope")] + public string? Scope { get; set; } +} diff --git a/PostFundManagement.Domain/Api/Models/TokenResponse.cs b/PostFundManagement.Domain/Api/Models/TokenResponse.cs new file mode 100644 index 0000000..f79e736 --- /dev/null +++ b/PostFundManagement.Domain/Api/Models/TokenResponse.cs @@ -0,0 +1,16 @@ +namespace PostFundManagement.Domain.Api.Models; + +public sealed class TokenResponse +{ + [JsonPropertyName("access_token")] + public string? AccessToken { get; set; } + + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + + [JsonPropertyName("token_type")] + public string? TokenType { get; set; } + + [JsonPropertyName("scope")] + public string? Scope { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Api/OpenApiBearerSecuritySchemeTransformer.cs b/PostFundManagement.Domain/Api/OpenApiBearerSecuritySchemeTransformer.cs new file mode 100644 index 0000000..d090efc --- /dev/null +++ b/PostFundManagement.Domain/Api/OpenApiBearerSecuritySchemeTransformer.cs @@ -0,0 +1,16 @@ +namespace PostFundManagement.Domain.Api; + +public sealed class OpenApiBearerSecuritySchemeTransformer : IOpenApiDocumentTransformer +{ + public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) + { + var bearerScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.Http, + Scheme = "bearer", + Description = "JWT Authorization header using the Bearer scheme", + }; + + document.AddComponent("Bearer", bearerScheme); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Extensions/Api.cs b/PostFundManagement.Domain/Extensions/Api.cs new file mode 100644 index 0000000..dd39fa6 --- /dev/null +++ b/PostFundManagement.Domain/Extensions/Api.cs @@ -0,0 +1,177 @@ +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Api; +using PostFundManagement.Domain.Api.Configuration; + +namespace PostFundManagement.Domain.Extensions; + +public static class Api +{ + public static void ConfigureCookieOidcSameSiteSupport(this IServiceCollection services) => + services.Configure(options => + { + options.MinimumSameSitePolicy = SameSiteMode.Unspecified; + options.OnAppendCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); + options.OnDeleteCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); + }); + + public static void CheckSameSite(HttpContext httpContext, CookieOptions options) + { + if (options.SameSite == SameSiteMode.None) + { + bool isSecure = httpContext.Request.IsHttps; + + if (!isSecure && httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out var proto)) + isSecure = string.Equals(proto, "https", StringComparison.OrdinalIgnoreCase); + + if (!isSecure && httpContext.Request.Headers.TryGetValue("Forwarded", out var forwarded)) + isSecure = forwarded.ToString().Contains("proto=https", StringComparison.OrdinalIgnoreCase); + + if (!isSecure) options.SameSite = SameSiteMode.Unspecified; + } + } + + public static IServiceCollection AddLiteCharmsApiSecurity(this IServiceCollection services, IConfiguration configuration) + { + var configSection = configuration.GetSection(nameof(SecuritySettings)); + + var authOptions = new SecuritySettings(); + configSection.Bind(authOptions); + + services.Configure(configSection); + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.Authority = authOptions.Authority; + options.Audience = authOptions.Audience; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidIssuer = authOptions.Authority, + ValidateAudience = true, + ValidateIssuer = true, + }; + }); + + services.AddAuthorization(); + + return services; + } + + public static WebApplication AddSecurityEndpoints(this WebApplication app) + { + app.MapGet("/login", async (HttpContext context, string redirectUri = "/") => + { + await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties + { + RedirectUri = redirectUri, + }); + }); + + app.MapGet("/logout", async (HttpContext context, string? redirectUri = null) => + { + var idToken = await context.GetTokenAsync("id_token"); + + if (string.IsNullOrWhiteSpace(redirectUri)) + { + var host = context.Request.Host.ToUriComponent(); + redirectUri = $"https://{host}/"; + } + + var authProperties = new AuthenticationProperties { RedirectUri = redirectUri, }; + + if (!string.IsNullOrEmpty(idToken)) + authProperties.Parameters.Add("id_token_hint", idToken); + + await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties); + }); + + return app; + } + + public static IServiceCollection AddApiServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddHttpClient(); + + services.AddApiVersioning(options => + { + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = true; + options.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(), + new QueryStringApiVersionReader("version"), + new QueryStringApiVersionReader("version"), + new MediaTypeApiVersionReader("version")); + }) + .AddApiExplorer(options => + { + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + }); + + var urls = configuration["ASPNETCORE_URLS"] ?? configuration["Urls"]; + var healthUrl = "http://localhost:8080/health"; + + if (!string.IsNullOrWhiteSpace(urls)) + { + string firstUrl = urls.Split(';').FirstOrDefault(s => s.Contains("http://", StringComparison.InvariantCultureIgnoreCase))! + .Replace("0.0.0.0", "localhost") + .Replace("*", "localhost") + .Replace("+", "localhost"); + + healthUrl = $"{firstUrl.TrimEnd('/')}/health"; + } + + services.AddHealthChecksUI(setup => + { + setup.SetNotifyUnHealthyOneTimeUntilChange(); + setup.AddHealthCheckEndpoint("primary, heal", healthUrl); + setup.SetHeaderText("Midrand Books"); + }) + .AddInMemoryStorage(); + + services.AddOutputCache(options => + { + options.AddBasePolicy(builder => builder.Cache()); + options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10); + }); + + services.AddOpenApi(options => options.AddDocumentTransformer()); + + return services; + } + + public static IApplicationBuilder MapEndpoints(this WebApplication app, IDictionary versionGroups) + { + var endpoints = app.Services.GetRequiredService>(); + + foreach (var endpoint in endpoints) + { + var versionAttributes = endpoint.GetType().GetCustomAttributes().ToList(); + + if (versionAttributes.Count != 0) + { + foreach (var attr in versionAttributes) + if (versionGroups.TryGetValue(attr.MajorVersion, out var targetGroup)) + endpoint.Map(targetGroup); + } + else + endpoint.Map(app); + } + + return app; + } + + public static IServiceCollection AddEndpoints(this IServiceCollection services, Assembly assembly) + { + ServiceDescriptor[] discriptors = [.. assembly.DefinedTypes + .Where(t => t is { IsInterface: false, IsAbstract: false }) + .Where(t => t.IsAssignableTo(typeof(IEndpoint))) + .Select(t => ServiceDescriptor.Transient(typeof(IEndpoint), t))]; + + services.TryAddEnumerable(discriptors); + + return services; + } + + public static string ToEndpointName(this Type target, string? annotation = "") => + $"{target.Name.Replace("Endpoint", string.Empty)}{annotation}".ToLower(CultureInfo.CurrentCulture); +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Extensions/Mappers.cs b/PostFundManagement.Domain/Extensions/Mappers.cs index d9153cd..104defa 100644 --- a/PostFundManagement.Domain/Extensions/Mappers.cs +++ b/PostFundManagement.Domain/Extensions/Mappers.cs @@ -21,7 +21,6 @@ public static class Mappers UpdatedAt = entity.UpdatedAt, UpdatedBy = entity.UpdatedBy }; - public static AuditLog Map(this Entities.AuditLog entity) => new() { diff --git a/PostFundManagement.Domain/Extensions/Timezones.cs b/PostFundManagement.Domain/Extensions/Timezones.cs new file mode 100644 index 0000000..407d816 --- /dev/null +++ b/PostFundManagement.Domain/Extensions/Timezones.cs @@ -0,0 +1,27 @@ +namespace PostFundManagement.Domain.Extensions; + +public static class Timezones +{ + public static TimeZoneInfo SouthAfricanTimeZone => TimeZoneInfo.FindSystemTimeZoneById("South Africa Standard Time"); + + public static string? LocaliseDateTime(this DateTime dateTime, TimeSpan offset) => offset.Hours > 0 + ? $"{dateTime:yyyy-MM-ddTHH:mm:ss.fff}+{offset.Hours:00}:{offset.Minutes:00}" + : $"{dateTime:yyyy-MM-ddTHH:mm:ss.fff}{offset.Hours:00}:{offset.Minutes:00}"; + + public static string? LocaliseDateTimeOffset(this DateTimeOffset dateTime, TimeSpan offset) => LocaliseDateTime(dateTime.DateTime, offset); + + public static DateTimeOffset ToDateTimeWithTimeZone(this DateTime source, TimeZoneInfo? timezone = null) + { + DateTime sourceDateAdjusted = source.Kind != DateTimeKind.Utc + ? new(source.Ticks, DateTimeKind.Utc) + : source; + + var localised = timezone is null + ? new DateTimeOffset(sourceDateAdjusted.Ticks, SouthAfricanTimeZone.BaseUtcOffset).LocaliseDateTimeOffset(SouthAfricanTimeZone.BaseUtcOffset) + : new DateTimeOffset(sourceDateAdjusted.Ticks, timezone!.BaseUtcOffset).LocaliseDateTimeOffset(timezone.BaseUtcOffset); + + return DateTimeOffset.Parse(localised!, CultureInfo.InvariantCulture); + } + + public static DateTime UtcNow(this TimeZoneInfo timezone) => ToDateTimeWithTimeZone(DateTime.Now, timezone).UtcDateTime; +} \ No newline at end of file diff --git a/PostFundManagement.Domain/PostFundManagement.Domain.csproj b/PostFundManagement.Domain/PostFundManagement.Domain.csproj index ab9ebe2..a0ee4f6 100644 --- a/PostFundManagement.Domain/PostFundManagement.Domain.csproj +++ b/PostFundManagement.Domain/PostFundManagement.Domain.csproj @@ -27,6 +27,7 @@ + diff --git a/PostFundManagement.Domain/Sdk/ISecurityConnectApi.cs b/PostFundManagement.Domain/Sdk/ISecurityConnectApi.cs new file mode 100644 index 0000000..eac858b --- /dev/null +++ b/PostFundManagement.Domain/Sdk/ISecurityConnectApi.cs @@ -0,0 +1,7 @@ +namespace PostFundManagement.Domain.Sdk; + +public interface ISecurityConnectApi +{ + [Post("/connect/token")] + ValueTask GetToken([Body(BodySerializationMethod.UrlEncoded)] Api.Models.TokenRequest request, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Services/TokenService.cs b/PostFundManagement.Domain/Services/TokenService.cs new file mode 100644 index 0000000..d4fcc56 --- /dev/null +++ b/PostFundManagement.Domain/Services/TokenService.cs @@ -0,0 +1,66 @@ +using PostFundManagement.Domain.Api.Configuration; +using PostFundManagement.Domain.Api.Models; +using PostFundManagement.Domain.Sdk; + +namespace PostFundManagement.Domain.Services; + +public sealed class TokenService(ISecurityConnectApi connectApi, IOptions clientOptions) +{ + private readonly SecurityClientSettings clientSettings = clientOptions.Value; + + public async Task> GenerateAsync(CancellationToken cancellationToken = default) + { + try + { + var request = new Api.Models.TokenRequest + { + ClientId = clientSettings.ClientId, + ClientSecret = clientSettings.ClientSecret, + GrantType = clientSettings.GrantType, + Scope = clientSettings.Scope, + }; + + using var response = await connectApi.GetToken(request, cancellationToken); + + var contentRaw = await response.Content.ReadAsStringAsync(cancellationToken); + + if (string.IsNullOrWhiteSpace(contentRaw)) + return Result.Fail(new Error($"The authentication endpoint returned an empty payload. Status code: {response.StatusCode}")); + + if (response.IsSuccessStatusCode) + { + var tokenResponse = JsonSerializer.Deserialize(contentRaw); + + return !string.IsNullOrWhiteSpace(tokenResponse?.AccessToken) + ? Result.Ok(tokenResponse) + : Result.Fail(new Error("Authentication succeeded, but no access token was found in the response payload.")); + } + + try + { + var errorResult = JsonSerializer.Deserialize(contentRaw); + + if (errorResult != null) + { + string summary = $"{errorResult.Error}: {errorResult.ErrorDescription}"; + + return Result.Fail(new Error(summary)); + } + } + catch + { + return Result.Fail(new Error($"Authentication failed: {contentRaw}")); + } + + return Result.Fail(new Error($"Authentication failed with status code: {response.StatusCode}")); + } + catch (OperationCanceledException ex) + { + return Result.Fail(new Error("The token generation request was canceled.").CausedBy(ex)); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/DataProtectionDbContext.cs b/PostFundManagement.Infrastructure/Database/DataProtectionDbContext.cs new file mode 100644 index 0000000..d74a0c6 --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/DataProtectionDbContext.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; + +namespace PostFundManagement.Infrastructure.Database; + +public sealed class DataProtectionDbContext(DbContextOptions options) : DbContext(options), IDataProtectionKeyContext +{ + public DbSet DataProtectionKeys { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => entity.ToTable(nameof(DataProtectionKeys), schema: "security")); + } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContextFactory.cs b/PostFundManagement.Infrastructure/Database/Factories/ApplicationDbContextFactory.cs similarity index 87% rename from PostFundManagement.Infrastructure/Database/ApplicationDbContextFactory.cs rename to PostFundManagement.Infrastructure/Database/Factories/ApplicationDbContextFactory.cs index a11f435..a0f4aea 100644 --- a/PostFundManagement.Infrastructure/Database/ApplicationDbContextFactory.cs +++ b/PostFundManagement.Infrastructure/Database/Factories/ApplicationDbContextFactory.cs @@ -1,6 +1,6 @@ -using static PostFundManagement.Infrastructure.Extensions.Constants; +using static PostFundManagement.Infrastructure.Extensions.Postgres; -namespace PostFundManagement.Infrastructure.Database; +namespace PostFundManagement.Infrastructure.Database.Factories; public sealed class ApplicationDbContextFactory : IDesignTimeDbContextFactory { diff --git a/PostFundManagement.Infrastructure/Database/Factories/DataProtectionDbContextFactory.cs b/PostFundManagement.Infrastructure/Database/Factories/DataProtectionDbContextFactory.cs new file mode 100644 index 0000000..d71cad2 --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Factories/DataProtectionDbContextFactory.cs @@ -0,0 +1,20 @@ +using static PostFundManagement.Infrastructure.Extensions.Postgres; + +namespace PostFundManagement.Infrastructure.Database.Factories; + +public sealed class DataProtectionDbContextFactory : IDesignTimeDbContextFactory +{ + public DataProtectionDbContext CreateDbContext(string[] args) + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddUserSecrets(typeof(DataProtectionDbContext).Assembly) + .AddEnvironmentVariables() + .Build(); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(configuration.GetConnectionString(DatabaseConfigName)); + + return new DataProtectionDbContext(optionsBuilder.Options); + } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/SecurityMigrations/20260820130901_Init.Designer.cs b/PostFundManagement.Infrastructure/Database/SecurityMigrations/20260820130901_Init.Designer.cs new file mode 100644 index 0000000..7b5f27f --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/SecurityMigrations/20260820130901_Init.Designer.cs @@ -0,0 +1,48 @@ +ο»Ώ// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PostFundManagement.Infrastructure.Database; + +#nullable disable + +namespace PostFundManagement.Infrastructure.Database.SecurityMigrations +{ + [DbContext(typeof(DataProtectionDbContext))] + [Migration("20260820130901_Init")] + partial class Init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text"); + + b.Property("Xml") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys", "security"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PostFundManagement.Infrastructure/Database/SecurityMigrations/20260820130901_Init.cs b/PostFundManagement.Infrastructure/Database/SecurityMigrations/20260820130901_Init.cs new file mode 100644 index 0000000..4eda272 --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/SecurityMigrations/20260820130901_Init.cs @@ -0,0 +1,41 @@ +ο»Ώusing Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace PostFundManagement.Infrastructure.Database.SecurityMigrations +{ + /// + public partial class Init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "security"); + + migrationBuilder.CreateTable( + name: "DataProtectionKeys", + schema: "security", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FriendlyName = table.Column(type: "text", nullable: true), + Xml = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DataProtectionKeys", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DataProtectionKeys", + schema: "security"); + } + } +} diff --git a/PostFundManagement.Infrastructure/Database/SecurityMigrations/DataProtectionDbContextModelSnapshot.cs b/PostFundManagement.Infrastructure/Database/SecurityMigrations/DataProtectionDbContextModelSnapshot.cs new file mode 100644 index 0000000..7b20fad --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/SecurityMigrations/DataProtectionDbContextModelSnapshot.cs @@ -0,0 +1,45 @@ +ο»Ώ// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PostFundManagement.Infrastructure.Database; + +#nullable disable + +namespace PostFundManagement.Infrastructure.Database.SecurityMigrations +{ + [DbContext(typeof(DataProtectionDbContext))] + partial class DataProtectionDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text"); + + b.Property("Xml") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys", "security"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PostFundManagement.Infrastructure/Extensions/Constants.cs b/PostFundManagement.Infrastructure/Extensions/Constants.cs deleted file mode 100644 index 66ac514..0000000 --- a/PostFundManagement.Infrastructure/Extensions/Constants.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace PostFundManagement.Infrastructure.Extensions; - -public static class Constants -{ - public const string DatabaseConfigName = "PfmDatabase"; -} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Extensions/Postgres.cs b/PostFundManagement.Infrastructure/Extensions/Postgres.cs index 2e943c1..00dd5cf 100644 --- a/PostFundManagement.Infrastructure/Extensions/Postgres.cs +++ b/PostFundManagement.Infrastructure/Extensions/Postgres.cs @@ -1,10 +1,21 @@ using PostFundManagement.Infrastructure.Database; -using static PostFundManagement.Infrastructure.Extensions.Constants; namespace PostFundManagement.Infrastructure.Extensions; public static class Postgres { + public const string DatabaseConfigName = "PfmDatabase"; + + public static IServiceCollection AddDataProtectionDatabase(this IServiceCollection services, IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString(DatabaseConfigName); + + services.AddPooledDbContextFactory(options => + options.UseNpgsql(connectionString)); + + return services; + } + public static IServiceCollection AddApplicationDbContext(this IServiceCollection services, IConfiguration configuration) { var connectionString = configuration.GetConnectionString(DatabaseConfigName) From 436ebf36a14f8b52a952df20f719633ee4699231 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 20 Aug 2026 15:23:23 +0200 Subject: [PATCH 35/50] Added local storage service --- .../Services/BrowserLocalStorageService.cs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 PostFundManagement.Domain/Services/BrowserLocalStorageService.cs diff --git a/PostFundManagement.Domain/Services/BrowserLocalStorageService.cs b/PostFundManagement.Domain/Services/BrowserLocalStorageService.cs new file mode 100644 index 0000000..4ba6015 --- /dev/null +++ b/PostFundManagement.Domain/Services/BrowserLocalStorageService.cs @@ -0,0 +1,78 @@ +namespace PostFundManagement.Domain.Services; + +public sealed class BrowserLocalStorageService(ProtectedLocalStorage storage) +{ + public async ValueTask DeleteAsync(string key) + { + try + { + await storage.DeleteAsync(key); + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask SaveAsync(string key, string value) + { + try + { + await storage.SetAsync(key, value); + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask SaveAsync(string key, TValue value) where TValue : class + { + try + { + await storage.SetAsync(key, value); + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetAsync(string key) + { + try + { + var retrieval = await storage.GetAsync(key); + + return retrieval.Success && !string.IsNullOrWhiteSpace(retrieval.Value) + ? Result.Ok(retrieval.Value) + : Result.Fail($"Could not find object by key {key}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetAsync(string key) where TValue : class + { + try + { + var retrieval = await storage.GetAsync(key); + + return retrieval.Success && retrieval.Value is not null + ? Result.Ok(retrieval.Value) + : Result.Fail($"Could not find object by key {key}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} \ No newline at end of file From 7415a53ee233cb42ba6e10a1e52b019e49e80ba0 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 20 Aug 2026 15:49:20 +0200 Subject: [PATCH 36/50] Moved entity configurations to their own folder --- .../Configuration/Email/Account.cs | 8 ++++++++ .../Configuration/Email/SmtpSettings.cs | 12 ++++++++++++ .../Configuration/{ => Entities}/Activity.cs | 8 ++++---- .../Configuration/{ => Entities}/AuditLog.cs | 8 ++++---- .../Configuration/{ => Entities}/Award.cs | 8 ++++---- .../Configuration/{ => Entities}/Beneficiary.cs | 8 ++++---- .../Configuration/{ => Entities}/Budget.cs | 8 ++++---- .../Configuration/{ => Entities}/ChangeRequest.cs | 8 ++++---- .../Configuration/{ => Entities}/ComplianceItem.cs | 8 ++++---- .../Configuration/{ => Entities}/Contract.cs | 10 +++++----- .../Configuration/{ => Entities}/Disbursement.cs | 8 ++++---- .../Configuration/{ => Entities}/Evidence.cs | 8 ++++---- .../Configuration/{ => Entities}/Indicator.cs | 8 ++++---- .../Configuration/{ => Entities}/Instrument.cs | 8 ++++---- .../Configuration/{ => Entities}/Invite.cs | 8 ++++---- .../Configuration/{ => Entities}/Issue.cs | 8 ++++---- .../Configuration/{ => Entities}/Milestone.cs | 8 ++++---- .../Configuration/{ => Entities}/Notification.cs | 8 ++++---- .../Configuration/{ => Entities}/Organisation.cs | 8 ++++---- .../Configuration/{ => Entities}/Outcome.cs | 8 ++++---- .../Configuration/{ => Entities}/Portfolio.cs | 8 ++++---- .../Configuration/{ => Entities}/Programme.cs | 8 ++++---- .../Configuration/{ => Entities}/Project.cs | 8 ++++---- .../Configuration/{ => Entities}/Risk.cs | 8 ++++---- .../Configuration/{ => Entities}/Rule.cs | 8 ++++---- .../Configuration/{ => Entities}/SiteVisit.cs | 8 ++++---- .../Configuration/{ => Entities}/User.cs | 8 ++++---- PostFundManagement.Domain/Entities/Activity.cs | 2 +- PostFundManagement.Domain/Entities/AuditLog.cs | 2 +- PostFundManagement.Domain/Entities/Award.cs | 2 +- PostFundManagement.Domain/Entities/Beneficiary.cs | 2 +- PostFundManagement.Domain/Entities/Budget.cs | 2 +- PostFundManagement.Domain/Entities/ChangeRequest.cs | 2 +- PostFundManagement.Domain/Entities/ComplianceItem.cs | 2 +- PostFundManagement.Domain/Entities/Contract.cs | 2 +- PostFundManagement.Domain/Entities/Disbursement.cs | 2 +- PostFundManagement.Domain/Entities/Evidence.cs | 2 +- PostFundManagement.Domain/Entities/Indicator.cs | 2 +- PostFundManagement.Domain/Entities/Instrument.cs | 2 +- PostFundManagement.Domain/Entities/Invite.cs | 2 +- PostFundManagement.Domain/Entities/Issue.cs | 2 +- PostFundManagement.Domain/Entities/Milestone.cs | 2 +- PostFundManagement.Domain/Entities/Notification.cs | 2 +- PostFundManagement.Domain/Entities/Organisation.cs | 2 +- PostFundManagement.Domain/Entities/Outcome.cs | 2 +- PostFundManagement.Domain/Entities/Portfolio.cs | 2 +- PostFundManagement.Domain/Entities/Programme.cs | 2 +- PostFundManagement.Domain/Entities/Project.cs | 2 +- PostFundManagement.Domain/Entities/Risk.cs | 2 +- PostFundManagement.Domain/Entities/Rule.cs | 2 +- PostFundManagement.Domain/Entities/SiteVisit.cs | 2 +- PostFundManagement.Domain/Entities/User.cs | 2 +- 52 files changed, 146 insertions(+), 126 deletions(-) create mode 100644 PostFundManagement.Domain/Configuration/Email/Account.cs create mode 100644 PostFundManagement.Domain/Configuration/Email/SmtpSettings.cs rename PostFundManagement.Domain/Configuration/{ => Entities}/Activity.cs (87%) rename PostFundManagement.Domain/Configuration/{ => Entities}/AuditLog.cs (73%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Award.cs (87%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Beneficiary.cs (80%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Budget.cs (80%) rename PostFundManagement.Domain/Configuration/{ => Entities}/ChangeRequest.cs (81%) rename PostFundManagement.Domain/Configuration/{ => Entities}/ComplianceItem.cs (80%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Contract.cs (74%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Disbursement.cs (81%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Evidence.cs (87%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Indicator.cs (78%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Instrument.cs (78%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Invite.cs (84%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Issue.cs (83%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Milestone.cs (81%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Notification.cs (81%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Organisation.cs (74%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Outcome.cs (74%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Portfolio.cs (74%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Programme.cs (80%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Project.cs (84%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Risk.cs (79%) rename PostFundManagement.Domain/Configuration/{ => Entities}/Rule.cs (82%) rename PostFundManagement.Domain/Configuration/{ => Entities}/SiteVisit.cs (77%) rename PostFundManagement.Domain/Configuration/{ => Entities}/User.cs (56%) diff --git a/PostFundManagement.Domain/Configuration/Email/Account.cs b/PostFundManagement.Domain/Configuration/Email/Account.cs new file mode 100644 index 0000000..005b1a0 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Email/Account.cs @@ -0,0 +1,8 @@ +namespace PostFundManagement.Domain.Configuration.Email; + +public sealed class Account +{ + public string? Username { get; set; } + + public string? Password { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/Email/SmtpSettings.cs b/PostFundManagement.Domain/Configuration/Email/SmtpSettings.cs new file mode 100644 index 0000000..9ba2ac5 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Email/SmtpSettings.cs @@ -0,0 +1,12 @@ +namespace PostFundManagement.Domain.Configuration.Email; + +public sealed class SmtpSettings +{ + public Account? Credentials { get; set; } + + public int Port { get; set; } + + public string? Host { get; set; } + + public bool UseSsl { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/Activity.cs b/PostFundManagement.Domain/Configuration/Entities/Activity.cs similarity index 87% rename from PostFundManagement.Domain/Configuration/Activity.cs rename to PostFundManagement.Domain/Configuration/Entities/Activity.cs index 63a5194..401224d 100644 --- a/PostFundManagement.Domain/Configuration/Activity.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Activity.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Activity : IEntityTypeConfiguration +public sealed class Activity : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Activity).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Activity).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.ProjectId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/AuditLog.cs b/PostFundManagement.Domain/Configuration/Entities/AuditLog.cs similarity index 73% rename from PostFundManagement.Domain/Configuration/AuditLog.cs rename to PostFundManagement.Domain/Configuration/Entities/AuditLog.cs index c0bd320..f99df02 100644 --- a/PostFundManagement.Domain/Configuration/AuditLog.cs +++ b/PostFundManagement.Domain/Configuration/Entities/AuditLog.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class AuditLog : IEntityTypeConfiguration +public sealed class AuditLog : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.AuditLog).Pluralize()); + builder.ToTable(nameof(Domain.Entities.AuditLog).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.EntityName).IsRequired().HasMaxLength(LabelLength); diff --git a/PostFundManagement.Domain/Configuration/Award.cs b/PostFundManagement.Domain/Configuration/Entities/Award.cs similarity index 87% rename from PostFundManagement.Domain/Configuration/Award.cs rename to PostFundManagement.Domain/Configuration/Entities/Award.cs index 0c68765..b5fd58e 100644 --- a/PostFundManagement.Domain/Configuration/Award.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Award.cs @@ -1,10 +1,10 @@ -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Award : IEntityTypeConfiguration +public sealed class Award : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Award).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Award).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.OrganisationId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Beneficiary.cs b/PostFundManagement.Domain/Configuration/Entities/Beneficiary.cs similarity index 80% rename from PostFundManagement.Domain/Configuration/Beneficiary.cs rename to PostFundManagement.Domain/Configuration/Entities/Beneficiary.cs index 7763db5..d9762a5 100644 --- a/PostFundManagement.Domain/Configuration/Beneficiary.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Beneficiary.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Beneficiary : IEntityTypeConfiguration +public sealed class Beneficiary : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Beneficiary).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Beneficiary).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.ProjectId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Budget.cs b/PostFundManagement.Domain/Configuration/Entities/Budget.cs similarity index 80% rename from PostFundManagement.Domain/Configuration/Budget.cs rename to PostFundManagement.Domain/Configuration/Entities/Budget.cs index dd8606f..6fde58a 100644 --- a/PostFundManagement.Domain/Configuration/Budget.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Budget.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Budget : IEntityTypeConfiguration +public sealed class Budget : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Budget).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Budget).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.ProjectId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/ChangeRequest.cs b/PostFundManagement.Domain/Configuration/Entities/ChangeRequest.cs similarity index 81% rename from PostFundManagement.Domain/Configuration/ChangeRequest.cs rename to PostFundManagement.Domain/Configuration/Entities/ChangeRequest.cs index 6767dac..4ad15fe 100644 --- a/PostFundManagement.Domain/Configuration/ChangeRequest.cs +++ b/PostFundManagement.Domain/Configuration/Entities/ChangeRequest.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class ChangeRequest : IEntityTypeConfiguration +public sealed class ChangeRequest : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.ChangeRequest).Pluralize()); + builder.ToTable(nameof(Domain.Entities.ChangeRequest).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.ProjectId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/ComplianceItem.cs b/PostFundManagement.Domain/Configuration/Entities/ComplianceItem.cs similarity index 80% rename from PostFundManagement.Domain/Configuration/ComplianceItem.cs rename to PostFundManagement.Domain/Configuration/Entities/ComplianceItem.cs index ca7c6da..f6cc845 100644 --- a/PostFundManagement.Domain/Configuration/ComplianceItem.cs +++ b/PostFundManagement.Domain/Configuration/Entities/ComplianceItem.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class ComplianceItem : IEntityTypeConfiguration +public sealed class ComplianceItem : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.ComplianceItem).Pluralize()); + builder.ToTable(nameof(Domain.Entities.ComplianceItem).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.ProjectId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Contract.cs b/PostFundManagement.Domain/Configuration/Entities/Contract.cs similarity index 74% rename from PostFundManagement.Domain/Configuration/Contract.cs rename to PostFundManagement.Domain/Configuration/Entities/Contract.cs index 7b359b4..4b6920c 100644 --- a/PostFundManagement.Domain/Configuration/Contract.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Contract.cs @@ -1,10 +1,10 @@ -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Contract : IEntityTypeConfiguration +public sealed class Contract : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Contract).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Contract).Pluralize()); builder.HasKey(pk => pk.Id); builder.HasIndex(f => f.AwardId).IsUnique(); @@ -21,7 +21,7 @@ public sealed class Contract : IEntityTypeConfiguration builder.HasOne(f => f.Award) .WithOne(f => f.Contract) - .HasForeignKey(fk => fk.AwardId) + .HasForeignKey(fk => fk.AwardId) .IsRequired() .OnDelete(DeleteBehavior.Restrict); diff --git a/PostFundManagement.Domain/Configuration/Disbursement.cs b/PostFundManagement.Domain/Configuration/Entities/Disbursement.cs similarity index 81% rename from PostFundManagement.Domain/Configuration/Disbursement.cs rename to PostFundManagement.Domain/Configuration/Entities/Disbursement.cs index 4e58f8e..ee085b1 100644 --- a/PostFundManagement.Domain/Configuration/Disbursement.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Disbursement.cs @@ -1,10 +1,10 @@ -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Disbursement : IEntityTypeConfiguration +public sealed class Disbursement : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Disbursement).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Disbursement).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.ProjectId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Evidence.cs b/PostFundManagement.Domain/Configuration/Entities/Evidence.cs similarity index 87% rename from PostFundManagement.Domain/Configuration/Evidence.cs rename to PostFundManagement.Domain/Configuration/Entities/Evidence.cs index fc2ff03..2b7c716 100644 --- a/PostFundManagement.Domain/Configuration/Evidence.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Evidence.cs @@ -1,10 +1,10 @@ -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Evidence : IEntityTypeConfiguration +public sealed class Evidence : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Evidence).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Evidence).Pluralize()); builder.HasKey(f => f.Id); builder.Property(f => f.ProjectId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Indicator.cs b/PostFundManagement.Domain/Configuration/Entities/Indicator.cs similarity index 78% rename from PostFundManagement.Domain/Configuration/Indicator.cs rename to PostFundManagement.Domain/Configuration/Entities/Indicator.cs index 5a0cde7..eefce24 100644 --- a/PostFundManagement.Domain/Configuration/Indicator.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Indicator.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Indicator : IEntityTypeConfiguration +public sealed class Indicator : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Indicator).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Indicator).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.ProjectId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Instrument.cs b/PostFundManagement.Domain/Configuration/Entities/Instrument.cs similarity index 78% rename from PostFundManagement.Domain/Configuration/Instrument.cs rename to PostFundManagement.Domain/Configuration/Entities/Instrument.cs index 298f536..eeae07b 100644 --- a/PostFundManagement.Domain/Configuration/Instrument.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Instrument.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Instrument : IEntityTypeConfiguration +public sealed class Instrument : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Instrument).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Instrument).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.ProgrammeId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Invite.cs b/PostFundManagement.Domain/Configuration/Entities/Invite.cs similarity index 84% rename from PostFundManagement.Domain/Configuration/Invite.cs rename to PostFundManagement.Domain/Configuration/Entities/Invite.cs index e76959b..631fada 100644 --- a/PostFundManagement.Domain/Configuration/Invite.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Invite.cs @@ -1,11 +1,11 @@ -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Invite : IEntityTypeConfiguration +public sealed class Invite : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Invite).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Invite).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.OrganisationId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Issue.cs b/PostFundManagement.Domain/Configuration/Entities/Issue.cs similarity index 83% rename from PostFundManagement.Domain/Configuration/Issue.cs rename to PostFundManagement.Domain/Configuration/Entities/Issue.cs index fd3f4fa..c1b5252 100644 --- a/PostFundManagement.Domain/Configuration/Issue.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Issue.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Issue : IEntityTypeConfiguration +public sealed class Issue : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Issue).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Issue).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.ProjectId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Milestone.cs b/PostFundManagement.Domain/Configuration/Entities/Milestone.cs similarity index 81% rename from PostFundManagement.Domain/Configuration/Milestone.cs rename to PostFundManagement.Domain/Configuration/Entities/Milestone.cs index e38e4ce..b8e7f77 100644 --- a/PostFundManagement.Domain/Configuration/Milestone.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Milestone.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Milestone : IEntityTypeConfiguration +public sealed class Milestone : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Milestone).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Milestone).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.ProjectId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Notification.cs b/PostFundManagement.Domain/Configuration/Entities/Notification.cs similarity index 81% rename from PostFundManagement.Domain/Configuration/Notification.cs rename to PostFundManagement.Domain/Configuration/Entities/Notification.cs index c2fd488..4c12bf1 100644 --- a/PostFundManagement.Domain/Configuration/Notification.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Notification.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Notification : IEntityTypeConfiguration +public sealed class Notification : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Notification).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Notification).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); diff --git a/PostFundManagement.Domain/Configuration/Organisation.cs b/PostFundManagement.Domain/Configuration/Entities/Organisation.cs similarity index 74% rename from PostFundManagement.Domain/Configuration/Organisation.cs rename to PostFundManagement.Domain/Configuration/Entities/Organisation.cs index 050c24f..b01684e 100644 --- a/PostFundManagement.Domain/Configuration/Organisation.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Organisation.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Organisation : IEntityTypeConfiguration +public sealed class Organisation : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Organisation).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Organisation).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.CreatedBy).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Outcome.cs b/PostFundManagement.Domain/Configuration/Entities/Outcome.cs similarity index 74% rename from PostFundManagement.Domain/Configuration/Outcome.cs rename to PostFundManagement.Domain/Configuration/Entities/Outcome.cs index bd1c1a4..28a7dfa 100644 --- a/PostFundManagement.Domain/Configuration/Outcome.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Outcome.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Outcome : IEntityTypeConfiguration +public sealed class Outcome : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Outcome).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Outcome).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.BeneficiaryId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Portfolio.cs b/PostFundManagement.Domain/Configuration/Entities/Portfolio.cs similarity index 74% rename from PostFundManagement.Domain/Configuration/Portfolio.cs rename to PostFundManagement.Domain/Configuration/Entities/Portfolio.cs index 60e5627..3e438fa 100644 --- a/PostFundManagement.Domain/Configuration/Portfolio.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Portfolio.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Portfolio : IEntityTypeConfiguration +public sealed class Portfolio : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Portfolio).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Portfolio).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.CreatedBy).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Programme.cs b/PostFundManagement.Domain/Configuration/Entities/Programme.cs similarity index 80% rename from PostFundManagement.Domain/Configuration/Programme.cs rename to PostFundManagement.Domain/Configuration/Entities/Programme.cs index a2b79c6..b80e328 100644 --- a/PostFundManagement.Domain/Configuration/Programme.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Programme.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Programme : IEntityTypeConfiguration +public sealed class Programme : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Programme).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Programme).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.PortfolioId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Project.cs b/PostFundManagement.Domain/Configuration/Entities/Project.cs similarity index 84% rename from PostFundManagement.Domain/Configuration/Project.cs rename to PostFundManagement.Domain/Configuration/Entities/Project.cs index e3e833d..162ac45 100644 --- a/PostFundManagement.Domain/Configuration/Project.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Project.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Project : IEntityTypeConfiguration +public sealed class Project : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Project).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Project).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.ProgrammeId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Risk.cs b/PostFundManagement.Domain/Configuration/Entities/Risk.cs similarity index 79% rename from PostFundManagement.Domain/Configuration/Risk.cs rename to PostFundManagement.Domain/Configuration/Entities/Risk.cs index 9f7584d..77b1794 100644 --- a/PostFundManagement.Domain/Configuration/Risk.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Risk.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Risk : IEntityTypeConfiguration +public sealed class Risk : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Risk).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Risk).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.ProjectId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/Rule.cs b/PostFundManagement.Domain/Configuration/Entities/Rule.cs similarity index 82% rename from PostFundManagement.Domain/Configuration/Rule.cs rename to PostFundManagement.Domain/Configuration/Entities/Rule.cs index c602ef2..f5fbcf7 100644 --- a/PostFundManagement.Domain/Configuration/Rule.cs +++ b/PostFundManagement.Domain/Configuration/Entities/Rule.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class Rule : IEntityTypeConfiguration +public sealed class Rule : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.Rule).Pluralize()); + builder.ToTable(nameof(Domain.Entities.Rule).Pluralize()); builder.HasKey(fk => fk.Id); builder.Property(f => f.ProgrammeId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/SiteVisit.cs b/PostFundManagement.Domain/Configuration/Entities/SiteVisit.cs similarity index 77% rename from PostFundManagement.Domain/Configuration/SiteVisit.cs rename to PostFundManagement.Domain/Configuration/Entities/SiteVisit.cs index b395ed8..d2301f8 100644 --- a/PostFundManagement.Domain/Configuration/SiteVisit.cs +++ b/PostFundManagement.Domain/Configuration/Entities/SiteVisit.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class SiteVisit : IEntityTypeConfiguration +public sealed class SiteVisit : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.SiteVisit).Pluralize()); + builder.ToTable(nameof(Domain.Entities.SiteVisit).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.ProjectId).IsRequired(); diff --git a/PostFundManagement.Domain/Configuration/User.cs b/PostFundManagement.Domain/Configuration/Entities/User.cs similarity index 56% rename from PostFundManagement.Domain/Configuration/User.cs rename to PostFundManagement.Domain/Configuration/Entities/User.cs index 0562e43..bb2148a 100644 --- a/PostFundManagement.Domain/Configuration/User.cs +++ b/PostFundManagement.Domain/Configuration/Entities/User.cs @@ -1,12 +1,12 @@ using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Domain.Configuration; +namespace PostFundManagement.Domain.Configuration.Entities; -public sealed class User : IEntityTypeConfiguration +public sealed class User : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Entities.User).Pluralize()); + builder.ToTable(nameof(Domain.Entities.User).Pluralize()); builder.HasKey(pk => pk.Id); builder.Property(f => f.Email).IsRequired().HasMaxLength(LabelLength); diff --git a/PostFundManagement.Domain/Entities/Activity.cs b/PostFundManagement.Domain/Entities/Activity.cs index 07ea73b..24ed074 100644 --- a/PostFundManagement.Domain/Entities/Activity.cs +++ b/PostFundManagement.Domain/Entities/Activity.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Activity : Models.Activity { public virtual User? Creator { get; set; } diff --git a/PostFundManagement.Domain/Entities/AuditLog.cs b/PostFundManagement.Domain/Entities/AuditLog.cs index 087ffeb..34b32b5 100644 --- a/PostFundManagement.Domain/Entities/AuditLog.cs +++ b/PostFundManagement.Domain/Entities/AuditLog.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class AuditLog : Models.AuditLog { public virtual User? Performer { get; set; } diff --git a/PostFundManagement.Domain/Entities/Award.cs b/PostFundManagement.Domain/Entities/Award.cs index 66617e2..4877d52 100644 --- a/PostFundManagement.Domain/Entities/Award.cs +++ b/PostFundManagement.Domain/Entities/Award.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Award : Models.Award { public virtual User? Creator { get; set; } diff --git a/PostFundManagement.Domain/Entities/Beneficiary.cs b/PostFundManagement.Domain/Entities/Beneficiary.cs index fec8473..d7a1c81 100644 --- a/PostFundManagement.Domain/Entities/Beneficiary.cs +++ b/PostFundManagement.Domain/Entities/Beneficiary.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Beneficiary : Models.Beneficiary { public virtual Project? Project { get; set; } diff --git a/PostFundManagement.Domain/Entities/Budget.cs b/PostFundManagement.Domain/Entities/Budget.cs index 888463f..9dcdf0d 100644 --- a/PostFundManagement.Domain/Entities/Budget.cs +++ b/PostFundManagement.Domain/Entities/Budget.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Budget : Models.Budget { public virtual Project? Project { get; set; } diff --git a/PostFundManagement.Domain/Entities/ChangeRequest.cs b/PostFundManagement.Domain/Entities/ChangeRequest.cs index cf4252b..542b6cd 100644 --- a/PostFundManagement.Domain/Entities/ChangeRequest.cs +++ b/PostFundManagement.Domain/Entities/ChangeRequest.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class ChangeRequest : Models.ChangeRequest { public virtual Project? Project { get; set; } diff --git a/PostFundManagement.Domain/Entities/ComplianceItem.cs b/PostFundManagement.Domain/Entities/ComplianceItem.cs index e3b19a2..a74e6fa 100644 --- a/PostFundManagement.Domain/Entities/ComplianceItem.cs +++ b/PostFundManagement.Domain/Entities/ComplianceItem.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class ComplianceItem : Models.ComplianceItem { public virtual Project? Project { get; set; } diff --git a/PostFundManagement.Domain/Entities/Contract.cs b/PostFundManagement.Domain/Entities/Contract.cs index dfa9550..4715b68 100644 --- a/PostFundManagement.Domain/Entities/Contract.cs +++ b/PostFundManagement.Domain/Entities/Contract.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Contract : Models.Contract { public virtual Award? Award { get; set; } diff --git a/PostFundManagement.Domain/Entities/Disbursement.cs b/PostFundManagement.Domain/Entities/Disbursement.cs index 44f2530..185cdc9 100644 --- a/PostFundManagement.Domain/Entities/Disbursement.cs +++ b/PostFundManagement.Domain/Entities/Disbursement.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Disbursement : Models.Disbursement { public virtual Project? Project { get; set; } diff --git a/PostFundManagement.Domain/Entities/Evidence.cs b/PostFundManagement.Domain/Entities/Evidence.cs index 7023532..4f5cc62 100644 --- a/PostFundManagement.Domain/Entities/Evidence.cs +++ b/PostFundManagement.Domain/Entities/Evidence.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Evidence : Models.Evidence { public virtual Project? Project { get; set; } diff --git a/PostFundManagement.Domain/Entities/Indicator.cs b/PostFundManagement.Domain/Entities/Indicator.cs index bc34d6b..24283af 100644 --- a/PostFundManagement.Domain/Entities/Indicator.cs +++ b/PostFundManagement.Domain/Entities/Indicator.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Indicator : Models.Indicator { public virtual Project? Project { get; set; } diff --git a/PostFundManagement.Domain/Entities/Instrument.cs b/PostFundManagement.Domain/Entities/Instrument.cs index d43f19c..fcda49e 100644 --- a/PostFundManagement.Domain/Entities/Instrument.cs +++ b/PostFundManagement.Domain/Entities/Instrument.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Instrument : Models.Instrument { public virtual User? Creator { get; set; } diff --git a/PostFundManagement.Domain/Entities/Invite.cs b/PostFundManagement.Domain/Entities/Invite.cs index a444f3c..298290f 100644 --- a/PostFundManagement.Domain/Entities/Invite.cs +++ b/PostFundManagement.Domain/Entities/Invite.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Invite : Models.Invite { public virtual Organisation? Organisation { get; set; } diff --git a/PostFundManagement.Domain/Entities/Issue.cs b/PostFundManagement.Domain/Entities/Issue.cs index a3cf5ef..dac7511 100644 --- a/PostFundManagement.Domain/Entities/Issue.cs +++ b/PostFundManagement.Domain/Entities/Issue.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Issue : Models.Issue { public virtual Project? Project { get; set; } diff --git a/PostFundManagement.Domain/Entities/Milestone.cs b/PostFundManagement.Domain/Entities/Milestone.cs index 8aa288c..bffad70 100644 --- a/PostFundManagement.Domain/Entities/Milestone.cs +++ b/PostFundManagement.Domain/Entities/Milestone.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Milestone : Models.Milestone { public virtual Project? Project { get; set; } diff --git a/PostFundManagement.Domain/Entities/Notification.cs b/PostFundManagement.Domain/Entities/Notification.cs index f14b0fc..d85b6b8 100644 --- a/PostFundManagement.Domain/Entities/Notification.cs +++ b/PostFundManagement.Domain/Entities/Notification.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Notification : Models.Notification { public virtual Organisation? Organisation { get; set; } diff --git a/PostFundManagement.Domain/Entities/Organisation.cs b/PostFundManagement.Domain/Entities/Organisation.cs index ca75f86..3c0ffbc 100644 --- a/PostFundManagement.Domain/Entities/Organisation.cs +++ b/PostFundManagement.Domain/Entities/Organisation.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Organisation : Models.Organisation { public virtual User? Creator { get; set; } diff --git a/PostFundManagement.Domain/Entities/Outcome.cs b/PostFundManagement.Domain/Entities/Outcome.cs index 4b5ce29..7e29e82 100644 --- a/PostFundManagement.Domain/Entities/Outcome.cs +++ b/PostFundManagement.Domain/Entities/Outcome.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Outcome : Models.Outcome { public virtual Beneficiary? Beneficiary { get; set; } diff --git a/PostFundManagement.Domain/Entities/Portfolio.cs b/PostFundManagement.Domain/Entities/Portfolio.cs index 48b30e6..08684e7 100644 --- a/PostFundManagement.Domain/Entities/Portfolio.cs +++ b/PostFundManagement.Domain/Entities/Portfolio.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Portfolio : Models.Portfolio { public virtual User? Creator { get; set; } diff --git a/PostFundManagement.Domain/Entities/Programme.cs b/PostFundManagement.Domain/Entities/Programme.cs index 5c98681..aa199db 100644 --- a/PostFundManagement.Domain/Entities/Programme.cs +++ b/PostFundManagement.Domain/Entities/Programme.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Programme : Models.Programme { public virtual User? Creator { get; set; } diff --git a/PostFundManagement.Domain/Entities/Project.cs b/PostFundManagement.Domain/Entities/Project.cs index 982fb00..5840cf0 100644 --- a/PostFundManagement.Domain/Entities/Project.cs +++ b/PostFundManagement.Domain/Entities/Project.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Project : Models.Project { public virtual User? Creator { get; set; } diff --git a/PostFundManagement.Domain/Entities/Risk.cs b/PostFundManagement.Domain/Entities/Risk.cs index edcaaaa..0750956 100644 --- a/PostFundManagement.Domain/Entities/Risk.cs +++ b/PostFundManagement.Domain/Entities/Risk.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Risk : Models.Risk { public virtual Project? Project { get; set; } diff --git a/PostFundManagement.Domain/Entities/Rule.cs b/PostFundManagement.Domain/Entities/Rule.cs index f33401b..ed1c9e6 100644 --- a/PostFundManagement.Domain/Entities/Rule.cs +++ b/PostFundManagement.Domain/Entities/Rule.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class Rule : Models.Rule { public virtual User? Creator { get; set; } diff --git a/PostFundManagement.Domain/Entities/SiteVisit.cs b/PostFundManagement.Domain/Entities/SiteVisit.cs index 0981a35..3efab66 100644 --- a/PostFundManagement.Domain/Entities/SiteVisit.cs +++ b/PostFundManagement.Domain/Entities/SiteVisit.cs @@ -1,6 +1,6 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public class SiteVisit : Models.SiteVisit { public virtual Project? Project { get; set; } diff --git a/PostFundManagement.Domain/Entities/User.cs b/PostFundManagement.Domain/Entities/User.cs index 723864a..19633b8 100644 --- a/PostFundManagement.Domain/Entities/User.cs +++ b/PostFundManagement.Domain/Entities/User.cs @@ -1,4 +1,4 @@ namespace PostFundManagement.Domain.Entities; -[EntityTypeConfiguration] +[EntityTypeConfiguration] public sealed class User : Models.User; \ No newline at end of file From 8fb4aac0c100f657e605ea88812bdc29610a0677 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 20 Aug 2026 16:14:35 +0200 Subject: [PATCH 37/50] Implemented Email and Hash Service --- .../Abstractions/IService.cs | 3 + .../Configuration/Hash/HasherSettings.cs | 10 + PostFundManagement.Domain/Enums.cs | 12 + .../Extensions/EmailTelemetry.cs | 12 + PostFundManagement.Domain/Extensions/Hash.cs | 23 ++ .../Models/Email/Attachment.cs | 8 + .../Models/Email/Body.cs | 26 +++ .../Models/Email/BodyProperties.cs | 8 + .../Models/Email/Message.cs | 19 ++ .../Models/Email/Party.cs | 8 + .../Models/Email/Response.cs | 20 ++ .../Services/EmailService.cs | 214 ++++++++++++++++++ .../Services/HashService.cs | 89 ++++++++ 13 files changed, 452 insertions(+) create mode 100644 PostFundManagement.Domain/Abstractions/IService.cs create mode 100644 PostFundManagement.Domain/Configuration/Hash/HasherSettings.cs create mode 100644 PostFundManagement.Domain/Extensions/EmailTelemetry.cs create mode 100644 PostFundManagement.Domain/Extensions/Hash.cs create mode 100644 PostFundManagement.Domain/Models/Email/Attachment.cs create mode 100644 PostFundManagement.Domain/Models/Email/Body.cs create mode 100644 PostFundManagement.Domain/Models/Email/BodyProperties.cs create mode 100644 PostFundManagement.Domain/Models/Email/Message.cs create mode 100644 PostFundManagement.Domain/Models/Email/Party.cs create mode 100644 PostFundManagement.Domain/Models/Email/Response.cs create mode 100644 PostFundManagement.Domain/Services/EmailService.cs create mode 100644 PostFundManagement.Domain/Services/HashService.cs diff --git a/PostFundManagement.Domain/Abstractions/IService.cs b/PostFundManagement.Domain/Abstractions/IService.cs new file mode 100644 index 0000000..4be0e2e --- /dev/null +++ b/PostFundManagement.Domain/Abstractions/IService.cs @@ -0,0 +1,3 @@ +namespace PostFundManagement.Domain.Abstractions; + +public interface IService; diff --git a/PostFundManagement.Domain/Configuration/Hash/HasherSettings.cs b/PostFundManagement.Domain/Configuration/Hash/HasherSettings.cs new file mode 100644 index 0000000..4547fde --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Hash/HasherSettings.cs @@ -0,0 +1,10 @@ +namespace PostFundManagement.Domain.Configuration.Hash; + +public sealed class HasherSettings +{ + public string? Salt { get; set; } + + public int MinHashLength { get; set; } + + public string? PayfastPassphrase { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Enums.cs b/PostFundManagement.Domain/Enums.cs index d251779..7482db7 100644 --- a/PostFundManagement.Domain/Enums.cs +++ b/PostFundManagement.Domain/Enums.cs @@ -1,5 +1,17 @@ namespace PostFundManagement.Domain; +public enum EmailStatuses : int +{ + GeneralError = 0, + AuthenticationError = 1, + ProtocolError = 2, + Connected = 3, + Disconnected = 4, + TooManyConnections = 5, + ConnectionAborted = 6, + Success = 7 +} + public enum NotificationStatus : int { Pending = 1, diff --git a/PostFundManagement.Domain/Extensions/EmailTelemetry.cs b/PostFundManagement.Domain/Extensions/EmailTelemetry.cs new file mode 100644 index 0000000..218a152 --- /dev/null +++ b/PostFundManagement.Domain/Extensions/EmailTelemetry.cs @@ -0,0 +1,12 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace PostFundManagement.Domain.Extensions; + +public static class EmailTelemetry +{ + public static readonly ActivitySource Source = new("LiteCharms.EmailService"); + public static readonly Meter Meter = new("LiteCharms.EmailService"); + public static readonly Counter EmailsSent = Meter.CreateCounter("emails_sent_total", "count", "Total successful emails sent"); + public static readonly Counter EmailsFailed = Meter.CreateCounter("emails_failed_total", "count", "Total failed email attempts"); +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Extensions/Hash.cs b/PostFundManagement.Domain/Extensions/Hash.cs new file mode 100644 index 0000000..b327472 --- /dev/null +++ b/PostFundManagement.Domain/Extensions/Hash.cs @@ -0,0 +1,23 @@ +using PostFundManagement.Domain.Configuration.Hash; +using PostFundManagement.Domain.Services; + +namespace PostFundManagement.Domain.Extensions; + +public static class Hash +{ + public const string HasherConfigSectionName = "HasherSettings"; + + public static IServiceCollection AddHashServices(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection(HasherConfigSectionName)); + + var settings = configuration.GetSection(HasherConfigSectionName).Get(); + + services.AddSingleton(_ => + new Hashids(settings!.Salt, minHashLength: settings.MinHashLength)); + + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Email/Attachment.cs b/PostFundManagement.Domain/Models/Email/Attachment.cs new file mode 100644 index 0000000..bf88e47 --- /dev/null +++ b/PostFundManagement.Domain/Models/Email/Attachment.cs @@ -0,0 +1,8 @@ +namespace PostFundManagement.Domain.Models.Email; + +public sealed class Attachment +{ + public string? Name { get; set; } + + public Stream? FileStream { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Email/Body.cs b/PostFundManagement.Domain/Models/Email/Body.cs new file mode 100644 index 0000000..ec34619 --- /dev/null +++ b/PostFundManagement.Domain/Models/Email/Body.cs @@ -0,0 +1,26 @@ +namespace PostFundManagement.Domain.Models.Email; + +public sealed class Body : IDisposable +{ + public string? Message { get; set; } + + public ReadOnlyCollection? Attachments { get; set; } + + public BodyProperties Properties { get; set; } = new(); + + public void Dispose() + { + if (Attachments is null) return; + + foreach (var attachment in Attachments!) + { + if (attachment is not null) + { + attachment.FileStream!.Close(); + attachment.FileStream!.Dispose(); + } + } + + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Email/BodyProperties.cs b/PostFundManagement.Domain/Models/Email/BodyProperties.cs new file mode 100644 index 0000000..e500930 --- /dev/null +++ b/PostFundManagement.Domain/Models/Email/BodyProperties.cs @@ -0,0 +1,8 @@ +namespace PostFundManagement.Domain.Models.Email; + +public sealed class BodyProperties +{ + public bool IsHtml { get; set; } + + public bool HasAttachments { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Email/Message.cs b/PostFundManagement.Domain/Models/Email/Message.cs new file mode 100644 index 0000000..0d43f0d --- /dev/null +++ b/PostFundManagement.Domain/Models/Email/Message.cs @@ -0,0 +1,19 @@ +namespace PostFundManagement.Domain.Models.Email; + +public sealed class Message : IDisposable +{ + public Party? Sender { get; set; } + + public Party? Recipient { get; set; } + + public string? Subject { get; set; } + + public Body? Body { get; set; } + + public void Dispose() + { + Body?.Dispose(); + + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Email/Party.cs b/PostFundManagement.Domain/Models/Email/Party.cs new file mode 100644 index 0000000..d59f570 --- /dev/null +++ b/PostFundManagement.Domain/Models/Email/Party.cs @@ -0,0 +1,8 @@ +namespace PostFundManagement.Domain.Models.Email; + +public sealed class Party +{ + public string? Name { get; set; } + + public string? Address { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Models/Email/Response.cs b/PostFundManagement.Domain/Models/Email/Response.cs new file mode 100644 index 0000000..b0873fe --- /dev/null +++ b/PostFundManagement.Domain/Models/Email/Response.cs @@ -0,0 +1,20 @@ +namespace PostFundManagement.Domain.Models.Email; + +public sealed class Response +{ + public int Code { get; set; } + + public string? Error { get; set; } + + public EmailStatuses Status { get; set; } + + private Response(EmailStatuses status, int code = 0, string? error = null) + { + Status = status; + Code = code; + Error = error; + } + + public static Response Create(EmailStatuses status, int code = 0, string? error = null) => + new(status, code, error); +} diff --git a/PostFundManagement.Domain/Services/EmailService.cs b/PostFundManagement.Domain/Services/EmailService.cs new file mode 100644 index 0000000..0a7610c --- /dev/null +++ b/PostFundManagement.Domain/Services/EmailService.cs @@ -0,0 +1,214 @@ +using System.Diagnostics; +using PostFundManagement.Domain.Configuration.Email; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models.Email; +using static PostFundManagement.Domain.Extensions.EmailTelemetry; + +namespace PostFundManagement.Domain.Services; + +public sealed class EmailService(IOptions options) : IDisposable +{ + private readonly SmtpSettings settings = options.Value; + + private readonly SmtpClient client = new(); + + private readonly int sendMaxCount = 10; + + private int sendCount = 0; + + public EmailStatuses Status { get; private set; } = EmailStatuses.Disconnected; + + public async ValueTask> SendEmailAsync(Message message, CancellationToken cancellationToken = default) + { + using var activity = EmailTelemetry.Source.StartActivity("Email Send"); + + activity?.SetTag("email.recipient", message.Recipient?.Address); + + try + { + if (Status != EmailStatuses.Connected) + { + activity?.SetStatus(ActivityStatusCode.Error, "Disconnected"); + + return Result.Fail("Smtp service is disconnected."); + } + + var email = ConstructEmail(message, cancellationToken); + + var response = await client.SendAsync(email, cancellationToken); + + bool emailSent = response.Contains("OK", StringComparison.InvariantCultureIgnoreCase); + + message.Dispose(); + + Interlocked.Increment(ref sendCount); + + if (sendCount % sendMaxCount == 0) + { + using var delayActivity = EmailTelemetry.Source.StartActivity("Rate Limit Pause"); + + sendCount = 0; + + await Task.Delay(1000, cancellationToken); + } + + if (emailSent) + { + EmailTelemetry.EmailsSent.Add(1, new TagList { { "host", settings.Host } }); + + return Result.Ok(Response.Create(EmailStatuses.Success)); + } + + await DisconnectAsync(cancellationToken); + + var failCheckResult = HandleNegativeResponse(response); + + if (failCheckResult.IsFailed) return failCheckResult; + + Status = EmailStatuses.Disconnected; + + return Result.Fail("General error, disconnected"); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + private static MimeMessage ConstructEmail(Message message, CancellationToken cancellationToken) + { + var email = new MimeMessage(); + email.From.Add(new MailboxAddress(message.Sender!.Name, message.Sender.Address!)); + email.To.Add(new MailboxAddress(message.Recipient!.Name, message.Recipient!.Address!)); + email.Subject = message.Subject!; + + var bodyBuilder = new BodyBuilder(); + + if (message.Body!.Properties.HasAttachments) + foreach (var attachment in message.Body?.Attachments!) + bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken); + + if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message; + if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message; + + email.Body = bodyBuilder.ToMessageBody(); + + return email; + } + + private Result HandleNegativeResponse(string response) + { + if (response.Contains("421", StringComparison.Ordinal)) + { + Status = EmailStatuses.TooManyConnections; + + return Result.Fail(response); + } + + if (response.Contains("451", StringComparison.Ordinal)) + { + Status = EmailStatuses.ConnectionAborted; + + return Result.Fail(response); + } + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "error_message", response } }); + + return Result.Fail(response); + } + + public async ValueTask> ConnectAsync(CancellationToken cancellationToken = default) + { + using var activity = EmailTelemetry.Source.StartActivity("Email Connect"); + activity?.SetTag("email.smtp.connect", settings.Host); + + try + { + if (Status is EmailStatuses.Connected) return Result.Ok(Response.Create(Status)); + + await client.ConnectAsync(settings.Host!, settings.Port, settings.UseSsl, cancellationToken); + await client.AuthenticateAsync(settings.Credentials!.Username!, settings.Credentials.Password!, cancellationToken); + + Status = EmailStatuses.Connected; + + activity?.SetStatus(ActivityStatusCode.Ok, "Connected"); + + return Result.Ok(Response.Create(Status)); + } + catch (MailKit.ProtocolException ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + Status = EmailStatuses.ProtocolError; + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + catch (Exception ex) when (ex is MailKit.Security.SslHandshakeException || ex is MailKit.Security.AuthenticationException) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + Status = EmailStatuses.AuthenticationError; + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + Status = EmailStatuses.GeneralError; + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DisconnectAsync(CancellationToken cancellationToken = default) + { + using var activity = EmailTelemetry.Source.StartActivity("Email Disconnect"); + activity?.SetTag("email.smtp.disconnect", settings.Host); + + try + { + if (Status is EmailStatuses.Disconnected) return Result.Ok(); + + await client.DisconnectAsync(true, cancellationToken); + + activity?.SetStatus(ActivityStatusCode.Ok, "Disconnected"); + + Status = EmailStatuses.Disconnected; + + return Result.Ok(); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + Status = EmailStatuses.GeneralError; + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public void Dispose() + { + client.Dispose(); + + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Services/HashService.cs b/PostFundManagement.Domain/Services/HashService.cs new file mode 100644 index 0000000..fc36262 --- /dev/null +++ b/PostFundManagement.Domain/Services/HashService.cs @@ -0,0 +1,89 @@ +using PostFundManagement.Domain.Abstractions; + +namespace PostFundManagement.Domain.Services; + +public sealed partial class HashService(IHashids hasher) : IService +{ + [GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex HexHashRegex { get; } + + [GeneratedRegex(@"\A[0-9a-fA-F]{32}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex Md5Regex { get; } + + [GeneratedRegex(@"\A[0-9a-fA-F]{64}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex Sha256Regex { get; } + + public static bool IsMd5Hash(string? value) => + !string.IsNullOrWhiteSpace(value) && Md5Regex.IsMatch(value); + + public static bool IsSha256Hash(string? value) => + !string.IsNullOrWhiteSpace(value) && Sha256Regex.IsMatch(value); + + public static string? StringToSha256Hash(string? input) => + string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input))); + + public static string? StreamToSha256Hash(Stream stream) => + stream is null ? null : Convert.ToHexString(SHA256.HashData(stream)); + + public static string? BytesToSha256Hash(byte[] bytes) => + bytes is null ? null : Convert.ToHexString(SHA256.HashData(bytes)); + + public static Result ToMd5Hash(string input) + { + if (string.IsNullOrEmpty(input)) + return Result.Fail("Input content cannot be null or empty for MD5 processing."); + + byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input)); + return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant()); + } + + public Result HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input) + ? Result.Fail("Input must be a valid hexadecimal string.") + : Result.Ok(hasher.EncodeHex(input)); + + public Result HashEncodeIntId(int id) => id < 0 + ? Result.Fail("Id cannot be negative.") + : Result.Ok(hasher.Encode(id)); + + public Result HashEncodeLongId(long id) => id < 0 + ? Result.Fail("Id cannot be negative.") + : Result.Ok(hasher.EncodeLong(id)); + + public Result DecodeIntIdHash(string hash) + { + if (string.IsNullOrWhiteSpace(hash)) return Result.Fail("Invalid token layout."); + + int[] decoded = hasher.Decode(hash); + + return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail("Invalid or modified Int hash token."); + } + + public Result DecodeLongIdHash(string hash) + { + if (string.IsNullOrWhiteSpace(hash)) return Result.Fail("Invalid token layout."); + + long[] decoded = hasher.DecodeLong(hash); + + return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail("Invalid or modified Long hash token."); + } + + public Result DecodeHexHash(string hex) + { + try + { + string decoded = hasher.DecodeHex(hex); + + return string.IsNullOrEmpty(decoded) + ? Result.Fail("Invalid or corrupted hex hash.") + : Result.Ok(decoded); + } + catch (FormatException fex) + { + return Result.Fail(new Error("Invalid hash structure.").CausedBy(fex)); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} \ No newline at end of file From eef9ade66df39a76435c5d444cba73867dacfeb1 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 20 Aug 2026 16:18:21 +0200 Subject: [PATCH 38/50] Added monitoring extensions --- PostFundManagement.Domain/Extensions/Otel.cs | 52 ++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 PostFundManagement.Domain/Extensions/Otel.cs diff --git a/PostFundManagement.Domain/Extensions/Otel.cs b/PostFundManagement.Domain/Extensions/Otel.cs new file mode 100644 index 0000000..41c0fb3 --- /dev/null +++ b/PostFundManagement.Domain/Extensions/Otel.cs @@ -0,0 +1,52 @@ +namespace PostFundManagement.Domain.Extensions; + +public static class Otel +{ + public static WebApplicationBuilder AddWebTelemetry(this WebApplicationBuilder builder) + { + var serviceName = builder.Configuration.GetValue("Monitoring:ServiceName") ?? "Pfm"; + var endpoint = builder.Configuration.GetValue("Monitoring:Address")!; + var apiKey = builder.Configuration.GetValue("Monitoring:ApiKey"); + + var resourceBuilder = ResourceBuilder.CreateDefault() + .AddService(serviceName); + + var otlpHeaders = !string.IsNullOrEmpty(apiKey) ? $"x-otlp-api-key={apiKey}" : null; + + builder.Logging.AddOpenTelemetry(logging => + { + logging.SetResourceBuilder(resourceBuilder); + logging.AddOtlpExporter(opt => + { + opt.Endpoint = new Uri(endpoint); + opt.Protocol = OtlpExportProtocol.Grpc; + opt.Headers = otlpHeaders; + }); + }); + + builder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing + .SetResourceBuilder(resourceBuilder) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddOtlpExporter(opt => + { + opt.Endpoint = new Uri(endpoint); + opt.Protocol = OtlpExportProtocol.Grpc; + opt.Headers = otlpHeaders; + })) + .WithMetrics(metrics => metrics + .SetResourceBuilder(resourceBuilder) + .AddMeter(serviceName) + .AddAspNetCoreInstrumentation() + .AddRuntimeInstrumentation() + .AddOtlpExporter(opt => + { + opt.Endpoint = new Uri(endpoint); + opt.Protocol = OtlpExportProtocol.Grpc; + opt.Headers = otlpHeaders; + })); + + return builder; + } +} \ No newline at end of file From f4c680d19d5fcaf333964d26473731c4b74a3247 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 20 Aug 2026 16:32:13 +0200 Subject: [PATCH 39/50] Added Mediator and Quartz components --- .../Extensions/Constants.cs | 2 + .../Extensions/MediatorTelemetry.cs | 15 +++ .../Extensions/Quartz.cs | 104 ++++++++++++++++++ .../Mediator/LoggingPipelineBehavior.cs | 29 +++++ .../Mediator/TelemetryPipelineBehavior.cs | 69 ++++++++++++ .../Quartz/JobOrchestrator.cs | 87 +++++++++++++++ .../Quartz/MediatorJob.cs | 52 +++++++++ .../Quartz/RetryJobListener.cs | 21 ++++ .../Factories/ApplicationDbContextFactory.cs | 2 +- .../DataProtectionDbContextFactory.cs | 2 +- .../Extensions/Postgres.cs | 3 +- 11 files changed, 382 insertions(+), 4 deletions(-) create mode 100644 PostFundManagement.Domain/Extensions/MediatorTelemetry.cs create mode 100644 PostFundManagement.Domain/Extensions/Quartz.cs create mode 100644 PostFundManagement.Domain/Mediator/LoggingPipelineBehavior.cs create mode 100644 PostFundManagement.Domain/Mediator/TelemetryPipelineBehavior.cs create mode 100644 PostFundManagement.Domain/Quartz/JobOrchestrator.cs create mode 100644 PostFundManagement.Domain/Quartz/MediatorJob.cs create mode 100644 PostFundManagement.Domain/Quartz/RetryJobListener.cs diff --git a/PostFundManagement.Domain/Extensions/Constants.cs b/PostFundManagement.Domain/Extensions/Constants.cs index 6d7291a..392a3fd 100644 --- a/PostFundManagement.Domain/Extensions/Constants.cs +++ b/PostFundManagement.Domain/Extensions/Constants.cs @@ -2,6 +2,8 @@ namespace PostFundManagement.Domain.Extensions; public static class Constants { + public const string DatabaseConfigName = "PfmDatabase"; + public const int LabelLength = 256; public const int ShortLabelLength = 100; diff --git a/PostFundManagement.Domain/Extensions/MediatorTelemetry.cs b/PostFundManagement.Domain/Extensions/MediatorTelemetry.cs new file mode 100644 index 0000000..812ec2b --- /dev/null +++ b/PostFundManagement.Domain/Extensions/MediatorTelemetry.cs @@ -0,0 +1,15 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace PostFundManagement.Domain.Extensions; + +public static class MediatorTelemetry +{ + public const string ServiceName = "LiteCharms.Mediator"; + + public static readonly ActivitySource Source = new(ServiceName); + public static readonly Meter Meter = new(ServiceName); + + public static readonly Counter RequestCounter = Meter.CreateCounter("mediator_requests_total"); + public static readonly Histogram RequestDuration = Meter.CreateHistogram("mediator_request_duration_ms"); +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Extensions/Quartz.cs b/PostFundManagement.Domain/Extensions/Quartz.cs new file mode 100644 index 0000000..f8aeaec --- /dev/null +++ b/PostFundManagement.Domain/Extensions/Quartz.cs @@ -0,0 +1,104 @@ +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Quartz; +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Extensions; + +public static class Quartz +{ + public const string DefaultSchedulerName = "tech-shop"; + + public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString(DatabaseConfigName); + + services.ConfigureCommon(); + + services.AddQuartz(config => + { + config.SchedulerName = schedulerName; + config.SchedulerId = "AUTO"; + + config.UseSimpleTypeLoader(); + config.UseDefaultThreadPool(options => options.MaxConcurrency = 0); + config.UseTimeZoneConverter(); + + config.UsePersistentStore(storage => + { + storage.PerformSchemaValidation = false; + + storage.UseSystemTextJsonSerializer(); + storage.SetProperty("quartz.jobStore.clustered", "true"); + storage.SetProperty("quartz.jobStore.tablePrefix", "qrtz_"); + + storage.UsePostgres(connectionString!); + storage.UseClustering(cluster => + { + cluster.CheckinInterval = TimeSpan.FromSeconds(30); + cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90); + }); + }); + }); + + return services; + } + + public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString(DatabaseConfigName); + + services.ConfigureCommon(); + + services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true); + + services.AddQuartz(config => + { + config.SchedulerName = schedulerName; + config.SchedulerId = "AUTO"; + config.InterruptJobsOnShutdown = true; + config.InterruptJobsOnShutdownWithWait = true; + config.MaxBatchSize = 5; + + config.UseSimpleTypeLoader(); + config.UseDefaultThreadPool(options => options.MaxConcurrency = 1); + config.UseTimeZoneConverter(); + + config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString(CultureInfo.InvariantCulture)); + + config.UsePersistentStore(storage => + { + storage.PerformSchemaValidation = false; + + storage.UseSystemTextJsonSerializer(); + storage.SetProperty("quartz.jobStore.clustered", "true"); + storage.SetProperty("quartz.jobStore.tablePrefix", "qrtz_"); + + storage.UsePostgres(connectionString!); + storage.UseClustering(cluster => + { + cluster.CheckinInterval = TimeSpan.FromSeconds(30); + cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90); + }); + }); + }); + + return services; + } + + private static IServiceCollection ConfigureCommon(this IServiceCollection services) + { + services.Configure(options => + { + options.Scheduling.IgnoreDuplicates = true; + options.Scheduling.OverWriteExistingData = true; + + options["quartz.plugin.jobHistory.type"] = "Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins"; + options["quartz.plugin.triggerHistory.type"] = "Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins"; + }); + + services.AddTransient(); + services.AddTransient(); + + return services; + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Mediator/LoggingPipelineBehavior.cs b/PostFundManagement.Domain/Mediator/LoggingPipelineBehavior.cs new file mode 100644 index 0000000..750e6a7 --- /dev/null +++ b/PostFundManagement.Domain/Mediator/LoggingPipelineBehavior.cs @@ -0,0 +1,29 @@ +namespace PostFundManagement.Domain.Mediator; + +public sealed class LoggingPipelineBehavior(ILogger> logger) : + IPipelineBehavior + where TRequest : IRequest + where TResponse : ResultBase, new() +{ + public async ValueTask Handle(TRequest message, MessageHandlerDelegate next, CancellationToken cancellationToken) + { + TResponse? response = await next(message, cancellationToken); + + if (response is null) + logger.LogCritical("{Request} {TypeName} was returned as null", typeof(TRequest).Name, typeof(TRequest).Name); + + if(response?.IsFailed == true || response?.Errors?.Any() == true) + { + foreach (var error in response.Errors) + { + if (!string.IsNullOrWhiteSpace(error.Message)) + logger.LogWarning("{Request} {Error}", typeof(TRequest).Name, error.Message); + + if (error?.Reasons?.Count > 0) + error.Reasons.ForEach(r => logger.LogError("{Request} {Reason}", typeof(TRequest).Name, r.ToString())); + } + } + + return response; + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Mediator/TelemetryPipelineBehavior.cs b/PostFundManagement.Domain/Mediator/TelemetryPipelineBehavior.cs new file mode 100644 index 0000000..07774d6 --- /dev/null +++ b/PostFundManagement.Domain/Mediator/TelemetryPipelineBehavior.cs @@ -0,0 +1,69 @@ +using System.Diagnostics; +using PostFundManagement.Domain.Extensions; + +namespace PostFundManagement.Domain.Mediator; + +public sealed class TelemetryPipelineBehavior : + IPipelineBehavior + where TRequest : IRequest + where TResponse : ResultBase, new() +{ + public async ValueTask Handle(TRequest message, MessageHandlerDelegate next, CancellationToken cancellationToken) + { + var requestName = typeof(TRequest).Name; + + using var activity = MediatorTelemetry.Source.StartActivity(requestName); + + activity?.SetTag("mediator.request_type", typeof(TRequest).FullName); + + var stopWatch = Stopwatch.StartNew(); + var status = "Success"; + + try + { + TResponse? response = await next(message, cancellationToken); + + if (response is null) + { + status = "NullResponse"; + activity?.SetStatus(ActivityStatusCode.Error, "Response was null"); + + return response; + } + + if (response.IsFailed) + { + status = "Failed"; + activity?.SetStatus(ActivityStatusCode.Error, "Request failed"); + + var firstError = response.Errors.FirstOrDefault()?.Message ?? "Unknown Error"; + activity?.SetTag("error.message", firstError); + + foreach (var error in response.Errors) + activity?.AddEvent(new ActivityEvent("Result Error", tags: new() { { "message", error.Message } })); + } + else + activity?.SetStatus(ActivityStatusCode.Ok); + + return response; + } + catch (Exception ex) + { + status = "Exception"; + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + + activity?.AddException(ex); + + throw; + } + finally + { + stopWatch.Stop(); + + var tags = new TagList { { "request", requestName }, { "status", status } }; + + MediatorTelemetry.RequestCounter.Add(1, tags); + MediatorTelemetry.RequestDuration.Record(stopWatch.Elapsed.TotalMilliseconds, tags); + } + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Quartz/JobOrchestrator.cs b/PostFundManagement.Domain/Quartz/JobOrchestrator.cs new file mode 100644 index 0000000..48cb645 --- /dev/null +++ b/PostFundManagement.Domain/Quartz/JobOrchestrator.cs @@ -0,0 +1,87 @@ +using PostFundManagement.Domain.Abstractions; + +namespace PostFundManagement.Domain.Quartz; + +public sealed class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestrator +{ + public async ValueTask SendAsync(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : IEvent + { + var chainedJobGroup = "onetime-jobs"; + + var scheduler = await schedulerFactory.GetScheduler(cancellationToken); + var jobKey = new JobKey($"{notification.Name.ToLower(CultureInfo.InvariantCulture)}-{notification.CorrelationId.ToLower(CultureInfo.InvariantCulture)}", chainedJobGroup); + var triggerKey = new TriggerKey($"{jobKey.Name}-trigger", chainedJobGroup); + + var job = JobBuilder.Create>() + .WithIdentity(jobKey) + .WithDescription($"Correlation ID: {notification.CorrelationId}") + .UsingJobData(new JobDataMap { ["Payload"] = JsonSerializer.Serialize(notification) }) + .DisallowConcurrentExecution() + .RequestRecovery() + .Build(); + + var trigger = global::Quartz.TriggerBuilder.Create() + .WithIdentity(triggerKey) + .StartNow() + .Build(); + + await scheduler.ScheduleJob(job, new List { trigger }.AsReadOnly(), replace: true, cancellationToken); + } + + public async ValueTask ScheduleAsync(TNotification notification, string cronExpression, CancellationToken cancellationToken = default) + where TNotification : IEvent + { + var chainedJobGroup = "scheduled-jobs"; + + var scheduler = await schedulerFactory.GetScheduler(cancellationToken); + var jobKey = new JobKey($"{notification.Name.ToLower(CultureInfo.InvariantCulture)}", chainedJobGroup); + var triggerKey = new TriggerKey($"{jobKey.Name}-trigger", chainedJobGroup); + + var job = JobBuilder.Create>() + .WithIdentity(jobKey) + .WithDescription($"Correlation ID: {notification.CorrelationId}") + .UsingJobData(new JobDataMap { ["Payload"] = JsonSerializer.Serialize(notification) }) + .DisallowConcurrentExecution() + .StoreDurably() + .Build(); + + var now = DateTime.UtcNow; + + var trigger = global::Quartz.TriggerBuilder.Create() + .WithIdentity(triggerKey) + .WithDescription($"Scheduled via Main Job at {now:g}") + .WithCronSchedule(cronExpression, cron => cron + .WithMisfireHandlingInstructionIgnoreMisfires()) + .StartAt((DateTimeOffset)now) + .Build(); + + await scheduler.AddJob(job, replace: true, cancellationToken); + + if (await scheduler.CheckExists(triggerKey, cancellationToken)) + await scheduler.RescheduleJob(triggerKey, trigger, cancellationToken); + else + await scheduler.ScheduleJob(job, new List { trigger }.AsReadOnly(), replace: true, cancellationToken); + } + + public async ValueTask InterruptAsync(string eventName, string? correlationId = null, CancellationToken cancellationToken = default) + { + var scheduler = await schedulerFactory.GetScheduler(cancellationToken); + + var jobKeyName = string.Empty; + var jobGroup = string.Empty; + + if (!string.IsNullOrWhiteSpace(correlationId)) + { + jobKeyName = $"{eventName.ToLower(CultureInfo.InvariantCulture)}-{correlationId.ToLower(CultureInfo.InvariantCulture)}"; + jobGroup = "onetime-jobs"; + } + else + { + jobKeyName = eventName.ToLower(CultureInfo.InvariantCulture); + jobGroup = "scheduled-jobs"; + } + + return await scheduler.Interrupt(JobKey.Create(jobKeyName, jobGroup), cancellationToken); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Quartz/MediatorJob.cs b/PostFundManagement.Domain/Quartz/MediatorJob.cs new file mode 100644 index 0000000..f42c2d5 --- /dev/null +++ b/PostFundManagement.Domain/Quartz/MediatorJob.cs @@ -0,0 +1,52 @@ +using System.Diagnostics; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Extensions; + +namespace PostFundManagement.Domain.Quartz; + +[DisallowConcurrentExecution] +public sealed class MediatorJob(IMediator mediator) : IJob where TNotification : IEvent +{ + public async Task Execute(IJobExecutionContext context) + { + if (context.Recovering) + Trace.WriteLine($"CRITICAL RECOVERY: Resurrecting job '{typeof(TNotification).Name}' after a previous cluster node crashed mid-execution."); + + var data = context.MergedJobDataMap["Payload"] as string; + + if (string.IsNullOrWhiteSpace(data)) + { + Trace.WriteLine("Job Payload missing, job ended"); + + return; + } + + var notification = JsonSerializer.Deserialize(data); + + if (notification is null) + { + Trace.WriteLine("Notification could not be Json converted from data string, job ended"); + + return; + } + + using var activity = MediatorTelemetry.Source.StartActivity(typeof(TNotification).Name); + + activity?.SetTag("event.correlation_id", notification.CorrelationId); + + try + { + await mediator.Publish(notification, context.CancellationToken); + + Trace.WriteLine("Job published successfully"); + } + catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) + { + Trace.WriteLine($"Job '{typeof(TNotification).Name}' was gracefully interrupted by the cluster control plane."); + + activity?.SetStatus(ActivityStatusCode.Ok); + + return; + } + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Quartz/RetryJobListener.cs b/PostFundManagement.Domain/Quartz/RetryJobListener.cs new file mode 100644 index 0000000..9565773 --- /dev/null +++ b/PostFundManagement.Domain/Quartz/RetryJobListener.cs @@ -0,0 +1,21 @@ +namespace PostFundManagement.Domain.Quartz; + +public sealed class RetryJobListener : IJobListener +{ + public string Name => "RetryJobListener"; + + public int RetryCount { get; set; } = 3; + + public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default) => Task.CompletedTask; + + public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default) => Task.CompletedTask; + + public async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default) + { + if (context.CancellationToken.IsCancellationRequested) + return; + + if (jobException is not null && context.RefireCount < RetryCount) + jobException.RefireImmediately = true; + } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/Factories/ApplicationDbContextFactory.cs b/PostFundManagement.Infrastructure/Database/Factories/ApplicationDbContextFactory.cs index a0f4aea..7d06651 100644 --- a/PostFundManagement.Infrastructure/Database/Factories/ApplicationDbContextFactory.cs +++ b/PostFundManagement.Infrastructure/Database/Factories/ApplicationDbContextFactory.cs @@ -1,4 +1,4 @@ -using static PostFundManagement.Infrastructure.Extensions.Postgres; +using static PostFundManagement.Domain.Extensions.Constants; namespace PostFundManagement.Infrastructure.Database.Factories; diff --git a/PostFundManagement.Infrastructure/Database/Factories/DataProtectionDbContextFactory.cs b/PostFundManagement.Infrastructure/Database/Factories/DataProtectionDbContextFactory.cs index d71cad2..b54ff53 100644 --- a/PostFundManagement.Infrastructure/Database/Factories/DataProtectionDbContextFactory.cs +++ b/PostFundManagement.Infrastructure/Database/Factories/DataProtectionDbContextFactory.cs @@ -1,4 +1,4 @@ -using static PostFundManagement.Infrastructure.Extensions.Postgres; +using static PostFundManagement.Domain.Extensions.Constants; namespace PostFundManagement.Infrastructure.Database.Factories; diff --git a/PostFundManagement.Infrastructure/Extensions/Postgres.cs b/PostFundManagement.Infrastructure/Extensions/Postgres.cs index 00dd5cf..c12d4b5 100644 --- a/PostFundManagement.Infrastructure/Extensions/Postgres.cs +++ b/PostFundManagement.Infrastructure/Extensions/Postgres.cs @@ -1,11 +1,10 @@ using PostFundManagement.Infrastructure.Database; +using static PostFundManagement.Domain.Extensions.Constants; namespace PostFundManagement.Infrastructure.Extensions; public static class Postgres { - public const string DatabaseConfigName = "PfmDatabase"; - public static IServiceCollection AddDataProtectionDatabase(this IServiceCollection services, IConfiguration configuration) { var connectionString = configuration.GetConnectionString(DatabaseConfigName); From 1931899d534ce04ebd143e87179e8f014506cd18 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 20 Aug 2026 16:46:17 +0200 Subject: [PATCH 40/50] Added S3 support and services --- .../Abstractions/IS3Service.cs | 7 ++ .../Abstractions/S3ServiceBase.cs | 83 +++++++++++++++++++ .../Configuration/S3/S3Settings.cs | 16 ++++ .../Extensions/Constants.cs | 12 +++ .../Services/ContractS3Service.cs | 11 +++ .../Services/EvidenceS3Service.cs | 11 +++ .../Services/GeneralS3Service.cs | 11 +++ 7 files changed, 151 insertions(+) create mode 100644 PostFundManagement.Domain/Abstractions/IS3Service.cs create mode 100644 PostFundManagement.Domain/Abstractions/S3ServiceBase.cs create mode 100644 PostFundManagement.Domain/Configuration/S3/S3Settings.cs create mode 100644 PostFundManagement.Domain/Services/ContractS3Service.cs create mode 100644 PostFundManagement.Domain/Services/EvidenceS3Service.cs create mode 100644 PostFundManagement.Domain/Services/GeneralS3Service.cs diff --git a/PostFundManagement.Domain/Abstractions/IS3Service.cs b/PostFundManagement.Domain/Abstractions/IS3Service.cs new file mode 100644 index 0000000..c47083b --- /dev/null +++ b/PostFundManagement.Domain/Abstractions/IS3Service.cs @@ -0,0 +1,7 @@ +namespace PostFundManagement.Domain.Abstractions; + +public interface IS3Service +{ + Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default); + Task DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Abstractions/S3ServiceBase.cs b/PostFundManagement.Domain/Abstractions/S3ServiceBase.cs new file mode 100644 index 0000000..e78cba1 --- /dev/null +++ b/PostFundManagement.Domain/Abstractions/S3ServiceBase.cs @@ -0,0 +1,83 @@ +using PostFundManagement.Domain.Services; + +namespace PostFundManagement.Domain.Abstractions; + +public abstract class S3ServiceBase(IAmazonS3 amazonS3) +{ + protected readonly IAmazonS3 Client = amazonS3; + + protected abstract string BucketName { get; } + protected abstract string CdnBaseUrl { get; } + + public virtual async Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) + { + try + { + if (string.IsNullOrWhiteSpace(BucketName)) + return Result.Fail("Bucket name is not configured."); + + if (string.IsNullOrWhiteSpace(CdnBaseUrl)) + return Result.Fail("CDN base URL is not configured."); + + using var stream = new MemoryStream(); + + await fileStream.CopyToAsync(stream, cancellationToken); + await fileStream.DisposeAsync(); + + stream.Seek(0, SeekOrigin.Begin); + + var fileHash = HashService.StreamToSha256Hash(stream); + + if(string.IsNullOrWhiteSpace(fileHash)) + return Result.Fail("Failed to compute file hash."); + + var fileKey = $"{fileHash.ToLower(CultureInfo.InvariantCulture)}{Path.GetExtension(fileName)}"; + + var putRequest = new PutObjectRequest + { + BucketName = BucketName, + Key = fileKey, + InputStream = stream, + ContentType = contentType, + UseChunkEncoding = false, + }; + + stream.Seek(0, SeekOrigin.Begin); + + var response = await Client.PutObjectAsync(putRequest, cancellationToken); + + return response.HttpStatusCode != System.Net.HttpStatusCode.OK + ? Result.Fail($"Failed to upload {fileName} to S3.") + : Result.Ok($"{CdnBaseUrl}/{fileKey}"); + } + catch (Exception ex) + { + return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); + } + } + + public virtual async Task DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default) + { + try + { + if (string.IsNullOrWhiteSpace(BucketName)) + return Result.Fail("Bucket name is not configured."); + + var deleteRequest = new DeleteObjectRequest + { + BucketName = BucketName, + Key = fileKey + }; + + var response = await Client.DeleteObjectAsync(deleteRequest, cancellationToken); + + return response.HttpStatusCode != System.Net.HttpStatusCode.NoContent + ? Result.Fail($"Failed to delete {fileKey} from S3.") + : Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error($"Error deleting {fileKey} from S3: {ex.Message}").CausedBy(ex)); + } + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/S3/S3Settings.cs b/PostFundManagement.Domain/Configuration/S3/S3Settings.cs new file mode 100644 index 0000000..9e36e5a --- /dev/null +++ b/PostFundManagement.Domain/Configuration/S3/S3Settings.cs @@ -0,0 +1,16 @@ +namespace PostFundManagement.Domain.Configuration.S3; + +public sealed class S3Settings +{ + public string? ServiceUrl { get; set; } + + public string? AccessKey { get; set; } + + public string? SecretKey { get; set; } + + public string? BucketName { get; set; } + + public string? Region { get; set; } + + public string? CdnBaseUrl { get; set; } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Extensions/Constants.cs b/PostFundManagement.Domain/Extensions/Constants.cs index 392a3fd..dd57bd2 100644 --- a/PostFundManagement.Domain/Extensions/Constants.cs +++ b/PostFundManagement.Domain/Extensions/Constants.cs @@ -2,6 +2,18 @@ namespace PostFundManagement.Domain.Extensions; public static class Constants { + public const string GeneralS3SettingsSection = "PfmS3Settings"; + + public const string EvidenceS3SettingsSection = "EvidenceS3Settings"; + + public const string ContractS3SettingsSection = "ContractS3Settings"; + + public const string GeneralQuotesBucketName = "pfm.general"; + + public const string EvidenceQuotesBucketName = "pfm.evidence"; + + public const string ContractQuotesBucketName = "pfm.contract"; + public const string DatabaseConfigName = "PfmDatabase"; public const int LabelLength = 256; diff --git a/PostFundManagement.Domain/Services/ContractS3Service.cs b/PostFundManagement.Domain/Services/ContractS3Service.cs new file mode 100644 index 0000000..b7f632b --- /dev/null +++ b/PostFundManagement.Domain/Services/ContractS3Service.cs @@ -0,0 +1,11 @@ +using PostFundManagement.Domain.Abstractions; +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Services; + +public sealed class ContractS3Service(IConfiguration configuration, [FromKeyedServices(ContractQuotesBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + protected override string BucketName => configuration.GetSection($"{ContractS3SettingsSection}:BucketName").Value ?? ""; + protected override string CdnBaseUrl => configuration.GetSection($"{ContractS3SettingsSection}:CdnBaseUrl").Value ?? ""; +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Services/EvidenceS3Service.cs b/PostFundManagement.Domain/Services/EvidenceS3Service.cs new file mode 100644 index 0000000..097715b --- /dev/null +++ b/PostFundManagement.Domain/Services/EvidenceS3Service.cs @@ -0,0 +1,11 @@ +using PostFundManagement.Domain.Abstractions; +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Services; + +public sealed class EvidenceS3Service(IConfiguration configuration, [FromKeyedServices(EvidenceQuotesBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + protected override string BucketName => configuration.GetSection($"{EvidenceS3SettingsSection}:BucketName").Value ?? ""; + protected override string CdnBaseUrl => configuration.GetSection($"{EvidenceS3SettingsSection}:CdnBaseUrl").Value ?? ""; +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Services/GeneralS3Service.cs b/PostFundManagement.Domain/Services/GeneralS3Service.cs new file mode 100644 index 0000000..5b425df --- /dev/null +++ b/PostFundManagement.Domain/Services/GeneralS3Service.cs @@ -0,0 +1,11 @@ +using PostFundManagement.Domain.Abstractions; +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Services; + +public sealed class GeneralS3Service(IConfiguration configuration, [FromKeyedServices(GeneralQuotesBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + protected override string BucketName => configuration.GetSection($"{GeneralS3SettingsSection}:BucketName").Value ?? ""; + protected override string CdnBaseUrl => configuration.GetSection($"{GeneralS3SettingsSection}:CdnBaseUrl").Value ?? ""; +} \ No newline at end of file From 25cba4a3d52a5b16fd1edce944350ce86fa1414b Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 20 Aug 2026 16:51:48 +0200 Subject: [PATCH 41/50] Added S3 service registration --- .../Extensions/Constants.cs | 6 +- PostFundManagement.Domain/Extensions/S3.cs | 64 +++++++++++++++++++ .../Services/ContractS3Service.cs | 2 +- .../Services/EvidenceS3Service.cs | 2 +- .../Services/GeneralS3Service.cs | 2 +- 5 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 PostFundManagement.Domain/Extensions/S3.cs diff --git a/PostFundManagement.Domain/Extensions/Constants.cs b/PostFundManagement.Domain/Extensions/Constants.cs index dd57bd2..c16be94 100644 --- a/PostFundManagement.Domain/Extensions/Constants.cs +++ b/PostFundManagement.Domain/Extensions/Constants.cs @@ -8,11 +8,11 @@ public static class Constants public const string ContractS3SettingsSection = "ContractS3Settings"; - public const string GeneralQuotesBucketName = "pfm.general"; + public const string GeneralBucketName = "pfm.general"; - public const string EvidenceQuotesBucketName = "pfm.evidence"; + public const string EvidenceBucketName = "pfm.evidence"; - public const string ContractQuotesBucketName = "pfm.contract"; + public const string ContractBucketName = "pfm.contract"; public const string DatabaseConfigName = "PfmDatabase"; diff --git a/PostFundManagement.Domain/Extensions/S3.cs b/PostFundManagement.Domain/Extensions/S3.cs new file mode 100644 index 0000000..add63c4 --- /dev/null +++ b/PostFundManagement.Domain/Extensions/S3.cs @@ -0,0 +1,64 @@ +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Services; +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Extensions; + +public static class S3 +{ + public static IServiceCollection AddGarageS3(this IServiceCollection services, IConfiguration configuration) + { + if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{GeneralS3SettingsSection}:ServiceUrl").Value)) + { + services.AddKeyedSingleton(GeneralBucketName, (provider, client) => + new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{GeneralS3SettingsSection}:AccessKey").Value, + configuration.GetSection($"{GeneralS3SettingsSection}:SecretKey").Value), + new AmazonS3Config + { + ServiceURL = configuration.GetSection($"{GeneralS3SettingsSection}:ServiceUrl").Value, + AuthenticationRegion = configuration.GetSection($"{GeneralS3SettingsSection}:Region").Value, + ForcePathStyle = true, + EndpointDiscoveryEnabled = true, + UseHttp = configuration.GetSection($"{GeneralS3SettingsSection}:ServiceUrl").Value!.Contains("http://", StringComparison.InvariantCultureIgnoreCase), + })); + + services.AddKeyedScoped(GeneralBucketName); + } + + if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{EvidenceS3SettingsSection}:ServiceUrl").Value)) + { + services.AddKeyedSingleton(EvidenceS3SettingsSection, (provider, client) => + new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{EvidenceS3SettingsSection}:AccessKey").Value, + configuration.GetSection($"{EvidenceS3SettingsSection}:SecretKey").Value), + new AmazonS3Config + { + ServiceURL = configuration.GetSection($"{EvidenceS3SettingsSection}:ServiceUrl").Value, + AuthenticationRegion = configuration.GetSection($"{EvidenceS3SettingsSection}:Region").Value, + ForcePathStyle = true, + EndpointDiscoveryEnabled = true, + UseHttp = configuration.GetSection($"{EvidenceS3SettingsSection}:ServiceUrl").Value!.Contains("http://", StringComparison.InvariantCultureIgnoreCase), + })); + + services.AddKeyedScoped(EvidenceS3SettingsSection); + } + + if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{ContractS3SettingsSection}:ServiceUrl").Value)) + { + services.AddKeyedSingleton(ContractS3SettingsSection, (provider, client) => + new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{ContractS3SettingsSection}:AccessKey").Value, + configuration.GetSection($"{ContractS3SettingsSection}:SecretKey").Value), + new AmazonS3Config + { + ServiceURL = configuration.GetSection($"{ContractS3SettingsSection}:ServiceUrl").Value, + AuthenticationRegion = configuration.GetSection($"{ContractS3SettingsSection}:Region").Value, + ForcePathStyle = true, + EndpointDiscoveryEnabled = true, + UseHttp = configuration.GetSection($"{ContractS3SettingsSection}:ServiceUrl").Value!.Contains("http://", StringComparison.InvariantCultureIgnoreCase), + })); + + services.AddKeyedScoped(ContractS3SettingsSection); + } + + return services; + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Services/ContractS3Service.cs b/PostFundManagement.Domain/Services/ContractS3Service.cs index b7f632b..481f37e 100644 --- a/PostFundManagement.Domain/Services/ContractS3Service.cs +++ b/PostFundManagement.Domain/Services/ContractS3Service.cs @@ -3,7 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants; namespace PostFundManagement.Domain.Services; -public sealed class ContractS3Service(IConfiguration configuration, [FromKeyedServices(ContractQuotesBucketName)] IAmazonS3 amazonS3) : +public sealed class ContractS3Service(IConfiguration configuration, [FromKeyedServices(ContractBucketName)] IAmazonS3 amazonS3) : S3ServiceBase(amazonS3), IS3Service { protected override string BucketName => configuration.GetSection($"{ContractS3SettingsSection}:BucketName").Value ?? ""; diff --git a/PostFundManagement.Domain/Services/EvidenceS3Service.cs b/PostFundManagement.Domain/Services/EvidenceS3Service.cs index 097715b..67de5ac 100644 --- a/PostFundManagement.Domain/Services/EvidenceS3Service.cs +++ b/PostFundManagement.Domain/Services/EvidenceS3Service.cs @@ -3,7 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants; namespace PostFundManagement.Domain.Services; -public sealed class EvidenceS3Service(IConfiguration configuration, [FromKeyedServices(EvidenceQuotesBucketName)] IAmazonS3 amazonS3) : +public sealed class EvidenceS3Service(IConfiguration configuration, [FromKeyedServices(EvidenceBucketName)] IAmazonS3 amazonS3) : S3ServiceBase(amazonS3), IS3Service { protected override string BucketName => configuration.GetSection($"{EvidenceS3SettingsSection}:BucketName").Value ?? ""; diff --git a/PostFundManagement.Domain/Services/GeneralS3Service.cs b/PostFundManagement.Domain/Services/GeneralS3Service.cs index 5b425df..79b0fb6 100644 --- a/PostFundManagement.Domain/Services/GeneralS3Service.cs +++ b/PostFundManagement.Domain/Services/GeneralS3Service.cs @@ -3,7 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants; namespace PostFundManagement.Domain.Services; -public sealed class GeneralS3Service(IConfiguration configuration, [FromKeyedServices(GeneralQuotesBucketName)] IAmazonS3 amazonS3) : +public sealed class GeneralS3Service(IConfiguration configuration, [FromKeyedServices(GeneralBucketName)] IAmazonS3 amazonS3) : S3ServiceBase(amazonS3), IS3Service { protected override string BucketName => configuration.GetSection($"{GeneralS3SettingsSection}:BucketName").Value ?? ""; From e3058e0439826083afaf6c80a6cfce9ae924a576 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 20 Aug 2026 16:54:01 +0200 Subject: [PATCH 42/50] Added editor configuration to solution --- .editorconfig | 287 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6ab8a85 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,287 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] +# IDE0250: Prefer make struct 'readonly' +dotnet_diagnostic.IDE0250.severity = warning + +# IDE0060: Remove unused parameters (Good cleanup pairing) +dotnet_diagnostic.IDE0060.severity = warning + +# CA1852: Seal internal types (Available in modern .NET) +dotnet_diagnostic.CA1852.severity = warning + +# MA0018: Add sealed modifier to types that are never inherited +dotnet_diagnostic.MA0018.severity = warning + +# Enforce that classes should be sealed +dotnet_diagnostic.MA0053.severity = warning + +# CRITICAL: Force the analyzer to also flag PUBLIC classes, not just internal ones +meziantou_analyzer.MA0053.public_class_should_be_sealed = true +MA0053.public_class_should_be_sealed = true + +# Keep the rule active as a warning by default +dotnet_diagnostic.MA0048.severity = warning + +# Specific exclusions for Meziantou.Analyzer MA0048 +# Disable the rule for enums +meziantou_analyzer.MA0048.exclude_enums = true + +# Disable the rule for records +meziantou_analyzer.MA0048.exclude_records = true + +#EXCLUDE specific files that are meant to hold grouped enums/records +dotnet_diagnostic.MA0048.severity = warning + +# Disable the requirement to specify ConfigureAwait(false) +dotnet_diagnostic.MA0004.severity = none + +# ALTERNATIVE: Exclude any file ending with 'Enums.cs' or 'Records.cs' +# (e.g., BillingEnums.cs, CustomerRecords.cs) +[**/*{Enums,Records}.cs] +dotnet_diagnostic.MA0048.severity = none + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Code Actions #### + +# Type members +dotnet_hide_advanced_members = false +dotnet_member_insertion_location = with_other_members_of_the_same_kind +dotnet_property_generation_behavior = prefer_throwing_properties + +# Symbol search +dotnet_search_reference_assemblies = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_prefer_system_hash_code = true +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_non_hidden_explicit_cast_in_source = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = true +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_anonymous_function = true +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_prefer_system_threading_lock = true +csharp_style_namespace_declarations = block_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_simple_property_accessors = true +csharp_style_prefer_top_level_statements = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_implicitly_typed_lambda_expression = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_unbound_generic_type_in_nameof = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case From b48c66375648c20a6516948bfddc21a0b1bd3d84 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 20 Aug 2026 17:04:32 +0200 Subject: [PATCH 43/50] Added readme --- README.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..7490ecd --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# PostFundManagement (PFM) Platform + +An enterprise system designed to manage the entire post-funding grant and project lifecycle. PFM provides end-to-end oversight across strategic portfolios, programme allocation, project execution tracking, financial disbursements, monitoring & evaluation (M&E), and candidate notifications. + +--- + +## πŸ›οΈ System Architecture & Domain Model + +The application follows a modular, hierarchical architecture where high-level funding directives cascade into measurable project outcomes and evidence verification. + +[Portfolios] ───> [Programmes] ───> [Projects] ───> [Awards] ───> [Contracts] ───> [Beneficiaries] ──> [Outcomes] +β”‚ β”‚ β”‚ +β–Ό β”‚ β–Ό +[Rules] β”œβ”€> [Milestones] ──> [Activities] ──> [Evidences] +β”œβ”€> [Disbursements] +β”œβ”€> [Indicators] +β”œβ”€> [Risks] & [Issues] +└─> [SiteVisits] & [ComplianceItems] + +### **1. Portfolio & Programme Governance** +* **Portfolios**: Top-level containers for strategic investment themes. +* **Programmes**: Direct implementation vehicles linked to specific financial instruments (`Instruments`) and operational guidelines (`Rules`). + +### **2. Project Execution & Monitoring** +* **Projects**: Central operational units categorized by `IndustrySector` and `ProjectType`. +* **Milestones & Activities**: Project progress is tracked through structured milestones. Project Managers author `Activities` against specific milestones to log ground work. +* **Risks, Issues & Compliance**: Managed via dedicated tracking logs (`Risks`, `Issues`, `ComplianceItems`, `ChangeRequests`) to maintain project integrity. + +### **3. Grants, Contracting & Disbursements** +* **Awards & Contracts**: Binding grant agreements signed with target `Organisations`. +* **Beneficiaries & Outcomes**: Social and economic impact measurement down to individual beneficiary groups. +* **Disbursements**: Milestone-gated tranche releases tied to approved evidence and verification audits. + +### **4. Monitoring, Evaluation & Evidence (M&E)** +* **Indicators**: KPI baseline, target, and actual metrics tracking. +* **Evidences**: Verifiable artifacts (`EvidenceType`) uploaded alongside activities, indicators, or milestones. +* **Site Visits**: Independent audit logs (`SiteVisits`) submitted by field inspectors. + +### **5. Notifications & Platform Services** +* Multi-channel delivery engine (`NotificationPlatform`) tracking candidate invitations, reminders, and status updates (`NotificationStatus`) across Email, SMS, WhatsApp, In-App, and Webhooks. + +--- + +## 🧩 Key System Enums + +| Enum | Scope | Purpose | +| :--- | :--- | :--- | +| `IndustrySector` | Project Domain | Standardizes reporting across sectors (e.g., ICT, Agriculture, Manufacturing). | +| `ProjectType` | Project Domain | Classifies project scope (e.g., R&D, Infrastructure, Capacity Building). | +| `EvidenceType` | M&E / Verification | Categorizes uploaded proof (e.g., Attendance Register, Geotagged Photo, Bank Statement). | +| `NotificationPlatform` | System / Comms | Outlines dispatch channels (Email, SMS, WhatsApp, Push, Webhooks). | +| `NotificationStatus` | System / Comms | Tracks communication lifecycle (Pending, Queued, Sent, Delivered, Failed). | + +--- + +## πŸ› οΈ Tech Stack & Patterns + +* **Backend Framework**: .NET 8 / 9 C# Web API +* **Database**: PostgreSQL +* **ORM**: Entity Framework Core (Fluent API Configuration) +* **Architecture**: Clean Architecture / Domain-Driven Design (DDD) with CQRS (`MediatR`) +* **Utilities**: `Humanizer` (Automated entity-to-table pluralization) + +--- + +## πŸ“ Solution Structure + +```text +src/ + β”œβ”€β”€ PostFundManagement.Domain/ # Core Domain Entities, Enums & Interfaces + β”œβ”€β”€ PostFundManagement.Infrastructure/ # EF Core Configurations, DbContext, Migrations + β”œβ”€β”€ PostFundManagement.Application/ # CQRS Handlers, DTOs, Business Rules & Logic + └── PostFundManagement.Api/ # REST Controllers, Middleware, Auth & Swagger +``` + +## πŸš€ Getting Started + +### Prerequisites + +.NET 8.0 SDK or higher +PostgreSQL 14+ +dotnet-ef CLI Tool (dotnet tool install --global dotnet-ef) + +## Setup & Execution + +## Clone Repository + +1. Clone Repository: + +```bash +git clone [https://github.com/your-org/PostFundManagement.git](https://github.com/your-org/PostFundManagement.git) +cd PostFundManagement +``` + +2. Configure Database Connection: +Update appsettings.Development.json in the API project with your local PostgreSQL details: + +```json +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Database=pfm_db;Username=postgres;Password=your_password" + } +} +``` + +3. Apply Database Migrations: + +```bash +dotnet ef database update --project src/PostFundManagement.Infrastructure +``` + +4. Launch Application: + +```bash +dotnet run --project src/PostFundManagement.Api +``` + +## πŸ“„ License + +Internal Proprietary Software β€” All rights reserved. From 8f7af903442ce49758d0fc265740a8a0bb913965 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 20 Aug 2026 17:14:41 +0200 Subject: [PATCH 44/50] Signed assemblies --- .../PostFundManagement.Api.csproj | 1 + PostFundManagement.Api/Program.cs | 1 + .../PostFundManagement.Domain.csproj | 1 + .../PostFundManagement.Infrastructure.csproj | 1 + PostFundManagement.snk | Bin 0 -> 596 bytes 5 files changed, 4 insertions(+) create mode 100644 PostFundManagement.snk diff --git a/PostFundManagement.Api/PostFundManagement.Api.csproj b/PostFundManagement.Api/PostFundManagement.Api.csproj index f1c628a..891a5fe 100644 --- a/PostFundManagement.Api/PostFundManagement.Api.csproj +++ b/PostFundManagement.Api/PostFundManagement.Api.csproj @@ -5,6 +5,7 @@ enable enable 59a05094-4ac2-472d-af04-3407e909432d + ..\PostFundManagement.snk diff --git a/PostFundManagement.Api/Program.cs b/PostFundManagement.Api/Program.cs index 23c494a..aaf8987 100644 --- a/PostFundManagement.Api/Program.cs +++ b/PostFundManagement.Api/Program.cs @@ -2,6 +2,7 @@ using Scalar.AspNetCore; var builder = WebApplication.CreateBuilder(args); +builder.Services.AddApiVersioning(); builder.Services.AddOpenApi(); var app = builder.Build(); diff --git a/PostFundManagement.Domain/PostFundManagement.Domain.csproj b/PostFundManagement.Domain/PostFundManagement.Domain.csproj index a0ee4f6..990128d 100644 --- a/PostFundManagement.Domain/PostFundManagement.Domain.csproj +++ b/PostFundManagement.Domain/PostFundManagement.Domain.csproj @@ -4,6 +4,7 @@ net10.0 enable enable + ..\PostFundManagement.snk diff --git a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj index 7a4e7e4..9ce4b54 100644 --- a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj +++ b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj @@ -5,6 +5,7 @@ enable enable c4131f8d-ff78-432e-86d2-f6e131bd4cd3 + ..\PostFundManagement.snk diff --git a/PostFundManagement.snk b/PostFundManagement.snk new file mode 100644 index 0000000000000000000000000000000000000000..bb8c7f765d162407553037afd96120648ab8bf5a GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096+<@0Wp?I7ctTxPe(vvz;2z|w_k0qJN* z&4{EOA=2H_26GMjyg}>a>)N~rrA>F~9Fs_UbzZ&bswX{{gs3jA6*{J3FZ3)evF4yI z?tJzx4I$L~+s{}rPSJhxP#VYc=OINnW(5sf4KHH?3)z?m6NqjlTpU``egS?+2lD5K zGNqrPa*prXGNv@dhw0k&1Yt?FusQXuH^>AR8nGyDHC`33KDNrf~G@ zCU6EgO6#?{l{LDMohG(OK;s=hEXC>N2I5wj74%Gt+w)do6|f-b@>h->-8zTBF1r`4 zF#k{ipFwbVqtDQlQ#wOlntR;@6TOZR9%uW?D>|lnl{hvj6_XR2_0PBaLy<%=uw~yh z Date: Thu, 20 Aug 2026 17:45:27 +0200 Subject: [PATCH 45/50] Added api boostrap logic --- .../PostFundManagement.Api.csproj | 94 ++++++++++++++-- PostFundManagement.Api/Program.cs | 103 +++++++++++++++--- PostFundManagement.Api/appsettings.json | 14 ++- .../Extensions/Constants.cs | 2 + PostFundManagement.Domain/Extensions/Email.cs | 23 ++++ .../Extensions/Quartz.cs | 2 - 6 files changed, 213 insertions(+), 25 deletions(-) create mode 100644 PostFundManagement.Domain/Extensions/Email.cs diff --git a/PostFundManagement.Api/PostFundManagement.Api.csproj b/PostFundManagement.Api/PostFundManagement.Api.csproj index 891a5fe..746d861 100644 --- a/PostFundManagement.Api/PostFundManagement.Api.csproj +++ b/PostFundManagement.Api/PostFundManagement.Api.csproj @@ -8,18 +8,96 @@ ..\PostFundManagement.snk + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + diff --git a/PostFundManagement.Api/Program.cs b/PostFundManagement.Api/Program.cs index aaf8987..4f84139 100644 --- a/PostFundManagement.Api/Program.cs +++ b/PostFundManagement.Api/Program.cs @@ -1,23 +1,100 @@ -using Scalar.AspNetCore; +using PostFundManagement.Api.Extensions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Mediator; +using PostFundManagement.Infrastructure.Extensions; +using static PostFundManagement.Domain.Extensions.Constants; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddApiVersioning(); -builder.Services.AddOpenApi(); +builder.Services.AddEndpointsApiExplorer() + .AddApiVersioning(options => options.ApiVersionReader = new HeaderApiVersionReader()); + +builder.Services.AddApiVersioning().AddApiExplorer(); + +builder.Services.AddEndpoints(Assembly.GetExecutingAssembly()); +builder.Services.AddApiServices(builder.Configuration); + +builder.Services.AddMediator(); +builder.Services.AddSecurityApiSdk(builder.Configuration); +builder.Services.AddWebSecurity(builder.Configuration); + +builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TelemetryPipelineBehavior<,>)); +builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingPipelineBehavior<,>)); + +builder.Services.AddQuartzSchedulerClient(DefaultSchedulerName, builder.Configuration); + +builder.Services.AddEmailServices(builder.Configuration); + +builder.Services.AddHttpClient(); +builder.Services.AddHashServices(builder.Configuration); +builder.Services.AddDataProtectionDatabase(builder.Configuration); +builder.Services.AddApplicationDbContext(builder.Configuration); + +// TODO: add healthcheck services +// builder.Services.AddMidrandShopPostgresHealthCheck(); +// builder.Services.AddMidrandShopQuartzHealthCheck(); +// builder.Services.AddHealthChecksSupport(builder.Configuration); var app = builder.Build(); -if (app.Environment.IsDevelopment()) -{ - app.MapScalarApiReference(options => - { - options.WithTitle("Post-fund Management API") - .WithTheme(ScalarTheme.BluePlanet); - }); +var schedulerFactory = app.Services.GetRequiredService(); +var scheduler = await schedulerFactory.GetScheduler(DefaultSchedulerName); - app.MapOpenApi(); -} +if (!scheduler!.IsStarted) + await scheduler.Start(); +app.UseHsts(); app.UseHttpsRedirection(); -app.Run(); +app.UseRouting(); +app.UseAuthentication(); +app.UseAuthorization(); + +ApiVersionSet versionSet = app.NewApiVersionSet("v1") + .HasApiVersion(new ApiVersion(1)) + .HasApiVersion(new ApiVersion(2)) + .ReportApiVersions() + .Build(); + +var versionGroups = new Dictionary +{ + { 1, app.MapGroup("v{version:apiVersion}").WithApiVersionSet(versionSet) } +}; + +app.MapEndpoints(versionGroups); + +app.UseHealthChecks("/health", new HealthCheckOptions +{ + Predicate = _ => true, + AllowCachingResponses = true, + ResponseWriter = HealthChecks.UI.Client.UIResponseWriter.WriteHealthCheckUIResponse +}); + +app.MapHealthChecksUI(options => { options.UIPath = "/healthui"; }); +app.UseHealthChecks("/ready"); + +app.MapOpenApi(); + +var apiVersions = app.DescribeApiVersions() + .OrderByDescending(o => o.ApiVersion.MajorVersion) + .ToList(); + +foreach (var description in app.DescribeApiVersions().OrderByDescending(o => o.ApiVersion.MajorVersion)) + app.MapScalarApiReference($"/openapi/{description.GroupName}", (options, context) => + { + options.AddServer(new ScalarServer($"https://{context.Request.Host}")); + options.WithOpenApiRoutePattern($"/openapi/{description.GroupName}.json"); + options.WithTheme(ScalarTheme.DeepSpace); + options.Agent = new ScalarAgentOptions { Disabled = true }; + options.Authentication = new ScalarAuthenticationOptions { PreferredSecuritySchemes = ["Bearer"] }; + }); + +var latestVersionGroup = apiVersions.FirstOrDefault()?.GroupName ?? "v1"; + +app.MapGet("/", () => Results.Redirect($"/openapi/{latestVersionGroup}")) + .ExcludeFromDescription(); + +if (!app.Environment.IsDevelopment()) + app.UseExceptionHandler("/Error", createScopeForErrors: true); + +app.Run(); \ No newline at end of file diff --git a/PostFundManagement.Api/appsettings.json b/PostFundManagement.Api/appsettings.json index a2002e3..9cefeec 100644 --- a/PostFundManagement.Api/appsettings.json +++ b/PostFundManagement.Api/appsettings.json @@ -1,5 +1,15 @@ { - "MachineIdentity": { + "GeneralS3Settings": { + "ServiceUrl": "http://192.168.1.177:30900", + "Region": "garage", + "BucketName": "pfm.general", + "CdnBaseUrl": "https://pfm.general.cdn.khongisa.co.za" + }, + "Monitoring": { + "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", + "ServiceName": "MidrandBooks.DEV" + }, + "SecuritySettings": { "Authority": "https://sts.security.khongisa.co.za", "Audience": "pfm-api-dev" }, @@ -10,4 +20,4 @@ } }, "AllowedHosts": "*" -} +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Extensions/Constants.cs b/PostFundManagement.Domain/Extensions/Constants.cs index c16be94..1d7d474 100644 --- a/PostFundManagement.Domain/Extensions/Constants.cs +++ b/PostFundManagement.Domain/Extensions/Constants.cs @@ -2,6 +2,8 @@ namespace PostFundManagement.Domain.Extensions; public static class Constants { + public const string DefaultSchedulerName = "pfm-scheduler"; + public const string GeneralS3SettingsSection = "PfmS3Settings"; public const string EvidenceS3SettingsSection = "EvidenceS3Settings"; diff --git a/PostFundManagement.Domain/Extensions/Email.cs b/PostFundManagement.Domain/Extensions/Email.cs new file mode 100644 index 0000000..3e2da51 --- /dev/null +++ b/PostFundManagement.Domain/Extensions/Email.cs @@ -0,0 +1,23 @@ +using PostFundManagement.Domain.Configuration.Email; +using PostFundManagement.Domain.Services; + +namespace PostFundManagement.Domain.Extensions; + +public static class Email +{ + public const string EmailFromName = "PFM Team"; + public const string EmailFromAddress = "info@pfm.co.za"; // TODO: replace with real address + + public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection("Email")); + + services.AddSingleton(); + + services.AddOpenTelemetry() + .WithTracing(tracing => tracing.AddSource("Pfm.EmailService")) + .WithMetrics(metrics => metrics.AddMeter("Pfm.EmailService")); + + return services; + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Extensions/Quartz.cs b/PostFundManagement.Domain/Extensions/Quartz.cs index f8aeaec..e52d909 100644 --- a/PostFundManagement.Domain/Extensions/Quartz.cs +++ b/PostFundManagement.Domain/Extensions/Quartz.cs @@ -6,8 +6,6 @@ namespace PostFundManagement.Domain.Extensions; public static class Quartz { - public const string DefaultSchedulerName = "tech-shop"; - public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration) { var connectionString = configuration.GetConnectionString(DatabaseConfigName); From ab2ef9da313e96a4fd9e11f1f9415aaf9e4b741a Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 21 Aug 2026 11:19:06 +0200 Subject: [PATCH 46/50] Added healthchecks Configured scheduler schema Added dataprotection support --- .gitignore | 2 + .../Extensions/Postgres.cs | 3 +- .../Extensions/{Api.cs => Security.cs} | 4 +- PostFundManagement.Api/Program.cs | 13 +- .../Extensions/Quartz.cs | 18 +- .../Health/PostgresHealthCheck.cs | 28 +++ .../Health/QuartzHealthCheck.cs | 28 +++ .../Database/quartz_tables.sql | 193 ++++++++++++++++++ 8 files changed, 275 insertions(+), 14 deletions(-) rename {PostFundManagement.Infrastructure => PostFundManagement.Api}/Extensions/Postgres.cs (92%) rename PostFundManagement.Api/Extensions/{Api.cs => Security.cs} (98%) create mode 100644 PostFundManagement.Domain/Health/PostgresHealthCheck.cs create mode 100644 PostFundManagement.Domain/Health/QuartzHealthCheck.cs create mode 100644 PostFundManagement.Infrastructure/Database/quartz_tables.sql diff --git a/.gitignore b/.gitignore index d7bd59d..db1a675 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,8 @@ bld/ # Visual Studio 2015/2017 cache/options directory .vs/ .vscode/ +bin/ +certificates/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ diff --git a/PostFundManagement.Infrastructure/Extensions/Postgres.cs b/PostFundManagement.Api/Extensions/Postgres.cs similarity index 92% rename from PostFundManagement.Infrastructure/Extensions/Postgres.cs rename to PostFundManagement.Api/Extensions/Postgres.cs index c12d4b5..e549daa 100644 --- a/PostFundManagement.Infrastructure/Extensions/Postgres.cs +++ b/PostFundManagement.Api/Extensions/Postgres.cs @@ -1,7 +1,8 @@ +using Microsoft.EntityFrameworkCore; using PostFundManagement.Infrastructure.Database; using static PostFundManagement.Domain.Extensions.Constants; -namespace PostFundManagement.Infrastructure.Extensions; +namespace PostFundManagement.Api.Extensions; public static class Postgres { diff --git a/PostFundManagement.Api/Extensions/Api.cs b/PostFundManagement.Api/Extensions/Security.cs similarity index 98% rename from PostFundManagement.Api/Extensions/Api.cs rename to PostFundManagement.Api/Extensions/Security.cs index 2f3c3f5..d638f7d 100644 --- a/PostFundManagement.Api/Extensions/Api.cs +++ b/PostFundManagement.Api/Extensions/Security.cs @@ -6,7 +6,7 @@ using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Api.Extensions; -public static class Api +public static class Security { public static IServiceCollection AddSecurityApiSdk(this IServiceCollection services, IConfiguration configuration) { @@ -42,6 +42,8 @@ public static class Api public static IServiceCollection AddWebSecurity(this IServiceCollection services, IConfiguration configuration) { + services.AddAuthorization(); + var certString = configuration["DataProtection:Certificate"] ?? configuration["DataProtection__Certificate"]; var certPassword = configuration["DataProtection:Password"] ?? configuration["DataProtection__Password"]; diff --git a/PostFundManagement.Api/Program.cs b/PostFundManagement.Api/Program.cs index 4f84139..eb6749b 100644 --- a/PostFundManagement.Api/Program.cs +++ b/PostFundManagement.Api/Program.cs @@ -1,7 +1,8 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; using PostFundManagement.Api.Extensions; using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Health; using PostFundManagement.Domain.Mediator; -using PostFundManagement.Infrastructure.Extensions; using static PostFundManagement.Domain.Extensions.Constants; var builder = WebApplication.CreateBuilder(args); @@ -21,7 +22,7 @@ builder.Services.AddWebSecurity(builder.Configuration); builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TelemetryPipelineBehavior<,>)); builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingPipelineBehavior<,>)); -builder.Services.AddQuartzSchedulerClient(DefaultSchedulerName, builder.Configuration); +builder.Services.AddQuartzScheduler(DefaultSchedulerName, builder.Configuration); builder.Services.AddEmailServices(builder.Configuration); @@ -30,10 +31,10 @@ builder.Services.AddHashServices(builder.Configuration); builder.Services.AddDataProtectionDatabase(builder.Configuration); builder.Services.AddApplicationDbContext(builder.Configuration); -// TODO: add healthcheck services -// builder.Services.AddMidrandShopPostgresHealthCheck(); -// builder.Services.AddMidrandShopQuartzHealthCheck(); -// builder.Services.AddHealthChecksSupport(builder.Configuration); +builder.Services.AddHealthChecks() + .AddCheck("QuartzScheduler") + .AddCheck("PostgresDatabase") + .AddCheck("Self", () => HealthCheckResult.Healthy()); var app = builder.Build(); diff --git a/PostFundManagement.Domain/Extensions/Quartz.cs b/PostFundManagement.Domain/Extensions/Quartz.cs index e52d909..a77bcc1 100644 --- a/PostFundManagement.Domain/Extensions/Quartz.cs +++ b/PostFundManagement.Domain/Extensions/Quartz.cs @@ -27,15 +27,18 @@ public static class Quartz storage.UseSystemTextJsonSerializer(); storage.SetProperty("quartz.jobStore.clustered", "true"); - storage.SetProperty("quartz.jobStore.tablePrefix", "qrtz_"); - - storage.UsePostgres(connectionString!); + + storage.UsePostgres(options => + { + options.ConnectionString = connectionString!; + options.TablePrefix = "quartz.qrtz_"; + }); storage.UseClustering(cluster => { cluster.CheckinInterval = TimeSpan.FromSeconds(30); cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90); }); - }); + }); }); return services; @@ -69,9 +72,12 @@ public static class Quartz storage.UseSystemTextJsonSerializer(); storage.SetProperty("quartz.jobStore.clustered", "true"); - storage.SetProperty("quartz.jobStore.tablePrefix", "qrtz_"); - storage.UsePostgres(connectionString!); + storage.UsePostgres(options => + { + options.ConnectionString = connectionString!; + options.TablePrefix = "quartz.qrtz_"; + }); storage.UseClustering(cluster => { cluster.CheckinInterval = TimeSpan.FromSeconds(30); diff --git a/PostFundManagement.Domain/Health/PostgresHealthCheck.cs b/PostFundManagement.Domain/Health/PostgresHealthCheck.cs new file mode 100644 index 0000000..39b58ce --- /dev/null +++ b/PostFundManagement.Domain/Health/PostgresHealthCheck.cs @@ -0,0 +1,28 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Health; + +public sealed class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck +{ + private readonly string connectionString = configuration.GetConnectionString(DatabaseConfigName)!; + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + await using var dataSource = NpgsqlDataSource.Create(connectionString); + await using var connection = await dataSource.OpenConnectionAsync(cancellationToken); + + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT 1"; + + await command.ExecuteScalarAsync(cancellationToken); + + return HealthCheckResult.Healthy($"{DatabaseConfigName} is responsive."); + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy($"{DatabaseConfigName} is unreachable.", ex); + } + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Health/QuartzHealthCheck.cs b/PostFundManagement.Domain/Health/QuartzHealthCheck.cs new file mode 100644 index 0000000..c11ffdc --- /dev/null +++ b/PostFundManagement.Domain/Health/QuartzHealthCheck.cs @@ -0,0 +1,28 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Health; + +public sealed class QuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck +{ + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var scheduler = await schedulerFactory.GetScheduler(DefaultSchedulerName, cancellationToken); + + if(scheduler == null) + return HealthCheckResult.Unhealthy($"Scheduler with name '{DefaultSchedulerName}' not found."); + + if (!scheduler.IsStarted) + return HealthCheckResult.Unhealthy($"{DefaultSchedulerName} Quartz scheduler is not running"); + + await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken); + + return HealthCheckResult.Healthy($"{DefaultSchedulerName} Quartz scheduler is ready"); + } + catch (SchedulerException) + { + return HealthCheckResult.Unhealthy($"{DefaultSchedulerName} Quartz scheduler cannot connect to the store"); + } + } +} \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/quartz_tables.sql b/PostFundManagement.Infrastructure/Database/quartz_tables.sql new file mode 100644 index 0000000..39002d2 --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/quartz_tables.sql @@ -0,0 +1,193 @@ +-- This script is for PostgreSQL + +-- This initializes the database to pristine for Quartz, by first removing any existing Quartz tables +-- and then recreating them from scratch. +-- Should you only require it to create the tables, set DropDb to 0. +-- 1. Create the dedicated schema +CREATE SCHEMA IF NOT EXISTS quartz; + +-- 2. Set the search path so the creation script targets this schema +SET search_path TO quartz; + +DO $$ + DECLARE DropDb INT := 1; -- Set this to 0 to skip DROP statements, 1 to include them +BEGIN + IF DropDb = 1 THEN + SET client_min_messages = WARNING; + DROP TABLE IF EXISTS qrtz_fired_triggers; + DROP TABLE IF EXISTS qrtz_paused_trigger_grps; + DROP TABLE IF EXISTS qrtz_scheduler_state; + DROP TABLE IF EXISTS qrtz_locks; + DROP TABLE IF EXISTS qrtz_simprop_triggers; + DROP TABLE IF EXISTS qrtz_simple_triggers; + DROP TABLE IF EXISTS qrtz_cron_triggers; + DROP TABLE IF EXISTS qrtz_blob_triggers; + DROP TABLE IF EXISTS qrtz_triggers; + DROP TABLE IF EXISTS qrtz_job_details; + DROP TABLE IF EXISTS qrtz_calendars; + SET client_min_messages = NOTICE; + END IF; +END $$; + +CREATE TABLE qrtz_job_details + ( + sched_name TEXT NOT NULL, + job_name TEXT NOT NULL, + job_group TEXT NOT NULL, + description TEXT NULL, + job_class_name TEXT NOT NULL, + is_durable BOOL NOT NULL, + is_nonconcurrent BOOL NOT NULL, + is_update_data BOOL NOT NULL, + requests_recovery BOOL NOT NULL, + job_data BYTEA NULL, + PRIMARY KEY (sched_name, job_name, job_group) +); + +CREATE TABLE qrtz_triggers + ( + sched_name TEXT NOT NULL, + trigger_name TEXT NOT NULL, + trigger_group TEXT NOT NULL, + job_name TEXT NOT NULL, + job_group TEXT NOT NULL, + description TEXT NULL, + next_fire_time BIGINT NULL, + prev_fire_time BIGINT NULL, + priority INTEGER NULL, + trigger_state TEXT NOT NULL, + trigger_type TEXT NOT NULL, + start_time BIGINT NOT NULL, + end_time BIGINT NULL, + calendar_name TEXT NULL, + misfire_instr SMALLINT NULL, + misfire_orig_fire_time BIGINT NULL, + execution_group VARCHAR(200) NULL, + preferred_node VARCHAR(200) NULL, + preferred_node_auto BOOL NOT NULL DEFAULT FALSE, + job_data BYTEA NULL, + PRIMARY KEY (sched_name, trigger_name, trigger_group), + FOREIGN KEY (sched_name, job_name, job_group) + REFERENCES qrtz_job_details (sched_name, job_name, job_group) +); + +CREATE TABLE qrtz_simple_triggers + ( + sched_name TEXT NOT NULL, + trigger_name TEXT NOT NULL, + trigger_group TEXT NOT NULL, + repeat_count BIGINT NOT NULL, + repeat_interval BIGINT NOT NULL, + times_triggered BIGINT NOT NULL, + PRIMARY KEY (sched_name, trigger_name, trigger_group), + FOREIGN KEY (sched_name, trigger_name, trigger_group) + REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group) + ON DELETE CASCADE +); + +CREATE TABLE qrtz_simprop_triggers + ( + sched_name TEXT NOT NULL, + trigger_name TEXT NOT NULL, + trigger_group TEXT NOT NULL, + str_prop_1 TEXT NULL, + str_prop_2 TEXT NULL, + str_prop_3 TEXT NULL, + int_prop_1 INTEGER NULL, + int_prop_2 INTEGER NULL, + long_prop_1 BIGINT NULL, + long_prop_2 BIGINT NULL, + dec_prop_1 NUMERIC NULL, + dec_prop_2 NUMERIC NULL, + bool_prop_1 BOOL NULL, + bool_prop_2 BOOL NULL, + time_zone_id TEXT NULL, + PRIMARY KEY (sched_name, trigger_name, trigger_group), + FOREIGN KEY (sched_name, trigger_name, trigger_group) + REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group) + ON DELETE CASCADE +); + +CREATE TABLE qrtz_cron_triggers + ( + sched_name TEXT NOT NULL, + trigger_name TEXT NOT NULL, + trigger_group TEXT NOT NULL, + cron_expression TEXT NOT NULL, + time_zone_id TEXT, + PRIMARY KEY (sched_name, trigger_name, trigger_group), + FOREIGN KEY (sched_name, trigger_name, trigger_group) + REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group) + ON DELETE CASCADE +); + +CREATE TABLE qrtz_blob_triggers + ( + sched_name TEXT NOT NULL, + trigger_name TEXT NOT NULL, + trigger_group TEXT NOT NULL, + blob_data BYTEA NULL, + PRIMARY KEY (sched_name, trigger_name, trigger_group), + FOREIGN KEY (sched_name, trigger_name, trigger_group) + REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group) + ON DELETE CASCADE +); + +CREATE TABLE qrtz_calendars + ( + sched_name TEXT NOT NULL, + calendar_name TEXT NOT NULL, + calendar BYTEA NOT NULL, + PRIMARY KEY (sched_name, calendar_name) +); + +CREATE TABLE qrtz_paused_trigger_grps + ( + sched_name TEXT NOT NULL, + trigger_group TEXT NOT NULL, + PRIMARY KEY (sched_name, trigger_group) +); + +CREATE TABLE qrtz_fired_triggers + ( + sched_name TEXT NOT NULL, + entry_id TEXT NOT NULL, + trigger_name TEXT NOT NULL, + trigger_group TEXT NOT NULL, + instance_name TEXT NOT NULL, + fired_time BIGINT NOT NULL, + sched_time BIGINT NOT NULL, + priority INTEGER NOT NULL, + state TEXT NOT NULL, + job_name TEXT NULL, + job_group TEXT NULL, + is_nonconcurrent BOOL NOT NULL, + requests_recovery BOOL NULL, + execution_group VARCHAR(200) NULL, + PRIMARY KEY (sched_name, entry_id) +); + +CREATE TABLE qrtz_scheduler_state + ( + sched_name TEXT NOT NULL, + instance_name TEXT NOT NULL, + last_checkin_time BIGINT NOT NULL, + checkin_interval BIGINT NOT NULL, + PRIMARY KEY (sched_name, instance_name) +); + +CREATE TABLE qrtz_locks + ( + sched_name TEXT NOT NULL, + lock_name TEXT NOT NULL, + PRIMARY KEY (sched_name, lock_name) +); + +CREATE INDEX idx_qrtz_j_g_n ON qrtz_job_details (sched_name, job_group, job_name); +CREATE INDEX idx_qrtz_t_j ON qrtz_triggers (sched_name, job_name, job_group); +CREATE INDEX idx_qrtz_t_c ON qrtz_triggers (sched_name, calendar_name); +CREATE INDEX idx_qrtz_t_g_n ON qrtz_triggers (sched_name, trigger_group, trigger_name); +CREATE INDEX idx_qrtz_t_nft_st ON qrtz_triggers (sched_name, trigger_state, next_fire_time); +CREATE INDEX idx_qrtz_ft_inst_job_req_rcvry ON qrtz_fired_triggers (sched_name, instance_name, requests_recovery); +CREATE INDEX idx_qrtz_ft_j_g ON qrtz_fired_triggers (sched_name, job_name, job_group); +CREATE INDEX idx_qrtz_ft_t_g ON qrtz_fired_triggers (sched_name, trigger_name, trigger_group); \ No newline at end of file From 5a325b0a7574a5d7274ebb49e8026ef305ffbeda Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 21 Aug 2026 11:26:25 +0200 Subject: [PATCH 47/50] Updated nuget packages --- .../PostFundManagement.Api.csproj | 23 +++---------------- .../PostFundManagement.Domain.csproj | 10 ++++---- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/PostFundManagement.Api/PostFundManagement.Api.csproj b/PostFundManagement.Api/PostFundManagement.Api.csproj index 746d861..32a48db 100644 --- a/PostFundManagement.Api/PostFundManagement.Api.csproj +++ b/PostFundManagement.Api/PostFundManagement.Api.csproj @@ -29,6 +29,8 @@ + + @@ -45,7 +47,7 @@ - + @@ -58,19 +60,6 @@ - - - - - - - - - - - - - @@ -78,17 +67,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - diff --git a/PostFundManagement.Domain/PostFundManagement.Domain.csproj b/PostFundManagement.Domain/PostFundManagement.Domain.csproj index 990128d..7c49a94 100644 --- a/PostFundManagement.Domain/PostFundManagement.Domain.csproj +++ b/PostFundManagement.Domain/PostFundManagement.Domain.csproj @@ -38,7 +38,7 @@ - + @@ -56,7 +56,7 @@ - + @@ -66,7 +66,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -172,8 +172,8 @@ - - + + From 9f1146130eb5f41bf8d09c4fb95616fb0e0ddc78 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 21 Aug 2026 11:30:46 +0200 Subject: [PATCH 48/50] Updated global usings --- PostFundManagement.Api/Extensions/Postgres.cs | 1 - PostFundManagement.Api/PostFundManagement.Api.csproj | 6 ++++-- PostFundManagement.Api/Program.cs | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PostFundManagement.Api/Extensions/Postgres.cs b/PostFundManagement.Api/Extensions/Postgres.cs index e549daa..351b55f 100644 --- a/PostFundManagement.Api/Extensions/Postgres.cs +++ b/PostFundManagement.Api/Extensions/Postgres.cs @@ -1,4 +1,3 @@ -using Microsoft.EntityFrameworkCore; using PostFundManagement.Infrastructure.Database; using static PostFundManagement.Domain.Extensions.Constants; diff --git a/PostFundManagement.Api/PostFundManagement.Api.csproj b/PostFundManagement.Api/PostFundManagement.Api.csproj index 32a48db..dd1ba0b 100644 --- a/PostFundManagement.Api/PostFundManagement.Api.csproj +++ b/PostFundManagement.Api/PostFundManagement.Api.csproj @@ -29,8 +29,9 @@ - + + @@ -78,8 +79,9 @@ + - + diff --git a/PostFundManagement.Api/Program.cs b/PostFundManagement.Api/Program.cs index eb6749b..1c6611e 100644 --- a/PostFundManagement.Api/Program.cs +++ b/PostFundManagement.Api/Program.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.Diagnostics.HealthChecks; using PostFundManagement.Api.Extensions; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Health; From de20ace27dc4d44fc1fc46b604ea75d585399a52 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 21 Aug 2026 11:38:18 +0200 Subject: [PATCH 49/50] Added license file --- LICENSE | 18 ++++++++++++++++++ .../PostFundManagement.Api.csproj | 1 + .../PostFundManagement.Infrastructure.csproj | 1 + 3 files changed, 20 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ad0fbfd --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +PROPRIETARY LICENSE + +Copyright (c) 2026 IFRI TECHNOLOGIES (PTY) Ltd. All rights reserved. + +This software and its associated documentation (the "Software") are the +proprietary property of IFRI TECHNOLOGIES (PTY) Ltd. + +The Software is provided for internal use only. Unauthorized copying, +distribution, modification, or use of this file via any medium is +strictly prohibited. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/PostFundManagement.Api/PostFundManagement.Api.csproj b/PostFundManagement.Api/PostFundManagement.Api.csproj index dd1ba0b..fcfd489 100644 --- a/PostFundManagement.Api/PostFundManagement.Api.csproj +++ b/PostFundManagement.Api/PostFundManagement.Api.csproj @@ -6,6 +6,7 @@ enable 59a05094-4ac2-472d-af04-3407e909432d ..\PostFundManagement.snk + ..\LICENSE diff --git a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj index 9ce4b54..3ce39ed 100644 --- a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj +++ b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj @@ -6,6 +6,7 @@ enable c4131f8d-ff78-432e-86d2-f6e131bd4cd3 ..\PostFundManagement.snk + ..\LICENSE From 7535c60ca631cd31a2703b3f260ad10b61efc7f4 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 21 Aug 2026 11:40:39 +0200 Subject: [PATCH 50/50] Enabled assembly signing --- PostFundManagement.Api/PostFundManagement.Api.csproj | 1 + PostFundManagement.Domain/PostFundManagement.Domain.csproj | 2 ++ .../PostFundManagement.Infrastructure.csproj | 1 + 3 files changed, 4 insertions(+) diff --git a/PostFundManagement.Api/PostFundManagement.Api.csproj b/PostFundManagement.Api/PostFundManagement.Api.csproj index fcfd489..66b2287 100644 --- a/PostFundManagement.Api/PostFundManagement.Api.csproj +++ b/PostFundManagement.Api/PostFundManagement.Api.csproj @@ -6,6 +6,7 @@ enable 59a05094-4ac2-472d-af04-3407e909432d ..\PostFundManagement.snk + True ..\LICENSE diff --git a/PostFundManagement.Domain/PostFundManagement.Domain.csproj b/PostFundManagement.Domain/PostFundManagement.Domain.csproj index 7c49a94..f84a4fc 100644 --- a/PostFundManagement.Domain/PostFundManagement.Domain.csproj +++ b/PostFundManagement.Domain/PostFundManagement.Domain.csproj @@ -5,6 +5,8 @@ enable enable ..\PostFundManagement.snk + True + ..\LICENSE diff --git a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj index 3ce39ed..2882d52 100644 --- a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj +++ b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj @@ -6,6 +6,7 @@ enable c4131f8d-ff78-432e-86d2-f6e131bd4cd3 ..\PostFundManagement.snk + True ..\LICENSE