Security #1
+287
@@ -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
|
||||
@@ -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/
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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<DataProtectionDbContext>(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<ApplicationDbContext>(builder => builder.UseNpgsql(connectionString));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -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<SecurityClientSettings>(configSection);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authOptions.Authority))
|
||||
return services;
|
||||
|
||||
if (!authOptions.Authority.EndsWith("/", StringComparison.Ordinal)) authOptions.Authority += "/";
|
||||
|
||||
services.AddRefitClient<ISecurityConnectApi>()
|
||||
.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<TokenService>();
|
||||
|
||||
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<DataProtectionDbContext>()
|
||||
.ProtectKeysWithCertificate(certificate)
|
||||
.SetApplicationName("LiteCharmsApp");
|
||||
|
||||
services.Configure<DataProtectionOptions>(options => options.ApplicationDiscriminator = "LiteCharmsApp");
|
||||
|
||||
services.ConfigureCookieOidcSameSiteSupport();
|
||||
|
||||
var configSection = configuration.GetSection(nameof(SecuritySettings));
|
||||
|
||||
var authOptions = new SecuritySettings();
|
||||
configSection.Bind(authOptions);
|
||||
|
||||
services.Configure<SecuritySettings>(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;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,97 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>59a05094-4ac2-472d-af04-3407e909432d</UserSecretsId>
|
||||
<AssemblyOriginatorKeyFile>..\PostFundManagement.snk</AssemblyOriginatorKeyFile>
|
||||
<SignAssembly>True</SignAssembly>
|
||||
<PackageLicenseFile>..\LICENSE</PackageLicenseFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Security (IODC)-->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
|
||||
<PackageReference Include="IdentityModel.AspNetCore" Version="4.3.0" />
|
||||
<PackageReference Include="IdentityModel.AspNetCore.OAuth2introspection" Version="6.2.0" />
|
||||
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
|
||||
<PackageReference Include="IdentityModel" Version="6.2.0" />
|
||||
<PackageReference Include="KubernetesClient" Version="19.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" Version="10.0.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.11" />
|
||||
|
||||
<Using Include="Microsoft.AspNetCore.Authentication" />
|
||||
<Using Include="Microsoft.AspNetCore.Authentication.Cookies" />
|
||||
<Using Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Health Checks -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI" Version="9.0.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.NpgSql" Version="9.0.0" />
|
||||
|
||||
<Using Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
|
||||
<Using Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- API Versioning -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AccessTokenClient.Extensions" Version="5.1.0" />
|
||||
<PackageReference Include="Asp.Versioning.Abstractions" Version="10.2.1" />
|
||||
<PackageReference Include="Asp.Versioning.Http" Version="10.2.2" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.2.1" />
|
||||
|
||||
<Using Include="Asp.Versioning" />
|
||||
<Using Include="Asp.Versioning.Builder" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- API Documentation -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.11" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.17.1" />
|
||||
|
||||
<Using Include="Scalar.AspNetCore" />
|
||||
<Using Include="Microsoft.OpenApi" />
|
||||
<Using Include="Microsoft.AspNetCore.OpenApi" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- file nesting -->
|
||||
<ItemGroup>
|
||||
<ProjectCapability Include="ConfigurableFileNesting" />
|
||||
<ProjectCapability Include="ConfigurableFileNestingFeatureEnabled" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- CQRS -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
<Using Include="FluentResults" />
|
||||
<Using Include="Mediator" />
|
||||
<Using Include="Quartz" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Shared Global Usings -->
|
||||
<ItemGroup>
|
||||
<Using Include="System.Web" />
|
||||
<Using Include="System.Diagnostics" />
|
||||
<Using Include="System.Reflection" />
|
||||
<Using Include="Microsoft.AspNetCore.Mvc" />
|
||||
<Using Include="Microsoft.EntityFrameworkCore" />
|
||||
<Using Include="System.ComponentModel.DataAnnotations" />
|
||||
<Using Include="Microsoft.Extensions.DependencyInjection.Extensions" />
|
||||
<Using Include="System.Security.Cryptography.X509Certificates" />
|
||||
<Using Include="Microsoft.AspNetCore.DataProtection" />
|
||||
<Using Include="Refit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PostFundManagement.Domain\PostFundManagement.Domain.csproj" />
|
||||
<ProjectReference Include="..\PostFundManagement.Infrastructure\PostFundManagement.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
@PostFundManagement.Api_HostAddress = http://localhost:5112
|
||||
|
||||
GET {{PostFundManagement.Api_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -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<QuartzHealthCheck>("QuartzScheduler")
|
||||
.AddCheck<PostgresHealthCheck>("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<ISchedulerFactory>();
|
||||
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<int, RouteGroupBuilder>
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
{ 1, app.MapGroup("v{version:apiVersion}").WithApiVersionSet(versionSet) }
|
||||
};
|
||||
|
||||
app.MapGet("/weatherforecast", () =>
|
||||
app.MapEndpoints(versionGroups);
|
||||
|
||||
app.UseHealthChecks("/health", new HealthCheckOptions
|
||||
{
|
||||
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");
|
||||
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();
|
||||
|
||||
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
|
||||
{
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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}}"
|
||||
}
|
||||
@@ -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}}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace PostFundManagement.Domain.Abstractions;
|
||||
|
||||
public interface IEndpoint
|
||||
{
|
||||
void Map(IEndpointRouteBuilder builder);
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace PostFundManagement.Domain.Abstractions;
|
||||
|
||||
public interface IJobOrchestrator
|
||||
{
|
||||
ValueTask SendAsync<TNotification>(TNotification notification, CancellationToken cancellationToken = default)
|
||||
where TNotification : IEvent;
|
||||
|
||||
ValueTask ScheduleAsync<TNotification>(TNotification notification, string cronExpression, CancellationToken cancellationToken = default)
|
||||
where TNotification : IEvent;
|
||||
|
||||
ValueTask<bool> InterruptAsync(string eventName, string? correlationId = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace PostFundManagement.Domain.Abstractions;
|
||||
|
||||
public interface IS3Service
|
||||
{
|
||||
Task<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default);
|
||||
Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace PostFundManagement.Domain.Abstractions;
|
||||
|
||||
public interface IService;
|
||||
@@ -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<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BucketName))
|
||||
return Result.Fail<string>("Bucket name is not configured.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(CdnBaseUrl))
|
||||
return Result.Fail<string>("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<string>("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<string>($"Failed to upload {fileName} to S3.")
|
||||
: Result.Ok($"{CdnBaseUrl}/{fileKey}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<string>(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<Result> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace PostFundManagement.Domain;
|
||||
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace PostFundManagement.Domain.Configuration.Email;
|
||||
|
||||
public sealed class Account
|
||||
{
|
||||
public string? Username { get; set; }
|
||||
|
||||
public string? Password { get; set; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Activity : IEntityTypeConfiguration<Domain.Entities.Activity>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Activity> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class AuditLog : IEntityTypeConfiguration<Domain.Entities.AuditLog>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.AuditLog> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Award : IEntityTypeConfiguration<Domain.Entities.Award>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Award> 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<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Beneficiary : IEntityTypeConfiguration<Domain.Entities.Beneficiary>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Beneficiary> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Budget : IEntityTypeConfiguration<Domain.Entities.Budget>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Budget> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class ChangeRequest : IEntityTypeConfiguration<Domain.Entities.ChangeRequest>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.ChangeRequest> 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<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class ComplianceItem : IEntityTypeConfiguration<Domain.Entities.ComplianceItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.ComplianceItem> 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<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Contract : IEntityTypeConfiguration<Domain.Entities.Contract>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Contract> 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<int>().HasDefaultValueSql("1");
|
||||
|
||||
builder.HasOne(f => f.Award)
|
||||
.WithOne(f => f.Contract)
|
||||
.HasForeignKey<Domain.Entities.Contract>(fk => fk.AwardId)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(f => f.Creator)
|
||||
.WithMany()
|
||||
.HasForeignKey(fk => fk.CreatedBy)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.NoAction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Disbursement : IEntityTypeConfiguration<Domain.Entities.Disbursement>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Disbursement> 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<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Evidence : IEntityTypeConfiguration<Domain.Entities.Evidence>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Evidence> 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<int>().HasDefaultValueSql("1");
|
||||
builder.Property(f => f.Type).IsRequired().HasConversion<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Indicator : IEntityTypeConfiguration<Domain.Entities.Indicator>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Indicator> 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<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Instrument : IEntityTypeConfiguration<Domain.Entities.Instrument>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Instrument> 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<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Invite : IEntityTypeConfiguration<Domain.Entities.Invite>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Invite> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Issue : IEntityTypeConfiguration<Domain.Entities.Issue>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Issue> 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<int>().HasDefaultValueSql("1");
|
||||
builder.Property(f => f.Status).IsRequired().HasConversion<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Milestone : IEntityTypeConfiguration<Domain.Entities.Milestone>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Milestone> 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<int>().HasDefaultValueSql("1");
|
||||
builder.Property(f => f.Priority).IsRequired().HasConversion<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Notification : IEntityTypeConfiguration<Domain.Entities.Notification>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Notification> 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<int>().HasDefaultValueSql("1");
|
||||
builder.Property(f => f.Status).IsRequired().HasConversion<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Organisation : IEntityTypeConfiguration<Domain.Entities.Organisation>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Organisation> 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<int>().HasDefaultValueSql("5");
|
||||
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1");
|
||||
|
||||
builder.HasOne(f => f.Creator)
|
||||
.WithMany()
|
||||
.HasForeignKey(fk => fk.CreatedBy)
|
||||
.IsRequired()
|
||||
.OnDelete(DeleteBehavior.NoAction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Outcome : IEntityTypeConfiguration<Domain.Entities.Outcome>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Outcome> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Portfolio : IEntityTypeConfiguration<Domain.Entities.Portfolio>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Portfolio> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Programme : IEntityTypeConfiguration<Domain.Entities.Programme>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Programme> 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<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Project : IEntityTypeConfiguration<Domain.Entities.Project>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Project> 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<int>().HasDefaultValueSql("11");
|
||||
builder.Property(f => f.ProjectType).IsRequired().HasConversion<int>().HasDefaultValueSql("10");
|
||||
builder.Property(f => f.Status).IsRequired().HasConversion<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Risk : IEntityTypeConfiguration<Domain.Entities.Risk>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Risk> 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<int>().HasDefaultValueSql("3");
|
||||
builder.Property(f => f.Impact).IsRequired().HasConversion<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class Rule : IEntityTypeConfiguration<Domain.Entities.Rule>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.Rule> 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<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class SiteVisit : IEntityTypeConfiguration<Domain.Entities.SiteVisit>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.SiteVisit> 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<int>().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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Configuration.Entities;
|
||||
|
||||
public sealed class User : IEntityTypeConfiguration<Domain.Entities.User>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Domain.Entities.User> 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<int>().HasDefaultValueSql("2");
|
||||
builder.Property(f => f.LastLoginAt).IsRequired(false);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Activity, Activity>]
|
||||
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<Evidence> Evidences { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.AuditLog, AuditLog>]
|
||||
public class AuditLog : Models.AuditLog
|
||||
{
|
||||
public virtual User? Performer { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Award, Award>]
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Beneficiary, Beneficiary>]
|
||||
public class Beneficiary : Models.Beneficiary
|
||||
{
|
||||
public virtual Project? Project { get; set; }
|
||||
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
public virtual User? User { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Budget, Budget>]
|
||||
public class Budget : Models.Budget
|
||||
{
|
||||
public virtual Project? Project { get; set; }
|
||||
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
public virtual User? Approver { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.ChangeRequest, ChangeRequest>]
|
||||
public class ChangeRequest : Models.ChangeRequest
|
||||
{
|
||||
public virtual Project? Project { get; set; }
|
||||
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
public virtual User? Approver { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.ComplianceItem, ComplianceItem>]
|
||||
public class ComplianceItem : Models.ComplianceItem
|
||||
{
|
||||
public virtual Project? Project { get; set; }
|
||||
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
public virtual User? Verifier { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Contract, Contract>]
|
||||
public class Contract : Models.Contract
|
||||
{
|
||||
public virtual Award? Award { get; set; }
|
||||
|
||||
public virtual User? Creator { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Disbursement, Disbursement>]
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Evidence, Evidence>]
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Indicator, Indicator>]
|
||||
public class Indicator : Models.Indicator
|
||||
{
|
||||
public virtual Project? Project { get; set; }
|
||||
|
||||
public virtual User? Creator { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Instrument, Instrument>]
|
||||
public class Instrument : Models.Instrument
|
||||
{
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
public virtual Programme? Programme { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Invite, Invite>]
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Issue, Issue>]
|
||||
public class Issue : Models.Issue
|
||||
{
|
||||
public virtual Project? Project { get; set; }
|
||||
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
public virtual User? Assignee { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Milestone, Milestone>]
|
||||
public class Milestone : Models.Milestone
|
||||
{
|
||||
public virtual Project? Project { get; set; }
|
||||
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
public virtual User? Updator { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Notification, Notification>]
|
||||
public class Notification : Models.Notification
|
||||
{
|
||||
public virtual Organisation? Organisation { get; set; }
|
||||
|
||||
public virtual User? RecipientUser { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Organisation, Organisation>]
|
||||
public class Organisation : Models.Organisation
|
||||
{
|
||||
public virtual User? Creator { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Outcome, Outcome>]
|
||||
public class Outcome : Models.Outcome
|
||||
{
|
||||
public virtual Beneficiary? Beneficiary { get; set; }
|
||||
|
||||
public virtual User? Creator { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Portfolio, Portfolio>]
|
||||
public class Portfolio : Models.Portfolio
|
||||
{
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
public virtual User? Owner { get; set; }
|
||||
|
||||
public virtual ICollection<Programme> Programmes { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Programme, Programme>]
|
||||
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<Instrument> Instruments { get; set; } = [];
|
||||
|
||||
public virtual ICollection<Rule> Rules { get; set; } = [];
|
||||
|
||||
public virtual ICollection<Project> Projects { get; set; } = [];
|
||||
|
||||
public virtual ICollection<Award> Awards { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Project, Project>]
|
||||
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<Award> Awards { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Risk, Risk>]
|
||||
public class Risk : Models.Risk
|
||||
{
|
||||
public virtual Project? Project { get; set; }
|
||||
|
||||
public virtual User? Creator { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.Rule, Rule>]
|
||||
public class Rule : Models.Rule
|
||||
{
|
||||
public virtual User? Creator { get; set; }
|
||||
|
||||
public virtual User? Updator { get; set; }
|
||||
|
||||
public virtual Programme? Programme { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.SiteVisit, SiteVisit>]
|
||||
public class SiteVisit : Models.SiteVisit
|
||||
{
|
||||
public virtual Project? Project { get; set; }
|
||||
|
||||
public virtual User? Creator { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace PostFundManagement.Domain.Entities;
|
||||
|
||||
[EntityTypeConfiguration<Configuration.Entities.User, User>]
|
||||
public sealed class User : Models.User;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<CookiePolicyOptions>(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<SecuritySettings>(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<OpenApiBearerSecuritySchemeTransformer>());
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IApplicationBuilder MapEndpoints(this WebApplication app, IDictionary<int, RouteGroupBuilder> versionGroups)
|
||||
{
|
||||
var endpoints = app.Services.GetRequiredService<IEnumerable<IEndpoint>>();
|
||||
|
||||
foreach (var endpoint in endpoints)
|
||||
{
|
||||
var versionAttributes = endpoint.GetType().GetCustomAttributes<ApiVersionTargetAttribute>().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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<SmtpSettings>(configuration.GetSection("Email"));
|
||||
|
||||
services.AddSingleton<EmailService>();
|
||||
|
||||
services.AddOpenTelemetry()
|
||||
.WithTracing(tracing => tracing.AddSource("Pfm.EmailService"))
|
||||
.WithMetrics(metrics => metrics.AddMeter("Pfm.EmailService"));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -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<long> EmailsSent = Meter.CreateCounter<long>("emails_sent_total", "count", "Total successful emails sent");
|
||||
public static readonly Counter<long> EmailsFailed = Meter.CreateCounter<long>("emails_failed_total", "count", "Total failed email attempts");
|
||||
}
|
||||
@@ -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<HasherSettings>(configuration.GetSection(HasherConfigSectionName));
|
||||
|
||||
var settings = configuration.GetSection(HasherConfigSectionName).Get<HasherSettings>();
|
||||
|
||||
services.AddSingleton<IHashids>(_ =>
|
||||
new Hashids(settings!.Salt, minHashLength: settings.MinHashLength));
|
||||
|
||||
services.AddSingleton<HashService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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<long> RequestCounter = Meter.CreateCounter<long>("mediator_requests_total");
|
||||
public static readonly Histogram<double> RequestDuration = Meter.CreateHistogram<double>("mediator_request_duration_ms");
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace PostFundManagement.Domain.Extensions;
|
||||
|
||||
public static class Otel
|
||||
{
|
||||
public static WebApplicationBuilder AddWebTelemetry(this WebApplicationBuilder builder)
|
||||
{
|
||||
var serviceName = builder.Configuration.GetValue<string>("Monitoring:ServiceName") ?? "Pfm";
|
||||
var endpoint = builder.Configuration.GetValue<string>("Monitoring:Address")!;
|
||||
var apiKey = builder.Configuration.GetValue<string>("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;
|
||||
}
|
||||
}
|
||||
@@ -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<QuartzOptions>(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<RetryJobListener>();
|
||||
services.AddTransient<IJobOrchestrator, JobOrchestrator>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -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<IAmazonS3, AmazonS3Client>(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<IS3Service, GeneralS3Service>(GeneralBucketName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{EvidenceS3SettingsSection}:ServiceUrl").Value))
|
||||
{
|
||||
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(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<IS3Service, EvidenceS3Service>(EvidenceS3SettingsSection);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{ContractS3SettingsSection}:ServiceUrl").Value))
|
||||
{
|
||||
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(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<IS3Service, ContractS3Service>(ContractS3SettingsSection);
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<HealthCheckResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Health;
|
||||
|
||||
public sealed class QuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
|
||||
{
|
||||
public async Task<HealthCheckResult> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace PostFundManagement.Domain.Mediator;
|
||||
|
||||
public sealed class LoggingPipelineBehavior<TRequest, TResponse>(ILogger<LoggingPipelineBehavior<TRequest, TResponse>> logger) :
|
||||
IPipelineBehavior<TRequest, TResponse?>
|
||||
where TRequest : IRequest<TResponse>
|
||||
where TResponse : ResultBase, new()
|
||||
{
|
||||
public async ValueTask<TResponse?> Handle(TRequest message, MessageHandlerDelegate<TRequest, TResponse?> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Diagnostics;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
|
||||
namespace PostFundManagement.Domain.Mediator;
|
||||
|
||||
public sealed class TelemetryPipelineBehavior<TRequest, TResponse> :
|
||||
IPipelineBehavior<TRequest, TResponse?>
|
||||
where TRequest : IRequest<TResponse>
|
||||
where TResponse : ResultBase, new()
|
||||
{
|
||||
public async ValueTask<TResponse?> Handle(TRequest message, MessageHandlerDelegate<TRequest, TResponse?> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user