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 diff --git a/.gitignore b/.gitignore index 6a182b5..db1a675 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ *.userosscache *.sln.docstates *.env +*.env.json # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -36,6 +37,7 @@ obj/x64/ [Rr]eleases/x86/ bin/x86/ obj/x86/ +bin/ [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ @@ -55,6 +57,9 @@ 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/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/Extensions/Postgres.cs b/PostFundManagement.Api/Extensions/Postgres.cs new file mode 100644 index 0000000..351b55f --- /dev/null +++ b/PostFundManagement.Api/Extensions/Postgres.cs @@ -0,0 +1,27 @@ +using PostFundManagement.Infrastructure.Database; +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Api.Extensions; + +public static class Postgres +{ + 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) + ?? 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.Api/Extensions/Security.cs b/PostFundManagement.Api/Extensions/Security.cs new file mode 100644 index 0000000..d638f7d --- /dev/null +++ b/PostFundManagement.Api/Extensions/Security.cs @@ -0,0 +1,126 @@ +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 Security +{ + 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) + { + services.AddAuthorization(); + + 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 3f01fc2..66b2287 100644 --- a/PostFundManagement.Api/PostFundManagement.Api.csproj +++ b/PostFundManagement.Api/PostFundManagement.Api.csproj @@ -1,13 +1,97 @@ - + net10.0 enable enable + 59a05094-4ac2-472d-af04-3407e909432d + ..\PostFundManagement.snk + True + ..\LICENSE + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + 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..1c6611e 100644 --- a/PostFundManagement.Api/Program.cs +++ b/PostFundManagement.Api/Program.cs @@ -1,41 +1,100 @@ +using PostFundManagement.Api.Extensions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Health; +using PostFundManagement.Domain.Mediator; +using static PostFundManagement.Domain.Extensions.Constants; + var builder = WebApplication.CreateBuilder(args); -// Add services to the container. -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi -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.AddQuartzScheduler(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); + +builder.Services.AddHealthChecks() + .AddCheck("QuartzScheduler") + .AddCheck("PostgresDatabase") + .AddCheck("Self", () => HealthCheckResult.Healthy()); var app = builder.Build(); -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.MapOpenApi(); -} +var schedulerFactory = app.Services.GetRequiredService(); +var scheduler = await schedulerFactory.GetScheduler(DefaultSchedulerName); +if (!scheduler!.IsStarted) + await scheduler.Start(); + +app.UseHsts(); app.UseHttpsRedirection(); -var summaries = new[] +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 { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + { 1, app.MapGroup("v{version:apiVersion}").WithApiVersionSet(versionSet) } }; -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.MapEndpoints(versionGroups); -app.Run(); - -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +app.UseHealthChecks("/health", new HealthCheckOptions { - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} + 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.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..9cefeec 100644 --- a/PostFundManagement.Api/appsettings.json +++ b/PostFundManagement.Api/appsettings.json @@ -1,4 +1,18 @@ { + "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" + }, "Logging": { "LogLevel": { "Default": "Information", @@ -6,4 +20,4 @@ } }, "AllowedHosts": "*" -} +} \ No newline at end of file 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.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/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/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/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/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/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/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/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/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/Entities/Activity.cs b/PostFundManagement.Domain/Configuration/Entities/Activity.cs new file mode 100644 index 0000000..401224d --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Activity.cs @@ -0,0 +1,56 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Activity : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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/Entities/AuditLog.cs b/PostFundManagement.Domain/Configuration/Entities/AuditLog.cs new file mode 100644 index 0000000..f99df02 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/AuditLog.cs @@ -0,0 +1,25 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class AuditLog : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.Entities.AuditLog).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.EntityName).IsRequired().HasMaxLength(LabelLength); + builder.Property(f => f.EntityId).IsRequired(); + builder.Property(f => f.Action).IsRequired().HasMaxLength(ShortLabelLength); + builder.Property(f => f.Changes).IsRequired(false).HasColumnType("jsonb"); + 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/Configuration/Entities/Award.cs b/PostFundManagement.Domain/Configuration/Entities/Award.cs new file mode 100644 index 0000000..b5fd58e --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Award.cs @@ -0,0 +1,57 @@ +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Award : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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().HasConversion().HasDefaultValueSql("1"); + 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/Configuration/Entities/Beneficiary.cs b/PostFundManagement.Domain/Configuration/Entities/Beneficiary.cs new file mode 100644 index 0000000..d9762a5 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Beneficiary.cs @@ -0,0 +1,40 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Beneficiary : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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(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(); + + 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/Configuration/Entities/Budget.cs b/PostFundManagement.Domain/Configuration/Entities/Budget.cs new file mode 100644 index 0000000..6fde58a --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Budget.cs @@ -0,0 +1,38 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Budget : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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(LabelLength); + 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/Configuration/Entities/ChangeRequest.cs b/PostFundManagement.Domain/Configuration/Entities/ChangeRequest.cs new file mode 100644 index 0000000..4ad15fe --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/ChangeRequest.cs @@ -0,0 +1,41 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class ChangeRequest : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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(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"); + + 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/Configuration/Entities/ComplianceItem.cs b/PostFundManagement.Domain/Configuration/Entities/ComplianceItem.cs new file mode 100644 index 0000000..f6cc845 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/ComplianceItem.cs @@ -0,0 +1,39 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class ComplianceItem : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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(LabelLength); + builder.Property(f => f.Requirements).IsRequired().HasMaxLength(MediumTextLength); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); + + 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/Configuration/Entities/Contract.cs b/PostFundManagement.Domain/Configuration/Entities/Contract.cs new file mode 100644 index 0000000..4b6920c --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Contract.cs @@ -0,0 +1,34 @@ +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Contract : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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().HasConversion().HasDefaultValueSql("1"); + + 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/Configuration/Entities/Disbursement.cs b/PostFundManagement.Domain/Configuration/Entities/Disbursement.cs new file mode 100644 index 0000000..ee085b1 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Disbursement.cs @@ -0,0 +1,43 @@ +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Disbursement : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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().HasConversion().HasDefaultValueSql("1"); + + 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/Configuration/Entities/Evidence.cs b/PostFundManagement.Domain/Configuration/Entities/Evidence.cs new file mode 100644 index 0000000..2b7c716 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Evidence.cs @@ -0,0 +1,59 @@ +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Evidence : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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.ActivityId).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().HasConversion().HasDefaultValueSql("1"); + builder.Property(f => f.Type).IsRequired().HasConversion().HasDefaultValueSql("9"); + + 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.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) + .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/Configuration/Entities/Indicator.cs b/PostFundManagement.Domain/Configuration/Entities/Indicator.cs new file mode 100644 index 0000000..eefce24 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Indicator.cs @@ -0,0 +1,33 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Indicator : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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(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); + 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/Configuration/Entities/Instrument.cs b/PostFundManagement.Domain/Configuration/Entities/Instrument.cs new file mode 100644 index 0000000..eeae07b --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Instrument.cs @@ -0,0 +1,32 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Instrument : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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.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) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + } +} diff --git a/PostFundManagement.Domain/Configuration/Entities/Invite.cs b/PostFundManagement.Domain/Configuration/Entities/Invite.cs new file mode 100644 index 0000000..631fada --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Invite.cs @@ -0,0 +1,51 @@ + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Invite : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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/Entities/Issue.cs b/PostFundManagement.Domain/Configuration/Entities/Issue.cs new file mode 100644 index 0000000..c1b5252 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Issue.cs @@ -0,0 +1,41 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Issue : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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.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() + .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.Assignee) + .WithMany() + .HasForeignKey(fk => fk.AssignedTo) + .IsRequired() + .OnDelete(DeleteBehavior.SetNull); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/Entities/Milestone.cs b/PostFundManagement.Domain/Configuration/Entities/Milestone.cs new file mode 100644 index 0000000..b8e7f77 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Milestone.cs @@ -0,0 +1,40 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Milestone : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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(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() + .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/Configuration/Entities/Notification.cs b/PostFundManagement.Domain/Configuration/Entities/Notification.cs new file mode 100644 index 0000000..4c12bf1 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Notification.cs @@ -0,0 +1,36 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Notification : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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/Entities/Organisation.cs b/PostFundManagement.Domain/Configuration/Entities/Organisation.cs new file mode 100644 index 0000000..b01684e --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Organisation.cs @@ -0,0 +1,26 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Organisation : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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(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"); + + 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/Configuration/Entities/Outcome.cs b/PostFundManagement.Domain/Configuration/Entities/Outcome.cs new file mode 100644 index 0000000..28a7dfa --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Outcome.cs @@ -0,0 +1,30 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Outcome : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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(LabelLength); + builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength); + + 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/Configuration/Entities/Portfolio.cs b/PostFundManagement.Domain/Configuration/Entities/Portfolio.cs new file mode 100644 index 0000000..3e438fa --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Portfolio.cs @@ -0,0 +1,30 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Portfolio : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.Entities.Portfolio).Pluralize()); + + builder.HasKey(pk => pk.Id); + 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(LabelLength); + builder.Property(f => f.Description).IsRequired(false).HasMaxLength(TextLength); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Owner) + .WithMany() + .HasForeignKey(fk => fk.OwnedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/Entities/Programme.cs b/PostFundManagement.Domain/Configuration/Entities/Programme.cs new file mode 100644 index 0000000..b80e328 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Programme.cs @@ -0,0 +1,39 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Programme : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.Entities.Programme).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.PortfolioId).IsRequired(); + 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(LabelLength); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); + + builder.HasOne(f => f.Portfolio) + .WithMany(f => f.Programmes) + .HasForeignKey(fk => fk.PortfolioId) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.Owner) + .WithMany() + .IsRequired() + .HasForeignKey(fk => fk.OwnedBy) + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/Entities/Project.cs b/PostFundManagement.Domain/Configuration/Entities/Project.cs new file mode 100644 index 0000000..162ac45 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Project.cs @@ -0,0 +1,43 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Project : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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(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.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/Configuration/Entities/Risk.cs b/PostFundManagement.Domain/Configuration/Entities/Risk.cs new file mode 100644 index 0000000..77b1794 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Risk.cs @@ -0,0 +1,32 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Risk : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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().HasConversion().HasDefaultValueSql("3"); + builder.Property(f => f.Impact).IsRequired().HasConversion().HasDefaultValueSql("1"); + builder.Property(f => f.Name).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.Creator) + .WithMany() + .HasForeignKey(fk => fk.CreatedBy) + .IsRequired() + .OnDelete(DeleteBehavior.NoAction); + } +} \ No newline at end of file diff --git a/PostFundManagement.Domain/Configuration/Entities/Rule.cs b/PostFundManagement.Domain/Configuration/Entities/Rule.cs new file mode 100644 index 0000000..f5fbcf7 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/Rule.cs @@ -0,0 +1,39 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class Rule : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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(LabelLength); + builder.Property(f => f.Version).IsRequired().HasDefaultValue(1); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("2"); + + 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/Configuration/Entities/SiteVisit.cs b/PostFundManagement.Domain/Configuration/Entities/SiteVisit.cs new file mode 100644 index 0000000..d2301f8 --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/SiteVisit.cs @@ -0,0 +1,32 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class SiteVisit : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.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(); + builder.Property(f => f.InspectorName).IsRequired(); + builder.Property(f => f.Findings).IsRequired(false).HasMaxLength(LargeTextLength); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1"); + + 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/Configuration/Entities/User.cs b/PostFundManagement.Domain/Configuration/Entities/User.cs new file mode 100644 index 0000000..bb2148a --- /dev/null +++ b/PostFundManagement.Domain/Configuration/Entities/User.cs @@ -0,0 +1,16 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Configuration.Entities; + +public sealed class User : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Domain.Entities.User).Pluralize()); + + builder.HasKey(pk => pk.Id); + builder.Property(f => f.Email).IsRequired().HasMaxLength(LabelLength); + 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/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/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/Entities/Activity.cs b/PostFundManagement.Domain/Entities/Activity.cs new file mode 100644 index 0000000..24ed074 --- /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/AuditLog.cs b/PostFundManagement.Domain/Entities/AuditLog.cs new file mode 100644 index 0000000..34b32b5 --- /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.Domain/Entities/Award.cs b/PostFundManagement.Domain/Entities/Award.cs new file mode 100644 index 0000000..4877d52 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Award.cs @@ -0,0 +1,19 @@ +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; } + + public virtual Contract? Contract { get; set; } +} \ 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..d7a1c81 --- /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.Domain/Entities/Budget.cs b/PostFundManagement.Domain/Entities/Budget.cs new file mode 100644 index 0000000..9dcdf0d --- /dev/null +++ b/PostFundManagement.Domain/Entities/Budget.cs @@ -0,0 +1,11 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +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.Domain/Entities/ChangeRequest.cs b/PostFundManagement.Domain/Entities/ChangeRequest.cs new file mode 100644 index 0000000..542b6cd --- /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.Domain/Entities/ComplianceItem.cs b/PostFundManagement.Domain/Entities/ComplianceItem.cs new file mode 100644 index 0000000..a74e6fa --- /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/Entities/Contract.cs b/PostFundManagement.Domain/Entities/Contract.cs new file mode 100644 index 0000000..4715b68 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Contract.cs @@ -0,0 +1,9 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +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.Domain/Entities/Disbursement.cs b/PostFundManagement.Domain/Entities/Disbursement.cs new file mode 100644 index 0000000..185cdc9 --- /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/Entities/Evidence.cs b/PostFundManagement.Domain/Entities/Evidence.cs new file mode 100644 index 0000000..4f5cc62 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Evidence.cs @@ -0,0 +1,17 @@ +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 Activity? Activity { 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/Entities/Indicator.cs b/PostFundManagement.Domain/Entities/Indicator.cs new file mode 100644 index 0000000..24283af --- /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.Domain/Entities/Instrument.cs b/PostFundManagement.Domain/Entities/Instrument.cs new file mode 100644 index 0000000..fcda49e --- /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 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..298290f --- /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 new file mode 100644 index 0000000..dac7511 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Issue.cs @@ -0,0 +1,11 @@ +namespace PostFundManagement.Domain.Entities; + +[EntityTypeConfiguration] +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/Milestone.cs b/PostFundManagement.Domain/Entities/Milestone.cs new file mode 100644 index 0000000..bffad70 --- /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.Domain/Entities/Notification.cs b/PostFundManagement.Domain/Entities/Notification.cs new file mode 100644 index 0000000..d85b6b8 --- /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/Organisation.cs b/PostFundManagement.Domain/Entities/Organisation.cs new file mode 100644 index 0000000..3c0ffbc --- /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/Entities/Outcome.cs b/PostFundManagement.Domain/Entities/Outcome.cs new file mode 100644 index 0000000..7e29e82 --- /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.Domain/Entities/Portfolio.cs b/PostFundManagement.Domain/Entities/Portfolio.cs new file mode 100644 index 0000000..08684e7 --- /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..aa199db --- /dev/null +++ b/PostFundManagement.Domain/Entities/Programme.cs @@ -0,0 +1,19 @@ +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 ICollection Instruments { get; set; } = []; + + 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 new file mode 100644 index 0000000..5840cf0 --- /dev/null +++ b/PostFundManagement.Domain/Entities/Project.cs @@ -0,0 +1,13 @@ +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; } + + public virtual ICollection Awards { get; set; } = []; +} \ 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..0750956 --- /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.Domain/Entities/Rule.cs b/PostFundManagement.Domain/Entities/Rule.cs new file mode 100644 index 0000000..ed1c9e6 --- /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.Domain/Entities/SiteVisit.cs b/PostFundManagement.Domain/Entities/SiteVisit.cs new file mode 100644 index 0000000..3efab66 --- /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/Entities/User.cs b/PostFundManagement.Domain/Entities/User.cs new file mode 100644 index 0000000..19633b8 --- /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..7482db7 --- /dev/null +++ b/PostFundManagement.Domain/Enums.cs @@ -0,0 +1,200 @@ +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, + 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 + 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, + 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, + Removed = 5 +} + +public enum ActivityStatus : int +{ + Active = 1, + Inactive = 2, + Disabled = 3 +} \ 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/Constants.cs b/PostFundManagement.Domain/Extensions/Constants.cs new file mode 100644 index 0000000..1d7d474 --- /dev/null +++ b/PostFundManagement.Domain/Extensions/Constants.cs @@ -0,0 +1,30 @@ +namespace PostFundManagement.Domain.Extensions; + +public static class Constants +{ + public const string DefaultSchedulerName = "pfm-scheduler"; + + public const string GeneralS3SettingsSection = "PfmS3Settings"; + + public const string EvidenceS3SettingsSection = "EvidenceS3Settings"; + + public const string ContractS3SettingsSection = "ContractS3Settings"; + + public const string GeneralBucketName = "pfm.general"; + + public const string EvidenceBucketName = "pfm.evidence"; + + public const string ContractBucketName = "pfm.contract"; + + public const string DatabaseConfigName = "PfmDatabase"; + + 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/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/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/Extensions/Mappers.cs b/PostFundManagement.Domain/Extensions/Mappers.cs new file mode 100644 index 0000000..104defa --- /dev/null +++ b/PostFundManagement.Domain/Extensions/Mappers.cs @@ -0,0 +1,324 @@ +using PostFundManagement.Domain.Models; + +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, + 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, + ActivityId = entity.ActivityId, + Type = entity.Type + }; + + 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, + 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() + { + 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, + AssignedTo = entity.AssignedTo, + Resolution = entity.Resolution + }; + + 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, + Priority = entity.Priority + }; + + 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 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, + 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, + ProjectType = entity.ProjectType, + Sector = entity.Sector + }; + + 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 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/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 diff --git a/PostFundManagement.Domain/Extensions/Quartz.cs b/PostFundManagement.Domain/Extensions/Quartz.cs new file mode 100644 index 0000000..a77bcc1 --- /dev/null +++ b/PostFundManagement.Domain/Extensions/Quartz.cs @@ -0,0 +1,108 @@ +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Quartz; +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Domain.Extensions; + +public static class Quartz +{ + 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.UsePostgres(options => + { + options.ConnectionString = connectionString!; + options.TablePrefix = "quartz.qrtz_"; + }); + 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.UsePostgres(options => + { + options.ConnectionString = connectionString!; + options.TablePrefix = "quartz.qrtz_"; + }); + 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/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/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/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.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/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/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..cbb27e0 --- /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..f16036b --- /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? UpdatedAt { 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/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/Models/Evidence.cs b/PostFundManagement.Domain/Models/Evidence.cs new file mode 100644 index 0000000..ac4bd82 --- /dev/null +++ b/PostFundManagement.Domain/Models/Evidence.cs @@ -0,0 +1,30 @@ +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 long ActivityId { 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 EvidenceType Type { 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..3cd09ab --- /dev/null +++ b/PostFundManagement.Domain/Models/Instrument.cs @@ -0,0 +1,20 @@ +namespace PostFundManagement.Domain.Models; + +public class Instrument +{ + public long Id { get; set; } + + public long ProgrammeId { 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/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 new file mode 100644 index 0000000..4bdbf45 --- /dev/null +++ b/PostFundManagement.Domain/Models/Issue.cs @@ -0,0 +1,26 @@ +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 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 new file mode 100644 index 0000000..c42d74e --- /dev/null +++ b/PostFundManagement.Domain/Models/Milestone.cs @@ -0,0 +1,24 @@ +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; } + + 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/Organisation.cs b/PostFundManagement.Domain/Models/Organisation.cs new file mode 100644 index 0000000..b3e1821 --- /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 OrganisationType 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..e05fd15 --- /dev/null +++ b/PostFundManagement.Domain/Models/Programme.cs @@ -0,0 +1,22 @@ +namespace PostFundManagement.Domain.Models; + +public class Programme +{ + public long Id { get; set; } + + public long PortfolioId { 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..795f75a --- /dev/null +++ b/PostFundManagement.Domain/Models/Project.cs @@ -0,0 +1,30 @@ +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 IndustrySector Sector { get; set; } + + public ProjectType ProjectType { 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..9a94b7a --- /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 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/User.cs b/PostFundManagement.Domain/Models/User.cs new file mode 100644 index 0000000..32aa308 --- /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..f84a4fc 100644 --- a/PostFundManagement.Domain/PostFundManagement.Domain.csproj +++ b/PostFundManagement.Domain/PostFundManagement.Domain.csproj @@ -4,6 +4,211 @@ net10.0 enable enable + ..\PostFundManagement.snk + True + ..\LICENSE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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.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/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 diff --git a/PostFundManagement.Domain/Services/ContractS3Service.cs b/PostFundManagement.Domain/Services/ContractS3Service.cs new file mode 100644 index 0000000..481f37e --- /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(ContractBucketName)] 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/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/EvidenceS3Service.cs b/PostFundManagement.Domain/Services/EvidenceS3Service.cs new file mode 100644 index 0000000..67de5ac --- /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(EvidenceBucketName)] 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..79b0fb6 --- /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(GeneralBucketName)] 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 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 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/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/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 diff --git a/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs new file mode 100644 index 0000000..0f4a87b --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/ApplicationDbContext.cs @@ -0,0 +1,56 @@ +using PostFundManagement.Domain.Entities; + +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(); + + public DbSet ChangeRequests => Set(); + + public DbSet ComplianceItems => Set(); + + public DbSet Issues => Set(); + + public DbSet Outcomes => Set(); + + public DbSet Beneficiaries => Set(); + + public DbSet Evidences => Set(); + + public DbSet Risks => Set(); + + public DbSet Indicators => Set(); + + public DbSet Disbursements => Set(); + + public DbSet Milestones => Set(); + + public DbSet Budgets => Set(); + + public DbSet Contracts => Set(); + + public DbSet Awards => Set(); + + public DbSet Projects => Set(); + + public DbSet Organisations => Set(); + + public DbSet Rules => Set(); + + public DbSet Programmes => Set(); + + public DbSet Portfolios => Set(); + + public DbSet Instruments => Set(); + + public DbSet Users => Set(); +} \ 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/Factories/ApplicationDbContextFactory.cs b/PostFundManagement.Infrastructure/Database/Factories/ApplicationDbContextFactory.cs new file mode 100644 index 0000000..7d06651 --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Factories/ApplicationDbContextFactory.cs @@ -0,0 +1,24 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +namespace PostFundManagement.Infrastructure.Database.Factories; + +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(DatabaseConfigName) + ?? throw new InvalidOperationException($"Connection string '{DatabaseConfigName}' 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/Database/Factories/DataProtectionDbContextFactory.cs b/PostFundManagement.Infrastructure/Database/Factories/DataProtectionDbContextFactory.cs new file mode 100644 index 0000000..b54ff53 --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Factories/DataProtectionDbContextFactory.cs @@ -0,0 +1,20 @@ +using static PostFundManagement.Domain.Extensions.Constants; + +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/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/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/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 new file mode 100644 index 0000000..a739df6 --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,1963 @@ +// +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.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/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/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 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 b760144..2882d52 100644 --- a/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj +++ b/PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj @@ -4,6 +4,44 @@ net10.0 enable enable + c4131f8d-ff78-432e-86d2-f6e131bd4cd3 + ..\PostFundManagement.snk + True + ..\LICENSE + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + 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 diff --git a/PostFundManagement.snk b/PostFundManagement.snk new file mode 100644 index 0000000..bb8c7f7 Binary files /dev/null and b/PostFundManagement.snk differ 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.