1 Commits
Author SHA1 Message Date
hermes 67c08b40ca Refactored gitignore 2026-08-21 11:45:33 +02:00
205 changed files with 56 additions and 16320 deletions
-287
View File
@@ -1,287 +0,0 @@
# 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
-2
View File
@@ -10,7 +10,6 @@
*.userosscache
*.sln.docstates
*.env
*.env.json
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
@@ -37,7 +36,6 @@ obj/x64/
[Rr]eleases/x86/
bin/x86/
obj/x86/
bin/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
-18
View File
@@ -1,18 +0,0 @@
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.
@@ -1,41 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Communications;
[ApiVersionTarget(1)]
public class GetInvitesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/communications/invites", async (IDbContextFactory<ApplicationDbContext> contextFactory,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
{
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100;
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Invites.AsNoTracking();
var totalCount = await query.CountAsync(cancellationToken);
var items = await query.OrderByDescending(i => i.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
})
.RequireAuthorization()
.WithDescription("Get a list of sent invitations")
.WithName(typeof(GetInvitesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Communications);
}
}
@@ -1,63 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Communications;
[ApiVersionTarget(1)]
public class SendInviteEndpoint : IEndpoint
{
public record InviteRequest(long OrganisationId, long InvitedOrganisationId, long AwardId, long ContractId, Guid Recipient, string Message, NotificationPlatform Platform);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/communications/invite", async (InviteRequest request, ClaimsPrincipal principal,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
if (!Guid.TryParse(sidClaim, out var currentUserId))
return Results.BadRequest("Missing or invalid 'sid' claim.");
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var invite = new Domain.Entities.Invite
{
OrganisationId = request.OrganisationId,
InvitedOrganisationId = request.InvitedOrganisationId,
AwardId = request.AwardId,
ContractId = request.ContractId,
Recipient = request.Recipient,
CreatedBy = currentUserId,
CreatedAt = DateTime.UtcNow
};
context.Invites.Add(invite);
context.Notifications.Add(new Domain.Entities.Notification
{
OrganisationId = request.OrganisationId,
Recipient = request.Recipient,
Platform = request.Platform,
Status = NotificationStatus.Pending,
Subject = "Invitation to Onboard",
Message = request.Message,
CreatedAt = DateTime.UtcNow
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Ok(invite)
: Results.BadRequest("Failed to create invite");
})
.RequireAuthorization()
.WithDescription("Trigger an invite to an organization or candidate (specifying NotificationPlatform)")
.WithName(typeof(SendInviteEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Invite>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Communications);
}
}
@@ -1,44 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
namespace PostFundManagement.Api.Endpoints.Communications;
public class SyncEvent : EventBase, IEvent
{
public string Name { get; set; } = nameof(SyncEvent);
}
public class SyncEventHandler : INotificationHandler<SyncEvent>
{
public ValueTask Handle(SyncEvent notification, CancellationToken cancellationToken)
{
Trace.WriteLine($"Integration Sync executed. Correlation ID: {notification.CorrelationId}");
return ValueTask.CompletedTask;
}
}
[ApiVersionTarget(1)]
public class SyncEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/integration/sync", async (IJobOrchestrator jobOrchestrator,
CancellationToken cancellationToken = default) =>
{
var syncEvent = new SyncEvent();
await jobOrchestrator.SendAsync(syncEvent, cancellationToken);
return Results.Ok(new { Queued = true, syncEvent.CorrelationId });
})
.RequireAuthorization()
.WithDescription("Trigger integration and data synchronization in the background")
.WithName(typeof(SyncEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Communications);
}
}
@@ -1,56 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Evidence;
[ApiVersionTarget(1)]
public class CreateIndicatorEndpoint : IEndpoint
{
public record CreateIndicatorRequest(long ProjectId, string Name, UnitOfMeasure UnitOfMeasure, decimal BaselineAmount, decimal TargetAmount);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/evidence/indicators", async (CreateIndicatorRequest request, ClaimsPrincipal principal,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
if (!Guid.TryParse(sidClaim, out var userId))
return Results.BadRequest("Missing or invalid 'sid' claim.");
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
return Results.BadRequest($"Project with ID {request.ProjectId} does not exist.");
var indicator = new Domain.Entities.Indicator
{
ProjectId = request.ProjectId,
Name = request.Name,
UnitOfMeasure = request.UnitOfMeasure,
BaselineAmount = request.BaselineAmount,
TargetAmount = request.TargetAmount,
ActualAmount = request.BaselineAmount,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
};
context.Indicators.Add(indicator);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Created($"api/evidence/indicators/{indicator.Id}", indicator)
: Results.BadRequest("Failed to create indicator");
})
.RequireAuthorization()
.WithDescription("Define indicators and baselines/targets (M08)")
.WithName(typeof(CreateIndicatorEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<PostFundManagement.Domain.Entities.Indicator>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Evidence);
}
}
@@ -1,55 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Evidence;
[ApiVersionTarget(1)]
public class CreateMilestoneEndpoint : IEndpoint
{
public record CreateMilestoneRequest(long ProjectId, DateTime DueAt, string Name, Priority Priority);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/evidence/milestones", async (CreateMilestoneRequest request, ClaimsPrincipal principal,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
if (!Guid.TryParse(sidClaim, out var userId))
return Results.BadRequest("Missing or invalid 'sid' claim.");
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
return Results.BadRequest($"Project with ID {request.ProjectId} does not exist.");
var milestone = new Domain.Entities.Milestone
{
ProjectId = request.ProjectId,
DueAt = request.DueAt,
Name = request.Name,
Priority = request.Priority,
Status = ApprovalStatus.Pending,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
};
context.Milestones.Add(milestone);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Created($"api/evidence/milestones/{milestone.Id}", milestone)
: Results.BadRequest("Failed to create a new milestone");
})
.RequireAuthorization()
.WithDescription("Define contractual milestones (M07)")
.WithName(typeof(CreateMilestoneRequest).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Milestone>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Evidence);
}
}
@@ -1,41 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Evidence;
[ApiVersionTarget(1)]
public class GetEvidenceFilesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/evidence/files", async (IDbContextFactory<ApplicationDbContext> contextFactory,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
{
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100;
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Evidences.AsNoTracking();
var totalCount = await query.CountAsync(cancellationToken);
var items = await query.OrderByDescending(e => e.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
})
.RequireAuthorization()
.WithDescription("Get a list of all uploaded evidence files metadata")
.WithName(typeof(GetEvidenceFilesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Evidence);
}
}
@@ -1,43 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Evidence;
[ApiVersionTarget(1)]
public class GetIndicatorsEndpoint : IEndpoint
{
public record RecordActualRequest(decimal ActualAmount);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/evidence/indicators", async (IDbContextFactory<ApplicationDbContext> contextFactory,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
{
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100;
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Indicators.AsNoTracking();
var totalCount = await query.CountAsync(cancellationToken);
var items = await query.OrderByDescending(i => i.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
})
.RequireAuthorization()
.WithDescription("Get a list of indicators")
.WithName(typeof(GetIndicatorsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Evidence);
}
}
@@ -1,41 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Evidence;
[ApiVersionTarget(1)]
public class GetMilestonesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/evidence/milestones", async (IDbContextFactory<ApplicationDbContext> contextFactory,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
{
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100;
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Milestones.AsNoTracking();
var totalCount = await query.CountAsync(cancellationToken);
var items = await query.OrderByDescending(m => m.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
})
.RequireAuthorization()
.WithDescription("Get a list of all milestones")
.WithName(typeof(GetMilestonesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Evidence);
}
}
@@ -1,33 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Evidence;
[ApiVersionTarget(1)]
public class GetProjectMilestonesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/evidence/milestones/project/{projectId:long}", async (long projectId,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var milestones = await context.Milestones.AsNoTracking()
.Where(m => m.ProjectId == projectId)
.OrderBy(m => m.DueAt)
.ToListAsync(cancellationToken);
return Results.Ok(milestones);
})
.RequireAuthorization()
.WithDescription("Get a list of milestones for a project")
.WithName(typeof(GetProjectMilestonesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Evidence);
}
}
@@ -1,40 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Evidence;
[ApiVersionTarget(1)]
public class RecordIndicatorActualAmountEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/evidence/indicators/{id:long}/actuals/{actualAmount:decimal}", async (long id, decimal actualAmount,
ClaimsPrincipal principal, IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var indicator = await context.Indicators.FirstOrDefaultAsync(i => i.Id == id, cancellationToken);
if (indicator is null)
return Results.NotFound($"Indicator with ID {id} does not exist.");
indicator.ActualAmount = actualAmount;
context.Indicators.Update(indicator);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Ok(indicator)
: Results.BadRequest("Failed to record actual amount");
})
.RequireAuthorization()
.WithDescription("Record actual values for indicators (M08)")
.WithName(typeof(RecordIndicatorActualAmountEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Indicator>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Evidence);
}
}
@@ -1,78 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Evidence;
[ApiVersionTarget(1)]
public class UploadEvidenceFilesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/evidence/files", async (HttpRequest request, ClaimsPrincipal principal, IDbContextFactory<ApplicationDbContext> contextFactory,
[FromKeyedServices(Constants.EvidenceS3SettingsSection)] IS3Service s3Service, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
if (!Guid.TryParse(sidClaim, out var userId))
return Results.BadRequest("Missing or invalid 'sid' claim.");
if (!request.HasFormContentType)
return Results.BadRequest("Request must be a multipart form.");
var form = await request.ReadFormAsync(cancellationToken);
var file = form.Files.GetFile("file");
if (file == null || file.Length == 0)
return Results.BadRequest("No file uploaded or file is empty.");
_ = long.TryParse(form["projectId"], out var projectId);
_ = long.TryParse(form["milestoneId"], out var milestoneId);
_ = long.TryParse(form["indicatorId"], out var indicatorId);
_ = long.TryParse(form["activityId"], out var activityId);
_ = Enum.TryParse<EvidenceType>(form["type"], out var type);
using var stream = file.OpenReadStream();
var uploadResult = await s3Service.UploadFileAsync(file.FileName, stream, file.ContentType, cancellationToken);
if (uploadResult.IsFailed)
return Results.BadRequest(uploadResult.Errors.FirstOrDefault()?.Message ?? "File upload failed.");
var documentUrl = uploadResult.Value;
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var evidence = new Domain.Entities.Evidence
{
ProjectId = projectId,
MilestoneId = milestoneId,
IndicatorId = indicatorId,
ActivityId = activityId,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow,
Version = 1,
Type = type,
DocumentUrl = documentUrl,
Status = ApprovalStatus.Pending
};
context.Evidences.Add(evidence);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Created($"api/evidence/files/{evidence.Id}", evidence)
: Results.BadRequest("Evidence upload failed");
})
.RequireAuthorization()
.DisableAntiforgery()
.WithDescription("Upload supporting evidence file to S3 and register metadata (M14)")
.WithName(typeof(UploadEvidenceFilesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<PostFundManagement.Domain.Entities.Evidence>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Evidence);
}
}
@@ -1,48 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public class ApproveDisbursementEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/execution/disbursements/{id:long}/approve", async (long id, ClaimsPrincipal principal,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
if (!Guid.TryParse(sidClaim, out var userId))
return Results.BadRequest("Missing or invalid 'sid' claim.");
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var disbursement = await context.Disbursements.FirstOrDefaultAsync(d => d.Id == id, cancellationToken);
if (disbursement == null)
return Results.NotFound($"Disbursement with ID {id} does not exist.");
disbursement.Status = ApprovalStatus.Approved;
disbursement.UpdatedBy = userId;
disbursement.UpdatedAt = DateTime.UtcNow;
context.Disbursements.Update(disbursement);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Ok(disbursement)
: Results.BadRequest("Failed to approve payment");
})
.RequireAuthorization()
.WithDescription("Approve disbursement and release payment (M06)")
.WithName(typeof(ApproveDisbursementEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Disbursement>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Execution);
}
}
@@ -1,57 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public class CreateDisbursementEndpoint : IEndpoint
{
public record CreateDisbursementRequest(long ProjectId, long? MilestoneId, decimal Amount);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/execution/disbursements", async (CreateDisbursementRequest request, ClaimsPrincipal principal,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
if (!Guid.TryParse(sidClaim, out var userId))
return Results.BadRequest("Missing or invalid 'sid' claim.");
if (request.Amount <= 0)
return Results.BadRequest("Amount must be greater than zero.");
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
return Results.BadRequest($"Project with ID {request.ProjectId} does not exist.");
var disbursement = new Domain.Entities.Disbursement
{
ProjectId = request.ProjectId,
MilestoneId = request.MilestoneId,
Amount = request.Amount,
Status = ApprovalStatus.Pending,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
};
context.Disbursements.Add(disbursement);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Created($"api/execution/disbursements/{disbursement.Id}", disbursement)
: Results.BadRequest("Failed to create disbursement");
})
.RequireAuthorization()
.WithDescription("Create a new disbursement schedule (M06)")
.WithName(typeof(CreateDisbursementEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Disbursement>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Execution);
}
}
@@ -1,55 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public class CreateRiskEndpoint : IEndpoint
{
public record CreateRiskRequest(long ProjectId, RiskLikelihood Likelihood, RiskImpact Impact, string Name, string Description);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/execution/risks", async (CreateRiskRequest request, ClaimsPrincipal principal,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
if (!Guid.TryParse(sidClaim, out var userId))
return Results.BadRequest("Missing or invalid 'sid' claim.");
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
return Results.BadRequest($"Project with ID {request.ProjectId} does not exist.");
var risk = new Domain.Entities.Risk
{
ProjectId = request.ProjectId,
Likelihood = request.Likelihood,
Impact = request.Impact,
Name = request.Name,
Description = request.Description,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
};
context.Risks.Add(risk);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Created($"api/execution/risks/{risk.Id}", risk)
: Results.BadRequest("Failed to create project risk");
})
.RequireAuthorization()
.WithDescription("Add risk to risk register (M09)")
.WithName(typeof(CreateRiskEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Risk>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Execution);
}
}
@@ -1,41 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public class GetDisbursementsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/execution/disbursements", async (IDbContextFactory<ApplicationDbContext> contextFactory,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
{
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100;
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Disbursements.AsNoTracking();
var totalCount = await query.CountAsync(cancellationToken);
var items = await query.OrderByDescending(d => d.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
})
.RequireAuthorization()
.WithDescription("Get a list of disbursements")
.WithName(typeof(GetDisbursementsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Execution);
}
}
@@ -1,33 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public class GetProjectRisksEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/execution/risks/project/{projectId:long}", async (long projectId,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var risks = await context.Risks.AsNoTracking()
.Where(r => r.ProjectId == projectId)
.OrderByDescending(r => r.CreatedAt)
.ToListAsync(cancellationToken);
return Results.Ok(risks);
})
.RequireAuthorization()
.WithDescription("Get a list of risks for a project")
.WithName(typeof(GetProjectRisksEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Execution);
}
}
@@ -1,41 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public class GetRisksEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/execution/risks", async (IDbContextFactory<ApplicationDbContext> contextFactory,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
{
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100;
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Risks.AsNoTracking();
var totalCount = await query.CountAsync(cancellationToken);
var items = await query.OrderByDescending(r => r.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
})
.RequireAuthorization()
.WithDescription("Get a list of all risks")
.WithName(typeof(GetRisksEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Execution);
}
}
@@ -1,52 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public class CreateOrganisationEndpoint : IEndpoint
{
public record CreateOrganisationRequest(string Name, string RegistrationNo, string Email, OrganisationType Type);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/governance/organisations", async (CreateOrganisationRequest request, ClaimsPrincipal principal,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
if (!Guid.TryParse(sidClaim, out var userId))
return Results.BadRequest("Missing or invalid 'sid' claim.");
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var organisation = new Domain.Entities.Organisation
{
Name = request.Name,
RegistrationNo = request.RegistrationNo,
Email = request.Email,
Type = request.Type,
Status = ActivityStatus.Active,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
};
context.Organisations.Add(organisation);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Created($"api/governance/organisations/{organisation.Id}", organisation)
: Results.BadRequest("Failed to create organisation");
})
.RequireAuthorization()
.WithDescription("Create a new organization profile")
.WithName(typeof(CreateOrganisationEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Organisation>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
}
}
@@ -1,54 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public class DashboardEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/dashboard", async (IDbContextFactory<ApplicationDbContext> contextFactory,
CancellationToken cancellationToken = default) =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var totalPortfolioValue = await context.Awards.SumAsync(a => a.Amount, cancellationToken);
var activeAwardsCount = await context.Awards.CountAsync(a => a.Status == ApprovalStatus.Approved, cancellationToken);
var disbursedAmount = await context.Disbursements.Where(d => d.Status == ApprovalStatus.Approved)
.SumAsync(d => d.Amount, cancellationToken);
var totalProjects = await context.Projects.CountAsync(cancellationToken);
var onTrackProjects = await context.Projects.CountAsync(p => p.Status == ApprovalStatus.Approved, cancellationToken);
var onTrackPercentage = totalProjects > 0
? Math.Round((double)onTrackProjects / totalProjects * 100, 1)
: 100.0;
var atRiskCount = await context.Risks.CountAsync(r => r.Impact == RiskImpact.Critical || r.Impact == RiskImpact.Major, cancellationToken);
var beneficiariesReached = await context.Beneficiaries.SumAsync(b => b.ReachedCount, cancellationToken);
return Results.Ok(new
{
PortfolioValue = totalPortfolioValue,
ActiveAwards = activeAwardsCount,
Disbursed = disbursedAmount,
OnTrackPercentage = onTrackPercentage,
AtRiskCount = atRiskCount,
BeneficiariesReached = beneficiariesReached
});
})
.RequireAuthorization()
.WithDescription("Get executive portfolio dashboard metrics and KPIs")
.WithName(typeof(DashboardEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
}
}
@@ -1,29 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public class GetInstrumentsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/instruments", async (IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var instruments = await context.Instruments.AsNoTracking().ToListAsync(cancellationToken);
return Results.Ok(instruments);
})
.RequireAuthorization()
.WithDescription("Get all funding instruments")
.WithName(typeof(GetInstrumentsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<List<Domain.Entities.Instrument>>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
}
}
@@ -1,31 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public class GetOrganisationByIdEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/organisations/{id:long}", async (long id,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var organisation = await context.Organisations.AsNoTracking().FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
return organisation != null ? Results.Ok(organisation) : Results.NotFound();
})
.RequireAuthorization()
.WithDescription("Get organization profile by ID")
.WithName(typeof(GetOrganisationByIdEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Organisation>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
}
}
@@ -1,44 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public class GetOrganisationsEndpoint : IEndpoint
{
public record CreateOrganisationRequest(string Name, string RegistrationNo, string Email, OrganisationType Type);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/organisations", async (IDbContextFactory<ApplicationDbContext> contextFactory,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
{
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100;
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Organisations.AsNoTracking();
var totalCount = await query.CountAsync(cancellationToken);
var items = await query.OrderBy(o => o.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
})
.RequireAuthorization()
.WithDescription("Get a paginated directory of organizations")
.WithName(typeof(GetOrganisationsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
}
}
@@ -1,30 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public class GetPortfoliosEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/portfolios", async (IDbContextFactory<ApplicationDbContext> contextFactory,
CancellationToken cancellationToken = default) =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var portfolios = await context.Portfolios.AsNoTracking().ToListAsync(cancellationToken);
return Results.Ok(portfolios);
})
.RequireAuthorization()
.WithDescription("Get all portfolios")
.WithName(typeof(GetPortfoliosEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<List<Domain.Entities.Portfolio>>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
}
}
@@ -1,30 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public class GetProgrammesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/programmes", async (IDbContextFactory<ApplicationDbContext> contextFactory,
CancellationToken cancellationToken = default) =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var programmes = await context.Programmes.AsNoTracking().ToListAsync(cancellationToken);
return Results.Ok(programmes);
})
.RequireAuthorization()
.WithDescription("Get all programmes")
.WithName(typeof(GetProgrammesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<List<Domain.Entities.Programme>>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
}
}
@@ -1,60 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public class CreateAwardEndpoint : IEndpoint
{
public record CreateAwardRequest(long OrganisationId, long ProgrammeId, decimal Amount);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/grants/awards", async (CreateAwardRequest request, ClaimsPrincipal principal,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
if (!Guid.TryParse(sidClaim, out var userId))
return Results.BadRequest("Missing or invalid 'sid' claim.");
if (request.Amount <= 0)
return Results.BadRequest("Approved amount must be greater than zero.");
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Organisations.AnyAsync(o => o.Id == request.OrganisationId, cancellationToken))
return Results.BadRequest($"Organisation with ID {request.OrganisationId} does not exist.");
if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken))
return Results.BadRequest($"Programme with ID {request.ProgrammeId} does not exist.");
var award = new Domain.Entities.Award
{
OrganisationId = request.OrganisationId,
ProgrammeId = request.ProgrammeId,
Amount = request.Amount,
Status = ApprovalStatus.Pending,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
};
context.Awards.Add(award);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Created($"api/grants/awards/{award.Id}", award)
: Results.BadRequest("Failred to create award");
})
.RequireAuthorization()
.WithDescription("Register an approved funding award (M01)")
.WithName(typeof(CreateAwardEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Award>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
}
}
@@ -1,55 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public class CreateBudgetEndpoint : IEndpoint
{
public record CreateBudgetRequest(long ProjectId, string Name, decimal Amount);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/grants/budgets", async (CreateBudgetRequest request, ClaimsPrincipal principal,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
if (!Guid.TryParse(sidClaim, out var userId))
return Results.BadRequest("Missing or invalid 'sid' claim.");
if (request.Amount <= 0)
return Results.BadRequest("Budget amount must be greater than zero.");
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
return Results.BadRequest($"Project with ID {request.ProjectId} does not exist.");
var budget = new Domain.Entities.Budget
{
ProjectId = request.ProjectId,
Name = request.Name,
Amount = request.Amount,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
};
context.Budgets.Add(budget);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Created($"api/grants/budgets/{budget.Id}", budget)
: Results.BadRequest("Failed to create budget");
})
.RequireAuthorization()
.WithDescription("Create a new budget line (M05)")
.WithName(typeof(CreateBudgetEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Budget>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
}
}
@@ -1,58 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public class CreateContractEndpoint : IEndpoint
{
public record CreateContractRequest(long AwardId, decimal TotalValue, DateTime EffectiveAt, DateTime ExpiresAt);
public record ExecuteContractRequest(string ExternalSignatureId, string SignedDocumentUrl);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/grants/contracts", async (CreateContractRequest request, ClaimsPrincipal principal,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
if (!Guid.TryParse(sidClaim, out var userId))
return Results.BadRequest("Missing or invalid 'sid' claim.");
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var award = await context.Awards.FirstOrDefaultAsync(a => a.Id == request.AwardId, cancellationToken);
if (award == null)
return Results.BadRequest($"Award with ID {request.AwardId} does not exist.");
var contract = new Domain.Entities.Contract
{
AwardId = request.AwardId,
TotalValue = request.TotalValue,
EffectiveAt = request.EffectiveAt,
ExpiresAt = request.ExpiresAt,
Status = ContractStatus.Draft,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
};
context.Contracts.Add(contract);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Created($"api/grants/contracts/{contract.Id}", contract)
: Results.BadRequest("Failed to create the contract");
})
.RequireAuthorization()
.WithDescription("Create a contract record from an approved award (M02)")
.WithName(typeof(CreateContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Contract>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
}
}
@@ -1,45 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public class ExecuteContractEndpoint : IEndpoint
{
public record ExecuteContractRequest(string ExternalSignatureId, string SignedDocumentUrl);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/grants/contracts/{id:long}/execute", async (long id, ExecuteContractRequest request,
ClaimsPrincipal principal, IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contract = await context.Contracts.FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
if (contract == null)
return Results.NotFound($"Contract with ID {id} does not exist.");
contract.ExternalSignatureId = request.ExternalSignatureId;
contract.SignedDocumentUrl = request.SignedDocumentUrl;
contract.Status = ContractStatus.Active;
context.Contracts.Update(contract);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Ok(contract)
: Results.BadRequest("Failed to execute contract");
})
.RequireAuthorization()
.WithDescription("Execute the contract and lock the contractual version")
.WithName(typeof(ExecuteContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Contract>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
}
}
@@ -1,32 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public class GetAwardByIdEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/grants/awards/{id:long}", async (long id, IDbContextFactory<ApplicationDbContext> contextFactory,
CancellationToken cancellationToken = default) =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var award = await context.Awards.AsNoTracking()
.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
return award != null ? Results.Ok(award) : Results.NotFound();
})
.RequireAuthorization()
.WithDescription("Get award by ID")
.WithName(typeof(GetAwardByIdEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Award>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
}
}
@@ -1,41 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public class GetAwardsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/grants/awards", async (IDbContextFactory<ApplicationDbContext> contextFactory,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
{
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100;
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Awards.AsNoTracking();
var totalCount = await query.CountAsync(cancellationToken);
var items = await query.OrderByDescending(a => a.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
})
.RequireAuthorization()
.WithDescription("Get a list of awards")
.WithName(typeof(GetAwardsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
}
}
@@ -1,33 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public class GetBudgetByIdEndpoint : IEndpoint
{
public record CreateBudgetRequest(long ProjectId, string Name, decimal Amount);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/grants/budgets/{id:long}", async (long id, IDbContextFactory<ApplicationDbContext> contextFactory,
CancellationToken cancellationToken = default) =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var budget = await context.Budgets.AsNoTracking().FirstOrDefaultAsync(b => b.Id == id, cancellationToken);
return budget != null ? Results.Ok(budget) : Results.NotFound();
})
.RequireAuthorization()
.WithDescription("Get budget by ID")
.WithName(typeof(GetBudgetByIdEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Budget>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
}
}
@@ -1,41 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public class GetBudgetsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/grants/budgets", async (IDbContextFactory<ApplicationDbContext> contextFactory,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
{
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100;
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Budgets.AsNoTracking();
var totalCount = await query.CountAsync(cancellationToken);
var items = await query.OrderByDescending(b => b.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
})
.RequireAuthorization()
.WithDescription("Get all budgets")
.WithName(typeof(GetBudgetsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
}
}
@@ -1,32 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public class GetContractByIdEndpoint : IEndpoint
{
public record CreateContractRequest(long AwardId, decimal TotalValue, DateTime EffectiveAt, DateTime ExpiresAt);
public record ExecuteContractRequest(string ExternalSignatureId, string SignedDocumentUrl);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/grants/contracts/{id:long}", async (long id, IDbContextFactory<ApplicationDbContext> contextFactory,
CancellationToken cancellationToken = default) =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contract = await context.Contracts.AsNoTracking().FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
return contract != null ? Results.Ok(contract) : Results.NotFound();
})
.RequireAuthorization()
.WithDescription("Get contract by ID")
.WithName(typeof(GetContractByIdEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Contract>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
}
}
@@ -1,41 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public class GetContractsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/grants/contracts", async (IDbContextFactory<ApplicationDbContext> contextFactory,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
{
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100;
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Contracts.AsNoTracking();
var totalCount = await query.CountAsync(cancellationToken);
var items = await query.OrderByDescending(c => c.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
})
.RequireAuthorization()
.WithDescription("Get all contracts")
.WithName(typeof(GetContractsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
}
}
@@ -1,62 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public class CreateActivityEndpoint : IEndpoint
{
public record CreateActivityRequest(long? MilestoneId, string Category, decimal Amount, string Title, string Description, DateTime? PerformedAt);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/projects/{projectId:long}/activities", async (long projectId, CreateActivityRequest request,
ClaimsPrincipal principal, IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
if (!Guid.TryParse(sidClaim, out var userId))
return Results.BadRequest("Missing or invalid 'sid' claim.");
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Projects.AnyAsync(p => p.Id == projectId, cancellationToken))
return Results.BadRequest($"Project with ID {projectId} does not exist.");
if (request.MilestoneId.HasValue)
if (!await context.Milestones.AnyAsync(m => m.Id == request.MilestoneId.Value, cancellationToken))
return Results.BadRequest($"Milestone with ID {request.MilestoneId.Value} does not exist.");
var activity = new Domain.Entities.Activity
{
ProjectId = projectId,
MilestoneId = request.MilestoneId,
Category = request.Category,
Amount = request.Amount,
Title = request.Title,
Description = request.Description,
PerformedAt = request.PerformedAt,
Status = ApprovalStatus.Pending,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
};
context.Activities.Add(activity);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Created($"api/projects/{projectId}/activities/{activity.Id}", activity)
: Results.BadRequest("Failed to create activity");
})
.RequireAuthorization()
.WithDescription("Add a new activity under project (M04)")
.WithName(typeof(CreateActivityEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Activity>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
}
}
@@ -1,56 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public class CreateProjectEndpoint : IEndpoint
{
public record CreateProjectRequest(long ProgrammeId, string Name, string Description, IndustrySector Sector, ProjectType ProjectType);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/projects", async (CreateProjectRequest request, ClaimsPrincipal principal,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
if (!Guid.TryParse(sidClaim, out var userId))
return Results.BadRequest("Missing or invalid 'sid' claim.");
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken))
return Results.BadRequest($"Programme with ID {request.ProgrammeId} does not exist.");
var project = new Domain.Entities.Project
{
ProgrammeId = request.ProgrammeId,
Name = request.Name,
Description = request.Description,
Sector = request.Sector,
ProjectType = request.ProjectType,
Status = ApprovalStatus.Pending,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
};
context.Projects.Add(project);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Created($"api/projects/{project.Id}", project)
: Results.BadRequest("Failed to create prject");
})
.RequireAuthorization()
.WithDescription("Create a new project (M04)")
.WithName(typeof(CreateProjectEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Project>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
}
}
@@ -1,33 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public class GetActivitiesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/{projectId:long}/activities", async (long projectId,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var activities = await context.Activities.AsNoTracking()
.Where(a => a.ProjectId == projectId)
.OrderByDescending(a => a.CreatedAt)
.ToListAsync(cancellationToken);
return Results.Ok(activities);
})
.RequireAuthorization()
.WithDescription("Get a list of activities for a project")
.WithName(typeof(GetActivitiesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
}
}
@@ -1,33 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public class GetProjectByIdEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/{id:long}", async (long id, IDbContextFactory<ApplicationDbContext> contextFactory,
CancellationToken cancellationToken = default) =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var project = await context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
return project != null
? Results.Ok(project)
: Results.NotFound();
})
.RequireAuthorization()
.WithDescription("Get project by ID")
.WithName(typeof(GetProjectByIdEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Project>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
}
}
@@ -1,41 +0,0 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public class GetProjectsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects", async (IDbContextFactory<ApplicationDbContext> contextFactory,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
{
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100;
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Projects.AsNoTracking();
var totalCount = await query.CountAsync(cancellationToken);
var items = await query.OrderByDescending(p => p.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
})
.RequireAuthorization()
.WithDescription("Get a list of projects")
.WithName(typeof(GetProjectsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
}
}
@@ -1,50 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public class UpdateProjectEndpoint : IEndpoint
{
public record UpdateProjectStatusRequest(ApprovalStatus Status);
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/projects/{id:long}/status", async (long id, UpdateProjectStatusRequest request, ClaimsPrincipal principal,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
if (!Guid.TryParse(sidClaim, out var userId))
return Results.BadRequest("Missing or invalid 'sid' claim.");
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var project = await context.Projects.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
if (project == null)
return Results.NotFound($"Project with ID {id} does not exist.");
project.Status = request.Status;
project.UpdatedBy = userId;
project.UpdatedAt = DateTime.UtcNow;
context.Projects.Update(project);
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Ok(project)
: Results.BadRequest("Failed to update project");
})
.RequireAuthorization()
.WithDescription("Update project status (M04)")
.WithName(typeof(UpdateProjectEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.Project>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
}
}
@@ -1,58 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Users;
[ApiVersionTarget(1)]
public class GetUsersEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/users", async (IDbContextFactory<ApplicationDbContext> contextFactory,
int page = 1, int pageSize = 10, ActivityStatus? status = null, string? email = null, string?
sortBy = null, bool sortDescending = false, CancellationToken cancellationToken = default) =>
{
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100;
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Users.AsNoTracking();
if (status.HasValue)
query = query.Where(u => u.Status == status.Value);
if (!string.IsNullOrWhiteSpace(email))
query = query.Where(u => u.Email != null && u.Email.Contains(email));
if (!string.IsNullOrWhiteSpace(sortBy))
query = sortBy.ToLowerInvariant() switch
{
"email" => sortDescending ? query.OrderByDescending(u => u.Email) : query.OrderBy(u => u.Email),
"lastloginat" => sortDescending ? query.OrderByDescending(u => u.LastLoginAt) : query.OrderBy(u => u.LastLoginAt),
"status" => sortDescending ? query.OrderByDescending(u => u.Status) : query.OrderBy(u => u.Status),
_ => sortDescending ? query.OrderByDescending(u => u.Id) : query.OrderBy(u => u.Id)
};
else
query = query.OrderBy(u => u.Id);
var totalCount = await query.CountAsync(cancellationToken);
var items = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken);
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
})
.RequireAuthorization()
.WithDescription("Paginated, searchable directory of system users. Supports filtering by ActivityStatus, searching by Email, and sorting")
.WithName(typeof(GetUsersEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Users);
}
}
@@ -1,63 +0,0 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Api;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Api.Endpoints.Users;
[ApiVersionTarget(1)]
public class SyncUserEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/users/sync", async (ClaimsPrincipal principal,
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
var emailClaim = principal.FindFirst("email")?.Value ?? principal.FindFirst(ClaimTypes.Email)?.Value;
if (string.IsNullOrWhiteSpace(sidClaim))
return Results.BadRequest("Missing 'sid' claim in user token.");
if (!Guid.TryParse(sidClaim, out var userId))
userId = new Guid(System.Security.Cryptography.MD5.HashData(System.Text.Encoding.UTF8.GetBytes(sidClaim)));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var user = await context.Users.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
var isNew = user == null;
if (isNew)
{
user = new Domain.Entities.User
{
Id = userId,
Email = emailClaim,
Status = ActivityStatus.Active,
LastLoginAt = DateTime.UtcNow
};
context.Users.Add(user);
}
else
{
user!.Email = emailClaim;
user.LastLoginAt = DateTime.UtcNow;
context.Users.Update(user);
}
return await context.SaveChangesAsync(cancellationToken) > 0
? Results.Ok(user)
: Results.BadRequest("Failed to sync user");
})
.RequireAuthorization()
.WithDescription("Synchronise caller user profile using id_token's SID, inserting/updating user details in database")
.WithName(typeof(SyncUserEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Entities.User>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Users);
}
}
@@ -1,27 +0,0 @@
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;
}
}
@@ -1,153 +0,0 @@
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("PfmApp");
services.Configure<DataProtectionOptions>(options => options.ApplicationDiscriminator = "PfmApp");
services.ConfigureCookieOidcSameSiteSupport();
var configSection = configuration.GetSection(nameof(SecurityClientSettings));
var authOptions = new SecurityClientSettings();
configSection.Bind(authOptions);
services.Configure<SecurityClientSettings>(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 = "PfmApp.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;
}
public static IServiceCollection AddApiSecurity(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;
}
}
@@ -1,100 +1,13 @@
<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="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" />
<Using Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<Using Include="Microsoft.IdentityModel.Tokens" />
<Using Include="System.Security.Claims" />
</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" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
</ItemGroup>
</Project>
@@ -0,0 +1,6 @@
@PostFundManagement.Api_HostAddress = http://localhost:5112
GET {{PostFundManagement.Api_HostAddress}}/weatherforecast/
Accept: application/json
###
+27 -85
View File
@@ -1,99 +1,41 @@
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);
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.AddApiSecurity(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.AddHashServices(builder.Configuration);
builder.Services.AddHttpClient();
builder.Services.AddDataProtectionDatabase(builder.Configuration);
builder.Services.AddApplicationDbContext(builder.Configuration);
builder.Services.AddHealthChecks()
.AddCheck<QuartzHealthCheck>("QuartzScheduler")
.AddCheck<PostgresHealthCheck>("PostgresDatabase")
.AddCheck("Self", () => HealthCheckResult.Healthy());
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
var app = builder.Build();
var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>();
var scheduler = await schedulerFactory.GetScheduler(DefaultSchedulerName);
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
if (!scheduler!.IsStarted)
await scheduler.Start();
app.UseHsts();
app.UseHttpsRedirection();
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>
var summaries = new[]
{
{ 1, app.MapGroup("v{version:apiVersion}").WithApiVersionSet(versionSet) }
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapEndpoints(versionGroups);
app.UseHealthChecks("/health", new HealthCheckOptions
app.MapGet("/weatherforecast", () =>
{
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);
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
-14
View File
@@ -1,18 +1,4 @@
{
"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",
-16
View File
@@ -1,16 +0,0 @@
@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}}"
}
-6
View File
@@ -1,6 +0,0 @@
### 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}}
@@ -1,12 +0,0 @@
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();
}
@@ -1,6 +0,0 @@
namespace PostFundManagement.Domain.Abstractions;
public interface IEndpoint
{
void Map(IEndpointRouteBuilder builder);
}
@@ -1,12 +0,0 @@
namespace PostFundManagement.Domain.Abstractions;
public interface IEvent : INotification
{
Guid Id { get; set; }
string Name { get; set; }
DateTimeOffset EnqueueAt { get; set; }
string CorrelationId { get; set; }
}
@@ -1,12 +0,0 @@
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);
}
@@ -1,7 +0,0 @@
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);
}
@@ -1,3 +0,0 @@
namespace PostFundManagement.Domain.Abstractions;
public interface IService;
@@ -1,83 +0,0 @@
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));
}
}
}
@@ -1,7 +0,0 @@
namespace PostFundManagement.Domain.Api;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class ApiVersionTargetAttribute(int majorVersion) : Attribute
{
public int MajorVersion { get; } = majorVersion;
}
@@ -1,14 +0,0 @@
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; }
}
@@ -1,12 +0,0 @@
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; }
}
@@ -1,13 +0,0 @@
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; }
}
@@ -1,20 +0,0 @@
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; }
}
@@ -1,16 +0,0 @@
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; }
}
@@ -1,16 +0,0 @@
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);
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace PostFundManagement.Domain;
public class Class1
{
}
@@ -1,8 +0,0 @@
namespace PostFundManagement.Domain.Configuration.Email;
public sealed class Account
{
public string? Username { get; set; }
public string? Password { get; set; }
}
@@ -1,12 +0,0 @@
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; }
}
@@ -1,56 +0,0 @@
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);
}
}
@@ -1,25 +0,0 @@
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);
}
}
@@ -1,57 +0,0 @@
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);
}
}
@@ -1,40 +0,0 @@
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);
}
}
@@ -1,38 +0,0 @@
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);
}
}
@@ -1,41 +0,0 @@
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);
}
}
@@ -1,39 +0,0 @@
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);
}
}
@@ -1,34 +0,0 @@
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);
}
}
@@ -1,43 +0,0 @@
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);
}
}
@@ -1,59 +0,0 @@
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);
}
}
@@ -1,33 +0,0 @@
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);
}
}
@@ -1,32 +0,0 @@
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);
}
}
@@ -1,51 +0,0 @@
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);
}
}
@@ -1,41 +0,0 @@
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);
}
}
@@ -1,40 +0,0 @@
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);
}
}
@@ -1,36 +0,0 @@
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);
}
}
@@ -1,26 +0,0 @@
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);
}
}
@@ -1,30 +0,0 @@
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);
}
}
@@ -1,30 +0,0 @@
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);
}
}
@@ -1,39 +0,0 @@
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);
}
}
@@ -1,43 +0,0 @@
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);
}
}
@@ -1,32 +0,0 @@
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);
}
}
@@ -1,39 +0,0 @@
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);
}
}
@@ -1,32 +0,0 @@
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);
}
}
@@ -1,16 +0,0 @@
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);
}
}
@@ -1,10 +0,0 @@
namespace PostFundManagement.Domain.Configuration.Hash;
public sealed class HasherSettings
{
public string? Salt { get; set; }
public int MinHashLength { get; set; }
public string? PayfastPassphrase { get; set; }
}
@@ -1,16 +0,0 @@
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; }
}
@@ -1,17 +0,0 @@
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; } = [];
}
@@ -1,7 +0,0 @@
namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<Configuration.Entities.AuditLog, AuditLog>]
public class AuditLog : Models.AuditLog
{
public virtual User? Performer { get; set; }
}

Some files were not shown because too many files have changed in this diff Show More